Repository: fedora-infra/bodhi

Issue:
dnf was retired on EPEL 7 and bodhi.client.bindings uses it
I learned today that [dnf was retired on EPEL 7](https://bugzilla.redhat.com/show_bug.cgi?id=1492610).  [```bodhi.client.bindings.BodhiClient.testable()```](https://github.com/fedora-infra/bodhi/blob/2.10.1/bodhi/client/bindings.py#L446-L468) uses it:

```
    def testable(self):
        """ Get a list of installed testing updates.
        This method is a generate that yields packages that you currently
        have installed that you have yet to test and provide feedback for.
        Only works on systems with dnf.
        """
        base = dnf.Base()
        sack = base.fill_sack(load_system_repo=True)
        query = sack.query()
        installed = query.installed()
        with open('/etc/fedora-release', 'r') as f:
            fedora = f.readlines()[0].split()[2]
        tag = 'f%s-updates-testing' % fedora
        builds = self.get_koji_session().listTagged(tag, latest=True)
        for build in builds:
            pkgs = installed.filter(name=build['name'], version=build['version'],
                                    release=build['release']).run()
            if len(pkgs):
                update_list = self.query(builds=build['nvr'])['updates']
                for update in update_list:
                    yield update
```

This is the only place in the entire codebase that depends on ```dnf```. Since this method isn't critical to Bodhi, we should code defensively here and have this method raise an ```Exception``` if ```dnf``` is not installed.

Unfortunately, this is causing all CI builds to [fail](https://ci.centos.org/job/bodhi-bodhi/309/) so we must address this immediately:

```
[31mImportError while importing test module '/root/payload/bodhi/tests/server/scripts/test_manage_releases.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
bodhi/tests/server/scripts/test_manage_releases.py:26: in <module>
    from bodhi.server.scripts import manage_releases
bodhi/server/scripts/manage_releases.py:35: in <module>
    from bodhi.client import bindings
bodhi/client/__init__.py:25: in <module>
    from bodhi.client import bindings
bodhi/client/bindings.py:40: in <module>
    import dnf
E   ImportError: No module named dnf[0m
```


Discussion:

Resolving patch:
diff --git a/bodhi/client/bindings.py b/bodhi/client/bindings.py
--- a/bodhi/client/bindings.py
+++ b/bodhi/client/bindings.py
@@ -37,7 +37,11 @@
 
 from iniparse.compat import ConfigParser
 from six.moves import configparser
-import dnf
+try:
+    import dnf
+except ImportError:  # pragma: no cover
+    # dnf is not available on EL 7.
+    dnf = None  # pragma: no cover
 import koji
 import six
 
@@ -468,6 +472,9 @@ def testable(self):
 
         Only works on systems with dnf.
         """
+        if dnf is None:
+            raise RuntimeError('dnf is required by this method and is not installed.')
+
         base = dnf.Base()
         sack = base.fill_sack(load_system_repo=True)
         query = sack.query()


Test patch:
diff --git a/bodhi/tests/client/test_bindings.py b/bodhi/tests/client/test_bindings.py
--- a/bodhi/tests/client/test_bindings.py
+++ b/bodhi/tests/client/test_bindings.py
@@ -1277,13 +1277,15 @@ class TestBodhiClient_testable(unittest.TestCase):
     @mock.patch('__builtin__.open', create=True)
     @mock.patch('bodhi.client.bindings.BodhiClient._load_cookies', mock.MagicMock())
     @mock.patch('bodhi.client.bindings.BodhiClient.get_koji_session')
-    @mock.patch('bodhi.client.bindings.dnf.Base.fill_sack')
-    def test_testable(self, fill_sack, get_koji_session, mock_open):
+    @mock.patch('bodhi.client.bindings.dnf')
+    def test_testable(self, dnf, get_koji_session, mock_open):
         """Assert correct behavior from the testable() method."""
+        fill_sack = mock.MagicMock()
+        dnf.Base.return_value.fill_sack = fill_sack
         get_koji_session.return_value.listTagged.return_value = [
             {'name': 'bodhi', 'version': '2.9.0', 'release': '1.fc26', 'nvr': 'bodhi-2.9.0-1.fc26'}]
-        fill_sack.return_value.query.return_value.installed.return_value.filter.return_value.run.\
-            return_value = ['bodhi-2.8.1-1.fc26']
+        fill_sack.return_value.query.return_value.installed.return_value.filter.\
+            return_value.run.return_value = ['bodhi-2.8.1-1.fc26']
         mock_open.return_value.__enter__.return_value.readlines.return_value = [
             'Fedora release 26 (Twenty Six)']
         client = bindings.BodhiClient()
@@ -1304,3 +1306,13 @@ def test_testable(self, fill_sack, get_koji_session, mock_open):
                                                                          latest=True)
         client.send_request.assert_called_once_with(
             'updates/', params={'builds': 'bodhi-2.9.0-1.fc26'}, verb='GET')
+
+    @mock.patch('bodhi.client.bindings.dnf', None)
+    def test_testable_no_dnf(self):
+        """Ensure that testable raises a RuntimeError if dnf is None."""
+        client = bindings.BodhiClient()
+
+        with self.assertRaises(RuntimeError) as exc:
+            list(client.testable())
+
+        self.assertEqual(str(exc.exception), 'dnf is required by this method and is not installed.')