You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

396 lines
15 KiB

#!/usr/bin/python
# Copyright (C) 2011, 2012 Canonical Ltd.
# Author: Martin Pitt <martin.pitt@canonical.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
'''Release a proposed stable release update.
Copy packages from -proposed to -updates, and optionally to -security and the
development release.
USAGE:
sru-release [-s] [-d] <release> <package> [<package> ...]
'''
from __future__ import print_function
from collections import defaultdict
from functools import partial
import optparse
import sys
import unittest
try:
from urllib.request import urlopen
except ImportError:
from urllib import urlopen
from launchpadlib.launchpad import Launchpad
# Each entry in this list is a list of source packages that are known
# to have inter-dependencies and must be released simultaneously.
# If possible, each list should be ordered such that earlier
# entries could be released slightly before subsequent entries.
RELEASE_TOGETHER_PACKAGE_GROUPS = [
['linux-hwe', 'linux-meta-hwe'],
['linux', 'linux-meta'],
['grub2', 'grub2-signed'],
['shim', 'shim-signed'],
]
MISSING_PACKAGES_FROM_GROUP = (
"The set of packages requested for release are listed as dangerous \n"
"to release without also releasing the following at the same time:\n"
" {missing}\n\n"
"For more information, see:\n"
" https://lists.ubuntu.com/archives/ubuntu-devel/2018-June/040380.html\n\n"
"To ignore this message, pass '--skip-package-group-check'.")
def check_package_sets(packages):
"""Return a re-ordered list of packages respecting the PACKAGE_SETS
defined above. If any packages are missing, raise error."""
# pkg2group is a dict where each key is a pkg in a group and value is the
# complete group.
pkg2group = {}
for pgroup in RELEASE_TOGETHER_PACKAGE_GROUPS:
for pkg in pgroup:
if pkg in pkg2group:
raise RuntimeError(
"Overlapping package groups. '%s' is in '%s' and '%s'." %
(pkg, pgroup, pkg2group[pkg]))
pkg2group[pkg] = pgroup
seen = set()
new_pkgs = []
for pkg in packages:
if pkg not in pkg2group:
add = [pkg]
else:
add = list(pkg2group[pkg])
new_pkgs.extend([a for a in add if a not in seen])
seen.update(add)
orig = set(packages)
new = set(new_pkgs)
if orig != new:
raise ValueError(
MISSING_PACKAGES_FROM_GROUP.format(
missing=' '.join(new.difference(orig))))
return new_pkgs
class CheckPackageSets(unittest.TestCase):
def test_expected_linux_order_fixed(self):
self.assertEqual(
['pkg1', 'linux', 'linux-meta', 'pkg2'],
check_package_sets(['pkg1', 'linux-meta', 'linux', 'pkg2']))
def test_raises_value_error_on_missing(self):
self.assertRaises(
ValueError, check_package_sets, ['pkg1', 'linux'])
def test_single_item_with_missing(self):
self.assertRaises(
ValueError, check_package_sets, ['linux'])
def test_single_item_without_missing(self):
self.assertEqual(
check_package_sets(['pkg1']), ['pkg1'])
def test_multiple_package_groups(self):
"""Just make sure that having multiple groups listed still errors."""
self.assertRaises(
ValueError, check_package_sets, ['pkg1', 'linux', 'grub2'])
def match_srubugs(options, changesfileurl):
'''match between bugs with verification- tag and bugs in changesfile'''
bugs = []
if changesfileurl is None:
return bugs
# Load changesfile
changelog = urlopen(changesfileurl)
bugnums = []
for l in changelog:
if l.startswith('Launchpad-Bugs-Fixed: '):
bugnums = l.split()[1:]
break
for b in bugnums:
if b in options.exclude_bug:
continue
try:
bugs.append(launchpad.bugs[int(b)])
except:
print('%s: bug %s does not exist or is not accessible' %
(changesfileurl, b))
return bugs
def update_sru_bug(bug, pkg):
'''Unsubscribe SRU team and comment on bug re: how to report regressions'''
m_subjects = [m.subject for m in bug.messages]
if 'Update Released' in m_subjects:
print('LP: #%s was not commented on' % bug.id)
return
sru_team = launchpad.people['ubuntu-sru']
bug.unsubscribe(person=sru_team)
text = ("The verification of the Stable Release Update for %s has "
"completed successfully and the package has now been released "
"to -updates. Subsequently, the Ubuntu Stable Release Updates "
"Team is being unsubscribed and will not receive messages "
"about this bug report. In the event that you encounter "
"a regression using the package from -updates please report "
"a new bug using ubuntu-bug and tag the bug report "
"regression-update so we can easily find any regressions." % pkg)
bug.newMessage(subject="Update Released", content=text)
bug.lp_save()
def get_versions(options, sourcename):
'''Get current package versions.
If options.pattern is True, return all versions for package names
matching options.pattern.
If options.pattern is False, only return one result.
Return map pkgname -> {'release': version, 'updates': version,
'proposed': version, 'changesfile': url_of_proposed_changes,
'published': proposed_date}
'''
versions = defaultdict(dict)
if options.esm:
pocket = 'Release'
else:
pocket = 'Proposed'
matches = src_archive.getPublishedSources(
source_name=sourcename, exact_match=not options.pattern,
status='Published', pocket=pocket, distro_series=series)
for match in matches:
# versions in all pockets
for pub in src_archive.getPublishedSources(
source_name=match.source_package_name, exact_match=True,
status='Published', distro_series=series):
key = pub.pocket.lower()
# special case for ESM ppas, which don't have pockets but need
# to be treated as -proposed
if options.esm and key == 'release':
key = 'proposed'
versions[pub.source_package_name][key] = (
pub.source_package_version)
if pocket in pub.pocket:
versions[pub.source_package_name]['changesfile'] = (
pub.changesFileUrl())
# When the destination archive differs from the source scan that too.
if dst_archive != src_archive:
for pub in dst_archive.getPublishedSources(
source_name=match.source_package_name, exact_match=True,
status='Published', distro_series=series):
key = 'security' # pub.pocket.lower()
versions[pub.source_package_name][key] = (
pub.source_package_version)
if pocket in pub.pocket:
versions[pub.source_package_name]['changesfile'] = (
pub.changesFileUrl())
# devel version
if devel_series:
for pub in src_archive.getPublishedSources(
source_name=match.source_package_name, exact_match=True,
status='Published', distro_series=devel_series):
if pub.pocket in ('Release', 'Proposed'):
versions[pub.source_package_name]['devel'] = (
pub.source_package_version)
else:
versions[match.source_package_name]['devel'] = None
return versions
def release_package(options, package):
'''Release a package.'''
pkg_versions_map = get_versions(options, package)
if not pkg_versions_map:
message = 'ERROR: No such package, ' + package + ', in -proposed, aborting\n'
sys.stderr.write(message)
sys.exit(1)
for pkg, versions in pkg_versions_map.iteritems():
print('--- Releasing %s ---' % pkg)
print('Proposed: %s' % versions['proposed'])
if 'security' in versions:
print('Security: %s' % versions['security'])
if 'updates' in versions:
print('Updates: %s' % versions['updates'])
else:
print('Release: %s' % versions.get('release'))
if options.devel and 'devel' in versions:
print('Devel: %s' % versions['devel'])
copy = partial(
dst_archive.copyPackage, from_archive=src_archive,
include_binaries=True, source_name=pkg,
version=versions['proposed'], auto_approve=True)
if options.devel:
if ('devel' not in versions or
versions['devel'] in (
versions.get('updates', 'notexisting'),
versions['release'])):
if not options.no_act:
copy(to_pocket='Proposed', to_series=devel_series.name)
print('Version in %s matches development series, '
'copied to %s-proposed' % (release, devel_series.name))
else:
print('ERROR: Version in %s does not match development '
'series, not copying' % release)
if options.no_act:
if options.release:
print('Would copy to %s' % release)
else:
print('Would copy to %s-updates' % release)
else:
if options.release:
# -proposed -> release
copy(to_pocket='Release', to_series=release)
print('Copied to %s' % release)
else:
# -proposed -> -updates
# only phasing updates for >=raring to start
if (release not in ('lucid', 'precise') and
package != 'linux' and
not package.startswith('linux-') and
not options.security):
copy(to_pocket='Updates', to_series=release,
phased_update_percentage=options.percentage)
else:
copy(to_pocket='Updates', to_series=release)
print('Copied to %s-updates' % release)
if not options.no_bugs:
sru_bugs = match_srubugs(options, versions['changesfile'])
tag = 'verification-needed-%s' % release
for sru_bug in sru_bugs:
if tag not in sru_bug.tags:
update_sru_bug(sru_bug, pkg)
# -proposed -> -security
if options.security:
if options.no_act:
print('Would copy to %s-security' % release)
else:
copy(to_pocket='Security', to_series=release)
print('Copied to %s-security' % release)
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == "run-tests":
sys.exit(unittest.main(argv=[sys.argv[0]] + sys.argv[2:]))
parser = optparse.OptionParser(
usage='usage: %prog [options] <release> <package> [<package> ...]')
parser.add_option(
'-l', '--launchpad', dest='launchpad_instance', default='production')
parser.add_option(
'--security', action='store_true', default=False,
help='Additionally copy to -security pocket')
parser.add_option(
'-d', '--devel', action='store_true', default=False,
help='Additionally copy to development release (only works if that '
'has the same version as <release>)')
parser.add_option(
'-r', '--release', action='store_true', default=False,
help='Copy to release pocket instead of -updates (useful for staging '
'uploads in development release)')
parser.add_option(
"-z", "--percentage", type="int", default=10,
metavar="PERCENTAGE", help="set phased update percentage")
parser.add_option(
'-n', '--no-act', action='store_true', default=False,
help='Only perform checks, but do not actually copy packages')
parser.add_option(
'-p', '--pattern', action='store_true', default=False,
help='Treat package names as patterns, not exact matches')
parser.add_option(
'--no-bugs', action='store_true', default=False,
help='Do not act on any bugs (helpful to avoid races).')
parser.add_option(
'--exclude-bug', action='append', default=[], metavar='BUG',
help='Do not update BUG.')
parser.add_option(
'-E', '--esm', action='store_true', default=False,
help='Copy from the kernel ESM proposed PPA to the ESM publication PPA')
parser.add_option(
'--skip-package-group-check', action='store_true', default=False,
help=('Skip the package set checks that require some packages '
'be released together'))
options, args = parser.parse_args()
if len(args) < 2:
parser.error(
'You must specify a release and source package(s), see --help')
if options.release and (options.security or options.devel):
parser.error('-r and -s/-d are mutually exclusive, see --help')
release = args.pop(0)
packages = args
if not options.skip_package_group_check:
try:
packages = check_package_sets(packages)
except ValueError as e:
sys.stderr.write(e.args[0] + '\n')
sys.exit(1)
launchpad = Launchpad.login_with(
'ubuntu-archive-tools', options.launchpad_instance, version='devel')
ubuntu = launchpad.distributions['ubuntu']
series = ubuntu.getSeries(name_or_version=release)
devel_series = ubuntu.current_series
if not devel_series:
sys.stderr.write(
'WARNING: No current development series, -d will not work\n')
devel_series = None
if release == 'precise':
sys.stdout.write(
'Called for precise; assuming kernel ESM publication\n')
options.esm = True
if options.esm:
# --security is meaningless for ESM everything is a security update.
options.security = False
options.release = True
src_archive = launchpad.archives.getByReference(
reference='~canonical-kernel-esm/ubuntu/proposed')
dst_archive = launchpad.archives.getByReference(
reference='~ubuntu-esm/ubuntu/esm')
else:
src_archive = dst_archive = ubuntu.getArchive(name='primary')
for package in packages:
release_package(options, package)