~ubuntu-archive/ubuntu-archive-tools/trunk

1399.1.27 by Brian Murray
move copy-report to python3
1
#! /usr/bin/python3
464.1.1 by Colin Watson
copy-report: import from cocoplum
2
464.1.3 by Colin Watson
copy-report: use Python-3-compatible print functions
3
from __future__ import print_function
4
464.1.4 by Colin Watson
copy-report: consolidate and sort imports
5
import atexit
1236 by Colin Watson
copy-report: port to requests
6
import bz2
464.1.13 by Colin Watson
copy-report: clarify tagfile section handling with namedtuples
7
from collections import namedtuple
464.1.4 by Colin Watson
copy-report: consolidate and sort imports
8
import optparse
464.1.1 by Colin Watson
copy-report: import from cocoplum
9
import os
10
import re
11
import shutil
12
import subprocess
464.1.4 by Colin Watson
copy-report: consolidate and sort imports
13
import tempfile
1399.1.27 by Brian Murray
move copy-report to python3
14
15
from urllib.parse import unquote
464.1.1 by Colin Watson
copy-report: import from cocoplum
16
17
import apt_pkg
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
18
from launchpadlib.launchpad import Launchpad
1236 by Colin Watson
copy-report: port to requests
19
import lzma
20
import requests
464.1.1 by Colin Watson
copy-report: import from cocoplum
21
464.1.8 by Colin Watson
copy-report: PEP-8
22
464.1.1 by Colin Watson
copy-report: import from cocoplum
23
# from dak, more or less
24
re_no_epoch = re.compile(r"^\d+:")
25
re_strip_revision = re.compile(r"-[^-]+$")
26
re_changelog_versions = re.compile(r"^\w[-+0-9a-z.]+ \(([^\(\) \t]+)\)")
27
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
28
default_mirrors = ":".join([
29
    '/home/ubuntu-archive/mirror/ubuntu',
30
    '/srv/archive.ubuntu.com/ubuntu',
677 by Colin Watson
make all scripts pass current stricter pep8(1) in raring
31
])
464.1.1 by Colin Watson
copy-report: import from cocoplum
32
tempdir = None
33
570 by Colin Watson
copy-report: Archive.getPublishedSources takes a series link, not a series name
34
series_by_name = {}
35
464.1.8 by Colin Watson
copy-report: PEP-8
36
464.1.1 by Colin Watson
copy-report: import from cocoplum
37
def ensure_tempdir():
38
    global tempdir
39
    if not tempdir:
40
        tempdir = tempfile.mkdtemp(prefix='copy-report')
41
        atexit.register(shutil.rmtree, tempdir)
42
464.1.8 by Colin Watson
copy-report: PEP-8
43
464.1.1 by Colin Watson
copy-report: import from cocoplum
44
def decompress_open(tagfile):
45
    if tagfile.startswith('http:') or tagfile.startswith('ftp:'):
46
        ensure_tempdir()
1245 by Colin Watson
copy-report: suppress automatic decompression by requests
47
        response = requests.get(tagfile, stream=True)
1236 by Colin Watson
copy-report: port to requests
48
        if response.status_code == 404:
1245 by Colin Watson
copy-report: suppress automatic decompression by requests
49
            response.close()
1236 by Colin Watson
copy-report: port to requests
50
            tagfile = tagfile.replace('.xz', '.bz2')
1245 by Colin Watson
copy-report: suppress automatic decompression by requests
51
            response = requests.get(tagfile, stream=True)
1236 by Colin Watson
copy-report: port to requests
52
        response.raise_for_status()
53
        if '.' in tagfile:
54
            suffix = '.' + tagfile.rsplit('.', 1)[1]
55
        else:
56
            suffix = ''
57
        fd, tagfile = tempfile.mkstemp(suffix=suffix, dir=tempdir)
58
        with os.fdopen(fd, 'wb') as f:
1245 by Colin Watson
copy-report: suppress automatic decompression by requests
59
            f.write(response.raw.read())
60
        response.close()
1236 by Colin Watson
copy-report: port to requests
61
    elif not os.path.exists(tagfile):
62
        tagfile = tagfile.replace('.xz', '.bz2')
63
64
    if tagfile.endswith('.xz'):
65
        decompressor = lzma.LZMAFile
66
    elif tagfile.endswith('.bz2'):
67
        decompressor = bz2.BZ2File
68
    else:
69
        decompressor = None
70
71
    if decompressor is not None:
72
        fd, decompressed = tempfile.mkstemp(dir=tempdir)
73
        dcf = decompressor(tagfile)
74
        try:
75
            with os.fdopen(fd, 'wb') as f:
76
                f.write(dcf.read())
77
        finally:
78
            dcf.close()
79
        return open(decompressed, 'rb')
80
    else:
81
        return open(tagfile, 'rb')
464.1.1 by Colin Watson
copy-report: import from cocoplum
82
464.1.8 by Colin Watson
copy-report: PEP-8
83
464.1.13 by Colin Watson
copy-report: clarify tagfile section handling with namedtuples
84
Section = namedtuple("Section", ["version", "directory", "files"])
85
86
464.1.1 by Colin Watson
copy-report: import from cocoplum
87
def tagfiletodict(tagfile):
88
    suite = {}
464.1.2 by Colin Watson
copy-report: port to python-apt 0.8 API
89
    for section in apt_pkg.TagFile(decompress_open(tagfile)):
90
        files = [s.strip().split()[2] for s in section["Files"].split('\n')]
464.1.13 by Colin Watson
copy-report: clarify tagfile section handling with namedtuples
91
        suite[section["Package"]] = Section(
92
            version=section["Version"], directory=section["Directory"],
93
            files=files)
464.1.1 by Colin Watson
copy-report: import from cocoplum
94
    return suite
95
464.1.8 by Colin Watson
copy-report: PEP-8
96
464.1.13 by Colin Watson
copy-report: clarify tagfile section handling with namedtuples
97
def find_dsc(options, pkg, section):
98
    dsc_filename = [s for s in section.files if s.endswith('.dsc')][0]
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
99
    for mirror in options.mirrors:
464.1.13 by Colin Watson
copy-report: clarify tagfile section handling with namedtuples
100
        path = '%s/%s/%s' % (mirror, section.directory, dsc_filename)
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
101
        if os.path.exists(path):
571 by Colin Watson
copy-report: continue to next .dsc if one fails to unpack (e.g. incomplete mirror)
102
            yield path
103
    ensure_tempdir()
104
    spph = options.archive.getPublishedSources(
105
        source_name=pkg, version=section.version, exact_match=True)[0]
106
    outdir = tempfile.mkdtemp(dir=tempdir)
107
    filenames = []
108
    for url in spph.sourceFileUrls():
594 by Colin Watson
copy-report: unquote URLs
109
        filename = os.path.join(outdir, unquote(os.path.basename(url)))
1245 by Colin Watson
copy-report: suppress automatic decompression by requests
110
        response = requests.get(url, stream=True)
1236 by Colin Watson
copy-report: port to requests
111
        response.raise_for_status()
112
        with open(filename, 'wb') as f:
1245 by Colin Watson
copy-report: suppress automatic decompression by requests
113
            f.write(response.raw.read())
114
        response.close()
571 by Colin Watson
copy-report: continue to next .dsc if one fails to unpack (e.g. incomplete mirror)
115
        filenames.append(filename)
116
    yield [s for s in filenames if s.endswith('.dsc')][0]
117
118
119
class BrokenSourcePackage(Exception):
120
    pass
464.1.1 by Colin Watson
copy-report: import from cocoplum
121
464.1.8 by Colin Watson
copy-report: PEP-8
122
464.1.1 by Colin Watson
copy-report: import from cocoplum
123
def get_changelog_versions(pkg, dsc, version):
124
    ensure_tempdir()
125
126
    upstream_version = re_no_epoch.sub('', version)
127
    upstream_version = re_strip_revision.sub('', upstream_version)
128
725 by Colin Watson
copy-report: use os.devnull
129
    with open(os.devnull, 'w') as devnull:
464.1.12 by Colin Watson
copy-report: pass -q to dpkg-source to get rid of warnings about -sn and 3.0 (quilt)
130
        ret = subprocess.call(
131
            ['dpkg-source', '-q', '--no-check', '-sn', '-x', dsc],
132
            stdout=devnull, cwd=tempdir)
464.1.1 by Colin Watson
copy-report: import from cocoplum
133
134
    # It's in the archive, so these assertions must hold.
582 by Colin Watson
copy-report: fix reversed test
135
    if ret != 0:
581 by Colin Watson
copy-report: include .dsc path in BrokenSourcePackage exception
136
        raise BrokenSourcePackage(dsc)
571 by Colin Watson
copy-report: continue to next .dsc if one fails to unpack (e.g. incomplete mirror)
137
464.1.1 by Colin Watson
copy-report: import from cocoplum
138
    unpacked = '%s/%s-%s' % (tempdir, pkg, upstream_version)
139
    assert os.path.isdir(unpacked)
140
    changelog_path = '%s/debian/changelog' % unpacked
141
    assert os.path.exists(changelog_path)
142
464.1.6 by Colin Watson
copy-report: use with statements a bit more
143
    with open(changelog_path) as changelog:
144
        versions = set()
145
        for line in changelog:
146
            m = re_changelog_versions.match(line)
147
            if m:
148
                versions.add(m.group(1))
464.1.1 by Colin Watson
copy-report: import from cocoplum
149
150
    shutil.rmtree(unpacked)
151
152
    return versions
153
464.1.8 by Colin Watson
copy-report: PEP-8
154
464.1.13 by Colin Watson
copy-report: clarify tagfile section handling with namedtuples
155
def descended_from(options, pkg, section1, section2):
156
    if apt_pkg.version_compare(section1.version, section2.version) <= 0:
464.1.1 by Colin Watson
copy-report: import from cocoplum
157
        return False
571 by Colin Watson
copy-report: continue to next .dsc if one fails to unpack (e.g. incomplete mirror)
158
    exception = None
159
    for dsc in find_dsc(options, pkg, section1):
160
        try:
161
            versions = get_changelog_versions(pkg, dsc, section1.version)
1244 by Colin Watson
copy-report: fix descended_from exception handling to work in Python 3
162
        except BrokenSourcePackage as e:
163
            exception = e
571 by Colin Watson
copy-report: continue to next .dsc if one fails to unpack (e.g. incomplete mirror)
164
            continue
165
        return section1.version in versions
166
    raise exception
464.1.1 by Colin Watson
copy-report: import from cocoplum
167
464.1.8 by Colin Watson
copy-report: PEP-8
168
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
169
Candidate = namedtuple(
170
    "Candidate", ["package", "suite1", "suite2", "version1", "version2"])
171
172
570 by Colin Watson
copy-report: Archive.getPublishedSources takes a series link, not a series name
173
def get_series(options, name):
174
    if name not in series_by_name:
175
        series_by_name[name] = options.distro.getSeries(name_or_version=name)
176
    return series_by_name[name]
177
178
565 by Colin Watson
copy-report: skip copying packages that have already been copied (workaround for bug 1023372, but reasonably sensible anyway)
179
def already_copied(options, candidate):
180
    if "-" in candidate.suite2:
181
        series, pocket = candidate.suite2.split("-", 1)
182
        pocket = pocket.title()
183
    else:
184
        series = candidate.suite2
185
        pocket = "Release"
570 by Colin Watson
copy-report: Archive.getPublishedSources takes a series link, not a series name
186
    series = get_series(options, series)
565 by Colin Watson
copy-report: skip copying packages that have already been copied (workaround for bug 1023372, but reasonably sensible anyway)
187
    pubs = options.archive.getPublishedSources(
188
        source_name=candidate.package, version=candidate.version1,
189
        exact_match=True, distro_series=series, pocket=pocket)
190
    for pub in pubs:
191
        if pub.status in ("Pending", "Published"):
192
            return True
193
    return False
194
195
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
196
def copy(options, candidate):
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
197
    if "-" in candidate.suite2:
198
        to_series, to_pocket = candidate.suite2.split("-", 1)
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
199
        to_pocket = to_pocket.title()
200
    else:
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
201
        to_series = candidate.suite2
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
202
        to_pocket = "Release"
203
    options.archive.copyPackage(
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
204
        source_name=candidate.package, version=candidate.version1,
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
205
        from_archive=options.archive, to_pocket=to_pocket, to_series=to_series,
206
        include_binaries=True, auto_approve=True)
464.1.1 by Colin Watson
copy-report: import from cocoplum
207
464.1.8 by Colin Watson
copy-report: PEP-8
208
464.1.1 by Colin Watson
copy-report: import from cocoplum
209
def candidate_string(candidate):
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
210
    string = ('copy-package -y -b -s %s --to-suite %s -e %s %s' %
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
211
              (candidate.suite1, candidate.suite2, candidate.version1,
212
               candidate.package))
213
    if candidate.version2 is not None:
214
        string += '  # %s: %s' % (candidate.suite2, candidate.version2)
464.1.1 by Colin Watson
copy-report: import from cocoplum
215
    return string
216
464.1.8 by Colin Watson
copy-report: PEP-8
217
464.1.1 by Colin Watson
copy-report: import from cocoplum
218
def main():
464.1.2 by Colin Watson
copy-report: port to python-apt 0.8 API
219
    apt_pkg.init_system()
464.1.1 by Colin Watson
copy-report: import from cocoplum
220
221
    parser = optparse.OptionParser(usage="usage: %prog [options] [suites]")
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
222
    parser.add_option(
223
        "-l", "--launchpad", dest="launchpad_instance", default="production")
224
    parser.add_option(
225
        "--quick", action="store_true", help="don't examine changelogs")
226
    parser.add_option(
595 by Colin Watson
copy-report: rename --safe option to --copy-safe
227
        "--copy-safe", action="store_true",
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
228
        help="automatically copy safe candidates")
229
    parser.add_option(
230
        "--mirrors", default=default_mirrors,
231
        help="colon-separated list of local mirrors")
464.1.1 by Colin Watson
copy-report: import from cocoplum
232
    options, args = parser.parse_args()
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
233
234
    options.launchpad = Launchpad.login_with(
235
        "copy-report", options.launchpad_instance, version="devel")
236
    options.distro = options.launchpad.distributions["ubuntu"]
237
    options.archive = options.distro.main_archive
238
    options.mirrors = options.mirrors.split(":")
239
464.1.1 by Colin Watson
copy-report: import from cocoplum
240
    if args:
241
        suites = args
242
    else:
566 by Colin Watson
copy-report: fix reversed suite order (cosmetic)
243
        suites = reversed([
563 by Colin Watson
copy-report: Distribution.series, not Distribution.suites
244
            series.name
245
            for series in options.launchpad.distributions["ubuntu"].series
566 by Colin Watson
copy-report: fix reversed suite order (cosmetic)
246
            if series.status in ("Supported", "Current Stable Release")])
464.1.1 by Colin Watson
copy-report: import from cocoplum
247
248
    yes = []
249
    maybe = []
250
    no = []
251
252
    for suite in suites:
253
        for component in 'main', 'restricted', 'universe', 'multiverse':
1236 by Colin Watson
copy-report: port to requests
254
            tagfile1 = '%s/dists/%s-security/%s/source/Sources.xz' % (
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
255
                options.mirrors[0], suite, component)
1236 by Colin Watson
copy-report: port to requests
256
            tagfile2 = '%s/dists/%s-updates/%s/source/Sources.xz' % (
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
257
                options.mirrors[0], suite, component)
464.1.1 by Colin Watson
copy-report: import from cocoplum
258
            name1 = '%s-security' % suite
259
            name2 = '%s-updates' % suite
260
261
            suite1 = tagfiletodict(tagfile1)
262
            suite2 = tagfiletodict(tagfile2)
263
464.1.11 by Colin Watson
copy-report: rely on default iterator for mappings iterating over keys
264
            for package in sorted(suite1):
464.1.13 by Colin Watson
copy-report: clarify tagfile section handling with namedtuples
265
                section1 = suite1[package]
266
                section2 = suite2.get(package)
267
                if (section2 is None or
464.1.1 by Colin Watson
copy-report: import from cocoplum
268
                    (not options.quick and
464.1.13 by Colin Watson
copy-report: clarify tagfile section handling with namedtuples
269
                     descended_from(options, package, section1, section2))):
565 by Colin Watson
copy-report: skip copying packages that have already been copied (workaround for bug 1023372, but reasonably sensible anyway)
270
                    candidate = Candidate(
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
271
                        package=package, suite1=name1, suite2=name2,
565 by Colin Watson
copy-report: skip copying packages that have already been copied (workaround for bug 1023372, but reasonably sensible anyway)
272
                        version1=section1.version, version2=None)
273
                    if not already_copied(options, candidate):
274
                        yes.append(candidate)
464.1.8 by Colin Watson
copy-report: PEP-8
275
                elif apt_pkg.version_compare(
677 by Colin Watson
make all scripts pass current stricter pep8(1) in raring
276
                        section1.version, section2.version) > 0:
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
277
                    candidate = Candidate(
278
                        package=package, suite1=name1, suite2=name2,
279
                        version1=section1.version, version2=section2.version)
565 by Colin Watson
copy-report: skip copying packages that have already been copied (workaround for bug 1023372, but reasonably sensible anyway)
280
                    if already_copied(options, candidate):
281
                        pass
282
                    elif not options.quick:
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
283
                        no.append(candidate)
464.1.1 by Colin Watson
copy-report: import from cocoplum
284
                    else:
464.1.15 by Colin Watson
copy-report: clarify candidate handling with namedtuples
285
                        maybe.append(candidate)
464.1.1 by Colin Watson
copy-report: import from cocoplum
286
287
    if yes:
464.1.9 by Colin Watson
copy-report: print output even with --safe
288
        print("The following packages can be copied safely:")
289
        print("--------------------------------------------")
290
        print()
291
        for candidate in yes:
292
            print(candidate_string(candidate))
293
        print()
294
595 by Colin Watson
copy-report: rename --safe option to --copy-safe
295
        if options.copy_safe:
464.1.1 by Colin Watson
copy-report: import from cocoplum
296
            for candidate in yes:
464.1.7 by Colin Watson
copy-report: try multiple mirrors, eventually falling back to Archive.getPublishedSources; use Archive.copyPackage directly; suggest copy-package rather than copy-package.py
297
                copy(options, candidate)
464.1.1 by Colin Watson
copy-report: import from cocoplum
298
464.1.9 by Colin Watson
copy-report: print output even with --safe
299
    if maybe:
464.1.3 by Colin Watson
copy-report: use Python-3-compatible print functions
300
        print("Check that these packages are descendants before copying:")
301
        print("---------------------------------------------------------")
302
        print()
464.1.1 by Colin Watson
copy-report: import from cocoplum
303
        for candidate in maybe:
464.1.3 by Colin Watson
copy-report: use Python-3-compatible print functions
304
            print('#%s' % candidate_string(candidate))
305
        print()
464.1.1 by Colin Watson
copy-report: import from cocoplum
306
464.1.9 by Colin Watson
copy-report: print output even with --safe
307
    if no:
464.1.3 by Colin Watson
copy-report: use Python-3-compatible print functions
308
        print("The following packages need to be merged by hand:")
309
        print("-------------------------------------------------")
310
        print()
464.1.1 by Colin Watson
copy-report: import from cocoplum
311
        for candidate in no:
464.1.3 by Colin Watson
copy-report: use Python-3-compatible print functions
312
            print('#%s' % candidate_string(candidate))
313
        print()
464.1.1 by Colin Watson
copy-report: import from cocoplum
314
464.1.8 by Colin Watson
copy-report: PEP-8
315
464.1.1 by Colin Watson
copy-report: import from cocoplum
316
if __name__ == '__main__':
317
    main()