diff --git a/letsencrypt/client.py b/letsencrypt/client.py index a0272d7b7..3fb02f7a6 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -155,9 +155,11 @@ class Client(object): return cert_pem, cert_key.pem, chain_pem - def obtain_and_enroll_certificate(self, domains, authenticator, installer, - plugins, csr=None): - """Get a new certificate for the specified domains using the specified + def obtain_and_enroll_certificate( + self, domains, authenticator, installer, plugins, csr=None): + """Obtain and enroll certificate. + + Get a new certificate for the specified domains using the specified authenticator and installer, and then create a new renewable lineage containing it. @@ -175,8 +177,8 @@ class Client(object): :returns: A new :class:`letsencrypt.storage.RenewableCert` instance referred to the enrolled cert lineage, or False if the cert could not be obtained. + """ - # TODO: fully identify object types in docstring. cert_pem, privkey, chain_pem = self._obtain_certificate(domains, csr) self.config.namespace.authenticator = plugins.find_init( authenticator).name @@ -187,8 +189,13 @@ class Client(object): vars(self.config.namespace)) def obtain_certificate(self, domains): - """Public method to obtain a certificate for the specified domains - using this client object. Returns the tuple (cert, privkey, chain).""" + """Obtain certificate. + + Public method to obtain a certificate for the specified domains + using this client object. Returns the tuple (cert, privkey, + chain). + + """ return self._obtain_certificate(domains, None) def save_certificate(self, certr, cert_path, chain_path): diff --git a/letsencrypt/notify.py b/letsencrypt/notify.py index b4eec938f..cfbfa82b0 100644 --- a/letsencrypt/notify.py +++ b/letsencrypt/notify.py @@ -7,8 +7,12 @@ import subprocess def notify(subject, whom, what): - """Try to notify the addressee (whom) by e-mail, with Subject: - defined by subject and message body by what.""" + """Send email notification. + + Try to notify the addressee (``whom``) by e-mail, with Subject: + defined by ``subject`` and message body by ``what``. + + """ msg = email.message_from_string(what) msg.add_header("From", "Let's Encrypt renewal agent ") msg.add_header("To", whom) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index c436c2ccd..40bc5cf60 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -1,11 +1,12 @@ -"""Renewer tool to handle autorenewal and autodeployment of renewed -certs within lineages of successor certificates, according to -configuration.""" +"""Renewer tool. -# TODO: sanity checking consistency, validity, freshness? +Renewer tool handles autorenewal and autodeployment of renewed certs +within lineages of successor certificates, according to configuration. -# TODO: call new installer API to restart servers after deployment +.. todo:: Sanity checking consistency, validity, freshness? +.. todo:: Call new installer API to restart servers after deployment +""" import copy import os @@ -21,8 +22,11 @@ from letsencrypt.plugins import disco as plugins_disco class AttrDict(dict): - """A trick to allow accessing dictionary keys as object - attributes.""" + """Attribute dictionary. + + A trick to allow accessing dictionary keys as object attributes. + + """ def __init__(self, *args, **kwargs): super(AttrDict, self).__init__(*args, **kwargs) self.__dict__ = self @@ -31,14 +35,16 @@ class AttrDict(dict): def renew(cert, old_version): """Perform automated renewal of the referenced cert, if possible. - :param class:`letsencrypt.storage.RenewableCert` cert: the certificate + :param letsencrypt.storage.RenewableCert cert: The certificate lineage to attempt to renew. - :param int old_version: the version of the certificate lineage relative - to which the renewal should be attempted. + :param int old_version: The version of the certificate lineage + relative to which the renewal should be attempted. - :returns: int referring to newly created version of this cert lineage, - or False if renewal was not successful.""" + :returns: A number referring to newly created version of this cert + lineage, or ``False`` if renewal was not successful. + :rtype: `int` or `bool` + """ # TODO: handle partial success (some names can be renewed but not # others) # TODO: handle obligatory key rotation vs. optional key rotation vs. @@ -89,7 +95,7 @@ def renew(cert, old_version): def main(config=None): - """main function for autorenewer script.""" + """Main function for autorenewer script.""" # TODO: Distinguish automated invocation from manual invocation, # perhaps by looking at sys.argv[0] and inhibiting automated # invocations if /etc/letsencrypt/renewal.conf defaults have diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 1fb17c561..3b2cd58b0 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -1,6 +1,4 @@ -"""The RenewableCert class, representing renewable lineages of -certificates and storing the associated cert data and metadata.""" - +"""Renewable certificates storage.""" import copy import datetime import os @@ -23,14 +21,14 @@ def parse_time_interval(interval, textparser=parsedatetime.Calendar()): """Parse the time specified time interval. The interval can be in the English-language format understood by - parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', - or a sequence of such intervals like '6 months 1 week' or '3 days - 12 hours'. If an integer is found with no associated unit, it is + parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', or + a sequence of such intervals like '6 months 1 week' or '3 days 12 + hours'. If an integer is found with no associated unit, it is interpreted by default as a number of days. - :param str interval: the time interval to parse. + :param str interval: The time interval to parse. - :returns: the interpretation of the time interval. + :returns: The interpretation of the time interval. :rtype: :class:`datetime.timedelta`""" if interval.strip().isdigit(): @@ -40,59 +38,55 @@ def parse_time_interval(interval, textparser=parsedatetime.Calendar()): class RenewableCert(object): # pylint: disable=too-many-instance-attributes - """Represents a lineage of certificates that is under the management + """Renewable certificate. + + Represents a lineage of certificates that is under the management of the Let's Encrypt client, indicated by the existence of an associated renewal configuration file. - Note that the notion of "current version" for a lineage is maintained - on disk in the structure of symbolic links, and is not explicitly - stored in any instance variable in this object. The RenewableCert - object is able to determine information about the current (or other) - version by accessing data on disk, but does not inherently know any - of this information except by examining the symbolic links as needed. - The instance variables mentioned below point to symlinks that reflect - the notion of "current version" of each managed object, and it is - these paths that should be used when configuring servers to use the - certificate managed in a lineage. These paths are normally within - the "live" directory, and their symlink targets -- the actual cert - files -- are normally found within the "archive" directory. + Note that the notion of "current version" for a lineage is + maintained on disk in the structure of symbolic links, and is not + explicitly stored in any instance variable in this object. The + RenewableCert object is able to determine information about the + current (or other) version by accessing data on disk, but does not + inherently know any of this information except by examining the + symbolic links as needed. The instance variables mentioned below + point to symlinks that reflect the notion of "current version" of + each managed object, and it is these paths that should be used when + configuring servers to use the certificate managed in a lineage. + These paths are normally within the "live" directory, and their + symlink targets -- the actual cert files -- are normally found + within the "archive" directory. - :ivar cert: The path to the symlink representing the current version - of the certificate managed by this lineage. - :type cert: str - - :ivar privkey: The path to the symlink representing the current version - of the private key managed by this lineage. - :type privkey: str - - :ivar chain: The path to the symlink representing the current version + :ivar str cert: The path to the symlink representing the current + version of the certificate managed by this lineage. + :ivar str privkey: The path to the symlink representing the current + version of the private key managed by this lineage. + :ivar str chain: The path to the symlink representing the current version of the chain managed by this lineage. - :type chain: str - - :ivar fullchain: The path to the symlink representing the current version - of the fullchain (combined chain and cert) managed by this lineage. - :type fullchain: str - - :ivar configuration: The renewal configuration options associated with - this lineage, obtained from parsing the renewal configuration file - and/or systemwide defaults. - :type configuration: :class:`configobj.ConfigObj`""" + :ivar str fullchain: The path to the symlink representing the + current version of the fullchain (combined chain and cert) + managed by this lineage. + :ivar configobj.ConfigObj configuration: The renewal configuration + options associated with this lineage, obtained from parsing the + renewal configuration file and/or systemwide defaults. + """ def __init__(self, configfile, config_opts=None): """Instantiate a RenewableCert object from an existing lineage. - :param :class:`configobj.ConfigObj` configfile: an already-parsed - ConfigObj object made from reading the renewal config file that - defines this lineage. - :param :class:`configobj.ConfigObj` config_opts: systemwide defaults - for renewal properties not otherwise specified in the individual - renewal config file. + :param configobj.ConfigObj configfile: an already-parsed + ConfigObj object made from reading the renewal config file + that defines this lineage. :param configobj.ConfigObj + config_opts: systemwide defaults for renewal properties not + otherwise specified in the individual renewal config file. - :raises ValueError: if the configuration file's name didn't end in - ".conf", or the file is missing or broken. + :raises ValueError: if the configuration file's name didn't end + in ".conf", or the file is missing or broken. :raises TypeError: if the provided renewal configuration isn't a - ConfigObj object.""" + ConfigObj object. + """ if isinstance(configfile, configobj.ConfigObj): if not os.path.basename(configfile.filename).endswith(".conf"): raise ValueError("renewal config file name must end in .conf") @@ -127,10 +121,12 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes def consistent(self): """Are the files associated with this lineage self-consistent? - :returns: whether the files stored in connection with this - lineage appear to be correct and consistent with one another. - :rtype: bool""" + :returns: Whether the files stored in connection with this + lineage appear to be correct and consistent with one + another. + :rtype: bool + """ # Each element must be referenced with an absolute path if any(not os.path.isabs(x) for x in (self.cert, self.privkey, self.chain, self.fullchain)): @@ -182,7 +178,10 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes def fix(self): """Attempt to fix defects or inconsistencies in this lineage. - (Currently unimplemented.)""" + + .. todo:: Currently unimplemented. + + """ # TODO: Figure out what kinds of fixes are possible. For # example, checking if there is a valid version that # we can update the symlinks to. (Maybe involve @@ -201,9 +200,11 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") - :returns: the path to the current version of the specified member. - :rtype: str""" + :returns: The path to the current version of the specified + member. + :rtype: str + """ if kind not in ALL_FOUR: raise ValueError("unknown kind of item") link = getattr(self, kind) @@ -217,16 +218,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes def current_version(self, kind): """Returns numerical version of the specified item. - For example, if kind - is "chain" and the current chain link points to a file named - "chain7.pem", returns the integer 7. + For example, if kind is "chain" and the current chain link + points to a file named "chain7.pem", returns the integer 7. :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :returns: the current version of the specified member. - :rtype: int""" + :rtype: int + """ if kind not in ALL_FOUR: raise ValueError("unknown kind of item") pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) @@ -242,34 +243,36 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes def version(self, kind, version): """The filename that corresponds to the specified version and kind. - Warning: the specified version may not exist in this lineage. There - is no guarantee that the file path returned by this method actually - exists. + .. warning:: The specified version may not exist in this + lineage. There is no guarantee that the file path returned + by this method actually exists. :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :param int version: the desired version - :returns: the path to the specified version of the specified member. - :rtype: str""" + :returns: The path to the specified version of the specified member. + :rtype: str + """ if kind not in ALL_FOUR: raise ValueError("unknown kind of item") where = os.path.dirname(self.current_target(kind)) return os.path.join(where, "{0}{1}.pem".format(kind, version)) def available_versions(self, kind): - """Which alternative versions of the specified kind of item exist? + """Which lternative versions of the specified kind of item exist? The archive directory where the current version is stored is consulted to obtain the list of alternatives. - :param str kind: the lineage member item ("cert", "privkey", - "chain", or "fullchain") + :param str kind: the lineage member item ( + ``cert``, ``privkey``, ``chain``, or ``fullchain``) :returns: all of the version numbers that currently exist - :rtype: list of int""" + :rtype: `list` of `int` + """ if kind not in ALL_FOUR: raise ValueError("unknown kind of item") where = os.path.dirname(self.current_target(kind)) @@ -279,23 +282,25 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes return sorted([int(m.groups()[0]) for m in matches if m]) def newest_available_version(self, kind): - """What is the newest available version of the specified kind of item? + """Newest available version of the specified kind of item? - :param str kind: the lineage member item ("cert", "privkey", - "chain", or "fullchain") + :param str kind: the lineage member item (``cert``, + ``privkey``, ``chain``, or ``fullchain``) :returns: the newest available version of this member - :rtype: int""" + :rtype: int + """ return max(self.available_versions(kind)) def latest_common_version(self): - """What is the newest version for which all items are available? + """Newest version for which all items are available? - :returns: the newest available version for which all members (cert, - privkey, chain, and fullchain) exist - :rtype: int""" + :returns: the newest available version for which all members + (``cert, ``privkey``, ``chain``, and ``fullchain``) exist + :rtype: int + """ # TODO: this can raise ValueError if there is no version overlap # (it should probably return None instead) # TODO: this can raise a spurious AttributeError if the current @@ -304,13 +309,13 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes return max(n for n in versions[0] if all(n in v for v in versions[1:])) def next_free_version(self): - """What is the smallest version newer than all full or partial versions? + """Smallest version newer than all full or partial versions? - :returns: the smallest version number that is larger than any version - of any item currently stored in this lineage + :returns: the smallest version number that is larger than any + version of any item currently stored in this lineage :rtype: int - """ + """ # TODO: consider locking/mutual exclusion between updating processes # This isn't self.latest_common_version() + 1 because we don't want # collide with a version that might exist for one file type but not @@ -320,11 +325,12 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes def has_pending_deployment(self): """Is there a later version of all of the managed items? - :returns: True if there is a complete version of this lineage with - a larger version number than the current version, and False - otherwise - :rtype: bool""" + :returns: ``True`` if there is a complete version of this + lineage with a larger version number than the current + version, and ``False`` otherwis + :rtype: bool + """ # TODO: consider whether to assume consistency or treat # inconsistent/consistent versions differently smallest_current = min(self.current_version(x) for x in ALL_FOUR) @@ -338,8 +344,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") - :param int version: the desired version""" + :param int version: the desired version + """ if kind not in ALL_FOUR: raise ValueError("unknown kind of item") link = getattr(self, kind) @@ -383,10 +390,11 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param int version: the desired version number - :returns: the notBefore value from the specified cert version in this - lineage - :rtype: :class:`datetime.datetime`""" + :returns: the notBefore value from the specified cert version in + this lineage + :rtype: :class:`datetime.datetime` + """ return self._notafterbefore(lambda x509: x509.get_notBefore(), version) def notafter(self, version=None): @@ -396,24 +404,27 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param int version: the desired version number - :returns: the notAfter value from the specified cert version in this - lineage - :rtype: :class:`datetime.datetime`""" + :returns: the notAfter value from the specified cert version in + this lineage + :rtype: :class:`datetime.datetime` + """ return self._notafterbefore(lambda x509: x509.get_notAfter(), version) def should_autodeploy(self): """Should this lineage now automatically deploy a newer version? - This is a policy question and does not only depend on whether there - is a newer version of the cert. (This considers whether autodeployment - is enabled, whether a relevant newer version exists, and whether the - time interval for autodeployment has been reached.) + This is a policy question and does not only depend on whether + there is a newer version of the cert. (This considers whether + autodeployment is enabled, whether a relevant newer version + exists, and whether the time interval for autodeployment has + been reached.) - :returns: whether the lineage now ought to autodeploy an existing - newer cert version - :rtype: bool""" + :returns: whether the lineage now ought to autodeploy an + existing newer cert version + :rtype: bool + """ if ("autodeploy" not in self.configuration or self.configuration.as_bool("autodeploy")): if self.has_pending_deployment(): @@ -431,17 +442,19 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # pylint: disable=no-self-use,unused-argument """Is the specified cert version revoked according to OCSP? - Also returns True if the cert version is declared as intended to be - revoked according to Let's Encrypt OCSP extensions. (If no version - is specified, uses the current version.) + Also returns True if the cert version is declared as intended + to be revoked according to Let's Encrypt OCSP extensions. + (If no version is specified, uses the current version.) - This method is not yet implemented and currently always returns False. + This method is not yet implemented and currently always returns + False. :param int version: the desired version number :returns: whether the certificate is or will be revoked - :rtype: bool""" + :rtype: bool + """ # XXX: This query and its associated network service aren't # implemented yet, so we currently return False (indicating that the # certificate is not revoked). @@ -450,18 +463,19 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes def should_autorenew(self): """Should we now try to autorenew the most recent cert version? - This is a policy question and does not only depend on whether the - cert is expired. (This considers whether autorenewal is enabled, - whether the cert is revoked, and whether the time interval for - autorenewal has been reached.) + This is a policy question and does not only depend on whether + the cert is expired. (This considers whether autorenewal is + enabled, whether the cert is revoked, and whether the time + interval for autorenewal has been reached.) Note that this examines the numerically most recent cert version, not the currently deployed version. :returns: whether an attempt should now be made to autorenew the most current cert version in this lineage - :rtype: bool""" + :rtype: bool + """ if ("autorenew" not in self.configuration or self.configuration.as_bool("autorenew")): # Consider whether to attempt to autorenew this cert now @@ -486,28 +500,28 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-locals,too-many-arguments """Create a new certificate lineage. - Attempts to create a certificate lineage -- enrolled for potential - future renewal -- with the (suggested) lineage name lineagename, - and the associated cert, privkey, and chain (the associated - fullchain will be created automatically). Optional configurator - and renewalparams record the configuration that was originally - used to obtain this cert, so that it can be reused later during - automated renewal. + Attempts to create a certificate lineage -- enrolled for + potential future renewal -- with the (suggested) lineage name + lineagename, and the associated cert, privkey, and chain (the + associated fullchain will be created automatically). Optional + configurator and renewalparams record the configuration that was + originally used to obtain this cert, so that it can be reused + later during automated renewal. Returns a new RenewableCert object referring to the created lineage. (The actual lineage name, as well as all the relevant file paths, will be available within this object.) :param str lineagename: the suggested name for this lineage - (normally the current cert's first subject DNS name) + (normally the current cert's first subject DNS name) :param str cert: the initial certificate version in PEM format :param str privkey: the private key in PEM format :param str chain: the certificate chain in PEM format - :param :class:`configobj.ConfigObj` renewalparams: parameters that + :param configobj.ConfigObj renewalparams: parameters that should be used when instantiating authenticator and installer objects in the future to attempt to renew this cert or deploy new versions of it - :param :class:`configobj.ConfigObj` config: renewal configuration + :param configobj.ConfigObj config: renewal configuration defaults, affecting, for example, the locations of the directories where the associated files will be saved @@ -585,21 +599,24 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes def save_successor(self, prior_version, new_cert, new_privkey, new_chain): """Save new cert and chain as a successor of a prior version. - Returns the new version number that was created. Note: does NOT - update links to deploy this version. + Returns the new version number that was created. - :param int prior_version: the old version to which this version is - regarded as a successor (used to choose a privkey, if the key - has not changed, but otherwise this information is not permanently - recorded anywhere) + .. note:: this function does NOT update links to deploy this + version + + :param int prior_version: the old version to which this version + is regarded as a successor (used to choose a privkey, if the + key has not changed, but otherwise this information is not + permanently recorded anywhere) :param str new_cert: the new certificate, in PEM format - :param str new_privkey: the new private key, in PEM format, or None, - if the private key has not changed + :param str new_privkey: the new private key, in PEM format, + or ``None``, if the private key has not changed :param str new_chain: the new chain, in PEM format :returns: the new version number that was created - :rtype: int""" + :rtype: int + """ # XXX: assumes official archive location rather than examining links # XXX: consider using os.open for availablity of os.O_EXCL # XXX: ensure file permissions are correct; also create directories diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 7631ef8bf..061cd71e5 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -1,5 +1,4 @@ -"""Tests for letsencrypt/renewer.py""" - +"""Tests for letsencrypt.renewer.""" import datetime import os import tempfile @@ -13,23 +12,22 @@ import pytz from letsencrypt.storage import ALL_FOUR + def unlink_all(rc_object): - """Unlink all four items associated with this RenewableCert. - (Helper function.)""" + """Unlink all four items associated with this RenewableCert.""" for kind in ALL_FOUR: os.unlink(getattr(rc_object, kind)) def fill_with_sample_data(rc_object): - """Put dummy data into all four files of this RenewableCert. - (Helper function.)""" + """Put dummy data into all four files of this RenewableCert.""" for kind in ALL_FOUR: with open(getattr(rc_object, kind), "w") as f: f.write(kind) + class RenewableCertTests(unittest.TestCase): # pylint: disable=too-many-public-methods - """Tests for the RenewableCert class as well as other functions - within renewer.py.""" + """Tests for letsencrypt.renewer.*.""" def setUp(self): from letsencrypt import storage self.tempdir = tempfile.mkdtemp() @@ -646,5 +644,6 @@ class RenewableCertTests(unittest.TestCase): renewer.main(self.defaults) # The ValueError is caught inside and nothing happens. + if __name__ == "__main__": unittest.main() # pragma: no cover