1
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""FTPMaster utilities."""
13
'ObsoleteDistroseries',
21
from itertools import chain
27
from debian.deb822 import Changes
28
from zope.component import getUtility
30
from canonical.database.constants import UTC_NOW
31
from canonical.launchpad.helpers import filenameToContentType
32
from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
33
from canonical.librarian.interfaces import (
37
from canonical.librarian.utils import copy_and_close
38
from lp.app.errors import NotFoundError
39
from lp.archiveuploader.utils import determine_source_file_type
40
from lp.registry.interfaces.person import IPersonSet
41
from lp.registry.interfaces.pocket import pocketsuffix
42
from lp.registry.interfaces.series import SeriesStatus
43
from lp.registry.interfaces.sourcepackage import SourcePackageFileType
44
from lp.services.browser_helpers import get_plural_text
45
from lp.services.scripts.base import (
47
LaunchpadScriptFailure,
49
from lp.soyuz.adapters.packagelocation import (
50
build_package_location,
53
from lp.soyuz.enums import PackagePublishingStatus
54
from lp.soyuz.scripts.ftpmasterbase import (
60
class PubBinaryContent:
61
"""Binary publication container.
63
Currently used for auxiliary storage in PubSourceChecker.
66
def __init__(self, name, version, arch, component, section, priority):
68
self.version = version
70
self.component = component
71
self.section = section
72
self.priority = priority
75
def warn(self, message):
76
"""Append a warning in the message list."""
77
self.messages.append('W: %s' % message)
79
def error(self, message):
80
"""Append a error in the message list."""
81
self.messages.append('E: %s' % message)
83
def renderReport(self):
84
"""Render a report with the appended messages (self.messages).
86
Return None if no message was found, otherwise return
87
a properly formatted string, including
89
<TAB>BinaryName_Version Arch Component/Section/Priority
92
if not len(self.messages):
95
report = [('\t%s_%s %s %s/%s/%s'
96
% (self.name, self.version, self.arch,
97
self.component, self.section, self.priority))]
99
for message in self.messages:
100
report.append('\t\t%s' % message)
102
return "\n".join(report)
105
class PubBinaryDetails:
106
"""Store the component, section and priority of binary packages and, for
107
each binary package the most frequent component, section and priority.
109
These are stored in the following attributes:
111
- components: A dictionary mapping binary package names to other
112
dictionaries mapping component names to binary packages published
114
- sections: The same as components, but for sections.
115
- priorities: The same as components, but for priorities.
116
- correct_components: a dictionary mapping binary package name
117
to the most frequent (considered the correct) component name.
118
- correct_sections: same as correct_components, but for sections
119
- correct_priorities: same as correct_components, but for priorities
126
self.correct_components = {}
127
self.correct_sections = {}
128
self.correct_priorities = {}
130
def addBinaryDetails(self, bin):
131
"""Include a binary publication and update internal registers."""
132
name_components = self.components.setdefault(bin.name, {})
133
bin_component = name_components.setdefault(bin.component, [])
134
bin_component.append(bin)
136
name_sections = self.sections.setdefault(bin.name, {})
137
bin_section = name_sections.setdefault(bin.section, [])
138
bin_section.append(bin)
140
name_priorities = self.priorities.setdefault(bin.name, {})
141
bin_priority = name_priorities.setdefault(bin.priority, [])
142
bin_priority.append(bin)
144
def _getMostFrequentValue(self, data):
145
"""Return a dict of name and the most frequent value.
147
Used for self.{components, sections, priorities}
151
for name, items in data.iteritems():
153
for item, occurrences in items.iteritems():
154
if len(occurrences) > highest:
155
highest = len(occurrences)
160
def setCorrectValues(self):
161
"""Find out the correct values for the same binary name
163
Consider correct the most frequent.
165
self.correct_components = self._getMostFrequentValue(self.components)
166
self.correct_sections = self._getMostFrequentValue(self.sections)
167
self.correct_priorities = self._getMostFrequentValue(self.priorities)
170
class PubSourceChecker:
171
"""Map and probe a Source/Binaries publication couple.
173
Receive the source publication data and its binaries and perform
174
a group of heuristic consistency checks.
177
def __init__(self, name, version, component, section, urgency):
179
self.version = version
180
self.component = component
181
self.section = section
182
self.urgency = urgency
184
self.binaries_details = PubBinaryDetails()
186
def addBinary(self, name, version, architecture, component, section,
188
"""Append the binary data to the current publication list."""
189
bin = PubBinaryContent(
190
name, version, architecture, component, section, priority)
192
self.binaries.append(bin)
194
self.binaries_details.addBinaryDetails(bin)
197
"""Setup check environment and perform the required checks."""
198
self.binaries_details.setCorrectValues()
200
for bin in self.binaries:
201
self._checkComponent(bin)
202
self._checkSection(bin)
203
self._checkPriority(bin)
205
def _checkComponent(self, bin):
206
"""Check if the binary component matches the correct component.
208
'correct' is the most frequent component in this binary package
211
correct_component = self.binaries_details.correct_components[bin.name]
212
if bin.component != correct_component:
213
bin.warn('Component mismatch: %s != %s'
214
% (bin.component, correct_component))
216
def _checkSection(self, bin):
217
"""Check if the binary section matches the correct section.
219
'correct' is the most frequent section in this binary package
222
correct_section = self.binaries_details.correct_sections[bin.name]
223
if bin.section != correct_section:
224
bin.warn('Section mismatch: %s != %s'
225
% (bin.section, correct_section))
227
def _checkPriority(self, bin):
228
"""Check if the binary priority matches the correct priority.
230
'correct' is the most frequent priority in this binary package
233
correct_priority = self.binaries_details.correct_priorities[bin.name]
234
if bin.priority != correct_priority:
235
bin.warn('Priority mismatch: %s != %s'
236
% (bin.priority, correct_priority))
238
def renderReport(self):
239
"""Render a formatted report for the publication group.
241
Return None if no issue was annotated or an formatted string
244
SourceName_Version Component/Section/Urgency | # bin
249
for bin in self.binaries:
250
bin_report = bin.renderReport()
252
report.append(bin_report)
257
result = [('%s_%s %s/%s/%s | %s bin'
258
% (self.name, self.version, self.component,
259
self.section, self.urgency, len(self.binaries)))]
261
result.extend(report)
263
return "\n".join(result)
266
class ChrootManagerError(Exception):
267
"""Any error generated during the ChrootManager procedures."""
271
"""Chroot actions wrapper.
273
The 'distroarchseries' argument is mandatory and 'filepath' is
276
'filepath' is required by some allowed actions as source or destination,
278
ChrootManagerError will be raised if anything wrong occurred in this
279
class, things like missing parameter or infrastructure pieces not in
283
allowed_actions = ['add', 'update', 'remove', 'get']
285
def __init__(self, distroarchseries, filepath=None):
286
self.distroarchseries = distroarchseries
287
self.filepath = filepath
291
"""Upload the self.filepath contents to Librarian.
293
Return the respective ILibraryFileAlias instance.
294
Raises ChrootManagerError if it could not be found.
297
fd = open(self.filepath)
299
raise ChrootManagerError('Could not open: %s' % self.filepath)
301
flen = os.stat(self.filepath).st_size
302
filename = os.path.basename(self.filepath)
303
ftype = filenameToContentType(filename)
306
alias_id = getUtility(ILibrarianClient).addFile(
307
filename, flen, fd, contentType=ftype)
308
except UploadFailed, info:
309
raise ChrootManagerError("Librarian upload failed: %s" % info)
311
lfa = getUtility(ILibraryFileAliasSet)[alias_id]
313
self._messages.append(
314
"LibraryFileAlias: %d, %s bytes, %s"
315
% (lfa.id, lfa.content.filesize, lfa.content.md5))
319
def _getPocketChroot(self):
320
"""Retrive PocketChroot record.
322
Return the respective IPocketChroot instance.
323
Raises ChrootManagerError if it could not be found.
325
pocket_chroot = self.distroarchseries.getPocketChroot()
326
if pocket_chroot is None:
327
raise ChrootManagerError(
328
'Could not find chroot for %s'
329
% (self.distroarchseries.title))
331
self._messages.append(
332
"PocketChroot for '%s' (%d) retrieved."
333
% (pocket_chroot.distroarchseries.title, pocket_chroot.id))
338
"""Base method for add and update action."""
339
if self.filepath is None:
340
raise ChrootManagerError('Missing local chroot file path.')
341
alias = self._upload()
342
return self.distroarchseries.addOrUpdateChroot(alias)
345
"""Create a new PocketChroot record.
347
Raises ChrootManagerError if self.filepath isn't set.
348
Update of pre-existing PocketChroot record will be automatically
350
It's a bind to the self.update method.
352
pocket_chroot = self._update()
353
self._messages.append(
354
"PocketChroot for '%s' (%d) added."
355
% (pocket_chroot.distroarchseries.title, pocket_chroot.id))
358
"""Update a PocketChroot record.
360
Raises ChrootManagerError if filepath isn't set
361
Creation of non-existing PocketChroot records will be automatically
364
pocket_chroot = self._update()
365
self._messages.append(
366
"PocketChroot for '%s' (%d) updated."
367
% (pocket_chroot.distroarchseries.title, pocket_chroot.id))
370
"""Overwrite existing PocketChroot file to none.
372
Raises ChrootManagerError if the chroot record isn't found.
374
pocket_chroot = self._getPocketChroot()
375
self.distroarchseries.addOrUpdateChroot(None)
376
self._messages.append(
377
"PocketChroot for '%s' (%d) removed."
378
% (pocket_chroot.distroarchseries.title, pocket_chroot.id))
381
"""Download chroot file from Librarian and store."""
382
pocket_chroot = self._getPocketChroot()
384
if self.filepath is None:
385
abs_filepath = os.path.abspath(pocket_chroot.chroot.filename)
386
if os.path.exists(abs_filepath):
387
raise ChrootManagerError(
388
'cannot overwrite %s' % abs_filepath)
389
self._messages.append(
390
"Writing to '%s'." % abs_filepath)
391
local_file = open(pocket_chroot.chroot.filename, "w")
393
abs_filepath = os.path.abspath(self.filepath)
394
if os.path.exists(abs_filepath):
395
raise ChrootManagerError(
396
'cannot overwrite %s' % abs_filepath)
397
self._messages.append(
398
"Writing to '%s'." % abs_filepath)
399
local_file = open(abs_filepath, "w")
401
if pocket_chroot.chroot is None:
402
raise ChrootManagerError('Chroot was deleted.')
404
pocket_chroot.chroot.open()
405
copy_and_close(pocket_chroot.chroot, local_file)
408
class SyncSourceError(Exception):
409
"""Raised when an critical error occurs inside SyncSource.
411
The entire procedure should be aborted in order to avoid unknown problems.
416
"""Sync Source procedure helper class.
418
It provides the backend for retrieving files from Librarian or the
419
'sync source' location. Also provides a method to check the downloaded
421
'aptMD5Sum' is provided as a classmethod during the integration time.
424
def __init__(self, files, origin, logger, downloader, todistro):
425
"""Store local context.
427
files: a dictionary where the keys are the filename and the
428
value another dictionary with the file informations.
429
origin: a dictionary similar to 'files' but where the values
430
contain information for download files to be synchronized
432
downloader: a callable that fetchs URLs,
433
'downloader(url, destination)'
434
todistro: target distribution object
439
self.downloader = downloader
440
self.todistro = todistro
443
def generateMD5Sum(self, filename):
444
file_handle = open(filename)
445
md5sum = hashlib.md5(file_handle.read()).hexdigest()
449
def fetchFileFromLibrarian(self, filename):
450
"""Fetch file from librarian.
452
Store the contents in local path with the original filename.
453
Return the fetched filename if it was present in Librarian or None
457
libraryfilealias = self.todistro.main_archive.getFileByName(
459
except NotFoundError:
463
"%s: already in distro - downloading from librarian" %
466
output_file = open(filename, 'w')
467
libraryfilealias.open()
468
copy_and_close(libraryfilealias, output_file)
471
def fetchLibrarianFiles(self):
472
"""Try to fetch files from Librarian.
474
It raises SyncSourceError if anything else then an
475
orig tarball was found in Librarian.
476
Return the names of the files retrieved from the librarian.
479
for filename in self.files.keys():
480
if not self.fetchFileFromLibrarian(filename):
482
file_type = determine_source_file_type(filename)
483
# set the return code if an orig was, in fact,
484
# fetched from Librarian
486
SourcePackageFileType.ORIG_TARBALL,
487
SourcePackageFileType.COMPONENT_ORIG_TARBALL)
488
if file_type not in orig_types:
489
raise SyncSourceError(
490
'Oops, only orig tarball can be retrieved from '
492
retrieved.append(filename)
496
def fetchSyncFiles(self):
497
"""Fetch files from the original sync source.
499
Return DSC filename, which should always come via this path.
502
for filename in self.files.keys():
503
file_type = determine_source_file_type(filename)
504
if file_type == SourcePackageFileType.DSC:
505
dsc_filename = filename
506
if os.path.exists(filename):
507
self.logger.info(" - <%s: cached>" % (filename))
510
" - <%s: downloading from %s>" %
511
(filename, self.origin["url"]))
512
download_f = ("%s%s" % (self.origin["url"],
513
self.files[filename]["remote filename"]))
515
self.downloader(download_f, filename)
518
def checkDownloadedFiles(self):
519
"""Check md5sum and size match Source.
521
If anything fails SyncSourceError will be raised.
523
for filename in self.files.keys():
524
actual_md5sum = self.generateMD5Sum(filename)
525
expected_md5sum = self.files[filename]["md5sum"]
526
if actual_md5sum != expected_md5sum:
527
raise SyncSourceError(
528
"%s: md5sum check failed (%s [actual] "
529
"vs. %s [expected])."
530
% (filename, actual_md5sum, expected_md5sum))
532
actual_size = os.stat(filename)[stat.ST_SIZE]
533
expected_size = int(self.files[filename]["size"])
534
if actual_size != expected_size:
535
raise SyncSourceError(
536
"%s: size mismatch (%s [actual] vs. %s [expected])."
537
% (filename, actual_size, expected_size))
540
class LpQueryDistro(LaunchpadScript):
541
"""Main class for scripts/ftpmaster-tools/lp-query-distro.py."""
543
def __init__(self, *args, **kwargs):
544
"""Initialize dynamic 'usage' message and LaunchpadScript parent.
546
Also initialize the list 'allowed_arguments'.
548
self.allowed_actions = [
549
'current', 'development', 'supported', 'pending_suites', 'archs',
550
'official_archs', 'nominated_arch_indep', 'pocket_suffixes']
551
self.usage = '%%prog <%s>' % ' | '.join(self.allowed_actions)
552
LaunchpadScript.__init__(self, *args, **kwargs)
554
def add_my_options(self):
555
"""Add 'distribution' and 'suite' context options."""
556
self.parser.add_option(
557
'-d', '--distribution', dest='distribution_name',
558
default='ubuntu', help='Context distribution name.')
559
self.parser.add_option(
560
'-s', '--suite', dest='suite', default=None,
561
help='Context suite name.')
564
"""Main procedure, basically a runAction wrapper.
566
Execute the given and allowed action using the default presenter
567
(see self.runAction for further information).
571
def _buildLocation(self):
572
"""Build a PackageLocation object
574
The location will correspond to the given 'distribution' and 'suite',
575
Any PackageLocationError occurring at this point will be masked into
576
LaunchpadScriptFailure.
579
self.location = build_package_location(
580
distribution_name=self.options.distribution_name,
581
suite=self.options.suite)
582
except PackageLocationError, err:
583
raise LaunchpadScriptFailure(err)
585
def defaultPresenter(self, result):
586
"""Default result presenter.
588
Directly prints result in the standard output (print).
592
def runAction(self, presenter=None):
593
"""Run a given initialized action (self.action_name).
595
It accepts an optional 'presenter' which will be used to
596
store/present the action result.
598
Ensure at least one argument was passed, known as 'action'.
599
Verify if the given 'action' is listed as an 'allowed_action'.
600
Raise LaunchpadScriptFailure if those requirements were not
603
It builds context 'location' object (see self._buildLocation).
605
It may raise LaunchpadScriptFailure is the 'action' is not properly
606
supported by the current code (missing corresponding property).
608
if presenter is None:
609
presenter = self.defaultPresenter
611
if len(self.args) != 1:
612
raise LaunchpadScriptFailure('<action> is required')
614
[self.action_name] = self.args
616
if self.action_name not in self.allowed_actions:
617
raise LaunchpadScriptFailure(
618
'Action "%s" is not supported' % self.action_name)
620
self._buildLocation()
623
action_result = getattr(self, self.action_name)
624
except AttributeError:
625
raise AssertionError(
626
"No handler found for action '%s'" % self.action_name)
628
presenter(action_result)
630
def checkNoSuiteDefined(self):
631
"""Raises LaunchpadScriptError if a suite location was passed.
633
It is re-used in action properties to avoid conflicting contexts,
634
i.e, passing an arbitrary 'suite' and asking for the CURRENT suite
635
in the context distribution.
637
if self.options.suite is not None:
638
raise LaunchpadScriptFailure(
639
"Action does not accept defined suite.")
643
"""Return the name of the CURRENT distroseries.
645
It is restricted for the context distribution.
647
It may raise LaunchpadScriptFailure if a suite was passed on the
648
command-line or if not CURRENT distroseries was found.
650
self.checkNoSuiteDefined()
651
series = self.location.distribution.getSeriesByStatus(
652
SeriesStatus.CURRENT)
654
raise LaunchpadScriptFailure("No CURRENT series.")
656
return series[0].name
659
def development(self):
660
"""Return the name of the DEVELOPMENT distroseries.
662
It is restricted for the context distribution.
664
It may raise `LaunchpadScriptFailure` if a suite was passed on the
667
Return the first FROZEN distroseries found if there is no
668
DEVELOPMENT one available.
670
Raises `NotFoundError` if neither a CURRENT nor a FROZEN
671
candidate could be found.
673
self.checkNoSuiteDefined()
675
wanted_status = (SeriesStatus.DEVELOPMENT,
677
for status in wanted_status:
678
series = self.location.distribution.getSeriesByStatus(status)
679
if series.count() > 0:
682
raise LaunchpadScriptFailure(
683
'There is no DEVELOPMENT distroseries for %s' %
684
self.location.distribution.name)
685
return series[0].name
689
"""Return the names of the distroseries currently supported.
691
'supported' means not EXPERIMENTAL or OBSOLETE.
693
It is restricted for the context distribution.
695
It may raise `LaunchpadScriptFailure` if a suite was passed on the
696
command-line or if there is not supported distroseries for the
699
Return a space-separated list of distroseries names.
701
self.checkNoSuiteDefined()
702
supported_series = []
703
unsupported_status = (SeriesStatus.EXPERIMENTAL,
704
SeriesStatus.OBSOLETE)
705
for distroseries in self.location.distribution:
706
if distroseries.status not in unsupported_status:
707
supported_series.append(distroseries.name)
709
if not supported_series:
710
raise LaunchpadScriptFailure(
711
'There is no supported distroseries for %s' %
712
self.location.distribution.name)
714
return " ".join(supported_series)
717
def pending_suites(self):
718
"""Return the suite names containing PENDING publication.
720
It check for sources and/or binary publications.
722
self.checkNoSuiteDefined()
723
pending_suites = set()
724
pending_sources = self.location.archive.getPublishedSources(
725
status=PackagePublishingStatus.PENDING)
726
for pub in pending_sources:
727
pending_suites.add((pub.distroseries, pub.pocket))
729
pending_binaries = self.location.archive.getAllPublishedBinaries(
730
status=PackagePublishingStatus.PENDING)
731
for pub in pending_binaries:
733
(pub.distroarchseries.distroseries, pub.pocket))
735
return " ".join([distroseries.name + pocketsuffix[pocket]
736
for distroseries, pocket in pending_suites])
740
"""Return a space-separated list of architecture tags.
742
It is restricted for the context distribution and suite.
744
architectures = self.location.distroseries.architectures
745
return " ".join(arch.architecturetag for arch in architectures)
748
def official_archs(self):
749
"""Return a space-separated list of official architecture tags.
751
It is restricted to the context distribution and suite.
753
architectures = self.location.distroseries.architectures
754
return " ".join(arch.architecturetag
755
for arch in architectures
759
def nominated_arch_indep(self):
760
"""Return the nominated arch indep architecture tag.
762
It is restricted to the context distribution and suite.
764
series = self.location.distroseries
765
return series.nominatedarchindep.architecturetag
768
def pocket_suffixes(self):
769
"""Return a space-separated list of non-empty pocket suffixes.
771
The RELEASE pocket (whose suffix is the empty string) is omitted.
773
The returned space-separated string is ordered alphabetically.
775
sorted_non_empty_suffixes = sorted(
776
suffix for suffix in pocketsuffix.values() if suffix != '')
777
return " ".join(sorted_non_empty_suffixes)
780
class PackageRemover(SoyuzScript):
781
"""SoyuzScript implementation for published package removal.."""
783
usage = '%prog -s warty mozilla-firefox'
784
description = 'REMOVE a published package.'
786
"The archive will be updated in the next publishing cycle.")
788
def add_my_options(self):
789
"""Adding local options."""
790
# XXX cprov 20071025: we need a hook for loading SoyuzScript default
791
# options automatically. This is ugly.
792
SoyuzScript.add_my_options(self)
795
self.parser.add_option("-b", "--binary", dest="binaryonly",
796
default=False, action="store_true",
797
help="Remove binaries only.")
798
self.parser.add_option("-S", "--source-only", dest="sourceonly",
799
default=False, action="store_true",
800
help="Remove source only.")
802
# Removal information options.
803
self.parser.add_option("-u", "--user", dest="user",
804
help="Launchpad user name.")
805
self.parser.add_option("-m", "--removal_comment",
806
dest="removal_comment",
807
help="Removal comment")
810
"""Execute the package removal task.
812
Build location and target objects.
814
Can raise SoyuzScriptError.
816
if len(self.args) == 0:
817
raise SoyuzScriptError(
818
"At least one non-option argument must be given, "
819
"a package name to be removed.")
821
if self.options.user is None:
822
raise SoyuzScriptError("Launchpad username must be given.")
824
if self.options.removal_comment is None:
825
raise SoyuzScriptError("Removal comment must be given.")
827
removed_by = getUtility(IPersonSet).getByName(self.options.user)
828
if removed_by is None:
829
raise SoyuzScriptError(
830
"Invalid launchpad usename: %s" % self.options.user)
833
for packagename in self.args:
834
if self.options.binaryonly:
836
self.findLatestPublishedBinaries(packagename))
837
elif self.options.sourceonly:
838
removables.append(self.findLatestPublishedSource(packagename))
840
source_pub = self.findLatestPublishedSource(packagename)
841
removables.append(source_pub)
842
removables.extend(source_pub.getPublishedBinaries())
844
self.logger.info("Removing candidates:")
845
for removable in removables:
846
self.logger.info('\t%s', removable.displayname)
848
self.logger.info("Removed-by: %s", removed_by.displayname)
849
self.logger.info("Comment: %s", self.options.removal_comment)
852
for removable in removables:
853
removable.requestDeletion(
854
removed_by=removed_by,
855
removal_comment=self.options.removal_comment)
856
removals.append(removable)
858
if len(removals) == 0:
859
self.logger.info("No package removed (bug ?!?).")
862
"%d %s successfully removed.", len(removals),
863
get_plural_text(len(removals), "package", "packages"))
865
# Information returned mainly for the benefit of the test harness.
869
class ObsoleteDistroseries(SoyuzScript):
870
"""`SoyuzScript` that obsoletes a distroseries."""
872
usage = "%prog -d <distribution> -s <suite>"
873
description = ("Make obsolete (schedule for removal) packages in an "
874
"obsolete distroseries.")
876
def add_my_options(self):
877
"""Add -d, -s, dry-run and confirmation options."""
878
SoyuzScript.add_distro_options(self)
879
SoyuzScript.add_transaction_options(self)
882
"""Execute package obsolescence procedure.
884
Modules using this class outside of its normal usage in the
885
main script can call this method to start the copy.
887
In this case the caller can override test_args on __init__
888
to set the command line arguments.
890
:raise SoyuzScriptError: If the distroseries is not provided or
891
it is already obsolete.
893
assert self.location, (
894
"location is not available, call SoyuzScript.setupLocation() "
895
"before calling mainTask().")
897
# Shortcut variable name to reduce long lines.
898
distroseries = self.location.distroseries
900
self._checkParameters(distroseries)
902
self.logger.info("Obsoleting all packages for distroseries %s in "
903
"the %s distribution." % (
905
distroseries.distribution.name))
907
# First, mark all Published sources as Obsolete.
908
sources = distroseries.getAllPublishedSources()
909
binaries = distroseries.getAllPublishedBinaries()
911
"Obsoleting published packages (%d sources, %d binaries)."
912
% (sources.count(), binaries.count()))
913
for package in chain(sources, binaries):
914
self.logger.debug("Obsoleting %s" % package.displayname)
915
package.requestObsolescence()
917
# Next, ensure that everything is scheduled for deletion. The
918
# dominator will normally leave some superseded publications
919
# uncondemned, for example sources that built NBSed binaries.
920
sources = distroseries.getAllUncondemnedSources()
921
binaries = distroseries.getAllUncondemnedBinaries()
923
"Scheduling deletion of other packages (%d sources, %d binaries)."
924
% (sources.count(), binaries.count()))
925
for package in chain(sources, binaries):
927
"Scheduling deletion of %s" % package.displayname)
928
package.scheduleddeletiondate = UTC_NOW
930
# The packages from both phases will be caught by death row
931
# processing the next time it runs. We skip the domination
932
# phase in the publisher because it won't consider stable
935
def _checkParameters(self, distroseries):
936
"""Sanity check the supplied script parameters."""
937
# Did the user provide a suite name? (distribution defaults
938
# to 'ubuntu' which is fine.)
939
if distroseries == distroseries.distribution.currentseries:
940
# SoyuzScript defaults to the latest series. Since this
941
# will never get obsoleted it's safe to assume that the
942
# user let this option default, so complain and exit.
943
raise SoyuzScriptError(
944
"Please specify a valid distroseries name with -s/--suite "
945
"and which is not the most recent distroseries.")
947
# Is the distroseries in an obsolete state? Bail out now if not.
948
if distroseries.status != SeriesStatus.OBSOLETE:
949
raise SoyuzScriptError(
950
"%s is not at status OBSOLETE." % distroseries.name)
953
class ManageChrootScript(SoyuzScript):
954
"""`SoyuzScript` that manages chroot files."""
956
usage = "%prog -d <distribution> -s <suite> -a <architecture> -f file"
957
description = "Manage the chroot files used by the builders."
958
success_message = "Success."
960
def add_my_options(self):
961
"""Add script options."""
962
SoyuzScript.add_distro_options(self)
963
SoyuzScript.add_transaction_options(self)
964
self.parser.add_option(
965
'-a', '--architecture', dest='architecture', default=None,
966
help='Architecture tag')
967
self.parser.add_option(
968
'-f', '--filepath', dest='filepath', default=None,
969
help='Chroot file path')
972
"""Set up a ChrootManager object and invoke it."""
973
if len(self.args) != 1:
974
raise SoyuzScriptError(
975
"manage-chroot.py <add|update|remove|get>")
979
series = self.location.distroseries
982
distroarchseries = series[self.options.architecture]
983
except NotFoundError, info:
984
raise SoyuzScriptError("Architecture not found: %s" % info)
986
# We don't want to have to force the user to confirm transactions
987
# for manage-chroot.py, so disable that feature of SoyuzScript.
988
self.options.confirm_all = True
991
"Initializing ChrootManager for '%s'" % (distroarchseries.title))
992
chroot_manager = ChrootManager(
993
distroarchseries, filepath=self.options.filepath)
995
if action in chroot_manager.allowed_actions:
996
chroot_action = getattr(chroot_manager, action)
999
"Allowed actions: %s" % chroot_manager.allowed_actions)
1000
raise SoyuzScriptError("Unknown action: %s" % action)
1004
except ChrootManagerError, info:
1005
raise SoyuzScriptError(info)
1007
# Collect extra debug messages from chroot_manager.
1008
for debug_message in chroot_manager._messages:
1009
self.logger.debug(debug_message)
1012
def generate_changes(dsc, dsc_files, suite, changelog, urgency, closes,
1013
lp_closes, section, priority, description,
1014
files_from_librarian, requested_by, origin):
1015
"""Generate a Changes object.
1017
:param dsc: A `Dsc` instance for the related source package.
1018
:param suite: Distribution name
1019
:param changelog: Relevant changelog data
1020
:param urgency: Urgency string (low, medium, high, etc)
1021
:param closes: Sequence of Debian bug numbers (as strings) fixed by
1023
:param section: Debian section
1024
:param priority: Package priority
1027
# XXX cprov 2007-07-03:
1028
# Changed-By can be extracted from most-recent changelog footer,
1032
changes["Origin"] = "%s/%s" % (origin["name"], origin["suite"])
1033
changes["Format"] = "1.7"
1034
changes["Date"] = time.strftime("%a, %d %b %Y %H:%M:%S %z")
1035
changes["Source"] = dsc["source"]
1036
changes["Binary"] = dsc["binary"]
1037
changes["Architecture"] = "source"
1038
changes["Version"] = dsc["version"]
1039
changes["Distribution"] = suite
1040
changes["Urgency"] = urgency
1041
changes["Maintainer"] = dsc["maintainer"]
1042
changes["Changed-By"] = requested_by
1044
changes["Description"] = "\n %s" % description
1046
changes["Closes"] = " ".join(closes)
1048
changes["Launchpad-bugs-fixed"] = " ".join(lp_closes)
1050
for filename in dsc_files:
1051
if filename in files_from_librarian:
1053
files.append({"md5sum": dsc_files[filename]["md5sum"],
1054
"size": dsc_files[filename]["size"],
1056
"priority": priority,
1060
changes["Files"] = files
1061
changes["Changes"] = "\n%s" % changelog