bodhi/config/app.cfg | 6 ++ bodhi/model.py | 42 +++++++++++++++++-- bodhi/templates/show.kid | 6 +- bodhi/tests/test_controllers.py | 85 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 124 insertions(+), 15 deletions(-)
New commits: commit 4a9336f4b911d52843b6e6ee3f86b4fc31c39a75 Author: Luke Macken lmacken@redhat.com Date: Wed Jan 20 10:37:51 2010 -0500
Require one admin +1, and a minimum karma of +2 before pushing critical path packages to pending releases.
diff --git a/bodhi/config/app.cfg b/bodhi/config/app.cfg index 2c94f87..317d7b5 100644 --- a/bodhi/config/app.cfg +++ b/bodhi/config/app.cfg @@ -74,7 +74,7 @@ release_team_address = None security_team = None
# Superuser groups -admin_groups = 'releng security_respons cvsadmin' +admin_groups = 'releng security_respons cvsadmin qa'
# Where do we send update announcements to ? # These variables should be named per: Release.prefix_id.lower()_announce_list @@ -155,6 +155,10 @@ tgcaptcha.key = 'Y`h`f&s}TZz' ## http://kojipkgs.fedoraproject.org/mash/rawhide-20100107/logs/critpath.txt critpath = "ConsoleKit ConsoleKit-libs ConsoleKit-x11 DeviceKit-power GConf2 GConf2-gtk MAKEDEV ModemManager NetworkManager NetworkManager-glib ORBit2 acl alsa-lib anaconda anaconda-yum-plugins at-spi atk attr audit audit-libs authconfig authconfig-gtk avahi avahi-autoipd avahi-glib basesystem bash binutils bluecurve-cursor-theme bzip2 bzip2-libs c-ares ca-certificates cairo checkpolicy chkconfig cloog-ppl compat-db47 comps-extras constantine-backgrounds constantine-backgrounds-single control-center control-center-filesystem coreutils coreutils-libs cpio cpp cracklib cracklib-dicts cracklib-python createrepo cronie cronie-anacron crontabs cryptsetup-luks cryptsetup-luks-libs cups-libs curl cyrus-sasl cyrus-sasl-lib dash db4 db4-utils dbus dbus-glib dbus-libs dbus-python dbus-x11 deltarpm desktop-backgrounds-basic desktop-file-utils device-mapper device-mapper-event device-mapper-event-libs device-mapper-libs dhclient diffutils dmidecode dmraid dmraid-events dnsmasq dosfstools dracut e2fsprogs e2fsprogs-libs efibootmgr eggdbus elfutils elfutils-libelf elfutils-libs ethtool evolution-data-server expat fedora-gnome-theme fedora-icon-theme fedora-logos fedora-release fedora-setup-keyboard file file-libs filesystem findutils fipscheck fipscheck-lib firstboot flac fontconfig freetype gamin gawk gcc gcc-c++ gdbm gdm genisoimage glib2 glibc glibc-common glibc-devel glibc-headers gmp gnome-desktop gnome-icon-theme gnome-keyring gnome-keyring-pam gnome-menus gnome-panel-libs gnome-python2 gnome-python2-canvas gnome-python2-gnome gnome-python2-gnomevfs gnome-session gnome-settings-daemon gnome-themes gnome-vfs2 gnupg2 gnutls gpgme grep grub grubby gstreamer gstreamer-tools gtk2 gtk2-engines gzip hal hal-info hal-libs hdparm hesiod hicolor-icon-theme hostname hwdata info initscripts iptables iptables-ipv6 iputils iscsi-initiator-utils iso-codes isomd5sum jasper-libs kbd kernel kernel-headers keyutils-libs koji kpartx krb5-libs less libICE libIDL libSM libX11 libX11-common libXScrnSaver libXau libXcomposite libXcursor libXdamage libXdmcp libXext libXfixes libXfont libXft libXi libXinerama libXmu libXrandr libXrender libXres libXt libXtst libXv libXvMC libXxf86misc libXxf86vm libacl libart_lgpl libasyncns libattr libblkid libbonobo libbonoboui libcanberra libcanberra-gtk2 libcap libcap-ng libcom_err libcroco libcurl libdaemon libdrm libedit libffi libfontenc libgail-gnome libgcc libgcrypt libglade2 libgnome libgnomecanvas libgnomekbd libgnomeui libgomp libgpg-error libgsf libgudev1 libgweather libical libidn libjpeg libmcpp libnl libnotify libogg libpcap libpciaccess libpng libproxy libproxy-bin libproxy-python librsvg2 libselinux libselinux-python libselinux-utils libsemanage libsepol libsndfile libsoup libss libssh2 libstdc++ libstdc++-devel libtasn1 libtdb libthai libtiff libtool-ltdl libudev libusb libuser libuser-python libutempter libuuid libvorbis libwnck libx86 libxcb libxkbfile libxklavier libxml2 libxml2-python libxslt linux-firmware livecd-tools logrotate lua lvm2 lvm2-libs lzma lzma-libs m4 makebootfat mash mcpp mdadm metacity mingetty mobile-broadband-provider-info module-init-tools mpfr mtools mysql-libs nash ncurses ncurses-base ncurses-libs net-tools newt newt-python notification-daemon notification-daemon-engine-slider nspr nss nss-softokn nss-softokn-freebl nss-sysinit nss-util ntp ntpdate openldap openssh openssh-clients openssh-server openssl pam pango parted passwd patch pciutils-libs pcre perl perl-Crypt-PasswdMD5 perl-Digest-SHA1 perl-Module-Pluggable perl-Pod-Escapes perl-Pod-Simple perl-libs perl-version pinentry pixman pkgconfig plymouth plymouth-gdm-hooks plymouth-libs plymouth-scripts plymouth-utils pm-utils policycoreutils polkit polkit-desktop-policy polkit-gnome popt postfix ppl ppp procmail procps psmisc pth pulseaudio-gdm-hooks pulseaudio-libs pulseaudio-libs-glib2 pungi pyOpenSSL pycairo pygobject2 pygpgme pygtk2 pygtk2-libglade pykickstart pyparted python python-bugzilla python-cryptsetup python-decorator python-deltarpm python-ethtool python-imgcreate python-iniparse python-kid python-krbV python-libs python-meh python-nss python-pyblock python-pycurl python-slip python-urlgrabber pyxf86config radeontool rarian rarian-compat readline redhat-menus redhat-rpm-config repoview rootfiles rpm rpm-build rpm-libs rpm-python rsyslog sed selinux-policy selinux-policy-targeted sendmail setserial setup setuptool sgml-common sgpio shadow-utils shared-mime-info slang sound-theme-freedesktop sqlite squashfs-tools startup-notification sudo sysklogd syslinux system-config-date system-config-firewall-base system-config-keyboard system-config-users sysvinit-tools tar tcp_wrappers-libs tzdata udev unique unzip upstart usermode usermode-gtk ustr util-linux-ng vbetool vim-minimal which wpa_supplicant xcb-util xdg-utils xkeyboard-config xml-common xorg-x11-drivers xorg-x11-drv-acecad xorg-x11-drv-aiptek xorg-x11-drv-apm xorg-x11-drv-ast xorg-x11-drv-ati xorg-x11-drv-cirrus xorg-x11-drv-dummy xorg-x11-drv-elographics xorg-x11-drv-evdev xorg-x11-drv-fbdev xorg-x11-drv-fpit xorg-x11-drv-geode xorg-x11-drv-glint xorg-x11-drv-hyperpen xorg-x11-drv-i128 xorg-x11-drv-i740 xorg-x11-drv-intel xorg-x11-drv-keyboard xorg-x11-drv-mach64 xorg-x11-drv-mga xorg-x11-drv-mouse xorg-x11-drv-mutouch xorg-x11-drv-neomagic xorg-x11-drv-nouveau xorg-x11-drv-nv xorg-x11-drv-openchrome xorg-x11-drv-penmount xorg-x11-drv-r128 xorg-x11-drv-rendition xorg-x11-drv-s3virge xorg-x11-drv-savage xorg-x11-drv-siliconmotion xorg-x11-drv-sis xorg-x11-drv-sisusb xorg-x11-drv-synaptics xorg-x11-drv-tdfx xorg-x11-drv-trident xorg-x11-drv-v4l xorg-x11-drv-vesa xorg-x11-drv-vmmouse xorg-x11-drv-vmware xorg-x11-drv-void xorg-x11-drv-voodoo xorg-x11-drv-wacom xorg-x11-server-Xorg xorg-x11-server-common xorg-x11-server-utils xorg-x11-xauth xorg-x11-xinit xorg-x11-xkb-utils xz xz-libs yum yum-metadata-parser yum-utils zenity zlib"
+# The number of admin approvals it takes to be able to push a critical path +# update to stable for a pending release. +critpath.num_admin_approvals = 1 +critpath.min_karma = 2
# The commented out values below are the defaults
diff --git a/bodhi/model.py b/bodhi/model.py index 3428379..e25ab34 100644 --- a/bodhi/model.py +++ b/bodhi/model.py @@ -410,10 +410,14 @@ class PackageUpdate(SQLObject): # [No Frozen Rawhide] Disable pushing critical path updates for # pending releases directly to stable. if action == 'stable' and self.release.locked and self.critpath: - if ('releng' in identity.current.groups or - 'qa' in identity.current.groups): - self.comment('Critical path update approved by %s' % - identity.current.user_name, author='bodhi') + admin_groups = config.get('admin_groups', 'qa releng').split() + for group in identity.current.groups: + if group in admin_groups: + if not self.critpath_approved: + action = 'testing' + self.comment('Critical path update approved', + author=identity.current.user_name) + break else: log.info('Forcing critical path update into testing') action = 'testing' @@ -683,6 +687,14 @@ class PackageUpdate(SQLObject): as stable. If it reaches the 'unstable_karma', it is unpushed. """ if not author: author = identity.current.user_name + + # Hack: Add admin groups to usernames (eg: "lmacken (releng)") + admin_groups = config.get('admin_groups', 'releng qa security_respons').split() + for group in identity.current.groups: + if group in admin_groups: + author += ' (%s)' % group + break + if not anonymous and karma != 0 and \ not filter(lambda c: c.author == author and c.karma == karma, self.comments): @@ -706,6 +718,7 @@ class PackageUpdate(SQLObject): log.info("Automatically unpushing %s" % self.title) self.obsolete() mail.send(self.submitter, 'unstable', self) + Comment(text=text, karma=karma, update=self, author=author, anonymous=anonymous)
@@ -838,6 +851,27 @@ class PackageUpdate(SQLObject): break return critical
+ @property + def num_admin_approvals(self): + """ Return the number of Releng/QA approvals of this update """ + approvals = 0 + for comment in self.comments: + # FIXME: + # We need to actually store the groups or approvals sanely. + # Hack, to get this working for F13 w/o changing the DB + if comment.author.endswith(')'): + group = comment.author[:-1].split('(')[-1] + if group in ('qa', 'releng'): + approvals += 1 + return approvals + + @property + def critpath_approved(self): + """ Return whether or not this critpath update has been approved """ + return self.num_admin_approvals >= config.get( + 'critpath.num_admin_approvals', 2) and \ + self.karma >= config.get('critpath.min_karma', 2) +
class Comment(SQLObject): timestamp = DateTimeCol(default=datetime.utcnow) diff --git a/bodhi/templates/show.kid b/bodhi/templates/show.kid index 2fa903b..f7c63cd 100644 --- a/bodhi/templates/show.kid +++ b/bodhi/templates/show.kid @@ -71,7 +71,7 @@ karma = "<img src="%s" align="top" /> <b>%d</b>" % (tg.url('/static/images/k Push to Testing </a> </td> - <span py:if="update.release.locked and update.critpath"> + <span py:if="update.release.locked and update.critpath and update.critpath_approved"> <span py:if="'qa' in tg.identity.groups or 'releng' in tg.identity.groups"> <td> <a href="${util.url('/request/stable/%s' % update.title)}" class="list"> @@ -81,7 +81,7 @@ karma = "<img src="%s" align="top" /> <b>%d</b>" % (tg.url('/static/images/k </td> </span> </span> - <span py:if="update.release.locked and not update.critpath"> + <span py:if="update.release.locked and not update.critpath and update.critpath_approved"> <td> <a href="${util.url('/request/stable/%s' % update.title)}" class="list"> <img src="${tg.url('/static/images/submit.png')}" border="0"/> @@ -120,7 +120,7 @@ karma = "<img src="%s" align="top" /> <b>%d</b>" % (tg.url('/static/images/k </td> <span py:if="update.status == 'testing'"> <span py:if="update.request == None"> - <span py:if="update.release.locked and update.critpath"> + <span py:if="update.release.locked and update.critpath and update.critpath_approved"> <span py:if="'qa' in tg.identity.groups or 'releng' in tg.identity.groups"> <td> <a href="${util.url('/request/stable/%s' % update.title)}" class="list"> diff --git a/bodhi/tests/test_controllers.py b/bodhi/tests/test_controllers.py index 4ba206e..de6755e 100644 --- a/bodhi/tests/test_controllers.py +++ b/bodhi/tests/test_controllers.py @@ -1201,7 +1201,6 @@ class TestControllers(testutil.DBTest): assert 'TurboGears-1.0.2.2-2.fc7' in json assert json['TurboGears-1.0.2.2-2.fc7']['notes'] == 'foobar'
- def test_updating_build_during_edit(self): session = login() create_release() @@ -1366,7 +1365,7 @@ class TestControllers(testutil.DBTest): } self.save_update(params, session) update = PackageUpdate.byTitle(params['builds']) - assert update.request == 'stable' + assert update.request == 'testing'
def test_critpath_to_frozen_release_available_actions(self): """ @@ -1398,11 +1397,12 @@ class TestControllers(testutil.DBTest): update = PackageUpdate.byTitle(params['builds']) assert update.request == 'testing'
- def test_critpath_to_frozen_release_available_actions_for_releng(self): + def test_critpath_to_pending_release_num_approved_comments(self): """ Ensure releng/qa can push critpath updates to stable for pending releases + after 1 releng/qa karma, and 1 other karma """ - session = login(group='releng') + releng = login(group='releng') create_release(locked=True) params = { 'builds' : 'kernel-2.6.31-1.fc7', @@ -1414,14 +1414,62 @@ class TestControllers(testutil.DBTest): 'request': None, 'unstable_karma' : -1, } - self.save_update(params, session) + self.save_update(params, releng) update = PackageUpdate.byTitle(params['builds']) testutil.create_request('/updates/%s' % params['builds'], - method='GET', headers=session) + method='GET', headers=releng)
+ # Ensure releng/QA can't push critpath updates alone assert "Push to Testing" in cherrypy.response.body[0] + assert "Push Critical Path update to Stable" not in cherrypy.response.body[0] + + # Have a developer +1 the update + developer = login(username='bob') + testutil.create_request('/updates/comment?text=foobar&title=%s&karma=1' % + params['builds'], method='POST', headers=developer) + testutil.create_request('/updates/%s' % params['builds'], + method='GET', headers=developer) + assert "Push Critical Path update to Stable" not in cherrypy.response.body[0] + update = PackageUpdate.byTitle(params['builds']) + assert not update.request + assert len(update.comments) == 1 + assert update.comments[0].author == 'bob' + + # Have another developer +1 it, so it gets up to +2 + # Ensure we can't push it to stable, until we get admin approval + testutil.create_request('/updates/comment?text=foobar&title=%s&karma=1' % + params['builds'], method='POST', + headers=login(username='foobar')) + testutil.create_request('/updates/%s' % params['builds'], + method='GET', headers=login(username='foobar')) + assert "Push Critical Path update to Stable" not in cherrypy.response.body[0] + update = PackageUpdate.byTitle(params['builds']) + assert not update.request + assert update.karma == 2 + + # Have releng try again, and ensure it can be pushed to stable + testutil.create_request('/updates/comment?text=foobar&title=%s&karma=1' % + params['builds'], method='POST', headers=releng) + update = PackageUpdate.byTitle(params['builds']) + assert not update.request + + testutil.create_request('/updates/%s' % params['builds'], + method='GET', headers=developer) + assert "Push Critical Path update to Stable" not in cherrypy.response.body[0] + + testutil.create_request('/updates/%s' % params['builds'], + method='GET', headers=releng) + update = PackageUpdate.byTitle(params['builds']) + print update.comments assert "Push Critical Path update to Stable" in cherrypy.response.body[0]
+ testutil.create_request('/updates/request/stable/%s' % + params['builds'], method='POST', + headers=releng) + + update = PackageUpdate.byTitle(params['builds']) + assert update.request == 'stable' + def test_critpath_to_frozen_release_testing(self): """ Ensure devs can *not* push critpath updates directly to stable @@ -1506,7 +1554,30 @@ class TestControllers(testutil.DBTest): testutil.create_request('/updates/%s' % params['builds'], method='GET', headers=session)
- assert "Mark Critical Path update as Stable" in cherrypy.response.body[0] + assert "Mark Critical Path update as Stable" not in cherrypy.response.body[0] + + testutil.create_request('/updates/comment?text=foobar&title=%s&karma=1' % + params['builds'], method='POST', headers=session) + + testutil.create_request('/updates/%s' % params['builds'], + method='GET', headers=session) + + assert "Mark Critical Path update as Stable" not in cherrypy.response.body[0] + + testutil.create_request('/updates/comment?text=foobar&title=%s&karma=1' % + params['builds'], method='POST', + headers=login(username='bob')) + + update = PackageUpdate.byTitle(params['builds']) + assert len(update.comments) == 2 + assert update.comments[1].author == 'bob', update.comments + + testutil.create_request('/updates/%s' % params['builds'], + method='GET', headers=session) + update = PackageUpdate.byTitle(params['builds']) + assert update.comments[0].author == 'guest (qa)' + + assert "Mark Critical Path update as Stable" in cherrypy.response.body[0], cherrypy.response.body[0]
def test_created_since(self): session = login()