Repository: fedora-infra/bodhi

Issue:
Make zchunk updateinfo a per-repo option
When I added support for zchunked updateinfo.xml in #2904, I missed the fact that my patch was creating zchunked updateinfo.xml for _every_ repository.  I don't think we want to be creating zchunk metadata for EPEL and Fedora 29 (and, apparently it's breaking some third-party mirroring tools, see https://bugzilla.redhat.com/show_bug.cgi?id=1721359), so we need to add a way of making the zchunked updateinfo.xml a per-repo option.

I'm happy to do the work on this, but I'm not sure where and how it should be implemented.



Discussion:
Potential fix (unable to test it):

File bodhi/server/metadata.py
The file is already making a special case of the Epel repo with respect
to enabling bzip2 compression, so this simply disables zchunk in the same
place, for the same repo release id.

```
--- metadata.py.orig	2019-07-09 21:39:45.377865136 +0930
+++ metadata.py	2019-07-09 21:57:13.671371593 +0930
@@ -35,7 +35,7 @@
 log = logging.getLogger(__name__)
 
 
-def insert_in_repo(comp_type, repodata, filetype, extension, source):
+def insert_in_repo(comp_type, repodata, filetype, extension, source, zck_supported):
     """
     Inject a file into the repodata with the help of createrepo_c.
 
@@ -48,6 +48,7 @@
         extension (str): The file extension (xml, sqlite).
         source (str): A file path. File holds the dump of metadata until
             copied to the repodata folder.
+        zck_supported (bool): Whether zchunk data is supported for clients of this repo.
     """
     log.info('Inserting %s.%s into %s', filetype, extension, repodata)
     target_fname = os.path.join(repodata, '%s.%s' % (filetype, extension))
@@ -55,7 +56,7 @@
     repomd_xml = os.path.join(repodata, 'repomd.xml')
     repomd = cr.Repomd(repomd_xml)
     add_list = [(filetype, comp_type)]
-    if hasattr(cr, 'ZCK_COMPRESSION') and comp_type != cr.ZCK_COMPRESSION:
+    if zck_supported and hasattr(cr, 'ZCK_COMPRESSION') and comp_type != cr.ZCK_COMPRESSION :
         add_list.append((filetype + "_zck", cr.ZCK_COMPRESSION))
     for (ft, ct) in add_list:
         # create a new record for our repomd.xml
@@ -73,7 +74,7 @@
     os.unlink(target_fname)
 
 
-def modifyrepo(comp_type, compose_path, filetype, extension, source):
+def modifyrepo(comp_type, compose_path, filetype, extension, source, zck_supported):
     """
     Inject a file into the repodata for each architecture with the help of createrepo_c.
 
@@ -85,6 +86,7 @@
         extension (str): The file extension (xml, sqlite).
         source (str): A file path. File holds the dump of metadata until
             copied to the repodata folder.
+        zck_supported (bool): Whether zchunk data is supported for clients of this repo.
     """
     repo_path = os.path.join(compose_path, 'compose', 'Everything')
     for arch in os.listdir(repo_path):
@@ -92,7 +94,7 @@
             repodata = os.path.join(repo_path, arch, 'tree', 'repodata')
         else:
             repodata = os.path.join(repo_path, arch, 'os', 'repodata')
-        insert_in_repo(comp_type, repodata, filetype, extension, source)
+        insert_in_repo(comp_type, repodata, filetype, extension, source, zck_supported)
 
 
 class UpdateInfoMetadata(object):
@@ -138,11 +140,19 @@
 
         self.comp_type = cr.XZ
 
-        if release.id_prefix == 'FEDORA-EPEL':
+        # Some repos such as FEDORA-EPEL, are primarily targeted at 
+        # distributions that use the yum client, which does not support zchunk metadata
+        self.legacy_repos = ['FEDORA-EPEL']
+        self.zck_supported = True
+
+        if release.id_prefix in self.legacy_repos:
             # FIXME: I'm not sure which versions of RHEL support xz metadata
             # compression, so use the lowest common denominator for now.
             self.comp_type = cr.BZ2
 
+            log.warning('Zchunk data is disabled for repo %s until it moves to a client with Zchunk support' % release.id_prefix)
+            self.zck_supported = False
+ 
         self.uinfo = cr.UpdateInfo()
         for update in self.updates:
             self.add_update(update)
@@ -289,5 +299,5 @@
         fd, tmp_file_path = tempfile.mkstemp()
         os.write(fd, self.uinfo.xml_dump().encode('utf-8'))
         os.close(fd)
-        modifyrepo(self.comp_type, compose_path, 'updateinfo', 'xml', tmp_file_path)
+        modifyrepo(self.comp_type, compose_path, 'updateinfo', 'xml', tmp_file_path, self.zck_supported)
         os.unlink(tmp_file_path)
```
Are we able to back this change out whilst a solution is sought? That way those of us who have been effected by the change can get back to working. :) I've been bitten by the EPEL repo.
hello Chris.
No sign that anyone in the world other than yourself, Jdieter, and myself care about this at all. I guess no-one is really using EPEL? It would be interesting to see if there are stats on the actual usage. Maybe it's pretty much dead? Really surprising to me but I suppose I can only guess that RHEL and CentOS users must already be limiting themselves to the regular RHEL/CentOS subset of packages and not using EPEL at all, as it seems to be only yourself, me, and Jdieter, and two people that commented on the sonatype nexus bug/issue, that care about it in the entire world. And even myself, I don't care about it any more, I'm using it only for a personal project in my time, my hobby project, I'm only using it at all because I guess I had some impression that 'enterprises' / organisations and governments still wanted to use (CentOS/Redhat + EPEL). But given the complete lack of interest in a completely broken EPEL repository around the world, there might be no good reason not to move to another distro. Been using debian lately and it has some worthwhile benefits, such as package repositories that actually work and aren't consistently broken for over two months with no sign of them ever being fixed. 
This really shows the flaws in using an online repo such as EPEL even with an open source philosophy. The metadata is generated by one thing and that's broken, making the entire repository pretty much useless unless one browses it with a web browser, manually saving some individual packages. 
We can do an open source bugfix patch for the EPEL repo metadata generation tool (as attached above) but, that doesn't help anyone at all, until one maintainer that's on holiday for months gets back and gets around to fixing it or applying the patch.    The repo is useless without usable repo metadata.

sigh.

Do note that this only affects people using that specific cache product. Most epel7 users aren't affected by this at all. 

That said, we should of course fix this. Anyone able to review the above patch or something like it?
Any news about that issues ?
It affects people like me using Nexus Proxy as cache. Our ansible Yum install is blocked because of this. We will try alternative solutions (like Pip) but for sure EPEL is used at many place in our build chain. 
Like @omerlin we're using Nexus Proxy as a cache, which is impossible at the moment.
Thank you for the feature, however it broke the world.
This is causing issues for lots of our users and customers. Is there any intention to fix this?
@jdieter if you're able, would you consider renaming this issue to something like "Revert breaking zchunk change for EPEL repositories with third party yum clients/proxies?"  I fear this issue is being continually overlooked because from the title it just looks like a feature request to make a feature optional.

Resolving patch:
diff --git a/bodhi/server/metadata.py b/bodhi/server/metadata.py
--- a/bodhi/server/metadata.py
+++ b/bodhi/server/metadata.py
@@ -35,7 +35,7 @@
 log = logging.getLogger(__name__)
 
 
-def insert_in_repo(comp_type, repodata, filetype, extension, source):
+def insert_in_repo(comp_type, repodata, filetype, extension, source, zchunk):
     """
     Inject a file into the repodata with the help of createrepo_c.
 
@@ -48,6 +48,7 @@ def insert_in_repo(comp_type, repodata, filetype, extension, source):
         extension (str): The file extension (xml, sqlite).
         source (str): A file path. File holds the dump of metadata until
             copied to the repodata folder.
+        zchunk (bool): Whether zchunk data is supported for clients of this repo.
     """
     log.info('Inserting %s.%s into %s', filetype, extension, repodata)
     target_fname = os.path.join(repodata, '%s.%s' % (filetype, extension))
@@ -55,7 +56,7 @@ def insert_in_repo(comp_type, repodata, filetype, extension, source):
     repomd_xml = os.path.join(repodata, 'repomd.xml')
     repomd = cr.Repomd(repomd_xml)
     add_list = [(filetype, comp_type)]
-    if hasattr(cr, 'ZCK_COMPRESSION') and comp_type != cr.ZCK_COMPRESSION:
+    if zchunk and hasattr(cr, 'ZCK_COMPRESSION') and comp_type != cr.ZCK_COMPRESSION:
         add_list.append((filetype + "_zck", cr.ZCK_COMPRESSION))
     for (ft, ct) in add_list:
         # create a new record for our repomd.xml
@@ -73,7 +74,7 @@ def insert_in_repo(comp_type, repodata, filetype, extension, source):
     os.unlink(target_fname)
 
 
-def modifyrepo(comp_type, compose_path, filetype, extension, source):
+def modifyrepo(comp_type, compose_path, filetype, extension, source, zchunk):
     """
     Inject a file into the repodata for each architecture with the help of createrepo_c.
 
@@ -85,6 +86,7 @@ def modifyrepo(comp_type, compose_path, filetype, extension, source):
         extension (str): The file extension (xml, sqlite).
         source (str): A file path. File holds the dump of metadata until
             copied to the repodata folder.
+        zchunk (bool): Whether zchunk data is supported for clients of this repo.
     """
     repo_path = os.path.join(compose_path, 'compose', 'Everything')
     for arch in os.listdir(repo_path):
@@ -92,7 +94,7 @@ def modifyrepo(comp_type, compose_path, filetype, extension, source):
             repodata = os.path.join(repo_path, arch, 'tree', 'repodata')
         else:
             repodata = os.path.join(repo_path, arch, 'os', 'repodata')
-        insert_in_repo(comp_type, repodata, filetype, extension, source)
+        insert_in_repo(comp_type, repodata, filetype, extension, source, zchunk)
 
 
 class UpdateInfoMetadata(object):
@@ -138,11 +140,22 @@ def __init__(self, release, request, db, composedir, close_shelf=True):
 
         self.comp_type = cr.XZ
 
-        if release.id_prefix == 'FEDORA-EPEL':
+        # Some repos such as FEDORA-EPEL, are primarily targeted at
+        # distributions that use the yum client, which does not support zchunk metadata
+        self.legacy_repos = ['FEDORA-EPEL']
+        self.zchunk = True
+
+        if release.id_prefix in self.legacy_repos:
             # FIXME: I'm not sure which versions of RHEL support xz metadata
             # compression, so use the lowest common denominator for now.
             self.comp_type = cr.BZ2
 
+            log.warning(
+                'Zchunk data is disabled for repo {release.id_prefix} until it moves to a client'
+                ' with Zchunk support'
+            )
+            self.zchunk = False
+
         self.uinfo = cr.UpdateInfo()
         for update in self.updates:
             self.add_update(update)
@@ -289,5 +302,10 @@ def insert_updateinfo(self, compose_path):
         fd, tmp_file_path = tempfile.mkstemp()
         os.write(fd, self.uinfo.xml_dump().encode('utf-8'))
         os.close(fd)
-        modifyrepo(self.comp_type, compose_path, 'updateinfo', 'xml', tmp_file_path)
+        modifyrepo(self.comp_type,
+                   compose_path,
+                   'updateinfo',
+                   'xml',
+                   tmp_file_path,
+                   self.zchunk)
         os.unlink(tmp_file_path)


Test patch:
diff --git a/bodhi/tests/server/base.py b/bodhi/tests/server/base.py
--- a/bodhi/tests/server/base.py
+++ b/bodhi/tests/server/base.py
@@ -435,4 +435,4 @@ def mkmetadatadir(path, updateinfo=None, comps=None, source=False):
     subprocess.check_call(createrepo_command)
     if updateinfo is not False:
         metadata.insert_in_repo(createrepo_c.XZ, os.path.join(path, 'repodata'), 'updateinfo',
-                                'xml', os.path.join(path, 'updateinfo.xml'))
+                                'xml', os.path.join(path, 'updateinfo.xml'), True)
diff --git a/bodhi/tests/server/test_metadata.py b/bodhi/tests/server/test_metadata.py
--- a/bodhi/tests/server/test_metadata.py
+++ b/bodhi/tests/server/test_metadata.py
@@ -299,6 +299,7 @@ def test___init___uses_bz2_for_epel(self):
         md = UpdateInfoMetadata(epel_7, UpdateRequest.stable, self.db, self.tempdir)
 
         self.assertEqual(md.comp_type, createrepo_c.BZ2)
+        self.assertFalse(md.zchunk)
 
     def test___init___uses_xz_for_fedora(self):
         """Assert that the __init__() method sets the comp_type attribute to cr.XZ for Fedora."""
@@ -307,6 +308,7 @@ def test___init___uses_xz_for_fedora(self):
         md = UpdateInfoMetadata(fedora, UpdateRequest.stable, self.db, self.tempdir)
 
         self.assertEqual(md.comp_type, createrepo_c.XZ)
+        self.assertTrue(md.zchunk)
 
     def test_extended_metadata_once(self):
         """Assert that a single call to update the metadata works as expected."""
@@ -405,7 +407,7 @@ def test_zchunk_metadata_coverage_xz_compression(self, mock_cr):
         mock_cr.Repomd = mock.MagicMock(return_value=mock_repomd)
 
         bodhi_metadata.insert_in_repo(bodhi_metadata.cr.XZ_COMPRESSION, self.tempcompdir,
-                                      'garbage', 'zck', '/dev/null')
+                                      'garbage', 'zck', '/dev/null', True)
 
         mock_cr.Repomd.assert_called_once_with(os.path.join(self.tempcompdir, 'repomd.xml'))
         self.assertEqual(
@@ -450,7 +452,7 @@ def test_zchunk_metadata_coverage_zchunk_skipped(self, mock_cr):
         mock_repomd.xml_dump = mock.MagicMock(return_value="test data")
         mock_cr.Repomd = mock.MagicMock(return_value=mock_repomd)
 
-        bodhi_metadata.insert_in_repo(99, self.tempcompdir, 'garbage', 'zck', '/dev/null')
+        bodhi_metadata.insert_in_repo(99, self.tempcompdir, 'garbage', 'zck', '/dev/null', True)
 
         mock_cr.Repomd.assert_called_once_with(os.path.join(self.tempcompdir, 'repomd.xml'))
         mock_cr.RepomdRecord.assert_called_once_with('garbage',
@@ -489,7 +491,7 @@ def test_zchunk_metadata_coverage_zchunk_unsupported(self, mock_cr):
         mock_cr.Repomd = mock.MagicMock(return_value=mock_repomd)
 
         bodhi_metadata.insert_in_repo(bodhi_metadata.cr.XZ_COMPRESSION, self.tempcompdir,
-                                      'garbage', 'xz', '/dev/null')
+                                      'garbage', 'xz', '/dev/null', True)
 
         mock_cr.Repomd.assert_called_once_with(os.path.join(self.tempcompdir, 'repomd.xml'))
         mock_cr.RepomdRecord.assert_called_once_with('garbage',