Repository: certbot/certbot

Issue:
Pin all dependency installation in the release script
This would prevent changes to our dependencies causing `tools/release.sh` to blow up in the middle of a release. This might be as simple as changing calls to `pip install` to `tools/pip_install.py`, but it requires some testing.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a0c6f0eb2..c6fac76fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,13 +17,17 @@ Certbot adheres to [Semantic Versioning](http://semver.org/).
 
 ### Fixed
 
-*
+* Older modules in the josepy library can now be accessed through acme.jose
+  like it could in previous versions of acme. This is only done to preserve
+  backwards compatibility and support for doing this with new modules in josepy
+  will not be added. Users of the acme library should switch to using josepy
+  directly if they haven't done so already.
 
 Despite us having broken lockstep, we are continuing to release new versions of
 all Certbot components during releases for the time being, however, the only
 package with changes other than its version number was:
 
-*
+* acme
 
 More details about these changes can be found on our GitHub repo.
 
diff --git a/acme/acme/__init__.py b/acme/acme/__init__.py
index e8a0b16a8..d2cb1ee06 100644
--- a/acme/acme/__init__.py
+++ b/acme/acme/__init__.py
@@ -10,3 +10,18 @@ supported version: `draft-ietf-acme-01`_.
   https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01
 
 """
+import sys
+
+import josepy
+
+# This code exists to keep backwards compatibility with people using acme.jose
+# before it became the standalone josepy package.
+#
+# It is based on
+# https://github.com/requests/requests/blob/1278ecdf71a312dc2268f3bfc0aabfab3c006dcf/requests/packages.py
+
+for mod in list(sys.modules):
+    # This traversal is apparently necessary such that the identities are
+    # preserved (acme.jose.* is josepy.*)
+    if mod == 'josepy' or mod.startswith('josepy.'):
+        sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod]
diff --git a/certbot/cli.py b/certbot/cli.py
index 8e599cfda..ff1827cb1 100644
--- a/certbot/cli.py
+++ b/certbot/cli.py
@@ -1215,6 +1215,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False):  # pylint: dis
         " one will be run.")
     helpful.add("renew", "--renew-hook",
                 action=_RenewHookAction, help=argparse.SUPPRESS)
+    helpful.add(
+        "renew", "--no-random-sleep-on-renew", action="store_false",
+        default=flag_default("random_sleep_on_renew"), dest="random_sleep_on_renew",
+        help=argparse.SUPPRESS)
     helpful.add(
         "renew", "--deploy-hook", action=_DeployHookAction,
         help='Command to be run in a shell once for each successfully'
diff --git a/certbot/constants.py b/certbot/constants.py
index eb4105f82..7d0e0c879 100644
--- a/certbot/constants.py
+++ b/certbot/constants.py
@@ -68,6 +68,7 @@ CLI_DEFAULTS = dict(
     directory_hooks=True,
     reuse_key=False,
     disable_renew_updates=False,
+    random_sleep_on_renew=True,
     eab_hmac_key=None,
     eab_kid=None,
 
diff --git a/certbot/main.py b/certbot/main.py
index b24c5c622..ac639bc80 100644
--- a/certbot/main.py
+++ b/certbot/main.py
@@ -4,9 +4,7 @@ from __future__ import print_function
 import functools
 import logging.handlers
 import os
-import random
 import sys
-import time
 
 import configobj
 import josepy as jose
@@ -1269,16 +1267,6 @@ def renew(config, unused_plugins):
     :rtype: None
 
     """
-    if not sys.stdin.isatty():
-        # Noninteractive renewals include a random delay in order to spread
-        # out the load on the certificate authority servers, even if many
-        # users all pick the same time for renewals.  This delay precedes
-        # running any hooks, so that side effects of the hooks (such as
-        # shutting down a web service) aren't prolonged unnecessarily.
-        sleep_time = random.randint(1, 60*8)
-        logger.info("Non-interactive renewal: random delay of %s seconds", sleep_time)
-        time.sleep(sleep_time)
-
     try:
         renewal.handle_renewal_request(config)
     finally:
diff --git a/certbot/renewal.py b/certbot/renewal.py
index 5b2e00740..4c592b27f 100644
--- a/certbot/renewal.py
+++ b/certbot/renewal.py
@@ -5,6 +5,9 @@ import itertools
 import logging
 import os
 import traceback
+import sys
+import time
+import random
 
 import six
 import zope.component
@@ -372,7 +375,7 @@ def _renew_describe_results(config, renew_successes, renew_failures,
     disp.notification("\n".join(out), wrap=False)
 
 
-def handle_renewal_request(config):
+def handle_renewal_request(config):  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
     """Examine each lineage; renew if due and report results"""
 
     # This is trivially False if config.domains is empty
@@ -396,6 +399,14 @@ def handle_renewal_request(config):
     renew_failures = []
     renew_skipped = []
     parse_failures = []
+
+    # Noninteractive renewals include a random delay in order to spread
+    # out the load on the certificate authority servers, even if many
+    # users all pick the same time for renewals.  This delay precedes
+    # running any hooks, so that side effects of the hooks (such as
+    # shutting down a web service) aren't prolonged unnecessarily.
+    apply_random_sleep = not sys.stdin.isatty() and config.random_sleep_on_renew
+
     for renewal_file in conf_files:
         disp = zope.component.getUtility(interfaces.IDisplay)
         disp.notification("Processing " + renewal_file, pause=False)
@@ -424,6 +435,15 @@ def handle_renewal_request(config):
                 from certbot import main
                 plugins = plugins_disco.PluginsRegistry.find_all()
                 if should_renew(lineage_config, renewal_candidate):
+                    # Apply random sleep upon first renewal if needed
+                    if apply_random_sleep:
+                        sleep_time = random.randint(1, 60 * 8)
+                        logger.info("Non-interactive renewal: random delay of %s seconds",
+                                    sleep_time)
+                        time.sleep(sleep_time)
+                        # We will sleep only once this day, folks.
+                        apply_random_sleep = False
+
                     # domains have been restored into lineage_config by reconstitute
                     # but they're unnecessary anyway because renew_cert here
                     # will just grab them from the certificate
diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto
index 0ffe6c5f5..9703b3506 100755
--- a/letsencrypt-auto-source/letsencrypt-auto
+++ b/letsencrypt-auto-source/letsencrypt-auto
@@ -1212,9 +1212,9 @@ requests-toolbelt==0.8.0 \
 chardet==3.0.2 \
     --hash=sha256:4f7832e7c583348a9eddd927ee8514b3bf717c061f57b21dbe7697211454d9bb \
     --hash=sha256:6ebf56457934fdce01fb5ada5582762a84eed94cad43ed877964aebbdd8174c0
-urllib3==1.21.1 \
-    --hash=sha256:8ed6d5c1ff9d6ba84677310060d6a3a78ca3072ce0684cb3c645023009c114b1 \
-    --hash=sha256:b14486978518ca0901a76ba973d7821047409d7f726f22156b24e83fd71382a5
+urllib3==1.24.1 \
+    --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \
+    --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22
 certifi==2017.4.17 \
     --hash=sha256:f4318671072f030a33c7ca6acaef720ddd50ff124d1388e50c1bda4cbd6d7010 \
     --hash=sha256:f7527ebf7461582ce95f7a9e03dd141ce810d40590834f4ec20cddd54234c10a
diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt
index 983d2bb95..eb297bc6e 100644
--- a/letsencrypt-auto-source/pieces/dependency-requirements.txt
+++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt
@@ -188,9 +188,9 @@ requests-toolbelt==0.8.0 \
 chardet==3.0.2 \
     --hash=sha256:4f7832e7c583348a9eddd927ee8514b3bf717c061f57b21dbe7697211454d9bb \
     --hash=sha256:6ebf56457934fdce01fb5ada5582762a84eed94cad43ed877964aebbdd8174c0
-urllib3==1.21.1 \
-    --hash=sha256:8ed6d5c1ff9d6ba84677310060d6a3a78ca3072ce0684cb3c645023009c114b1 \
-    --hash=sha256:b14486978518ca0901a76ba973d7821047409d7f726f22156b24e83fd71382a5
+urllib3==1.24.1 \
+    --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \
+    --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22
 certifi==2017.4.17 \
     --hash=sha256:f4318671072f030a33c7ca6acaef720ddd50ff124d1388e50c1bda4cbd6d7010 \
     --hash=sha256:f7527ebf7461582ce95f7a9e03dd141ce810d40590834f4ec20cddd54234c10a
diff --git a/tools/_release.sh b/tools/_release.sh
index 90c176b0e..ec2deda22 100755
--- a/tools/_release.sh
+++ b/tools/_release.sh
@@ -143,7 +143,7 @@ pip install -U pip
 # (or our dependencies) have conditional dependencies implemented with if
 # statements in setup.py and we have cached wheels lying around that would
 # cause those ifs to not be evaluated.
-pip install \
+python ../tools/pip_install.py \
   --no-cache-dir \
   --extra-index-url http://localhost:$PORT \
   $SUBPKGS
@@ -166,7 +166,7 @@ fi
 mkdir kgs
 kgs="kgs/$version"
 pip freeze | tee $kgs
-pip install pytest
+python ../tools/pip_install.py pytest
 for module in $subpkgs_modules ; do
     echo testing $module
     # use an empty configuration file rather than the one in the repo root
diff --git a/tox.ini b/tox.ini
index 61cee8eff..021c23949 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
 
 [tox]
 skipsdist = true
-envlist = modification,py{34,35,36},py27-cover,lint
+envlist = modification,py3,py27-cover,lint,mypy
 
 [base]
 # pip installs the requested packages in editable mode


Test patch:
diff --git a/acme/acme/jose_test.py b/acme/acme/jose_test.py
new file mode 100644
index 000000000..ecd483662
--- /dev/null
+++ b/acme/acme/jose_test.py
@@ -0,0 +1,43 @@
+"""Tests for acme.jose shim."""
+import importlib
+import unittest
+
+class JoseTest(unittest.TestCase):
+    """Tests for acme.jose shim."""
+
+    def _test_it(self, submodule, attribute):
+        if submodule:
+            acme_jose_path = 'acme.jose.' + submodule
+            josepy_path = 'josepy.' + submodule
+        else:
+            acme_jose_path = 'acme.jose'
+            josepy_path = 'josepy'
+        acme_jose = importlib.import_module(acme_jose_path)
+        josepy = importlib.import_module(josepy_path)
+
+        self.assertIs(acme_jose, josepy)
+        self.assertIs(getattr(acme_jose, attribute), getattr(josepy, attribute))
+
+    def test_top_level(self):
+        self._test_it('', 'RS512')
+
+    def test_submodules(self):
+        # This test ensures that the modules in josepy that were
+        # available at the time it was moved into its own package are
+        # available under acme.jose. Backwards compatibility with new
+        # modules or testing code is not maintained.
+        mods_and_attrs = [('b64', 'b64decode',),
+                          ('errors', 'Error',),
+                          ('interfaces', 'JSONDeSerializable',),
+                          ('json_util', 'Field',),
+                          ('jwa', 'HS256',),
+                          ('jwk', 'JWK',),
+                          ('jws', 'JWS',),
+                          ('util', 'ImmutableMap',),]
+
+        for mod, attr in mods_and_attrs:
+            self._test_it(mod, attr)
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover