Skip to content

Commit

Permalink
Merge da6a477 into 7951ba7
Browse files Browse the repository at this point in the history
  • Loading branch information
ohemorange committed Nov 29, 2016
2 parents 7951ba7 + da6a477 commit ed6e52e
Show file tree
Hide file tree
Showing 11 changed files with 522 additions and 123 deletions.
92 changes: 92 additions & 0 deletions certbot/cert_manager.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Tools for managing certificates."""
import datetime
import logging
import os
import pytz
import traceback
import zope.component

from certbot import configuration
from certbot import errors
from certbot import interfaces
from certbot import renewal
from certbot import storage
from certbot import util

logger = logging.getLogger(__name__)

Expand All @@ -30,6 +33,29 @@ def update_live_symlinks(config):
configuration.RenewerConfiguration(renewer_config),
update_symlinks=True)

def rename_lineage(config):
"""Rename the specified lineage to the new name.
:param config: Configuration.
:type config: :class:`certbot.interfaces.IConfig`
"""
if not config.certname:
raise errors.ConfigurationError("Specify a certificate name with "
"with flag --cert-name.")
if not config.new_certname:
raise errors.ConfigurationError("Specify a new name for certificate {0} "
"with flag --new-cert-name."
.format(config.certname))
lineage = lineage_for_certname(config, config.certname)
if not lineage:
raise errors.ConfigurationError("No existing certificate with name "
"{0} found.".format(config.certname))
storage.rename_renewal_config(config.certname, config.new_certname, config)
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("Successfully renamed {0} to {1}."
.format(config.certname, config.new_certname), pause=False)

def _report_lines(msgs):
"""Format a results report for a category of single-line renewal outcomes"""
return " " + "\n ".join(str(msg) for msg in msgs)
Expand Down Expand Up @@ -104,3 +130,69 @@ def certificates(config):

# Describe all the certs
_describe_certs(parsed_certs, parse_failures)

def _search_lineages(config, func, initial_rv):
"""Iterate func over unbroken lineages, allowing custom return conditions.
"""
cli_config = configuration.RenewerConfiguration(config)
configs_dir = cli_config.renewal_configs_dir
# Verify the directory is there
util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())

rv = initial_rv
for renewal_file in renewal.renewal_conf_files(cli_config):
try:
candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
except (errors.CertStorageError, IOError):
logger.debug("Renewal conf file %s is broken. Skipping.", renewal_file)
logger.debug("Traceback was:\n%s", traceback.format_exc())
continue
rv = func(candidate_lineage, rv)
return rv

def lineage_for_certname(config, certname):
"""Find a lineage object with name certname.
"""
def func(candidate_lineage, rv):
"""Return cert if it has name certname, else return rv
"""
matching_lineage_name_cert = rv
if candidate_lineage.lineagename == certname:
matching_lineage_name_cert = candidate_lineage
return matching_lineage_name_cert
return _search_lineages(config, func, None)

def domains_for_certname(config, certname):
"""Find the domains in the cert with name certname.
"""
def func(candidate_lineage, rv):
"""Return domains if certname matches, else return rv
"""
matching_domains = rv
if candidate_lineage.lineagename == certname:
matching_domains = candidate_lineage.names()
return matching_domains
return _search_lineages(config, func, None)

def find_duplicative_certs(config, domains):
"""Find existing certs that duplicate the request."""
def func(candidate_lineage, rv):
"""Return cert as identical_names_cert if it matches,
or subset_names_cert if it matches as subset
"""
# TODO: Handle these differently depending on whether they are
# expired or still valid?
identical_names_cert, subset_names_cert = rv
candidate_names = set(candidate_lineage.names())
if candidate_names == set(domains):
identical_names_cert = candidate_lineage
elif candidate_names.issubset(set(domains)):
# This logic finds and returns the largest subset-names cert
# in the case where there are several available.
if subset_names_cert is None:
subset_names_cert = candidate_lineage
elif len(candidate_names) > len(subset_names_cert.names()):
subset_names_cert = candidate_lineage
return (identical_names_cert, subset_names_cert)

return _search_lineages(config, func, (None, None))
23 changes: 21 additions & 2 deletions certbot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
rollback Rollback server configuration changes made during install
config_changes Show changes made to server config during installation
update_symlinks Update cert symlinks based on renewal config file
rename Update a certificate's name
plugins Display information about installed plugins
certificates Display information about certs configured with Certbot
Expand Down Expand Up @@ -326,7 +327,7 @@ def __init__(self, args, plugins, detect_defaults=False):
"register": main.register, "renew": main.renew,
"revoke": main.revoke, "rollback": main.rollback,
"everything": main.run, "update_symlinks": main.update_symlinks,
"certificates": main.certificates}
"certificates": main.certificates, "rename": main.rename}

# List of topics for which additional help can be provided
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
Expand Down Expand Up @@ -685,6 +686,19 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains "
"as a parameter.")
helpful.add(
[None, "run", "certonly"],
"--cert-name", dest="certname",
metavar="CERTNAME", default=None,
help="Certificate name to apply. Only one certificate name can be used "
"per Certbot run. Show certificate names by running certificates "
"command. If there is no existing certificate with this name and "
"domains are requested, create a new certificate with this name.")
helpful.add(
"rename",
"--new-cert-name", dest="new_certname",
metavar="NEW_CERTNAME", default=None,
help="New name for the certificate. Must be a valid filename.")
helpful.add(
[None, "testing", "renew", "certonly"],
"--dry-run", action="store_true", dest="dry_run",
Expand Down Expand Up @@ -737,6 +751,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"regardless of whether it is near expiry. (Often "
"--keep-until-expiring is more appropriate). Also implies "
"--expand.")
helpful.add(
"automation", "--renew-with-new-domains",
action="store_true", dest="renew_with_new_domains", help="If a "
"certificate already exists for the requested certificate name "
"but does not match the request domains, renew it now, "
"regardless of whether it is near expiry.")
helpful.add(
["automation", "renew", "certonly"],
"--allow-subset-of-names", action="store_true",
Expand Down Expand Up @@ -1014,7 +1034,6 @@ def __call__(self, parser, namespace, domain, option_string=None):
"""Just wrap add_domains in argparseese."""
add_domains(namespace, domain)


def add_domains(args_or_config, domains):
"""Registers new domains to be used during the current client run.
Expand Down
8 changes: 5 additions & 3 deletions certbot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ def obtain_certificate(self, domains):
return (self.obtain_certificate_from_csr(domains, csr, authzr=authzr)
+ (key, csr))

def obtain_and_enroll_certificate(self, domains):
def obtain_and_enroll_certificate(self, domains, certname):
"""Obtain and enroll certificate.
Get a new certificate for the specified domains using the specified
Expand All @@ -272,6 +272,7 @@ def obtain_and_enroll_certificate(self, domains):
:param list domains: Domains to request.
:param plugins: A PluginsFactory object.
:param str certname: Name of new cert
:returns: A new :class:`certbot.storage.RenewableCert` instance
referred to the enrolled cert lineage, False if the cert could not
Expand All @@ -286,13 +287,14 @@ def obtain_and_enroll_certificate(self, domains):
"Non-standard path(s), might not work with crontab installed "
"by your operating system package manager")

new_name = certname if certname else domains[0]
if self.config.dry_run:
logger.debug("Dry run: Skipping creating new lineage for %s",
domains[0])
new_name)
return None
else:
return storage.RenewableCert.new_lineage(
domains[0], OpenSSL.crypto.dump_certificate(
new_name, OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped),
key.pem, crypto_util.dump_pyopenssl_chain(chain),
configuration.RenewerConfiguration(self.config.namespace))
Expand Down
Loading

0 comments on commit ed6e52e

Please sign in to comment.