Repository: fedora-infra/bodhi

Issue:
bodhi is trying to push updates which have been gargbage collected in koji
https://bodhi.fedoraproject.org/updates/python-pluggy-0.3.0-2.fc23

http://koji.fedoraproject.org/koji/packageinfo?packageID=20838

When signing F-23 updates for push I get an error. When I investigate the NVR it's been garbage collected in koji

http://koji.fedoraproject.org/koji/buildinfo?buildID=680005

$ for i in 23 22 ; do NSS_HASH_ALG_SUPPORT=+MD5 ~/releng/scripts/sigulsign_unsigned.py fedora-$i -v --write-all --sigul-batch-size=1 $(cat /var/cache/sigul/{Stable,Testing}-F${i}); done
Passphrase for fedora-23:
2016-01-15 04:11:22,149 INFO: Setting up koji session
2016-01-15 04:11:23,440 INFO: Getting builds from arguments
2016-01-15 04:11:23,440 INFO: Got 92 builds
2016-01-15 04:11:23,440 INFO: Getting build IDs from Koji
2016-01-15 04:11:29,839 INFO: Getting rpms from each build
2016-01-15 04:11:35,346 INFO: Found 845 rpms
2016-01-15 04:11:35,346 INFO: Checking for unsigned rpms in koji
2016-01-15 04:11:42,768 INFO: Signing rpms via sigul
2016-01-15 04:11:42,768 INFO: Signing batch 1/11 with 1 rpms
2016-01-15 04:11:45,771 INFO: Signing batch 2/11 with 1 rpms
2016-01-15 04:11:49,889 INFO: Signing batch 3/11 with 1 rpms
2016-01-15 04:11:52,685 INFO: Signing batch 4/11 with 1 rpms
2016-01-15 04:11:55,366 INFO: Signing batch 5/11 with 1 rpms
2016-01-15 04:11:58,370 INFO: Signing batch 6/11 with 1 rpms
2016-01-15 04:12:01,686 INFO: Signing batch 7/11 with 1 rpms
2016-01-15 04:12:04,430 INFO: Signing batch 8/11 with 1 rpms
2016-01-15 04:12:06,502 ERROR: Error signing ['python-pluggy-0.3.0-2.fc23.noarch.rpm']
2016-01-15 04:12:06,503 INFO: Signing batch 9/11 with 1 rpms
2016-01-15 04:12:10,009 INFO: Signing batch 10/11 with 1 rpms
2016-01-15 04:12:13,296 INFO: Signing batch 11/11 with 1 rpms
2016-01-15 04:12:16,686 ERROR: Error signing ['python-pluggy-0.3.0-2.fc23.src.rpm']
2016-01-15 04:12:16,686 INFO: Calling koji to write 845 rpms
2016-01-15 04:12:22,847 ERROR: Error writing out python-pluggy-0.3.0-2.fc23.noarch.rpm
2016-01-15 04:12:22,847 ERROR:     GenericError: No such path: /mnt/koji/packages/python-pluggy/0.3.0/2.fc23/noarch/python-pluggy-0.3.0-2.fc23.noarch.rpm

2016-01-15 04:12:28,328 ERROR: Error writing out python-pluggy-0.3.0-2.fc23.src.rpm
2016-01-15 04:12:28,328 ERROR:     GenericError: No such path: /mnt/koji/packages/python-pluggy/0.3.0/2.fc23/src/python-pluggy-0.3.0-2.fc23.src.rpm

2016-01-15 04:12:30,276 ERROR: Errors during Signing:
2016-01-15 04:12:30,276 ERROR:      python-pluggy-0.3.0-2.fc23.noarch.rpm
2016-01-15 04:12:30,276 ERROR:      python-pluggy-0.3.0-2.fc23.src.rpm



Discussion:
I fixed this by hand for now.

The maintainer submitted an older version to stable for some reason. We should implement checks for this to ensure it cannot happen.


Resolving patch:
diff --git a/bodhi/services/updates.py b/bodhi/services/updates.py
--- a/bodhi/services/updates.py
+++ b/bodhi/services/updates.py
@@ -39,6 +39,7 @@
     validate_update_id,
     validate_requirements,
     validate_bugs,
+    validate_request,
 )
 
 
@@ -104,6 +105,7 @@ def get_update_for_editing(request):
                          validate_update_id,
                          validate_build_tags,
                          validate_acls,
+                         validate_request,
                      ),
                      permission='edit', renderer='json',
                      error_handler=bodhi.services.errors.json_handler)
diff --git a/bodhi/validators.py b/bodhi/validators.py
--- a/bodhi/validators.py
+++ b/bodhi/validators.py
@@ -12,11 +12,12 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
+import rpm
 import koji
 
 from datetime import datetime, timedelta
 
-from sqlalchemy.sql import or_
+from sqlalchemy.sql import or_, and_
 from pyramid.exceptions import HTTPNotFound, HTTPBadRequest
 from pyramid.httpexceptions import HTTPFound
 import pyramid.threadlocal
@@ -921,3 +922,42 @@ def validate_requirements(request):
                     requirement, ", ".join(valid_requirements)))
             request.errors.status = HTTPBadRequest.code
             return
+
+
+def validate_request(request):
+    """
+    Ensure that this update is newer than whatever is in the requested state
+    """
+    log.debug('validating request')
+    update = request.validated['update']
+    db = request.db
+
+    if 'request' not in request.validated:
+        # Invalid request. Let the colander error from our schemas.py bubble up.
+        return
+    if request.validated['request'] is UpdateRequest.stable:
+        target = UpdateStatus.stable
+    elif request.validated['request'] is UpdateRequest.testing:
+        target = UpdateStatus.testing
+    else:
+        # obsolete, unpush, revoke...
+        return
+
+    for build in update.builds:
+        nvr = get_nvr(build.nvr)
+        for other_build in db.query(Build).join(Update).filter(
+                and_(Build.package==build.package,
+                     Build.nvr != build.nvr,
+                     Update.status == target,
+                     Update.release == update.release),
+                ).all():
+
+            log.info('Checking against %s' % other_build.nvr)
+
+            if rpm.labelCompare(get_nvr(other_build.nvr), nvr) > 0:
+                log.debug('%s is older than %s', build.nvr, other_build.nvr)
+                request.errors.add('querystring', 'update',
+                        'Cannot submit %s to %s since it is older than %s' %
+                        (build.nvr, target.description, other_build.nvr))
+                request.errors.status = HTTPBadRequest.code
+                return


Test patch:
diff --git a/bodhi/tests/functional/test_updates.py b/bodhi/tests/functional/test_updates.py
--- a/bodhi/tests/functional/test_updates.py
+++ b/bodhi/tests/functional/test_updates.py
@@ -38,7 +38,9 @@
     UpdateRequest,
     Release,
     ReleaseState,
-    BuildrootOverride
+    BuildrootOverride,
+    UpdateStatus,
+    UpdateType,
 )
 
 YEAR = time.localtime().tm_year
@@ -2405,3 +2407,36 @@ def test_edit_update_with_expired_override(self, publish, *args):
         r = self.app.post_json('/updates/', args)
         up = r.json_body
         self.assertEquals(up['title'], nvr)
+
+    @mock.patch(**mock_taskotron_results)
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.notifications.publish')
+    def test_submit_older_build_to_stable(self, publish, *args):
+        """
+        Ensure we cannot submit an older build to stable when a newer one
+        already exists there.
+        """
+        update = self.db.query(Update).one()
+        update.status = UpdateStatus.stable
+        update.request = None
+        self.db.flush()
+
+        oldbuild = 'bodhi-1.0-1.fc17'
+
+        # Create a newer build
+        build = Build(nvr=oldbuild, package=update.builds[0].package)
+        self.db.add(build)
+        update = Update(title=oldbuild, builds=[build], type=UpdateType.bugfix,
+                        request=UpdateRequest.testing, notes=u'second update',
+                        user=update.user, release=update.release)
+        update.karma = 3
+        self.db.add(update)
+        self.db.flush()
+
+        # Try and submit an older build to stable
+        resp = self.app.post_json('/updates/%s/request' % oldbuild,
+                {'request': 'stable', 'csrf_token': self.get_csrf_token()},
+                status=400)
+        eq_(resp.json['status'], 'error')
+        eq_(resp.json['errors'][0]['description'],
+            'Cannot submit bodhi-1.0-1.fc17 to stable since it is older than bodhi-2.0-1.fc17')