Repository: fedora-infra/bodhi

Issue:
Bugs should only be modified if they are Fedora/EPEL-only
Bodhi occasionally tries to update bugs it should not, such as this one:

https://bugzilla.redhat.com/show_bug.cgi?id=1383657#c6

This is a top-level CVE bug and its state should not be modified by Bodhi since it tracks the issue in products other than Fedora and EPEL. Bodhi could simply check that the component is not 'vulnerability' and product 'Security Response' and skip making any state changes if it is.



Discussion:
I agree that this is important from a "politeness" perspective. Bodhi could make sure that BZs that are referenced in updates reference Fedora or EPEL bugs at creation time, and could re-ensure those states before modifying the bugs (in case they changed since Bodhi last checked). I think it would be fairly straightforward to make such a change and would be much friendlier on our part towards Red Hat's Bugzilla instance ☺

I noticed a Fedora bug : https://bugzilla.redhat.com/show_bug.cgi?id=1463547 in a package that I maintain for RHEL. I therefore cloned the Fedora bug to RHEL to track the issue: https://bugzilla.redhat.com/show_bug.cgi?id=1463608 . Bodhi then commented to the RHEL bug about updates to Fedora packages, and even changed its state MODIFIED → ON_QA. Why does it happen? Is it because the latter bug depends on the former? Or because the latter is a clone of the former? How to prevent it? Is it an instance of this issue?
@pcahyna in [your dist-git change](https://src.fedoraproject.org/rpms/libtommath/c/baf704e7cfb469cf3eef392966489ef9fb0ad66f?branch=master) you added the following %changelog entry:

```
 * Sun Jun 25 2017 Simone Caronni <negativo17@gmail.com> - 1.0-6
 - Update URL (#1463608, #1463547).
```

The Bodhi client that `fedpkg update` uses will simply parse the latest %changelog entry and add all of those BZs to the Bodhi update.

I wonder if we should implement product filtering on the client side (when `fedpkg update` is constructing the Bodhi update template file) and on the server side as well.
@ktdreyer Thanks for the explanation! I did not thought about the %changelog (and it was not my dist-git change). I think product filtering is the right thing.
@bowlofeggs I can work on this.
In fact, it seems the `Bugzilla.modified()` method already checks if `bug.product` is in `bz_products` list, am I wrong to suppose that we only need to add the same check to the other methods? (what does the bz_products list include? The default in the config file is empty...)

About the check at creation time, I don't think that would be necessary: from what I can see, the javascript that provides the bug list associated with a package gets data from apps.fedoraproject.org and only proposes Fedora and EPEL bugs.
@mattiaverga Ah interesting, I'd never noticed that Bodhi did avoid messing with bugs with modified, but did not do so for other states. I think you might be right - we should just make it so Bodhi doesn't alter bugs in other states too (like the check in modified).

One question I am unsure about is whether Bodhi should comment on these updates (without changing states), or whether it really should just do nothing at all. I think I lean towards it doing nothing at all, but I welcome feedback from others.

Fedora's production instance does [configure](https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/bodhi2/base/templates/production.ini.j2?id=39acdd1e6126b6a31865960a173fe27d81a95c3f#n464) the ```bz_products``` setting to be non-empty:

```
bz_products = Fedora,Fedora EPEL
```

Thus I think we simply need to make it so that the other methods also leave those BZs alone, and to add some tests to make sure those changes work.

Thanks for the offer to help!
IMHO commenting alone is harmless & maybe helpful.

Resolving patch:
diff --git a/bodhi/server/bugs.py b/bodhi/server/bugs.py
--- a/bodhi/server/bugs.py
+++ b/bodhi/server/bugs.py
@@ -167,6 +167,9 @@ def on_qa(self, bug_id, comment):
         """
         Change the status of this bug to ON_QA if it is not already ON_QA, VERIFIED, or CLOSED.
 
+        This method will only operate on bugs that are associated with products listed
+        in the bz_products setting.
+
         This will also comment on the bug with some details on how to test and provide feedback for
         this update.
 
@@ -176,6 +179,9 @@ def on_qa(self, bug_id, comment):
         """
         try:
             bug = self.bz.getbug(bug_id)
+            if bug.product not in config.get('bz_products'):
+                log.info("Skipping set on_qa on {0!r} bug #{1}".format(bug.product, bug_id))
+                return
             if bug.bug_status not in ('ON_QA', 'VERIFIED', 'CLOSED'):
                 log.debug("Setting Bug #%d to ON_QA" % bug_id)
                 bug.setstatus('ON_QA', comment=comment)
@@ -188,6 +194,9 @@ def close(self, bug_id, versions, comment):
         """
         Close the bug given by bug_id, mark it as fixed in the given versions, and add a comment.
 
+        This method will only operate on bugs that are associated with products listed
+        in the bz_products setting.
+
         Args:
             bug_id (int): The ID of the bug you wish to close.
             versions (dict): A mapping of package names to nvrs of those packages that close the
@@ -197,6 +206,9 @@ def close(self, bug_id, versions, comment):
         args = {'comment': comment}
         try:
             bug = self.bz.getbug(bug_id)
+            if bug.product not in config.get('bz_products'):
+                log.info("Skipping set closed on {0!r} bug #{1}".format(bug.product, bug_id))
+                return
             # If this bug is for one of these builds...
             if bug.component in versions:
                 version = versions[bug.component]
@@ -253,9 +265,10 @@ def update_details(self, bug, bug_entity):
 
     def modified(self, bug_id):
         """
-        Mark the given bug as MODIFIED.
+        Change the status of this bug to MODIFIED if not already MODIFIED, VERIFIED, or CLOSED.
 
-        The bug will only be marked MODIFIED if it is not already MODIFIED, VERIFIED, or CLOSED.
+        This method will only operate on bugs that are associated with products listed
+        in the bz_products setting.
 
         Args:
             bug_id (basestring or int): The bug you wish to mark MODIFIED.
@@ -263,7 +276,7 @@ def modified(self, bug_id):
         try:
             bug = self.bz.getbug(bug_id)
             if bug.product not in config.get('bz_products'):
-                log.info("Skipping %r bug" % bug.product)
+                log.info("Skipping set modified on {0!r} bug #{1}".format(bug.product, bug_id))
                 return
             if bug.bug_status not in ('MODIFIED', 'VERIFIED', 'CLOSED'):
                 log.info('Setting bug #%d status to MODIFIED' % bug_id)


Test patch:
diff --git a/bodhi/tests/server/test_bugs.py b/bodhi/tests/server/test_bugs.py
--- a/bodhi/tests/server/test_bugs.py
+++ b/bodhi/tests/server/test_bugs.py
@@ -89,10 +89,12 @@ def test_bz_with__bz_set(self, _connect):
         self.assertEqual(_connect.call_count, 0)
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_close_fault(self, exception):
         """Assert that an xmlrpc Fault is caught and logged by close()."""
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
+        bz._bz.getbug.return_value.product = 'aproduct'
         bz._bz.getbug.return_value.close.side_effect = xmlrpc_client.Fault(
             410, 'You must log in before using this part of Red Hat Bugzilla.')
 
@@ -102,11 +104,13 @@ def test_close_fault(self, exception):
         exception.assert_called_once_with('Unable to close bug #12345')
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_close_successful(self, exception):
         """Test the close() method with a success case."""
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
         bz._bz.getbug.return_value.component = 'bodhi'
+        bz._bz.getbug.return_value.product = 'aproduct'
 
         bz.close(12345, {'bodhi': 'bodhi-3.1.0-1.fc27'},
                  'Fixed. Closing bug and adding version to fixed_in field.')
@@ -119,11 +123,13 @@ def test_close_successful(self, exception):
         self.assertEqual(exception.call_count, 0)
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_close_fixedin_maxlength(self, exception):
         """Test the close() method when fixed_in field may go over 255 chars."""
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
         bz._bz.getbug.return_value.component = 'bodhi'
+        bz._bz.getbug.return_value.product = 'aproduct'
         fill_text = ' '.join([u'exactly-10', ] * 23)
         bz._bz.getbug.return_value.fixed_in = fill_text
 
@@ -137,11 +143,13 @@ def test_close_fixedin_maxlength(self, exception):
         self.assertEqual(exception.call_count, 0)
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_close_pre_existing_fixedin(self, exception):
         """Test the close() method at the edge of the allowed size of the fixedin field (254)."""
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
         bz._bz.getbug.return_value.component = 'bodhi'
+        bz._bz.getbug.return_value.product = 'aproduct'
         fill_text = ' '.join([u'exactly-10', ] * 21)
         bz._bz.getbug.return_value.fixed_in = fill_text
 
@@ -157,6 +165,20 @@ def test_close_pre_existing_fixedin(self, exception):
             fixedin=expected_fixedin)
         self.assertEqual(exception.call_count, 0)
 
+    @mock.patch('bodhi.server.bugs.log.info')
+    def test_close_product_skipped(self, info):
+        """Test the close() method when the bug's product is not in the bz_products config."""
+        bz = bugs.Bugzilla()
+        bz._bz = mock.MagicMock()
+        bz._bz.getbug.return_value.product = 'not fedora!'
+
+        bz.close(12345, {'bodhi': 'bodhi-35.103.109-1.fc27'},
+                 'Fixed. Closing bug and adding version to fixed_in field.')
+
+        bz._bz.getbug.assert_called_once_with(12345)
+        info.assert_called_once_with("Skipping set closed on 'not fedora!' bug #12345")
+        self.assertEqual(bz._bz.getbug.return_value.setstatus.call_count, 0)
+
     @mock.patch('bodhi.server.bugs.log.exception')
     def test_comment_successful(self, exception):
         """Test the comment() method with a success case."""
@@ -268,7 +290,7 @@ def test_modified_product_skipped(self, info):
         bz.modified(1411188)
 
         bz._bz.getbug.assert_called_once_with(1411188)
-        info.assert_called_once_with("Skipping 'not fedora!' bug")
+        info.assert_called_once_with("Skipping set modified on 'not fedora!' bug #1411188")
         self.assertEqual(bz._bz.getbug.return_value.setstatus.call_count, 0)
 
     @mock.patch('bodhi.server.bugs.log.exception')
@@ -337,12 +359,14 @@ def test_update_details_xmlrpc_fault(self, exception):
         exception.assert_called_once_with('Got fault from Bugzilla')
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_on_qa_failure(self, exception):
         """
         Test the on_qa() method with a failure case.
         """
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
+        bz._bz.getbug.return_value.product = 'aproduct'
         bz._bz.getbug.return_value.setstatus.side_effect = Exception(
             'You forgot to pay your oxygen bill. Your air supply will promptly be severed.')
 
@@ -354,12 +378,14 @@ def test_on_qa_failure(self, exception):
         exception.assert_called_once_with('Unable to alter bug #1411188')
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_on_qa_success(self, exception):
         """
         Test the on_qa() method with a success case.
         """
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
+        bz._bz.getbug.return_value.product = 'aproduct'
 
         bz.on_qa(1411188, 'A message.')
 
@@ -368,6 +394,7 @@ def test_on_qa_success(self, exception):
         self.assertEqual(exception.call_count, 0)
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_on_qa_skipped_because_closed(self, exception):
         """
         Test the on_qa() method when the bug is already CLOSED.
@@ -375,6 +402,7 @@ def test_on_qa_skipped_because_closed(self, exception):
         """
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
+        bz._bz.getbug.return_value.product = 'aproduct'
         bz._bz.getbug.return_value.bug_status = 'CLOSED'
 
         bz.on_qa(1411188, 'A message.')
@@ -385,6 +413,7 @@ def test_on_qa_skipped_because_closed(self, exception):
         self.assertEqual(exception.call_count, 0)
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_on_qa_skipped_because_verified(self, exception):
         """
         Test the on_qa() method when the bug is already VERIFIED.
@@ -392,6 +421,7 @@ def test_on_qa_skipped_because_verified(self, exception):
         """
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
+        bz._bz.getbug.return_value.product = 'aproduct'
         bz._bz.getbug.return_value.bug_status = 'VERIFIED'
 
         bz.on_qa(1411188, 'A message.')
@@ -402,6 +432,7 @@ def test_on_qa_skipped_because_verified(self, exception):
         self.assertEqual(exception.call_count, 0)
 
     @mock.patch('bodhi.server.bugs.log.exception')
+    @mock.patch.dict('bodhi.server.bugs.config', {'bz_products': 'aproduct'})
     def test_on_qa_skipped_because_already_set(self, exception):
         """
         Test the on_qa() method when the bug is already ON_QA.
@@ -409,6 +440,7 @@ def test_on_qa_skipped_because_already_set(self, exception):
         """
         bz = bugs.Bugzilla()
         bz._bz = mock.MagicMock()
+        bz._bz.getbug.return_value.product = 'aproduct'
         bz._bz.getbug.return_value.bug_status = 'ON_QA'
 
         bz.on_qa(1411188, 'A message.')
@@ -418,6 +450,19 @@ def test_on_qa_skipped_because_already_set(self, exception):
         bz._bz.getbug.return_value.setstatus.assert_not_called()
         self.assertEqual(exception.call_count, 0)
 
+    @mock.patch('bodhi.server.bugs.log.info')
+    def test_on_qa_product_skipped(self, info):
+        """Test the on_qa() method when the bug's product is not in the bz_products config."""
+        bz = bugs.Bugzilla()
+        bz._bz = mock.MagicMock()
+        bz._bz.getbug.return_value.product = 'not fedora!'
+
+        bz.on_qa(1411188, 'A message.')
+
+        bz._bz.getbug.assert_called_once_with(1411188)
+        info.assert_called_once_with("Skipping set on_qa on 'not fedora!' bug #1411188")
+        self.assertEqual(bz._bz.getbug.return_value.setstatus.call_count, 0)
+
 
 class TestFakeBugTracker(unittest.TestCase):
     """This test class contains tests for the FakeBugTracker class."""