Repository: certbot/certbot

Issue:
Yum install fails with RHEL v7.2
yum installation of letsencrypt.noarch and python2-letsencrypt.noarch fails when using RHEL's EPEL repository. The issue is the result of python-werkzeug - which is a dependency of both letsencrypt packages, but is not provided by EPEL. Manually installing the `python-werkzeug-0.9.1-2.el7.noarch.rpm` from the CentOS 7 Extras repository fixes the issue; installing Werkzeug by compiling from source or using pip does not resolve the issue, as yum will still list it as a failed dependency unless it sees the rpm has been installed.

Here is a list of all installed repos on the server where this fails:

Repo-id      : epel/x86_64
Repo-name    : Extra Packages for Enterprise Linux 7 - x86_64
Repo-revision: 1454890725
Repo-tags    : binary-x86_64
Repo-updated : Sun Feb  7 20:49:57 2016
Repo-pkgs    : 9,397
Repo-size    : 7.4 G
Repo-metalink: https://mirrors.fedoraproject.org/metalink?repo=epel-7&arch=x86_64
  Updated    : Sun Feb  7 20:49:57 2016
Repo-baseurl : http://s3-mirror-us-east-1.fedoraproject.org/pub/epel/7/x86_64/ (34 more)
Repo-expire  : 21,600 second(s) (last: Tue Feb  9 04:06:34 2016)
Repo-filename: /etc/yum.repos.d/epel.repo

Repo-id      : rhui-REGION-client-config-server-7/x86_64
Repo-name    : Red Hat Update Infrastructure 2.0 Client Configuration Server 7
Repo-revision: 1439271503
Repo-updated : Tue Aug 11 01:38:23 2015
Repo-pkgs    : 6
Repo-size    : 299 k
Repo-mirrors : https://rhui2-cds01.us-east-1.aws.ce.redhat.com/pulp/mirror/rhui-client-config/rhel/server/7/x86_64/os
Repo-baseurl : https://rhui2-cds01.us-east-1.aws.ce.redhat.com/pulp/repos//rhui-client-config/rhel/server/7/x86_64/os/ (1 more)
Repo-expire  : 21,600 second(s) (last: Tue Feb  9 04:06:34 2016)
Repo-filename: /etc/yum.repos.d/redhat-rhui-client-config.repo

Repo-id      : rhui-REGION-rhel-server-optional/7Server/x86_64
Repo-name    : Red Hat Enterprise Linux Server 7 Optional (RPMs)
Repo-revision: 1454007492
Repo-updated : Thu Jan 28 13:59:28 2016
Repo-pkgs    : 8,213
Repo-size    : 9.6 G
Repo-mirrors : https://rhui2-cds01.us-east-1.aws.ce.redhat.com/pulp/mirror/content/dist/rhel/rhui/server/7/7Server/x86_64/optional/os
Repo-baseurl : https://rhui2-cds02.us-east-1.aws.ce.redhat.com/pulp/repos//content/dist/rhel/rhui/server/7/7Server/x86_64/optional/os/ (1 more)
Repo-expire  : 21,600 second(s) (last: Tue Feb  9 04:06:34 2016)
Repo-filename: /etc/yum.repos.d/redhat-rhui.repo

Repo-id      : rhui-REGION-rhel-server-releases/7Server/x86_64
Repo-name    : Red Hat Enterprise Linux Server 7 (RPMs)
Repo-revision: 1454005863
Repo-updated : Thu Jan 28 13:33:14 2016
Repo-pkgs    : 10,152
Repo-size    : 12 G
Repo-mirrors : https://rhui2-cds01.us-east-1.aws.ce.redhat.com/pulp/mirror/content/dist/rhel/rhui/server/7/7Server/x86_64/os
Repo-baseurl : https://rhui2-cds01.us-east-1.aws.ce.redhat.com/pulp/repos//content/dist/rhel/rhui/server/7/7Server/x86_64/os/ (1 more)
Repo-expire  : 21,600 second(s) (last: Tue Feb  9 04:06:34 2016)
Repo-filename: /etc/yum.repos.d/redhat-rhui.repo

Repo-id      : rhui-REGION-rhel-server-rh-common/7Server/x86_64
Repo-name    : Red Hat Enterprise Linux Server 7 RH Common (RPMs)
Repo-revision: 1447968073
Repo-updated : Thu Nov 19 16:21:13 2015
Repo-pkgs    : 168
Repo-size    : 3.2 G
Repo-mirrors : https://rhui2-cds01.us-east-1.aws.ce.redhat.com/pulp/mirror/content/dist/rhel/rhui/server/7/7Server/x86_64/rh-common/os
Repo-baseurl : https://rhui2-cds02.us-east-1.aws.ce.redhat.com/pulp/repos//content/dist/rhel/rhui/server/7/7Server/x86_64/rh-common/os/ (1 more)
Repo-expire  : 21,600 second(s) (last: Tue Feb  9 04:06:34 2016)
Repo-filename: /etc/yum.repos.d/redhat-rhui.repo



Discussion:
I don't know how the AWS package repo stuff works, but there is a RHEL repository called Extras that should mirror what's in CentOS Extras.  It isn't enabled by default, so check yum.repos.d or with `yum-config-manager` to see if you can enable it. The python-werkzeug package is in rhel-7-server-extras-rpms.

From: https://fedoraproject.org/wiki/EPEL

> NOTE for RHN users
> You need to also enable the 'optional' repository to use EPEL packages as they depend on packages in that repository. This can be done by enabling the RHEL optional subchannel for RHN-Classic. For certificate-based subscriptions see Red Hat Subscription Management Guide. For EPEL 7, in addition to the 'optional' repository (rhel-7-server-optional-rpms), you also need to enable the 'extras' repository (rhel-7-server-extras-rpms).
> Idea.png
> NOTE for CentOS users
> You can install EPEL by running yum install epel-release. The package is included in the CentOS Extras repository, enabled by default.

In addition, if you find an issue with the RPM in EPEL, please open a bug with https://bugzilla.redhat.com/ so we have a better chance of fixing it.


Resolving patch:
diff --git a/acme/acme/client.py b/acme/acme/client.py
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -1,6 +1,7 @@
 """ACME client API."""
 import collections
 import datetime
+from email.utils import parsedate_tz
 import heapq
 import logging
 import time
@@ -11,7 +12,6 @@
 import OpenSSL
 import requests
 import sys
-import werkzeug
 
 from acme import errors
 from acme import jose
@@ -248,6 +248,9 @@ def answer_challenge(self, challb, response):
     def retry_after(cls, response, default):
         """Compute next `poll` time based on response ``Retry-After`` header.
 
+        Handles integers and various datestring formats per
+        https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37
+
         :param requests.Response response: Response from `poll`.
         :param int default: Default value (in seconds), used when
             ``Retry-After`` header is not present or invalid.
@@ -260,12 +263,16 @@ def retry_after(cls, response, default):
         try:
             seconds = int(retry_after)
         except ValueError:
-            # pylint: disable=no-member
-            decoded = werkzeug.parse_date(retry_after)  # RFC1123
-            if decoded is None:
-                seconds = default
-            else:
-                return decoded
+            # The RFC 2822 parser handles all of RFC 2616's cases in modern
+            # environments (primarily HTTP 1.1+ but also py27+)
+            when = parsedate_tz(retry_after)
+            if when is not None:
+                try:
+                    tz_secs = datetime.timedelta(when[-1] if when[-1] else 0)
+                    return datetime.datetime(*when[:7]) - tz_secs
+                except (ValueError, OverflowError):
+                    pass
+            seconds = default
 
         return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
 
@@ -357,7 +364,7 @@ def poll_and_request_issuance(
         attempts = collections.defaultdict(int)
         exhausted = set()
 
-        # priority queue with datetime (based on Retry-After) as key,
+        # priority queue with datetime.datetime (based on Retry-After) as key,
         # and original Authorization Resource as value
         waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs]
         # mapping between original Authorization Resource and the most
diff --git a/acme/setup.py b/acme/setup.py
--- a/acme/setup.py
+++ b/acme/setup.py
@@ -20,7 +20,6 @@
     'requests',
     'setuptools',  # pkg_resources
     'six',
-    'werkzeug',
 ]
 
 # env markers in extras_require cause problems with older pip: #517


Test patch:
diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py
--- a/acme/acme/client_test.py
+++ b/acme/acme/client_test.py
@@ -222,6 +222,17 @@ def test_retry_after_invalid(self, dt_mock):
             datetime.datetime(2015, 3, 27, 0, 0, 10),
             self.client.retry_after(response=self.response, default=10))
 
+    @mock.patch('acme.client.datetime')
+    def test_retry_after_overflow(self, dt_mock):
+        dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
+        dt_mock.timedelta = datetime.timedelta
+        dt_mock.datetime.side_effect = datetime.datetime
+
+        self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST"
+        self.assertEqual(
+            datetime.datetime(2015, 3, 27, 0, 0, 10),
+            self.client.retry_after(response=self.response, default=10))
+
     @mock.patch('acme.client.datetime')
     def test_retry_after_seconds(self, dt_mock):
         dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)