launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06128
[Merge] lp:~cjwatson/launchpad/split-ftpmaster into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/split-ftpmaster into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #75621 in Launchpad itself: "Split ftpmaster helper classes in individual files "
https://bugs.launchpad.net/launchpad/+bug/75621
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/split-ftpmaster/+merge/89193
== Summary ==
Celso noted in 2006 that the package that's now lp.soyuz.scripts.ftpmaster ought to be split up, as it consists of several independent pieces that share no context. It hasn't got a whole lot better since then. I split it up into five new packages:
lp.soyuz.scripts.chrootmanager
lp.soyuz.scripts.obsolete_distroseries
lp.soyuz.scripts.packageremover
lp.soyuz.scripts.pubsourcechecker
lp.soyuz.scripts.querydistro
Picking the package names was a bit awkward, as lp.soyuz.scripts has a mix of words run together and words separated by underscores. obsolete_distroseries was by analogy with initialize_distroseries, but for the rest it seemed to feel more natural to run words together.
The splitting was fairly mechanical and aided by pyflakes.vim to cut the imports down afterwards.
== lint ==
Some remaining long lines in lib/lp/soyuz/doc/manage-chroot.txt which I'm not sure if it's worth cleaning up.
--
https://code.launchpad.net/~cjwatson/launchpad/split-ftpmaster/+merge/89193
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/split-ftpmaster into lp:launchpad.
=== modified file 'lib/lp/archivepublisher/scripts/generate_contents_files.py'
--- lib/lp/archivepublisher/scripts/generate_contents_files.py 2012-01-01 02:58:52 +0000
+++ lib/lp/archivepublisher/scripts/generate_contents_files.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd. This software is licensed under the
+# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Archive Contents files generator."""
@@ -31,7 +31,7 @@
DatabaseBlockedPolicy,
SlaveOnlyDatabasePolicy,
)
-from lp.soyuz.scripts.ftpmaster import LpQueryDistro
+from lp.soyuz.scripts.querydistro import LpQueryDistro
COMPONENTS = [
=== modified file 'lib/lp/archivepublisher/scripts/publish_ftpmaster.py'
--- lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2012-01-01 02:58:52 +0000
+++ lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd. This software is licensed under the
+# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Master distro publishing script."""
@@ -28,7 +28,7 @@
from lp.services.utils import file_exists
from lp.soyuz.enums import ArchivePurpose
from lp.soyuz.scripts.custom_uploads_copier import CustomUploadsCopier
-from lp.soyuz.scripts.ftpmaster import LpQueryDistro
+from lp.soyuz.scripts.querydistro import LpQueryDistro
from lp.soyuz.scripts.processaccepted import ProcessAccepted
from lp.soyuz.scripts.publishdistro import PublishDistro
=== modified file 'lib/lp/scripts/utilities/importfascist.py'
--- lib/lp/scripts/utilities/importfascist.py 2011-12-30 01:48:17 +0000
+++ lib/lp/scripts/utilities/importfascist.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
import __builtin__
@@ -47,7 +47,6 @@
warned_database_imports = text_lines_to_set("""
- lp.soyuz.scripts.ftpmaster
lp.soyuz.scripts.gina.handlers
lp.registry.browser.distroseries
lp.translations.scripts.po_import
=== modified file 'lib/lp/soyuz/doc/manage-chroot.txt'
--- lib/lp/soyuz/doc/manage-chroot.txt 2012-01-06 11:08:30 +0000
+++ lib/lp/soyuz/doc/manage-chroot.txt 2012-01-19 08:38:27 +0000
@@ -1,4 +1,6 @@
-= Manage-Chroot Tool =
+==================
+Manage-Chroot Tool
+==================
This tool is used to add or update chroots for suites (distroseries)
and DistroArchSeries.
@@ -35,7 +37,7 @@
parameters.
>>> from lp.services.log.logger import FakeLogger
- >>> from lp.soyuz.scripts.ftpmaster import ManageChrootScript
+ >>> from lp.soyuz.scripts.chrootmanager import ManageChrootScript
>>> def getScriptObject(command, distribution='ubuntu', suite='hoary',
... arch='i386', filepath=filepath):
... test_args = ['-s', suite,
@@ -92,7 +94,7 @@
ERROR Allowed actions: ['add', 'update', 'remove', 'get']
Unknown action: bogus
-Specifiying a bad architecture results in an error.
+Specifying a bad architecture results in an error.
>>> manage_chroot = getScriptObject("add", arch="bogus")
>>> try:
@@ -176,7 +178,7 @@
DEBUG Initializing ChrootManager for 'The Hoary Hedgehog Release for i386 (x86)'
Chroot was deleted.
- >>> os.remove(filepath)
+ >>> os.remove(filepath)
When the librarian is not running, attempting to upload a chroot file
results in an appropriate error.
=== added file 'lib/lp/soyuz/scripts/chrootmanager.py'
--- lib/lp/soyuz/scripts/chrootmanager.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/scripts/chrootmanager.py 2012-01-19 08:38:27 +0000
@@ -0,0 +1,230 @@
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Chroot management utilities."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'ChrootManager',
+ 'ChrootManagerError',
+ 'ManageChrootScript',
+ ]
+
+import os
+
+from zope.component import getUtility
+
+from lp.app.errors import NotFoundError
+from lp.services.helpers import filenameToContentType
+from lp.services.librarian.interfaces import ILibraryFileAliasSet
+from lp.services.librarian.interfaces.client import (
+ ILibrarianClient,
+ UploadFailed,
+ )
+from lp.services.librarian.utils import copy_and_close
+from lp.soyuz.scripts.ftpmasterbase import (
+ SoyuzScript,
+ SoyuzScriptError,
+ )
+
+
+class ChrootManagerError(Exception):
+ """Any error generated during the ChrootManager procedures."""
+
+
+class ChrootManager:
+ """Chroot actions wrapper.
+
+ The 'distroarchseries' argument is mandatory and 'filepath' is
+ optional.
+
+ 'filepath' is required by some allowed actions as source or destination,
+
+ ChrootManagerError will be raised if anything wrong occurred in this
+ class, things like missing parameter or infrastructure pieces not in
+ place.
+ """
+
+ allowed_actions = ['add', 'update', 'remove', 'get']
+
+ def __init__(self, distroarchseries, filepath=None):
+ self.distroarchseries = distroarchseries
+ self.filepath = filepath
+ self._messages = []
+
+ def _upload(self):
+ """Upload the self.filepath contents to Librarian.
+
+ Return the respective ILibraryFileAlias instance.
+ Raises ChrootManagerError if it could not be found.
+ """
+ try:
+ fd = open(self.filepath)
+ except IOError:
+ raise ChrootManagerError('Could not open: %s' % self.filepath)
+
+ flen = os.stat(self.filepath).st_size
+ filename = os.path.basename(self.filepath)
+ ftype = filenameToContentType(filename)
+
+ try:
+ alias_id = getUtility(ILibrarianClient).addFile(
+ filename, flen, fd, contentType=ftype)
+ except UploadFailed, info:
+ raise ChrootManagerError("Librarian upload failed: %s" % info)
+
+ lfa = getUtility(ILibraryFileAliasSet)[alias_id]
+
+ self._messages.append(
+ "LibraryFileAlias: %d, %s bytes, %s"
+ % (lfa.id, lfa.content.filesize, lfa.content.md5))
+
+ return lfa
+
+ def _getPocketChroot(self):
+ """Retrive PocketChroot record.
+
+ Return the respective IPocketChroot instance.
+ Raises ChrootManagerError if it could not be found.
+ """
+ pocket_chroot = self.distroarchseries.getPocketChroot()
+ if pocket_chroot is None:
+ raise ChrootManagerError(
+ 'Could not find chroot for %s'
+ % (self.distroarchseries.title))
+
+ self._messages.append(
+ "PocketChroot for '%s' (%d) retrieved."
+ % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
+
+ return pocket_chroot
+
+ def _update(self):
+ """Base method for add and update action."""
+ if self.filepath is None:
+ raise ChrootManagerError('Missing local chroot file path.')
+ alias = self._upload()
+ return self.distroarchseries.addOrUpdateChroot(alias)
+
+ def add(self):
+ """Create a new PocketChroot record.
+
+ Raises ChrootManagerError if self.filepath isn't set.
+ Update of pre-existing PocketChroot record will be automatically
+ handled.
+ It's a bind to the self.update method.
+ """
+ pocket_chroot = self._update()
+ self._messages.append(
+ "PocketChroot for '%s' (%d) added."
+ % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
+
+ def update(self):
+ """Update a PocketChroot record.
+
+ Raises ChrootManagerError if filepath isn't set
+ Creation of non-existing PocketChroot records will be automatically
+ handled.
+ """
+ pocket_chroot = self._update()
+ self._messages.append(
+ "PocketChroot for '%s' (%d) updated."
+ % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
+
+ def remove(self):
+ """Overwrite existing PocketChroot file to none.
+
+ Raises ChrootManagerError if the chroot record isn't found.
+ """
+ pocket_chroot = self._getPocketChroot()
+ self.distroarchseries.addOrUpdateChroot(None)
+ self._messages.append(
+ "PocketChroot for '%s' (%d) removed."
+ % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
+
+ def get(self):
+ """Download chroot file from Librarian and store."""
+ pocket_chroot = self._getPocketChroot()
+
+ if self.filepath is None:
+ abs_filepath = os.path.abspath(pocket_chroot.chroot.filename)
+ if os.path.exists(abs_filepath):
+ raise ChrootManagerError(
+ 'cannot overwrite %s' % abs_filepath)
+ self._messages.append(
+ "Writing to '%s'." % abs_filepath)
+ local_file = open(pocket_chroot.chroot.filename, "w")
+ else:
+ abs_filepath = os.path.abspath(self.filepath)
+ if os.path.exists(abs_filepath):
+ raise ChrootManagerError(
+ 'cannot overwrite %s' % abs_filepath)
+ self._messages.append(
+ "Writing to '%s'." % abs_filepath)
+ local_file = open(abs_filepath, "w")
+
+ if pocket_chroot.chroot is None:
+ raise ChrootManagerError('Chroot was deleted.')
+
+ pocket_chroot.chroot.open()
+ copy_and_close(pocket_chroot.chroot, local_file)
+
+
+class ManageChrootScript(SoyuzScript):
+ """`SoyuzScript` that manages chroot files."""
+
+ usage = "%prog -d <distribution> -s <suite> -a <architecture> -f file"
+ description = "Manage the chroot files used by the builders."
+ success_message = "Success."
+
+ def add_my_options(self):
+ """Add script options."""
+ SoyuzScript.add_distro_options(self)
+ SoyuzScript.add_transaction_options(self)
+ self.parser.add_option(
+ '-a', '--architecture', dest='architecture', default=None,
+ help='Architecture tag')
+ self.parser.add_option(
+ '-f', '--filepath', dest='filepath', default=None,
+ help='Chroot file path')
+
+ def mainTask(self):
+ """Set up a ChrootManager object and invoke it."""
+ if len(self.args) != 1:
+ raise SoyuzScriptError(
+ "manage-chroot.py <add|update|remove|get>")
+
+ [action] = self.args
+
+ series = self.location.distroseries
+
+ try:
+ distroarchseries = series[self.options.architecture]
+ except NotFoundError, info:
+ raise SoyuzScriptError("Architecture not found: %s" % info)
+
+ # We don't want to have to force the user to confirm transactions
+ # for manage-chroot.py, so disable that feature of SoyuzScript.
+ self.options.confirm_all = True
+
+ self.logger.debug(
+ "Initializing ChrootManager for '%s'" % (distroarchseries.title))
+ chroot_manager = ChrootManager(
+ distroarchseries, filepath=self.options.filepath)
+
+ if action in chroot_manager.allowed_actions:
+ chroot_action = getattr(chroot_manager, action)
+ else:
+ self.logger.error(
+ "Allowed actions: %s" % chroot_manager.allowed_actions)
+ raise SoyuzScriptError("Unknown action: %s" % action)
+
+ try:
+ chroot_action()
+ except ChrootManagerError, info:
+ raise SoyuzScriptError(info)
+ else:
+ # Collect extra debug messages from chroot_manager.
+ for debug_message in chroot_manager._messages:
+ self.logger.debug(debug_message)
=== removed file 'lib/lp/soyuz/scripts/ftpmaster.py'
--- lib/lp/soyuz/scripts/ftpmaster.py 2012-01-11 11:50:01 +0000
+++ lib/lp/soyuz/scripts/ftpmaster.py 1970-01-01 00:00:00 +0000
@@ -1,868 +0,0 @@
-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""FTPMaster utilities."""
-
-__metaclass__ = type
-
-__all__ = [
- 'ChrootManager',
- 'ChrootManagerError',
- 'LpQueryDistro',
- 'ManageChrootScript',
- 'ObsoleteDistroseries',
- 'PackageRemover',
- 'PubSourceChecker',
- ]
-
-from itertools import chain
-import os
-
-from zope.component import getUtility
-
-from lp.app.errors import NotFoundError
-from lp.registry.interfaces.person import IPersonSet
-from lp.registry.interfaces.pocket import pocketsuffix
-from lp.registry.interfaces.series import SeriesStatus
-from lp.services.browser_helpers import get_plural_text
-from lp.services.database.constants import UTC_NOW
-from lp.services.helpers import filenameToContentType
-from lp.services.librarian.interfaces import ILibraryFileAliasSet
-from lp.services.librarian.interfaces.client import (
- ILibrarianClient,
- UploadFailed,
- )
-from lp.services.librarian.utils import copy_and_close
-from lp.services.scripts.base import (
- LaunchpadScript,
- LaunchpadScriptFailure,
- )
-from lp.soyuz.adapters.packagelocation import (
- build_package_location,
- PackageLocationError,
- )
-from lp.soyuz.enums import PackagePublishingStatus
-from lp.soyuz.scripts.ftpmasterbase import (
- SoyuzScript,
- SoyuzScriptError,
- )
-
-
-class PubBinaryContent:
- """Binary publication container.
-
- Currently used for auxiliary storage in PubSourceChecker.
- """
-
- def __init__(self, name, version, arch, component, section, priority):
- self.name = name
- self.version = version
- self.arch = arch
- self.component = component
- self.section = section
- self.priority = priority
- self.messages = []
-
- def warn(self, message):
- """Append a warning in the message list."""
- self.messages.append('W: %s' % message)
-
- def error(self, message):
- """Append a error in the message list."""
- self.messages.append('E: %s' % message)
-
- def renderReport(self):
- """Render a report with the appended messages (self.messages).
-
- Return None if no message was found, otherwise return
- a properly formatted string, including
-
- <TAB>BinaryName_Version Arch Component/Section/Priority
- <TAB><TAB>MESSAGE
- """
- if not len(self.messages):
- return
-
- report = [('\t%s_%s %s %s/%s/%s'
- % (self.name, self.version, self.arch,
- self.component, self.section, self.priority))]
-
- for message in self.messages:
- report.append('\t\t%s' % message)
-
- return "\n".join(report)
-
-
-class PubBinaryDetails:
- """Store the component, section and priority of binary packages and, for
- each binary package the most frequent component, section and priority.
-
- These are stored in the following attributes:
-
- - components: A dictionary mapping binary package names to other
- dictionaries mapping component names to binary packages published
- in this component.
- - sections: The same as components, but for sections.
- - priorities: The same as components, but for priorities.
- - correct_components: a dictionary mapping binary package name
- to the most frequent (considered the correct) component name.
- - correct_sections: same as correct_components, but for sections
- - correct_priorities: same as correct_components, but for priorities
- """
-
- def __init__(self):
- self.components = {}
- self.sections = {}
- self.priorities = {}
- self.correct_components = {}
- self.correct_sections = {}
- self.correct_priorities = {}
-
- def addBinaryDetails(self, bin):
- """Include a binary publication and update internal registers."""
- name_components = self.components.setdefault(bin.name, {})
- bin_component = name_components.setdefault(bin.component, [])
- bin_component.append(bin)
-
- name_sections = self.sections.setdefault(bin.name, {})
- bin_section = name_sections.setdefault(bin.section, [])
- bin_section.append(bin)
-
- name_priorities = self.priorities.setdefault(bin.name, {})
- bin_priority = name_priorities.setdefault(bin.priority, [])
- bin_priority.append(bin)
-
- def _getMostFrequentValue(self, data):
- """Return a dict of name and the most frequent value.
-
- Used for self.{components, sections, priorities}
- """
- results = {}
-
- for name, items in data.iteritems():
- highest = 0
- for item, occurrences in items.iteritems():
- if len(occurrences) > highest:
- highest = len(occurrences)
- results[name] = item
-
- return results
-
- def setCorrectValues(self):
- """Find out the correct values for the same binary name
-
- Consider correct the most frequent.
- """
- self.correct_components = self._getMostFrequentValue(self.components)
- self.correct_sections = self._getMostFrequentValue(self.sections)
- self.correct_priorities = self._getMostFrequentValue(self.priorities)
-
-
-class PubSourceChecker:
- """Map and probe a Source/Binaries publication couple.
-
- Receive the source publication data and its binaries and perform
- a group of heuristic consistency checks.
- """
-
- def __init__(self, name, version, component, section, urgency):
- self.name = name
- self.version = version
- self.component = component
- self.section = section
- self.urgency = urgency
- self.binaries = []
- self.binaries_details = PubBinaryDetails()
-
- def addBinary(self, name, version, architecture, component, section,
- priority):
- """Append the binary data to the current publication list."""
- bin = PubBinaryContent(
- name, version, architecture, component, section, priority)
-
- self.binaries.append(bin)
-
- self.binaries_details.addBinaryDetails(bin)
-
- def check(self):
- """Setup check environment and perform the required checks."""
- self.binaries_details.setCorrectValues()
-
- for bin in self.binaries:
- self._checkComponent(bin)
- self._checkSection(bin)
- self._checkPriority(bin)
-
- def _checkComponent(self, bin):
- """Check if the binary component matches the correct component.
-
- 'correct' is the most frequent component in this binary package
- group
- """
- correct_component = self.binaries_details.correct_components[bin.name]
- if bin.component != correct_component:
- bin.warn('Component mismatch: %s != %s'
- % (bin.component, correct_component))
-
- def _checkSection(self, bin):
- """Check if the binary section matches the correct section.
-
- 'correct' is the most frequent section in this binary package
- group
- """
- correct_section = self.binaries_details.correct_sections[bin.name]
- if bin.section != correct_section:
- bin.warn('Section mismatch: %s != %s'
- % (bin.section, correct_section))
-
- def _checkPriority(self, bin):
- """Check if the binary priority matches the correct priority.
-
- 'correct' is the most frequent priority in this binary package
- group
- """
- correct_priority = self.binaries_details.correct_priorities[bin.name]
- if bin.priority != correct_priority:
- bin.warn('Priority mismatch: %s != %s'
- % (bin.priority, correct_priority))
-
- def renderReport(self):
- """Render a formatted report for the publication group.
-
- Return None if no issue was annotated or an formatted string
- including:
-
- SourceName_Version Component/Section/Urgency | # bin
- <BINREPORTS>
- """
- report = []
-
- for bin in self.binaries:
- bin_report = bin.renderReport()
- if bin_report:
- report.append(bin_report)
-
- if not len(report):
- return
-
- result = [('%s_%s %s/%s/%s | %s bin'
- % (self.name, self.version, self.component,
- self.section, self.urgency, len(self.binaries)))]
-
- result.extend(report)
-
- return "\n".join(result)
-
-
-class ChrootManagerError(Exception):
- """Any error generated during the ChrootManager procedures."""
-
-
-class ChrootManager:
- """Chroot actions wrapper.
-
- The 'distroarchseries' argument is mandatory and 'filepath' is
- optional.
-
- 'filepath' is required by some allowed actions as source or destination,
-
- ChrootManagerError will be raised if anything wrong occurred in this
- class, things like missing parameter or infrastructure pieces not in
- place.
- """
-
- allowed_actions = ['add', 'update', 'remove', 'get']
-
- def __init__(self, distroarchseries, filepath=None):
- self.distroarchseries = distroarchseries
- self.filepath = filepath
- self._messages = []
-
- def _upload(self):
- """Upload the self.filepath contents to Librarian.
-
- Return the respective ILibraryFileAlias instance.
- Raises ChrootManagerError if it could not be found.
- """
- try:
- fd = open(self.filepath)
- except IOError:
- raise ChrootManagerError('Could not open: %s' % self.filepath)
-
- flen = os.stat(self.filepath).st_size
- filename = os.path.basename(self.filepath)
- ftype = filenameToContentType(filename)
-
- try:
- alias_id = getUtility(ILibrarianClient).addFile(
- filename, flen, fd, contentType=ftype)
- except UploadFailed, info:
- raise ChrootManagerError("Librarian upload failed: %s" % info)
-
- lfa = getUtility(ILibraryFileAliasSet)[alias_id]
-
- self._messages.append(
- "LibraryFileAlias: %d, %s bytes, %s"
- % (lfa.id, lfa.content.filesize, lfa.content.md5))
-
- return lfa
-
- def _getPocketChroot(self):
- """Retrive PocketChroot record.
-
- Return the respective IPocketChroot instance.
- Raises ChrootManagerError if it could not be found.
- """
- pocket_chroot = self.distroarchseries.getPocketChroot()
- if pocket_chroot is None:
- raise ChrootManagerError(
- 'Could not find chroot for %s'
- % (self.distroarchseries.title))
-
- self._messages.append(
- "PocketChroot for '%s' (%d) retrieved."
- % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
-
- return pocket_chroot
-
- def _update(self):
- """Base method for add and update action."""
- if self.filepath is None:
- raise ChrootManagerError('Missing local chroot file path.')
- alias = self._upload()
- return self.distroarchseries.addOrUpdateChroot(alias)
-
- def add(self):
- """Create a new PocketChroot record.
-
- Raises ChrootManagerError if self.filepath isn't set.
- Update of pre-existing PocketChroot record will be automatically
- handled.
- It's a bind to the self.update method.
- """
- pocket_chroot = self._update()
- self._messages.append(
- "PocketChroot for '%s' (%d) added."
- % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
-
- def update(self):
- """Update a PocketChroot record.
-
- Raises ChrootManagerError if filepath isn't set
- Creation of non-existing PocketChroot records will be automatically
- handled.
- """
- pocket_chroot = self._update()
- self._messages.append(
- "PocketChroot for '%s' (%d) updated."
- % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
-
- def remove(self):
- """Overwrite existing PocketChroot file to none.
-
- Raises ChrootManagerError if the chroot record isn't found.
- """
- pocket_chroot = self._getPocketChroot()
- self.distroarchseries.addOrUpdateChroot(None)
- self._messages.append(
- "PocketChroot for '%s' (%d) removed."
- % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
-
- def get(self):
- """Download chroot file from Librarian and store."""
- pocket_chroot = self._getPocketChroot()
-
- if self.filepath is None:
- abs_filepath = os.path.abspath(pocket_chroot.chroot.filename)
- if os.path.exists(abs_filepath):
- raise ChrootManagerError(
- 'cannot overwrite %s' % abs_filepath)
- self._messages.append(
- "Writing to '%s'." % abs_filepath)
- local_file = open(pocket_chroot.chroot.filename, "w")
- else:
- abs_filepath = os.path.abspath(self.filepath)
- if os.path.exists(abs_filepath):
- raise ChrootManagerError(
- 'cannot overwrite %s' % abs_filepath)
- self._messages.append(
- "Writing to '%s'." % abs_filepath)
- local_file = open(abs_filepath, "w")
-
- if pocket_chroot.chroot is None:
- raise ChrootManagerError('Chroot was deleted.')
-
- pocket_chroot.chroot.open()
- copy_and_close(pocket_chroot.chroot, local_file)
-
-
-class LpQueryDistro(LaunchpadScript):
- """Main class for scripts/ftpmaster-tools/lp-query-distro.py."""
-
- def __init__(self, *args, **kwargs):
- """Initialize dynamic 'usage' message and LaunchpadScript parent.
-
- Also initialize the list 'allowed_arguments'.
- """
- self.allowed_actions = [
- 'current', 'development', 'supported', 'pending_suites', 'archs',
- 'official_archs', 'nominated_arch_indep', 'pocket_suffixes']
- self.usage = '%%prog <%s>' % ' | '.join(self.allowed_actions)
- LaunchpadScript.__init__(self, *args, **kwargs)
-
- def add_my_options(self):
- """Add 'distribution' and 'suite' context options."""
- self.parser.add_option(
- '-d', '--distribution', dest='distribution_name',
- default='ubuntu', help='Context distribution name.')
- self.parser.add_option(
- '-s', '--suite', dest='suite', default=None,
- help='Context suite name.')
-
- def main(self):
- """Main procedure, basically a runAction wrapper.
-
- Execute the given and allowed action using the default presenter
- (see self.runAction for further information).
- """
- self.runAction()
-
- def _buildLocation(self):
- """Build a PackageLocation object
-
- The location will correspond to the given 'distribution' and 'suite',
- Any PackageLocationError occurring at this point will be masked into
- LaunchpadScriptFailure.
- """
- try:
- self.location = build_package_location(
- distribution_name=self.options.distribution_name,
- suite=self.options.suite)
- except PackageLocationError, err:
- raise LaunchpadScriptFailure(err)
-
- def defaultPresenter(self, result):
- """Default result presenter.
-
- Directly prints result in the standard output (print).
- """
- print result
-
- def runAction(self, presenter=None):
- """Run a given initialized action (self.action_name).
-
- It accepts an optional 'presenter' which will be used to
- store/present the action result.
-
- Ensure at least one argument was passed, known as 'action'.
- Verify if the given 'action' is listed as an 'allowed_action'.
- Raise LaunchpadScriptFailure if those requirements were not
- accomplished.
-
- It builds context 'location' object (see self._buildLocation).
-
- It may raise LaunchpadScriptFailure is the 'action' is not properly
- supported by the current code (missing corresponding property).
- """
- if presenter is None:
- presenter = self.defaultPresenter
-
- if len(self.args) != 1:
- raise LaunchpadScriptFailure('<action> is required')
-
- [self.action_name] = self.args
-
- if self.action_name not in self.allowed_actions:
- raise LaunchpadScriptFailure(
- 'Action "%s" is not supported' % self.action_name)
-
- self._buildLocation()
-
- try:
- action_result = getattr(self, self.action_name)
- except AttributeError:
- raise AssertionError(
- "No handler found for action '%s'" % self.action_name)
-
- presenter(action_result)
-
- def checkNoSuiteDefined(self):
- """Raises LaunchpadScriptError if a suite location was passed.
-
- It is re-used in action properties to avoid conflicting contexts,
- i.e, passing an arbitrary 'suite' and asking for the CURRENT suite
- in the context distribution.
- """
- if self.options.suite is not None:
- raise LaunchpadScriptFailure(
- "Action does not accept defined suite.")
-
- @property
- def current(self):
- """Return the name of the CURRENT distroseries.
-
- It is restricted for the context distribution.
-
- It may raise LaunchpadScriptFailure if a suite was passed on the
- command-line or if not CURRENT distroseries was found.
- """
- self.checkNoSuiteDefined()
- series = self.location.distribution.getSeriesByStatus(
- SeriesStatus.CURRENT)
- if not series:
- raise LaunchpadScriptFailure("No CURRENT series.")
-
- return series[0].name
-
- @property
- def development(self):
- """Return the name of the DEVELOPMENT distroseries.
-
- It is restricted for the context distribution.
-
- It may raise `LaunchpadScriptFailure` if a suite was passed on the
- command-line.
-
- Return the first FROZEN distroseries found if there is no
- DEVELOPMENT one available.
-
- Raises `NotFoundError` if neither a CURRENT nor a FROZEN
- candidate could be found.
- """
- self.checkNoSuiteDefined()
- series = None
- wanted_status = (SeriesStatus.DEVELOPMENT,
- SeriesStatus.FROZEN)
- for status in wanted_status:
- series = self.location.distribution.getSeriesByStatus(status)
- if series.count() > 0:
- break
- else:
- raise LaunchpadScriptFailure(
- 'There is no DEVELOPMENT distroseries for %s' %
- self.location.distribution.name)
- return series[0].name
-
- @property
- def supported(self):
- """Return the names of the distroseries currently supported.
-
- 'supported' means not EXPERIMENTAL or OBSOLETE.
-
- It is restricted for the context distribution.
-
- It may raise `LaunchpadScriptFailure` if a suite was passed on the
- command-line or if there is not supported distroseries for the
- distribution given.
-
- Return a space-separated list of distroseries names.
- """
- self.checkNoSuiteDefined()
- supported_series = []
- unsupported_status = (SeriesStatus.EXPERIMENTAL,
- SeriesStatus.OBSOLETE)
- for distroseries in self.location.distribution:
- if distroseries.status not in unsupported_status:
- supported_series.append(distroseries.name)
-
- if not supported_series:
- raise LaunchpadScriptFailure(
- 'There is no supported distroseries for %s' %
- self.location.distribution.name)
-
- return " ".join(supported_series)
-
- @property
- def pending_suites(self):
- """Return the suite names containing PENDING publication.
-
- It check for sources and/or binary publications.
- """
- self.checkNoSuiteDefined()
- pending_suites = set()
- pending_sources = self.location.archive.getPublishedSources(
- status=PackagePublishingStatus.PENDING)
- for pub in pending_sources:
- pending_suites.add((pub.distroseries, pub.pocket))
-
- pending_binaries = self.location.archive.getAllPublishedBinaries(
- status=PackagePublishingStatus.PENDING)
- for pub in pending_binaries:
- pending_suites.add(
- (pub.distroarchseries.distroseries, pub.pocket))
-
- return " ".join([distroseries.name + pocketsuffix[pocket]
- for distroseries, pocket in pending_suites])
-
- @property
- def archs(self):
- """Return a space-separated list of architecture tags.
-
- It is restricted for the context distribution and suite.
- """
- architectures = self.location.distroseries.architectures
- return " ".join(arch.architecturetag for arch in architectures)
-
- @property
- def official_archs(self):
- """Return a space-separated list of official architecture tags.
-
- It is restricted to the context distribution and suite.
- """
- architectures = self.location.distroseries.architectures
- return " ".join(arch.architecturetag
- for arch in architectures
- if arch.official)
-
- @property
- def nominated_arch_indep(self):
- """Return the nominated arch indep architecture tag.
-
- It is restricted to the context distribution and suite.
- """
- series = self.location.distroseries
- return series.nominatedarchindep.architecturetag
-
- @property
- def pocket_suffixes(self):
- """Return a space-separated list of non-empty pocket suffixes.
-
- The RELEASE pocket (whose suffix is the empty string) is omitted.
-
- The returned space-separated string is ordered alphabetically.
- """
- sorted_non_empty_suffixes = sorted(
- suffix for suffix in pocketsuffix.values() if suffix != '')
- return " ".join(sorted_non_empty_suffixes)
-
-
-class PackageRemover(SoyuzScript):
- """SoyuzScript implementation for published package removal.."""
-
- usage = '%prog -s warty mozilla-firefox'
- description = 'REMOVE a published package.'
- success_message = (
- "The archive will be updated in the next publishing cycle.")
-
- def add_my_options(self):
- """Adding local options."""
- # XXX cprov 20071025: we need a hook for loading SoyuzScript default
- # options automatically. This is ugly.
- SoyuzScript.add_my_options(self)
-
- # Mode options.
- self.parser.add_option("-b", "--binary", dest="binaryonly",
- default=False, action="store_true",
- help="Remove binaries only.")
- self.parser.add_option("-S", "--source-only", dest="sourceonly",
- default=False, action="store_true",
- help="Remove source only.")
-
- # Removal information options.
- self.parser.add_option("-u", "--user", dest="user",
- help="Launchpad user name.")
- self.parser.add_option("-m", "--removal_comment",
- dest="removal_comment",
- help="Removal comment")
-
- def mainTask(self):
- """Execute the package removal task.
-
- Build location and target objects.
-
- Can raise SoyuzScriptError.
- """
- if len(self.args) == 0:
- raise SoyuzScriptError(
- "At least one non-option argument must be given, "
- "a package name to be removed.")
-
- if self.options.user is None:
- raise SoyuzScriptError("Launchpad username must be given.")
-
- if self.options.removal_comment is None:
- raise SoyuzScriptError("Removal comment must be given.")
-
- removed_by = getUtility(IPersonSet).getByName(self.options.user)
- if removed_by is None:
- raise SoyuzScriptError(
- "Invalid launchpad username: %s" % self.options.user)
-
- removables = []
- for packagename in self.args:
- if self.options.binaryonly:
- removables.extend(
- self.findLatestPublishedBinaries(packagename))
- elif self.options.sourceonly:
- removables.append(self.findLatestPublishedSource(packagename))
- else:
- source_pub = self.findLatestPublishedSource(packagename)
- removables.append(source_pub)
- removables.extend(source_pub.getPublishedBinaries())
-
- self.logger.info("Removing candidates:")
- for removable in removables:
- self.logger.info('\t%s', removable.displayname)
-
- self.logger.info("Removed-by: %s", removed_by.displayname)
- self.logger.info("Comment: %s", self.options.removal_comment)
-
- removals = []
- for removable in removables:
- removable.requestDeletion(
- removed_by=removed_by,
- removal_comment=self.options.removal_comment)
- removals.append(removable)
-
- if len(removals) == 0:
- self.logger.info("No package removed (bug ?!?).")
- else:
- self.logger.info(
- "%d %s successfully removed.", len(removals),
- get_plural_text(len(removals), "package", "packages"))
-
- # Information returned mainly for the benefit of the test harness.
- return removals
-
-
-class ObsoleteDistroseries(SoyuzScript):
- """`SoyuzScript` that obsoletes a distroseries."""
-
- usage = "%prog -d <distribution> -s <suite>"
- description = ("Make obsolete (schedule for removal) packages in an "
- "obsolete distroseries.")
-
- def add_my_options(self):
- """Add -d, -s, dry-run and confirmation options."""
- SoyuzScript.add_distro_options(self)
- SoyuzScript.add_transaction_options(self)
-
- def mainTask(self):
- """Execute package obsolescence procedure.
-
- Modules using this class outside of its normal usage in the
- main script can call this method to start the copy.
-
- In this case the caller can override test_args on __init__
- to set the command line arguments.
-
- :raise SoyuzScriptError: If the distroseries is not provided or
- it is already obsolete.
- """
- assert self.location, (
- "location is not available, call SoyuzScript.setupLocation() "
- "before calling mainTask().")
-
- # Shortcut variable name to reduce long lines.
- distroseries = self.location.distroseries
-
- self._checkParameters(distroseries)
-
- self.logger.info("Obsoleting all packages for distroseries %s in "
- "the %s distribution." % (
- distroseries.name,
- distroseries.distribution.name))
-
- # First, mark all Published sources as Obsolete.
- sources = distroseries.getAllPublishedSources()
- binaries = distroseries.getAllPublishedBinaries()
- self.logger.info(
- "Obsoleting published packages (%d sources, %d binaries)."
- % (sources.count(), binaries.count()))
- for package in chain(sources, binaries):
- self.logger.debug("Obsoleting %s" % package.displayname)
- package.requestObsolescence()
-
- # Next, ensure that everything is scheduled for deletion. The
- # dominator will normally leave some superseded publications
- # uncondemned, for example sources that built NBSed binaries.
- sources = distroseries.getAllUncondemnedSources()
- binaries = distroseries.getAllUncondemnedBinaries()
- self.logger.info(
- "Scheduling deletion of other packages (%d sources, %d binaries)."
- % (sources.count(), binaries.count()))
- for package in chain(sources, binaries):
- self.logger.debug(
- "Scheduling deletion of %s" % package.displayname)
- package.scheduleddeletiondate = UTC_NOW
-
- # The packages from both phases will be caught by death row
- # processing the next time it runs. We skip the domination
- # phase in the publisher because it won't consider stable
- # distroseries.
-
- def _checkParameters(self, distroseries):
- """Sanity check the supplied script parameters."""
- # Did the user provide a suite name? (distribution defaults
- # to 'ubuntu' which is fine.)
- if distroseries == distroseries.distribution.currentseries:
- # SoyuzScript defaults to the latest series. Since this
- # will never get obsoleted it's safe to assume that the
- # user let this option default, so complain and exit.
- raise SoyuzScriptError(
- "Please specify a valid distroseries name with -s/--suite "
- "and which is not the most recent distroseries.")
-
- # Is the distroseries in an obsolete state? Bail out now if not.
- if distroseries.status != SeriesStatus.OBSOLETE:
- raise SoyuzScriptError(
- "%s is not at status OBSOLETE." % distroseries.name)
-
-
-class ManageChrootScript(SoyuzScript):
- """`SoyuzScript` that manages chroot files."""
-
- usage = "%prog -d <distribution> -s <suite> -a <architecture> -f file"
- description = "Manage the chroot files used by the builders."
- success_message = "Success."
-
- def add_my_options(self):
- """Add script options."""
- SoyuzScript.add_distro_options(self)
- SoyuzScript.add_transaction_options(self)
- self.parser.add_option(
- '-a', '--architecture', dest='architecture', default=None,
- help='Architecture tag')
- self.parser.add_option(
- '-f', '--filepath', dest='filepath', default=None,
- help='Chroot file path')
-
- def mainTask(self):
- """Set up a ChrootManager object and invoke it."""
- if len(self.args) != 1:
- raise SoyuzScriptError(
- "manage-chroot.py <add|update|remove|get>")
-
- [action] = self.args
-
- series = self.location.distroseries
-
- try:
- distroarchseries = series[self.options.architecture]
- except NotFoundError, info:
- raise SoyuzScriptError("Architecture not found: %s" % info)
-
- # We don't want to have to force the user to confirm transactions
- # for manage-chroot.py, so disable that feature of SoyuzScript.
- self.options.confirm_all = True
-
- self.logger.debug(
- "Initializing ChrootManager for '%s'" % (distroarchseries.title))
- chroot_manager = ChrootManager(
- distroarchseries, filepath=self.options.filepath)
-
- if action in chroot_manager.allowed_actions:
- chroot_action = getattr(chroot_manager, action)
- else:
- self.logger.error(
- "Allowed actions: %s" % chroot_manager.allowed_actions)
- raise SoyuzScriptError("Unknown action: %s" % action)
-
- try:
- chroot_action()
- except ChrootManagerError, info:
- raise SoyuzScriptError(info)
- else:
- # Collect extra debug messages from chroot_manager.
- for debug_message in chroot_manager._messages:
- self.logger.debug(debug_message)
=== added file 'lib/lp/soyuz/scripts/obsolete_distroseries.py'
--- lib/lp/soyuz/scripts/obsolete_distroseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/scripts/obsolete_distroseries.py 2012-01-19 08:38:27 +0000
@@ -0,0 +1,101 @@
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Make a distroseries obsolete."""
+
+__metaclass__ = type
+
+__all__ = ['ObsoleteDistroseries']
+
+from itertools import chain
+
+from lp.registry.interfaces.series import SeriesStatus
+from lp.services.database.constants import UTC_NOW
+from lp.soyuz.scripts.ftpmasterbase import (
+ SoyuzScript,
+ SoyuzScriptError,
+ )
+
+
+class ObsoleteDistroseries(SoyuzScript):
+ """`SoyuzScript` that obsoletes a distroseries."""
+
+ usage = "%prog -d <distribution> -s <suite>"
+ description = ("Make obsolete (schedule for removal) packages in an "
+ "obsolete distroseries.")
+
+ def add_my_options(self):
+ """Add -d, -s, dry-run and confirmation options."""
+ SoyuzScript.add_distro_options(self)
+ SoyuzScript.add_transaction_options(self)
+
+ def mainTask(self):
+ """Execute package obsolescence procedure.
+
+ Modules using this class outside of its normal usage in the
+ main script can call this method to start the copy.
+
+ In this case the caller can override test_args on __init__
+ to set the command line arguments.
+
+ :raise SoyuzScriptError: If the distroseries is not provided or
+ it is already obsolete.
+ """
+ assert self.location, (
+ "location is not available, call SoyuzScript.setupLocation() "
+ "before calling mainTask().")
+
+ # Shortcut variable name to reduce long lines.
+ distroseries = self.location.distroseries
+
+ self._checkParameters(distroseries)
+
+ self.logger.info("Obsoleting all packages for distroseries %s in "
+ "the %s distribution." % (
+ distroseries.name,
+ distroseries.distribution.name))
+
+ # First, mark all Published sources as Obsolete.
+ sources = distroseries.getAllPublishedSources()
+ binaries = distroseries.getAllPublishedBinaries()
+ self.logger.info(
+ "Obsoleting published packages (%d sources, %d binaries)."
+ % (sources.count(), binaries.count()))
+ for package in chain(sources, binaries):
+ self.logger.debug("Obsoleting %s" % package.displayname)
+ package.requestObsolescence()
+
+ # Next, ensure that everything is scheduled for deletion. The
+ # dominator will normally leave some superseded publications
+ # uncondemned, for example sources that built NBSed binaries.
+ sources = distroseries.getAllUncondemnedSources()
+ binaries = distroseries.getAllUncondemnedBinaries()
+ self.logger.info(
+ "Scheduling deletion of other packages (%d sources, %d binaries)."
+ % (sources.count(), binaries.count()))
+ for package in chain(sources, binaries):
+ self.logger.debug(
+ "Scheduling deletion of %s" % package.displayname)
+ package.scheduleddeletiondate = UTC_NOW
+
+ # The packages from both phases will be caught by death row
+ # processing the next time it runs. We skip the domination
+ # phase in the publisher because it won't consider stable
+ # distroseries.
+
+ def _checkParameters(self, distroseries):
+ """Sanity check the supplied script parameters."""
+ # Did the user provide a suite name? (distribution defaults
+ # to 'ubuntu' which is fine.)
+ if distroseries == distroseries.distribution.currentseries:
+ # SoyuzScript defaults to the latest series. Since this
+ # will never get obsoleted it's safe to assume that the
+ # user let this option default, so complain and exit.
+ raise SoyuzScriptError(
+ "Please specify a valid distroseries name with -s/--suite "
+ "and which is not the most recent distroseries.")
+
+ # Is the distroseries in an obsolete state? Bail out now if not.
+ if distroseries.status != SeriesStatus.OBSOLETE:
+ raise SoyuzScriptError(
+ "%s is not at status OBSOLETE." % distroseries.name)
=== added file 'lib/lp/soyuz/scripts/packageremover.py'
--- lib/lp/soyuz/scripts/packageremover.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/scripts/packageremover.py 2012-01-19 08:38:27 +0000
@@ -0,0 +1,106 @@
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""FTPMaster utilities."""
+
+__metaclass__ = type
+
+__all__ = ['PackageRemover']
+
+from zope.component import getUtility
+
+from lp.registry.interfaces.person import IPersonSet
+from lp.services.browser_helpers import get_plural_text
+from lp.soyuz.scripts.ftpmasterbase import (
+ SoyuzScript,
+ SoyuzScriptError,
+ )
+
+
+class PackageRemover(SoyuzScript):
+ """SoyuzScript implementation for published package removal.."""
+
+ usage = '%prog -s warty mozilla-firefox'
+ description = 'REMOVE a published package.'
+ success_message = (
+ "The archive will be updated in the next publishing cycle.")
+
+ def add_my_options(self):
+ """Adding local options."""
+ # XXX cprov 20071025: we need a hook for loading SoyuzScript default
+ # options automatically. This is ugly.
+ SoyuzScript.add_my_options(self)
+
+ # Mode options.
+ self.parser.add_option("-b", "--binary", dest="binaryonly",
+ default=False, action="store_true",
+ help="Remove binaries only.")
+ self.parser.add_option("-S", "--source-only", dest="sourceonly",
+ default=False, action="store_true",
+ help="Remove source only.")
+
+ # Removal information options.
+ self.parser.add_option("-u", "--user", dest="user",
+ help="Launchpad user name.")
+ self.parser.add_option("-m", "--removal_comment",
+ dest="removal_comment",
+ help="Removal comment")
+
+ def mainTask(self):
+ """Execute the package removal task.
+
+ Build location and target objects.
+
+ Can raise SoyuzScriptError.
+ """
+ if len(self.args) == 0:
+ raise SoyuzScriptError(
+ "At least one non-option argument must be given, "
+ "a package name to be removed.")
+
+ if self.options.user is None:
+ raise SoyuzScriptError("Launchpad username must be given.")
+
+ if self.options.removal_comment is None:
+ raise SoyuzScriptError("Removal comment must be given.")
+
+ removed_by = getUtility(IPersonSet).getByName(self.options.user)
+ if removed_by is None:
+ raise SoyuzScriptError(
+ "Invalid launchpad username: %s" % self.options.user)
+
+ removables = []
+ for packagename in self.args:
+ if self.options.binaryonly:
+ removables.extend(
+ self.findLatestPublishedBinaries(packagename))
+ elif self.options.sourceonly:
+ removables.append(self.findLatestPublishedSource(packagename))
+ else:
+ source_pub = self.findLatestPublishedSource(packagename)
+ removables.append(source_pub)
+ removables.extend(source_pub.getPublishedBinaries())
+
+ self.logger.info("Removing candidates:")
+ for removable in removables:
+ self.logger.info('\t%s', removable.displayname)
+
+ self.logger.info("Removed-by: %s", removed_by.displayname)
+ self.logger.info("Comment: %s", self.options.removal_comment)
+
+ removals = []
+ for removable in removables:
+ removable.requestDeletion(
+ removed_by=removed_by,
+ removal_comment=self.options.removal_comment)
+ removals.append(removable)
+
+ if len(removals) == 0:
+ self.logger.info("No package removed (bug ?!?).")
+ else:
+ self.logger.info(
+ "%d %s successfully removed.", len(removals),
+ get_plural_text(len(removals), "package", "packages"))
+
+ # Information returned mainly for the benefit of the test harness.
+ return removals
=== added file 'lib/lp/soyuz/scripts/pubsourcechecker.py'
--- lib/lp/soyuz/scripts/pubsourcechecker.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/scripts/pubsourcechecker.py 2012-01-19 08:38:27 +0000
@@ -0,0 +1,214 @@
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""FTPMaster utilities."""
+
+__metaclass__ = type
+
+__all__ = ['PubSourceChecker']
+
+
+class PubBinaryContent:
+ """Binary publication container.
+
+ Currently used for auxiliary storage in PubSourceChecker.
+ """
+
+ def __init__(self, name, version, arch, component, section, priority):
+ self.name = name
+ self.version = version
+ self.arch = arch
+ self.component = component
+ self.section = section
+ self.priority = priority
+ self.messages = []
+
+ def warn(self, message):
+ """Append a warning in the message list."""
+ self.messages.append('W: %s' % message)
+
+ def error(self, message):
+ """Append a error in the message list."""
+ self.messages.append('E: %s' % message)
+
+ def renderReport(self):
+ """Render a report with the appended messages (self.messages).
+
+ Return None if no message was found, otherwise return
+ a properly formatted string, including
+
+ <TAB>BinaryName_Version Arch Component/Section/Priority
+ <TAB><TAB>MESSAGE
+ """
+ if not len(self.messages):
+ return
+
+ report = [('\t%s_%s %s %s/%s/%s'
+ % (self.name, self.version, self.arch,
+ self.component, self.section, self.priority))]
+
+ for message in self.messages:
+ report.append('\t\t%s' % message)
+
+ return "\n".join(report)
+
+
+class PubBinaryDetails:
+ """Store the component, section and priority of binary packages and, for
+ each binary package the most frequent component, section and priority.
+
+ These are stored in the following attributes:
+
+ - components: A dictionary mapping binary package names to other
+ dictionaries mapping component names to binary packages published
+ in this component.
+ - sections: The same as components, but for sections.
+ - priorities: The same as components, but for priorities.
+ - correct_components: a dictionary mapping binary package name
+ to the most frequent (considered the correct) component name.
+ - correct_sections: same as correct_components, but for sections
+ - correct_priorities: same as correct_components, but for priorities
+ """
+
+ def __init__(self):
+ self.components = {}
+ self.sections = {}
+ self.priorities = {}
+ self.correct_components = {}
+ self.correct_sections = {}
+ self.correct_priorities = {}
+
+ def addBinaryDetails(self, bin):
+ """Include a binary publication and update internal registers."""
+ name_components = self.components.setdefault(bin.name, {})
+ bin_component = name_components.setdefault(bin.component, [])
+ bin_component.append(bin)
+
+ name_sections = self.sections.setdefault(bin.name, {})
+ bin_section = name_sections.setdefault(bin.section, [])
+ bin_section.append(bin)
+
+ name_priorities = self.priorities.setdefault(bin.name, {})
+ bin_priority = name_priorities.setdefault(bin.priority, [])
+ bin_priority.append(bin)
+
+ def _getMostFrequentValue(self, data):
+ """Return a dict of name and the most frequent value.
+
+ Used for self.{components, sections, priorities}
+ """
+ results = {}
+
+ for name, items in data.iteritems():
+ highest = 0
+ for item, occurrences in items.iteritems():
+ if len(occurrences) > highest:
+ highest = len(occurrences)
+ results[name] = item
+
+ return results
+
+ def setCorrectValues(self):
+ """Find out the correct values for the same binary name
+
+ Consider correct the most frequent.
+ """
+ self.correct_components = self._getMostFrequentValue(self.components)
+ self.correct_sections = self._getMostFrequentValue(self.sections)
+ self.correct_priorities = self._getMostFrequentValue(self.priorities)
+
+
+class PubSourceChecker:
+ """Map and probe a Source/Binaries publication couple.
+
+ Receive the source publication data and its binaries and perform
+ a group of heuristic consistency checks.
+ """
+
+ def __init__(self, name, version, component, section, urgency):
+ self.name = name
+ self.version = version
+ self.component = component
+ self.section = section
+ self.urgency = urgency
+ self.binaries = []
+ self.binaries_details = PubBinaryDetails()
+
+ def addBinary(self, name, version, architecture, component, section,
+ priority):
+ """Append the binary data to the current publication list."""
+ bin = PubBinaryContent(
+ name, version, architecture, component, section, priority)
+
+ self.binaries.append(bin)
+
+ self.binaries_details.addBinaryDetails(bin)
+
+ def check(self):
+ """Setup check environment and perform the required checks."""
+ self.binaries_details.setCorrectValues()
+
+ for bin in self.binaries:
+ self._checkComponent(bin)
+ self._checkSection(bin)
+ self._checkPriority(bin)
+
+ def _checkComponent(self, bin):
+ """Check if the binary component matches the correct component.
+
+ 'correct' is the most frequent component in this binary package
+ group
+ """
+ correct_component = self.binaries_details.correct_components[bin.name]
+ if bin.component != correct_component:
+ bin.warn('Component mismatch: %s != %s'
+ % (bin.component, correct_component))
+
+ def _checkSection(self, bin):
+ """Check if the binary section matches the correct section.
+
+ 'correct' is the most frequent section in this binary package
+ group
+ """
+ correct_section = self.binaries_details.correct_sections[bin.name]
+ if bin.section != correct_section:
+ bin.warn('Section mismatch: %s != %s'
+ % (bin.section, correct_section))
+
+ def _checkPriority(self, bin):
+ """Check if the binary priority matches the correct priority.
+
+ 'correct' is the most frequent priority in this binary package
+ group
+ """
+ correct_priority = self.binaries_details.correct_priorities[bin.name]
+ if bin.priority != correct_priority:
+ bin.warn('Priority mismatch: %s != %s'
+ % (bin.priority, correct_priority))
+
+ def renderReport(self):
+ """Render a formatted report for the publication group.
+
+ Return None if no issue was annotated or an formatted string
+ including:
+
+ SourceName_Version Component/Section/Urgency | # bin
+ <BINREPORTS>
+ """
+ report = []
+
+ for bin in self.binaries:
+ bin_report = bin.renderReport()
+ if bin_report:
+ report.append(bin_report)
+
+ if not len(report):
+ return
+
+ result = [('%s_%s %s/%s/%s | %s bin'
+ % (self.name, self.version, self.component,
+ self.section, self.urgency, len(self.binaries)))]
+
+ result.extend(report)
+
+ return "\n".join(result)
=== added file 'lib/lp/soyuz/scripts/querydistro.py'
--- lib/lp/soyuz/scripts/querydistro.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/scripts/querydistro.py 2012-01-19 08:38:27 +0000
@@ -0,0 +1,260 @@
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Distribution querying utility."""
+
+__metaclass__ = type
+
+__all__ = ['LpQueryDistro']
+
+from lp.registry.interfaces.pocket import pocketsuffix
+from lp.registry.interfaces.series import SeriesStatus
+from lp.services.scripts.base import (
+ LaunchpadScript,
+ LaunchpadScriptFailure,
+ )
+from lp.soyuz.adapters.packagelocation import (
+ build_package_location,
+ PackageLocationError,
+ )
+from lp.soyuz.enums import PackagePublishingStatus
+
+
+class LpQueryDistro(LaunchpadScript):
+ """Main class for scripts/ftpmaster-tools/lp-query-distro.py."""
+
+ def __init__(self, *args, **kwargs):
+ """Initialize dynamic 'usage' message and LaunchpadScript parent.
+
+ Also initialize the list 'allowed_arguments'.
+ """
+ self.allowed_actions = [
+ 'current', 'development', 'supported', 'pending_suites', 'archs',
+ 'official_archs', 'nominated_arch_indep', 'pocket_suffixes']
+ self.usage = '%%prog <%s>' % ' | '.join(self.allowed_actions)
+ LaunchpadScript.__init__(self, *args, **kwargs)
+
+ def add_my_options(self):
+ """Add 'distribution' and 'suite' context options."""
+ self.parser.add_option(
+ '-d', '--distribution', dest='distribution_name',
+ default='ubuntu', help='Context distribution name.')
+ self.parser.add_option(
+ '-s', '--suite', dest='suite', default=None,
+ help='Context suite name.')
+
+ def main(self):
+ """Main procedure, basically a runAction wrapper.
+
+ Execute the given and allowed action using the default presenter
+ (see self.runAction for further information).
+ """
+ self.runAction()
+
+ def _buildLocation(self):
+ """Build a PackageLocation object
+
+ The location will correspond to the given 'distribution' and 'suite',
+ Any PackageLocationError occurring at this point will be masked into
+ LaunchpadScriptFailure.
+ """
+ try:
+ self.location = build_package_location(
+ distribution_name=self.options.distribution_name,
+ suite=self.options.suite)
+ except PackageLocationError, err:
+ raise LaunchpadScriptFailure(err)
+
+ def defaultPresenter(self, result):
+ """Default result presenter.
+
+ Directly prints result in the standard output (print).
+ """
+ print result
+
+ def runAction(self, presenter=None):
+ """Run a given initialized action (self.action_name).
+
+ It accepts an optional 'presenter' which will be used to
+ store/present the action result.
+
+ Ensure at least one argument was passed, known as 'action'.
+ Verify if the given 'action' is listed as an 'allowed_action'.
+ Raise LaunchpadScriptFailure if those requirements were not
+ accomplished.
+
+ It builds context 'location' object (see self._buildLocation).
+
+ It may raise LaunchpadScriptFailure is the 'action' is not properly
+ supported by the current code (missing corresponding property).
+ """
+ if presenter is None:
+ presenter = self.defaultPresenter
+
+ if len(self.args) != 1:
+ raise LaunchpadScriptFailure('<action> is required')
+
+ [self.action_name] = self.args
+
+ if self.action_name not in self.allowed_actions:
+ raise LaunchpadScriptFailure(
+ 'Action "%s" is not supported' % self.action_name)
+
+ self._buildLocation()
+
+ try:
+ action_result = getattr(self, self.action_name)
+ except AttributeError:
+ raise AssertionError(
+ "No handler found for action '%s'" % self.action_name)
+
+ presenter(action_result)
+
+ def checkNoSuiteDefined(self):
+ """Raises LaunchpadScriptError if a suite location was passed.
+
+ It is re-used in action properties to avoid conflicting contexts,
+ i.e, passing an arbitrary 'suite' and asking for the CURRENT suite
+ in the context distribution.
+ """
+ if self.options.suite is not None:
+ raise LaunchpadScriptFailure(
+ "Action does not accept defined suite.")
+
+ @property
+ def current(self):
+ """Return the name of the CURRENT distroseries.
+
+ It is restricted for the context distribution.
+
+ It may raise LaunchpadScriptFailure if a suite was passed on the
+ command-line or if not CURRENT distroseries was found.
+ """
+ self.checkNoSuiteDefined()
+ series = self.location.distribution.getSeriesByStatus(
+ SeriesStatus.CURRENT)
+ if not series:
+ raise LaunchpadScriptFailure("No CURRENT series.")
+
+ return series[0].name
+
+ @property
+ def development(self):
+ """Return the name of the DEVELOPMENT distroseries.
+
+ It is restricted for the context distribution.
+
+ It may raise `LaunchpadScriptFailure` if a suite was passed on the
+ command-line.
+
+ Return the first FROZEN distroseries found if there is no
+ DEVELOPMENT one available.
+
+ Raises `NotFoundError` if neither a CURRENT nor a FROZEN
+ candidate could be found.
+ """
+ self.checkNoSuiteDefined()
+ series = None
+ wanted_status = (SeriesStatus.DEVELOPMENT,
+ SeriesStatus.FROZEN)
+ for status in wanted_status:
+ series = self.location.distribution.getSeriesByStatus(status)
+ if series.count() > 0:
+ break
+ else:
+ raise LaunchpadScriptFailure(
+ 'There is no DEVELOPMENT distroseries for %s' %
+ self.location.distribution.name)
+ return series[0].name
+
+ @property
+ def supported(self):
+ """Return the names of the distroseries currently supported.
+
+ 'supported' means not EXPERIMENTAL or OBSOLETE.
+
+ It is restricted for the context distribution.
+
+ It may raise `LaunchpadScriptFailure` if a suite was passed on the
+ command-line or if there is not supported distroseries for the
+ distribution given.
+
+ Return a space-separated list of distroseries names.
+ """
+ self.checkNoSuiteDefined()
+ supported_series = []
+ unsupported_status = (SeriesStatus.EXPERIMENTAL,
+ SeriesStatus.OBSOLETE)
+ for distroseries in self.location.distribution:
+ if distroseries.status not in unsupported_status:
+ supported_series.append(distroseries.name)
+
+ if not supported_series:
+ raise LaunchpadScriptFailure(
+ 'There is no supported distroseries for %s' %
+ self.location.distribution.name)
+
+ return " ".join(supported_series)
+
+ @property
+ def pending_suites(self):
+ """Return the suite names containing PENDING publication.
+
+ It check for sources and/or binary publications.
+ """
+ self.checkNoSuiteDefined()
+ pending_suites = set()
+ pending_sources = self.location.archive.getPublishedSources(
+ status=PackagePublishingStatus.PENDING)
+ for pub in pending_sources:
+ pending_suites.add((pub.distroseries, pub.pocket))
+
+ pending_binaries = self.location.archive.getAllPublishedBinaries(
+ status=PackagePublishingStatus.PENDING)
+ for pub in pending_binaries:
+ pending_suites.add(
+ (pub.distroarchseries.distroseries, pub.pocket))
+
+ return " ".join([distroseries.name + pocketsuffix[pocket]
+ for distroseries, pocket in pending_suites])
+
+ @property
+ def archs(self):
+ """Return a space-separated list of architecture tags.
+
+ It is restricted for the context distribution and suite.
+ """
+ architectures = self.location.distroseries.architectures
+ return " ".join(arch.architecturetag for arch in architectures)
+
+ @property
+ def official_archs(self):
+ """Return a space-separated list of official architecture tags.
+
+ It is restricted to the context distribution and suite.
+ """
+ architectures = self.location.distroseries.architectures
+ return " ".join(arch.architecturetag
+ for arch in architectures
+ if arch.official)
+
+ @property
+ def nominated_arch_indep(self):
+ """Return the nominated arch indep architecture tag.
+
+ It is restricted to the context distribution and suite.
+ """
+ series = self.location.distroseries
+ return series.nominatedarchindep.architecturetag
+
+ @property
+ def pocket_suffixes(self):
+ """Return a space-separated list of non-empty pocket suffixes.
+
+ The RELEASE pocket (whose suffix is the empty string) is omitted.
+
+ The returned space-separated string is ordered alphabetically.
+ """
+ sorted_non_empty_suffixes = sorted(
+ suffix for suffix in pocketsuffix.values() if suffix != '')
+ return " ".join(sorted_non_empty_suffixes)
=== modified file 'lib/lp/soyuz/scripts/tests/test_chrootmanager.py'
--- lib/lp/soyuz/scripts/tests/test_chrootmanager.py 2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/scripts/tests/test_chrootmanager.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""ChrootManager facilities tests."""
@@ -15,7 +15,7 @@
from lp.registry.interfaces.distribution import IDistributionSet
from lp.services.config import config
from lp.services.database.sqlbase import commit
-from lp.soyuz.scripts.ftpmaster import (
+from lp.soyuz.scripts.chrootmanager import (
ChrootManager,
ChrootManagerError,
)
=== modified file 'lib/lp/soyuz/scripts/tests/test_lpquerydistro.py'
--- lib/lp/soyuz/scripts/tests/test_lpquerydistro.py 2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/scripts/tests/test_lpquerydistro.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -16,7 +16,7 @@
from lp.services.config import config
from lp.services.database.sqlbase import flush_database_updates
from lp.services.scripts.base import LaunchpadScriptFailure
-from lp.soyuz.scripts.ftpmaster import LpQueryDistro
+from lp.soyuz.scripts.querydistro import LpQueryDistro
from lp.testing.layers import (
LaunchpadLayer,
LaunchpadZopelessLayer,
=== modified file 'lib/lp/soyuz/scripts/tests/test_obsoletedistroseries.py'
--- lib/lp/soyuz/scripts/tests/test_obsoletedistroseries.py 2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/scripts/tests/test_obsoletedistroseries.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -19,10 +19,8 @@
BinaryPackagePublishingHistory,
SourcePackagePublishingHistory,
)
-from lp.soyuz.scripts.ftpmaster import (
- ObsoleteDistroseries,
- SoyuzScriptError,
- )
+from lp.soyuz.scripts.ftpmasterbase import SoyuzScriptError
+from lp.soyuz.scripts.obsolete_distroseries import ObsoleteDistroseries
from lp.testing import (
TestCase,
TestCaseWithFactory,
=== modified file 'lib/lp/soyuz/scripts/tests/test_overrides_checker.py'
--- lib/lp/soyuz/scripts/tests/test_overrides_checker.py 2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/scripts/tests/test_overrides_checker.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""archive-override-check tool base class tests."""
@@ -7,7 +7,7 @@
from unittest import TestCase
-from lp.soyuz.scripts.ftpmaster import (
+from lp.soyuz.scripts.pubsourcechecker import (
PubBinaryContent,
PubBinaryDetails,
PubSourceChecker,
=== modified file 'lib/lp/soyuz/scripts/tests/test_populatearchive.py'
--- lib/lp/soyuz/scripts/tests/test_populatearchive.py 2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/scripts/tests/test_populatearchive.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -18,16 +18,14 @@
from lp.services.config import config
from lp.services.job.interfaces.job import JobStatus
from lp.services.log.logger import BufferLogger
+from lp.soyuz.adapters.packagelocation import PackageLocationError
from lp.soyuz.enums import (
ArchivePurpose,
PackagePublishingStatus,
)
from lp.soyuz.interfaces.archive import IArchiveSet
from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
-from lp.soyuz.scripts.ftpmaster import (
- PackageLocationError,
- SoyuzScriptError,
- )
+from lp.soyuz.scripts.ftpmasterbase import SoyuzScriptError
from lp.soyuz.scripts.populate_archive import ArchivePopulator
from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
from lp.testing import TestCaseWithFactory
=== modified file 'lib/lp/soyuz/scripts/tests/test_removepackage.py'
--- lib/lp/soyuz/scripts/tests/test_removepackage.py 2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/scripts/tests/test_removepackage.py 2012-01-19 08:38:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Functional Tests for PackageRemover script class.
@@ -26,10 +26,8 @@
BinaryPackagePublishingHistory,
SourcePackagePublishingHistory,
)
-from lp.soyuz.scripts.ftpmaster import (
- PackageRemover,
- SoyuzScriptError,
- )
+from lp.soyuz.scripts.ftpmasterbase import SoyuzScriptError
+from lp.soyuz.scripts.packageremover import PackageRemover
from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
from lp.testing.layers import LaunchpadZopelessLayer
@@ -393,7 +391,7 @@
the same result than not passing any component filter, because
all test publications are in main component.
"""
- source = self.test_publisher.getPubSource(sourcename='foo')
+ self.test_publisher.getPubSource(sourcename='foo')
self.layer.commit()
@@ -416,7 +414,7 @@
`SoyuzScriptError` because the selected publications are in main
component.
"""
- source = self.test_publisher.getPubSource(sourcename='foo')
+ self.test_publisher.getPubSource(sourcename='foo')
remover = self.getRemover(component='multiverse')
self.assertRaises(SoyuzScriptError, remover.mainTask)
=== modified file 'scripts/ftpmaster-tools/archive-override-check.py'
--- scripts/ftpmaster-tools/archive-override-check.py 2012-01-01 03:13:08 +0000
+++ scripts/ftpmaster-tools/archive-override-check.py 2012-01-19 08:38:27 +0000
@@ -1,6 +1,6 @@
#!/usr/bin/python -S
#
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Archive Override Check
@@ -20,7 +20,7 @@
from lp.services.config import config
from lp.services.scripts.base import LaunchpadScript
from lp.soyuz.enums import PackagePublishingStatus
-from lp.soyuz.scripts.ftpmaster import PubSourceChecker
+from lp.soyuz.scripts.pubsourcechecker import PubSourceChecker
class ArchiveOverrideCheckScript(LaunchpadScript):
=== modified file 'scripts/ftpmaster-tools/lp-query-distro.py'
--- scripts/ftpmaster-tools/lp-query-distro.py 2012-01-06 11:08:30 +0000
+++ scripts/ftpmaster-tools/lp-query-distro.py 2012-01-19 08:38:27 +0000
@@ -1,6 +1,6 @@
#!/usr/bin/python -S
#
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# pylint: disable-msg=W0403
@@ -27,7 +27,7 @@
import _pythonpath
-from lp.soyuz.scripts.ftpmaster import LpQueryDistro
+from lp.soyuz.scripts.querydistro import LpQueryDistro
if __name__ == '__main__':
=== modified file 'scripts/ftpmaster-tools/lp-remove-package.py'
--- scripts/ftpmaster-tools/lp-remove-package.py 2011-12-29 05:29:36 +0000
+++ scripts/ftpmaster-tools/lp-remove-package.py 2012-01-19 08:38:27 +0000
@@ -1,6 +1,6 @@
#!/usr/bin/python -S
#
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# pylint: disable-msg=W0403
@@ -11,11 +11,10 @@
import _pythonpath
from lp.services.config import config
-from lp.soyuz.scripts.ftpmaster import PackageRemover
+from lp.soyuz.scripts.packageremover import PackageRemover
if __name__ == '__main__':
script = PackageRemover(
'lp-remove-package', dbuser=config.archivepublisher.dbuser)
script.lock_and_run()
-
=== modified file 'scripts/ftpmaster-tools/manage-chroot.py'
--- scripts/ftpmaster-tools/manage-chroot.py 2012-01-06 11:08:30 +0000
+++ scripts/ftpmaster-tools/manage-chroot.py 2012-01-19 08:38:27 +0000
@@ -1,6 +1,6 @@
#!/usr/bin/python -S
#
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# Stop lint warning about relative import:
@@ -10,10 +10,9 @@
import _pythonpath
-from lp.soyuz.scripts.ftpmaster import ManageChrootScript
+from lp.soyuz.scripts.chrootmanager import ManageChrootScript
if __name__ == '__main__':
script = ManageChrootScript('manage-chroot', dbuser="fiera")
script.lock_and_run()
-
=== modified file 'scripts/ftpmaster-tools/obsolete-distroseries.py'
--- scripts/ftpmaster-tools/obsolete-distroseries.py 2011-12-29 05:29:36 +0000
+++ scripts/ftpmaster-tools/obsolete-distroseries.py 2012-01-19 08:38:27 +0000
@@ -1,6 +1,6 @@
#!/usr/bin/python -S
#
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# Stop lint warning about relative import:
@@ -15,11 +15,10 @@
import _pythonpath
from lp.services.config import config
-from lp.soyuz.scripts.ftpmaster import ObsoleteDistroseries
+from lp.soyuz.scripts.obsolete_distroseries import ObsoleteDistroseries
if __name__ == '__main__':
script = ObsoleteDistroseries(
'obsolete-distroseries', dbuser=config.archivepublisher.dbuser)
script.lock_and_run()
-