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
396 lines
15 KiB
6 years ago
|
#!/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)
|