← Back to team overview

launchpad-reviewers team mailing list archive

[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()
-