← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/split-txpkgupload into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/split-txpkgupload into lp:launchpad.

Commit message:
Split out lp.poppy to txpkgupload.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/split-txpkgupload/+merge/246320

Split out lp.poppy to txpkgupload.

The main involved bit here is dealing with twistd setup.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/split-txpkgupload into lp:launchpad.
=== modified file 'buildout.cfg'
--- buildout.cfg	2013-06-03 06:30:28 +0000
+++ buildout.cfg	2015-01-13 15:34:27 +0000
@@ -9,6 +9,7 @@
     iharness
     i18n
     txlongpoll
+    txpkgupload
 unzip = true
 eggs-directory = eggs
 download-cache = download-cache
@@ -97,3 +98,13 @@
 initialization = ${scripts:initialization}
 entry-points = twistd-for-txlongpoll=twisted.scripts.twistd:run
 scripts = twistd-for-txlongpoll
+
+[txpkgupload]
+recipe = z3c.recipe.scripts
+eggs = ${scripts:eggs}
+    txpkgupload
+include-site-packages = false
+allowed-eggs-from-site-packages =
+initialization = ${scripts:initialization}
+entry-points = twistd-for-txpkgupload=twisted.scripts.twistd:run
+scripts = twistd-for-txpkgupload

=== renamed file 'lib/lp/poppy/tests/poppy-sftp' => 'configs/development/txpkgupload-sftp'
=== renamed file 'lib/lp/poppy/tests/poppy-sftp.pub' => 'configs/development/txpkgupload-sftp.pub'
=== removed file 'daemons/poppy-sftp.tac'
--- daemons/poppy-sftp.tac	2015-01-12 18:53:31 +0000
+++ daemons/poppy-sftp.tac	1970-01-01 00:00:00 +0000
@@ -1,121 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-# This is a Twisted application config file.  To run, use:
-#     twistd -noy sftp.tac
-# or similar.  Refer to the twistd(1) man page for details.
-
-import logging
-
-from lazr.sshserver.auth import (
-    LaunchpadAvatar,
-    PublicKeyFromLaunchpadChecker,
-    )
-from lazr.sshserver.service import SSHService
-from lazr.sshserver.session import DoNothingSession
-
-from twisted.application import service
-from twisted.conch.interfaces import ISession
-from twisted.conch.ssh import filetransfer
-from twisted.cred.portal import IRealm, Portal
-from twisted.protocols.policies import TimeoutFactory
-from twisted.python import components
-from twisted.scripts.twistd import ServerOptions
-from twisted.web.xmlrpc import Proxy
-
-from zope.interface import implements
-
-from lp.services.config import config
-from lp.services.daemons import readyservice
-
-from lp.poppy import get_poppy_root
-from lp.poppy.twistedftp import (
-    FTPServiceFactory,
-    )
-from lp.poppy.twistedsftp import SFTPServer
-from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting
-
-
-def make_portal():
-    """Create and return a `Portal` for the SSH service.
-
-    This portal accepts SSH credentials and returns our customized SSH
-    avatars (see `LaunchpadAvatar`).
-    """
-    authentication_proxy = Proxy(
-        config.poppy.authentication_endpoint)
-    portal = Portal(Realm(authentication_proxy))
-    portal.registerChecker(
-        PublicKeyFromLaunchpadChecker(authentication_proxy))
-    return portal
-
-
-class Realm:
-    implements(IRealm)
-
-    def __init__(self, authentication_proxy):
-        self.authentication_proxy = authentication_proxy
-
-    def requestAvatar(self, avatar_id, mind, *interfaces):
-        # Fetch the user's details from the authserver
-        deferred = mind.lookupUserDetails(
-            self.authentication_proxy, avatar_id)
-
-        # Once all those details are retrieved, we can construct the avatar.
-        def got_user_dict(user_dict):
-            avatar = LaunchpadAvatar(user_dict)
-            return interfaces[0], avatar, avatar.logout
-
-        return deferred.addCallback(got_user_dict)
-
-
-def poppy_sftp_adapter(avatar):
-    return SFTPServer(avatar, get_poppy_root())
-
-
-# Force python logging to all go to the Twisted log.msg interface. The default
-# - output on stderr - will not be watched by anyone.
-from twisted.python import log
-stream = log.StdioOnnaStick()
-logging.basicConfig(stream=stream, level=logging.INFO)
-
-
-components.registerAdapter(
-    poppy_sftp_adapter, LaunchpadAvatar, filetransfer.ISFTPServer)
-
-components.registerAdapter(DoNothingSession, LaunchpadAvatar, ISession)
-
-
-# ftpport defaults to 2121 in schema-lazr.conf
-ftpservice = FTPServiceFactory.makeFTPService(port=config.poppy.ftp_port)
-
-# Construct an Application that has the Poppy SSH server,
-# and the Poppy FTP server.
-options = ServerOptions()
-options.parseOptions()
-application = service.Application('poppy-sftp')
-observer = set_up_oops_reporting(
-    'poppy-sftp', 'poppy', options.get('logfile'))
-application.addComponent(observer, ignoreClass=1)
-
-ftpservice.setServiceParent(application)
-
-
-def timeout_decorator(factory):
-    """Add idle timeouts to a factory."""
-    return TimeoutFactory(factory, timeoutPeriod=config.poppy.idle_timeout)
-
-svc = SSHService(
-    portal=make_portal(),
-    private_key_path=config.poppy.host_key_private,
-    public_key_path=config.poppy.host_key_public,
-    main_log='poppy',
-    access_log='poppy.access',
-    access_log_path=config.poppy.access_log,
-    strport=config.poppy.port,
-    factory_decorator=timeout_decorator,
-    banner=config.poppy.banner)
-svc.setServiceParent(application)
-
-# Service that announces when the daemon is ready
-readyservice.ReadyService().setServiceParent(application)

=== removed directory 'lib/lp/poppy'
=== removed file 'lib/lp/poppy/__init__.py'
--- lib/lp/poppy/__init__.py	2011-12-29 05:29:36 +0000
+++ lib/lp/poppy/__init__.py	1970-01-01 00:00:00 +0000
@@ -1,20 +0,0 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-# Make this directory into a Python package.
-
-import os
-
-from lp.services.config import config
-
-
-def get_poppy_root():
-    """Return the poppy root to use for this server.
-
-    If the POPPY_ROOT environment variable is set, use that. If not, use
-    config.poppy.fsroot.
-    """
-    poppy_root = os.environ.get('POPPY_ROOT', None)
-    if poppy_root:
-        return poppy_root
-    return config.poppy.fsroot

=== removed file 'lib/lp/poppy/filesystem.py'
--- lib/lp/poppy/filesystem.py	2010-08-20 20:31:18 +0000
+++ lib/lp/poppy/filesystem.py	1970-01-01 00:00:00 +0000
@@ -1,248 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__metaclass__ = type
-__all__ = [
-    'UploadFileSystem',
-    ]
-
-import datetime
-import os
-import stat
-
-from zope.interface import implements
-from zope.security.interfaces import Unauthorized
-from zope.server.interfaces.ftp import IFileSystem
-
-
-class UploadFileSystem:
-
-    implements(IFileSystem)
-
-    def __init__(self, rootpath):
-        self.rootpath = rootpath
-
-    def _full(self, path):
-        """Returns the full path name (i.e. rootpath + path)"""
-        full_path = os.path.join(self.rootpath, path)
-        if not os.path.realpath(full_path).startswith(self.rootpath):
-            raise OSError("Path not allowed:", path)
-        return full_path
-
-    def _sanitize(self, path):
-        if path.startswith('/'):
-            path = path[1:]
-        path = os.path.normpath(path)
-        return path
-
-    def type(self, path):
-        """Return the file type at path
-
-        The 'type' command returns 'f' for a file, 'd' for a directory and
-        None if there is no file.
-        """
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if os.path.exists(full_path):
-            if os.path.isdir(full_path):
-                return 'd'
-            elif os.path.isfile(full_path):
-                return 'f'
-
-    def names(self, path, filter=None):
-        """Return a sequence of the names in a directory
-
-        If the filter is not None, include only those names for which
-        the filter returns a true value.
-        """
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if not os.path.exists(full_path):
-            raise OSError("Not exists:", path)
-        filenames = os.listdir(os.path.join(self.rootpath, path))
-        files = []
-        for filename in filenames:
-            if not filter or filter(filename):
-                files.append(filename)
-        return files
-
-    def ls(self, path, filter=None):
-        """Return a sequence of information objects.
-
-        It considers the names in the given path (returned self.name())
-        and builds file information using self.lsinfo().
-        """
-        return [self.lsinfo(name) for name in self.names(path, filter)]
-
-    def readfile(self, path, outstream, start=0, end=None):
-        """Outputs the file at path to a stream.
-
-        Not allowed - see filesystem.txt.
-        """
-        raise Unauthorized
-
-    def lsinfo(self, path):
-        """Return information for a unix-style ls listing for the path
-
-        See zope3's interfaces/ftp.py:IFileSystem for details of the
-        dictionary's content.
-        """
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if not os.path.exists(full_path):
-            raise OSError("Not exists:", path)
-
-        info = {"owner_name": "upload",
-                "group_name": "upload",
-                "name": path.split("/")[-1]}
-
-        s = os.stat(full_path)
-
-        info["owner_readable"] = bool(s.st_mode & stat.S_IRUSR)
-        info["owner_writable"] = bool(s.st_mode & stat.S_IWUSR)
-        info["owner_executable"] = bool(s.st_mode & stat.S_IXUSR)
-        info["group_readable"] = bool(s.st_mode & stat.S_IRGRP)
-        info["group_writable"] = bool(s.st_mode & stat.S_IWGRP)
-        info["group_executable"] = bool(s.st_mode & stat.S_IXGRP)
-        info["other_readable"] = bool(s.st_mode & stat.S_IROTH)
-        info["other_writable"] = bool(s.st_mode & stat.S_IWOTH)
-        info["other_executable"] = bool(s.st_mode & stat.S_IXOTH)
-        info["mtime"] = datetime.datetime.fromtimestamp(self.mtime(path))
-        info["size"] = self.size(path)
-        info["type"] = self.type(path)
-        info["nlinks"] = s.st_nlink
-        return info
-
-    def mtime(self, path):
-        """Return the modification time for the file"""
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if os.path.exists(full_path):
-            return os.path.getmtime(full_path)
-
-    def size(self, path):
-        """Return the size of the file at path"""
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if os.path.exists(full_path):
-            return os.path.getsize(full_path)
-
-    def mkdir(self, path):
-        """Create a directory."""
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if os.path.exists(full_path):
-            if os.path.isfile(full_path):
-                raise OSError("File already exists:", path)
-            elif os.path.isdir(full_path):
-                raise OSError("Directory already exists:", path)
-            raise OSError("OOPS, can't create:", path)
-        else:
-            old_mask = os.umask(0)
-            try:
-                os.makedirs(full_path, 0775)
-            finally:
-                os.umask(old_mask)
-
-    def remove(self, path):
-        """Remove a file."""
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if os.path.exists(full_path):
-            if os.path.isfile(full_path):
-                os.unlink(full_path)
-            elif os.path.isdir(full_path):
-                raise OSError("Is a directory:", path)
-        else:
-            raise OSError("Not exists:", path)
-
-    def rmdir(self, path):
-        """Remove a directory.
-
-        Remove a target path recursively.
-        """
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if os.path.exists(full_path):
-            os.rmdir(full_path)
-        else:
-            raise OSError("Not exists:", path)
-
-    def rename(self, old, new):
-        """Rename a file."""
-        old = self._sanitize(old)
-        new = self._sanitize(new)
-        full_old = self._full(old)
-        full_new = self._full(new)
-
-        if os.path.isdir(full_new):
-            raise OSError("Is a directory:", new)
-
-        if os.path.exists(full_old):
-            if os.path.isfile(full_old):
-                os.rename(full_old, full_new)
-            elif os.path.isdir(full_old):
-                raise OSError("Is a directory:", old)
-        else:
-            raise OSError("Not exists:", old)
-
-    def writefile(self, path, instream, start=None, end=None, append=False):
-        """Write data to a file.
-
-        See zope3's interfaces/ftp.py:IFileSystem for details of the
-        handling of the various arguments.
-        """
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if os.path.exists(full_path):
-            if os.path.isdir(full_path):
-                raise OSError("Is a directory:", path)
-        else:
-            dirname = os.path.dirname(full_path)
-            if dirname:
-                if not os.path.exists(dirname):
-                    old_mask = os.umask(0)
-                    try:
-                        os.makedirs(dirname, 0775)
-                    finally:
-                        os.umask(old_mask)
-
-        if start and start < 0:
-            raise ValueError("Negative start argument:", start)
-        if end and end < 0:
-            raise ValueError("Negative end argument:", end)
-        if start and end and end <= start:
-            return
-        if append:
-            open_flag = 'a'
-        elif start or end:
-            open_flag = "r+"
-            if not os.path.exists(full_path):
-                open(full_path, 'w')
-
-        else:
-            open_flag = 'w'
-        outstream = open(full_path, open_flag)
-        if start:
-            outstream.seek(start)
-        chunk = instream.read()
-        while chunk:
-            outstream.write(chunk)
-            chunk = instream.read()
-        if not end:
-            outstream.truncate()
-        instream.close()
-        outstream.close()
-
-    def writable(self, path):
-        """Return boolean indicating whether a file at path is writable."""
-        path = self._sanitize(path)
-        full_path = self._full(path)
-        if os.path.exists(full_path):
-            if os.path.isfile(full_path):
-                return True
-            elif os.path.isdir(full_path):
-                return False
-        else:
-            return True
-

=== removed file 'lib/lp/poppy/hooks.py'
--- lib/lp/poppy/hooks.py	2012-06-29 08:40:05 +0000
+++ lib/lp/poppy/hooks.py	1970-01-01 00:00:00 +0000
@@ -1,164 +0,0 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__metaclass__ = type
-
-__all__ = [
-    'Hooks',
-    'PoppyInterfaceFailure',
-    ]
-
-
-import logging
-import os
-import shutil
-import stat
-import time
-
-from contrib.glock import GlobalLock
-
-
-class PoppyInterfaceFailure(Exception):
-    pass
-
-
-class Hooks:
-
-    clients = {}
-    LOG_MAGIC = "Post-processing finished"
-    _targetcount = 0
-
-    def __init__(self, targetpath, logger, allow_user, cmd=None,
-                 targetstart=0, perms=None, prefix=''):
-        self.targetpath = targetpath
-        self.logger = logging.getLogger("%s.Hooks" % logger.name)
-        self.cmd = cmd
-        self.allow_user = allow_user
-        self.perms = perms
-        self.prefix = prefix
-
-    @property
-    def targetcount(self):
-        """A guaranteed unique integer for ensuring unique upload dirs."""
-        Hooks._targetcount += 1
-        return Hooks._targetcount
-
-    def new_client_hook(self, fsroot, host, port):
-        """Prepare a new client record indexed by fsroot..."""
-        self.clients[fsroot] = {
-            "host": host,
-            "port": port
-            }
-        self.logger.debug("Accepting new session in fsroot: %s" % fsroot)
-        self.logger.debug("Session from %s:%s" % (host, port))
-
-    def client_done_hook(self, fsroot, host, port):
-        """A client has completed. If it authenticated then it stands a chance
-        of having uploaded a file to the set. If not; then it is simply an
-        aborted transaction and we remove the fsroot."""
-
-        if fsroot not in self.clients:
-            raise PoppyInterfaceFailure("Unable to find fsroot in client set")
-
-        self.logger.debug("Processing session complete in %s" % fsroot)
-
-        client = self.clients[fsroot]
-        if "distro" not in client:
-            # Login username defines the distribution context of the upload.
-            # So abort unauthenticated sessions by removing its contents
-            shutil.rmtree(fsroot)
-            return
-
-        # Protect from race condition between creating the directory
-        # and creating the distro file, and also in cases where the
-        # temporary directory and the upload directory are not in the
-        # same filesystem (non-atomic "rename").
-        lockfile_path = os.path.join(self.targetpath, ".lock")
-        self.lock = GlobalLock(lockfile_path)
-
-        # XXX cprov 20071024 bug=156795: We try to acquire the lock as soon
-        # as possible after creating the lockfile but are still open to
-        # a race.
-        self.lock.acquire(blocking=True)
-        mode = stat.S_IMODE(os.stat(lockfile_path).st_mode)
-
-        # XXX cprov 20081024 bug=185731: The lockfile permission can only be
-        # changed by its owner. Since we can't predict which process will
-        # create it in production systems we simply ignore errors when trying
-        # to grant the right permission. At least, one of the process will
-        # be able to do so.
-        try:
-            os.chmod(lockfile_path, mode | stat.S_IWGRP)
-        except OSError:
-            pass
-
-        try:
-            timestamp = time.strftime("%Y%m%d-%H%M%S")
-            path = "upload%s-%s-%06d" % (
-                self.prefix, timestamp, self.targetcount)
-            target_fsroot = os.path.join(self.targetpath, path)
-
-            # Create file to store the distro used.
-            self.logger.debug("Upload was targetted at %s" % client["distro"])
-            distro_filename = target_fsroot + ".distro"
-            distro_file = open(distro_filename, "w")
-            distro_file.write(client["distro"])
-            distro_file.close()
-
-            # Move the session directory to the target directory.
-            if os.path.exists(target_fsroot):
-                self.logger.warn("Targeted upload already present: %s" % path)
-                self.logger.warn("System clock skewed ?")
-            else:
-                try:
-                    shutil.move(fsroot, target_fsroot)
-                except (OSError, IOError):
-                    if not os.path.exists(target_fsroot):
-                        raise
-
-            # XXX cprov 20071024: We should replace os.system call by os.chmod
-            # and fix the default permission value accordingly in poppy-upload
-            if self.perms is not None:
-                os.system("chmod %s -R %s" % (self.perms, target_fsroot))
-
-            # Invoke processing script, if provided.
-            if self.cmd:
-                cmd = self.cmd
-                cmd = cmd.replace("@fsroot@", target_fsroot)
-                cmd = cmd.replace("@distro@", client["distro"])
-                self.logger.debug("Running upload handler: %s" % cmd)
-                os.system(cmd)
-        finally:
-            # We never delete the lockfile, this way the inode will be
-            # constant while the machine is up. See comment on 'acquire'
-            self.lock.release(skip_delete=True)
-
-        self.clients.pop(fsroot)
-        # This is mainly done so that tests know when the
-        # post-processing hook has finished.
-        self.logger.info(self.LOG_MAGIC)
-
-    def auth_verify_hook(self, fsroot, user, password):
-        """Verify that the username matches a distribution we care about.
-
-        The password is irrelevant to auth, as is the fsroot"""
-        if fsroot not in self.clients:
-            raise PoppyInterfaceFailure("Unable to find fsroot in client set")
-
-        # local authentication
-        self.clients[fsroot]["distro"] = self.allow_user
-        return True
-
-        # When we get on with the poppy path stuff, the below may be useful
-        # and is thus left in rather than being removed.
-
-        #try:
-        #    d = Distribution.byName(user)
-        #    if d:
-        #        self.logger.debug("Accepting login for %s" % user)
-        #        self.clients[fsroot]["distro"] = user
-        #        return True
-        #except object as e:
-        #    print e
-        #return False
-

=== removed directory 'lib/lp/poppy/tests'
=== removed file 'lib/lp/poppy/tests/__init__.py'
--- lib/lp/poppy/tests/__init__.py	2010-03-18 10:15:34 +0000
+++ lib/lp/poppy/tests/__init__.py	1970-01-01 00:00:00 +0000
@@ -1,5 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for Poppy."""
-

=== removed file 'lib/lp/poppy/tests/filesystem.txt'
--- lib/lp/poppy/tests/filesystem.txt	2010-03-17 11:04:20 +0000
+++ lib/lp/poppy/tests/filesystem.txt	1970-01-01 00:00:00 +0000
@@ -1,607 +0,0 @@
-
-This is an implementation of IFileSystem which the FTP Server in Zope3
-uses to know what to do when people make FTP commands.
-
-    >>> from lp.poppy.filesystem import UploadFileSystem
-
-The UploadFileSystem class implements the interface IFileSystem.
-
-    >>> from zope.server.interfaces.ftp import IFileSystem
-    >>> IFileSystem.implementedBy(UploadFileSystem)
-    True
-
-First we need to setup our test environment.
-
-    >>> import os
-    >>> import shutil
-    >>> import tempfile
-    >>> rootpath = tempfile.mkdtemp()
-
-    >>> testfile = "testfile"
-    >>> full_testfile = os.path.join(rootpath, testfile)
-    >>> testfile_contents = "contents of the file"
-    >>> open(full_testfile, 'w').write(testfile_contents)
-
-    >>> testdir = "testdir"
-    >>> full_testdir = os.path.join(rootpath, testdir)
-    >>> os.mkdir(full_testdir)
-    >>> propaganda = """
-    ...    GNU is aimed initially at machines in the 68000/16000 class with
-    ... virtual memory, because they are the easiest machines to make it run
-    ... on.  The extra effort to make it run on smaller machines will be left
-    ... to someone who wants to use it on them.
-    ... """
-
-When you create an UploadFileSystem you pass it a directory location
-to use.
-
-    >>> ufs = UploadFileSystem(rootpath)
-
-An UploadFileSystem object provides the interface IFileSystem.
-
-    >>> from zope.interface.verify import verifyObject
-    >>> verifyObject(IFileSystem, ufs)
-    True
-
-mkdir
-=====
-
-"mkdir" should work as expected, directory will be created as
-requested by the clients:
-
-    >>> ufs.mkdir("anything")
-    >>> os.path.exists(os.path.join(rootpath, "anything"))
-    True
-
-    >>> os.rmdir(os.path.join(rootpath, "anything"))
-
-It recursively creates directories:
-
-    >>> ufs.mkdir("anything/something/whatever")
-
-    >>> wanted_path = os.path.join(rootpath, "anything/something/whatever")
-
-    >>> os.path.exists(wanted_path)
-    True
-
-    >>> oct(os.stat(wanted_path).st_mode)
-    '040775'
-
-    >>> shutil.rmtree(os.path.join(rootpath, "anything"))
-
-rmdir
-=====
-
-Check if it complains on removal request of an existent dir
-
-    >>> ufs.rmdir("does-not-exist")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] does-not-exist
-
-Check if it works as expected after the directory creation:
-
-    >>> ufs.mkdir("new-dir")
-    >>> ufs.rmdir("new-dir")
-
-    >>> os.path.exists(os.path.join(rootpath, "new-dir"))
-    False
-
-
-lsinfo
-======
-
-Return information for a unix-style ls listing for the path.
-
-See zope3's interfaces/ftp.py:IFileSystem for details of the
-dictionary's content.
-
-Setup a default dictionary used for generating the dictionaries we
-expect lsinfo to return.
-
-    >>> def clean_mtime(stat_info):
-    ...     """Return a datetime from an mtime, sans microseconds."""
-    ...     mtime = stat_info.st_mtime
-    ...     datestamp = datetime.datetime.fromtimestamp(mtime)
-    ...     datestamp.replace(microsecond=0)
-    ...     return datestamp
-
-    >>> import copy
-    >>> import datetime
-    >>> import stat
-    >>> def_exp = {"type": 'f', 
-    ...     "owner_name": "upload",
-    ...     "owner_readable": True,
-    ...     "owner_writable": True,
-    ...	    "owner_executable": False,
-    ...	    "group_name": "upload",
-    ...	    "group_readable": True,
-    ...     "group_writable": False,
-    ...	    "group_executable": False,
-    ...	    "other_readable": True,
-    ...     "other_writable": False,
-    ...	    "other_executable": False,
-    ...     "nlinks": 1}
-    ...
-
-    >>> os.chmod(full_testfile, stat.S_IRUSR | stat.S_IWUSR | \
-    ...                         stat.S_IRGRP | stat.S_IROTH)
-    >>> exp = copy.copy(def_exp)
-    >>> s = os.stat(full_testfile)
-    >>> exp["name"] = testfile
-    >>> exp["mtime"] = clean_mtime(s)
-    >>> exp["size"] = s[stat.ST_SIZE]
-    >>> info = ufs.lsinfo(testfile)
-    >>> info == exp
-    True
-
-ls
-==
-
-`ls` a sequence of item info objects (see ls_info) for the files in a
-directory.
-
-    >>> expected = [exp]
-    >>> for i in [ "foo", "bar" ]:
-    ...     filename = os.path.join(rootpath, i)
-    ...     x = open(filename, 'w')
-    ...     os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR | \
-    ...                         stat.S_IRGRP | stat.S_IROTH)
-    ...     exp = copy.copy(def_exp)
-    ...	    s = os.stat(filename)
-    ...	    exp["name"] = i
-    ...	    exp["mtime"] = clean_mtime(s)
-    ...     exp["size"] = s[stat.ST_SIZE]
-    ...     expected.append(exp)
-    ...
-
-    >>> dir_exp = copy.copy(def_exp)
-    >>> s = os.stat(full_testdir)
-    >>> dir_exp["type"] = "d"
-    >>> dir_exp["name"] = testdir
-    >>> dir_exp["mtime"] = clean_mtime(s)
-    >>> dir_exp["size"] = s[stat.ST_SIZE]
-    >>> dir_exp["nlinks"] = s[stat.ST_NLINK]
-    >>> dir_exp["owner_executable"] = True
-    >>> dir_exp["other_executable"] = True
-    >>> dir_exp["group_executable"] = True
-    >>> expected.append(dir_exp)
-
-We need a helper function to turn the returned and expected data into reliably
-sorted orders for comparison.
-
-    >>> from operator import itemgetter
-    >>> def sorted_listings(ls_infos):
-    ...     # ls_infos will be a sequence of dictionaries.  They need to be
-    ...     # sorted for the sequences to compare equal, so do that on the
-    ...     # dictionary's 'name' key.  The equality test used here
-    ...     # doesn't care about the sort order of the dictionaries.
-    ...     return sorted(ls_infos, key=itemgetter('name'))
-
-    >>> expected.sort()
-    >>> returned = ufs.ls(".")
-    >>> returned.sort()
-    >>> sorted_listings(expected) == sorted_listings(returned)
-    True
-
-If `filter` is not None, include only those names for which `filter`
-returns a true value.
-
-    >>> def always_false_filter(name):
-    ...     return False
-    >>> def always_true_filter(name):
-    ...     return True
-    >>> def arbitrary_filter(name):
-    ...    if name == "foo" or name == "baz":
-    ...        return True
-    ...    else:
-    ...        return False
-    ...
-    >>> for i in expected:
-    ...     if i["name"] == "foo":
-    ...         filtered_expected = [i];
-    >>> returned = ufs.ls(".", always_true_filter)
-    >>> returned.sort()
-    >>> sorted_listings(expected) == sorted_listings(returned)
-    True
-    >>> returned = ufs.ls(".", always_false_filter)
-    >>> returned.sort()
-    >>> returned == []
-    True
-    >>> returned = ufs.ls(".", arbitrary_filter)
-    >>> returned.sort()
-    >>> sorted_listings(filtered_expected) == sorted_listings(returned)
-    True
-    >>> for i in [ "foo", "bar" ]:
-    ...     ufs.remove(i)
-    ...
-
-readfile
-========
-
-We are not implementing `readfile` as a precautionary measure, i.e. in
-case anyone bypasses the per-session separate directories they still
-aren't able to read any other files and therefore can't abuse the
-server for warez/child porn etc.
-
-Unlike `mkdir` and `rmdir` we will raise an exception so that the
-server returns an error to the client and the client does not receive
-bogus or empty data.
-
-    >>> ufs.readfile(testfile, None)
-    Traceback (most recent call last):
-    ...
-    Unauthorized
-
-The 'type' command returns 'f' for a file, 'd' for a directory and
-None if there is no file.
- 
-    >>> ufs.type(testfile)
-    'f'
-    
-    >>> ufs.type(testdir)
-    'd'
-    
-    >>> ufs.type("does-not-exist") is None
-    True
-
-size
-====
-
-The 'size' command returns the size of the file.  If the file does not
-exist None is returned.
-
-    >>> ufs.size("does-not-exist") is None
-    True
-    
-    >>> ufs.size(testfile) == os.path.getsize(full_testfile)
-    True
-    
-    >>> ufs.size(testdir) == os.path.getsize(full_testdir)
-    True
-
-mtime
-=====
-
-The 'mtime' command returns the mtime of the file.  If the file does not
-exist None is returned.
-
-    >>> ufs.size("does-not-exist") is None
-    True
-    
-    >>> ufs.mtime(testfile) == os.path.getmtime(full_testfile)
-    True
-    
-    >>> ufs.mtime(testdir) == os.path.getmtime(full_testdir)
-    True
-
-remove
-======
-
-The 'remove' command removes a file.  An exception is raised if the
-file does not exist or is a directory.
-
-    >>> ufs.remove("does-not-exist")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] does-not-exist
-    
-    >>> ufs.remove(testfile)
-    >>> os.path.exists(full_testfile)
-    False
-    >>> open(full_testfile, 'w').write("contents of the file")
-    
-    >>> ufs.remove(testdir)
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Is a directory:] testdir
-
-rename
-======
-
-The 'rename' command renames a file.  An exception is raised if the
-old filename doesn't exist or if the old or new filename is a
-directory.
-
-    >>> new_testfile = "baz"
-    >>> new_full_testfile = os.path.join(rootpath, new_testfile)
-    
-    >>> ufs.rename("does-not-exist", new_testfile)
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] does-not-exist
-
-    >>> new_testfile = "baz"
-    >>> new_full_testfile = os.path.join(rootpath, new_testfile)
-    >>> ufs.rename(testfile, new_testfile)
-    >>> os.path.exists(full_testfile)
-    False
-    >>> os.path.exists(new_full_testfile)
-    True
-    >>> open(new_full_testfile).read() == testfile_contents
-    True
-    >>> ufs.rename(new_testfile, testfile)
-    
-    >>> ufs.rename(testdir, new_testfile)
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Is a directory:] testdir
-    
-    >>> ufs.rename(testfile, testdir)
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Is a directory:] testdir
-
-names
-=====
-
-The `names` command returns a sequence of the names in the `path`.
-
-    >>> sorted(ufs.names("."))
-    ['testdir', 'testfile']
-
-`path` is normalized before used.
-
-    >>> sorted(ufs.names("some-directory/..")) 
-    ['testdir', 'testfile']
-
-'path' under the server root is not allowed:
-
-    >>> ufs.names("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-
-
-If the `filter` argument is provided, each name is only returned if
-the given `filter` function returns True for it.
-
-    >>> ufs.names(".", always_false_filter)
-    []
-
-    >>> sorted(ufs.names(".", always_true_filter))
-    ['testdir', 'testfile']
-
-    >>> for i in [ "foo", "bar", "baz", "bat" ]:
-    ...     x = open(os.path.join(rootpath, i), 'w')
-    >>> names = ufs.names(".", arbitrary_filter)
-    >>> names.sort()
-    >>> names == ['baz', 'foo']
-    True
-    >>> for i in [ "foo", "bar", "baz", "bat" ]:
-    ...     os.unlink(os.path.join(rootpath, i))
-
-writefile
-=========
-
-`writefile` writes data to a file.
-
-    >>> from StringIO import StringIO
-    >>> ufs.writefile("upload", StringIO(propaganda))
-    >>> open(os.path.join(rootpath, "upload")).read() == propaganda
-    True
-    >>> ufs.remove("upload")
-
-If neither `start` nor `end` are specified, then the file contents
-are overwritten.
-
-    >>> ufs.writefile(testfile, StringIO("MOO"))
-    >>> open(full_testfile).read() == "MOO"
-    True
-    >>> ufs.writefile(testfile, StringIO(testfile_contents))
-
-If `start` or `end` are specified, they must be non-negative.
-
-    >>> ufs.writefile("upload", StringIO(propaganda), -37)
-    Traceback (most recent call last):
-    ...
-    ValueError: ('Negative start argument:', -37)
-
-    >>> ufs.writefile("upload", StringIO(propaganda), 1, -43)
-    Traceback (most recent call last):
-    ...
-    ValueError: ('Negative end argument:', -43)
-
-If `start` or `end` is not None, then only part of the file is
-written. The remainder of the file is unchanged.
-
-    >>> ufs.writefile(testfile, StringIO("MOO"), 9, 12)
-    >>> open(full_testfile).read() == "contents MOOthe file"
-    True
-    >>> ufs.writefile(testfile, StringIO(testfile_contents))
-
-If `end` is None, then the file is truncated after the data are
-written.  
-
-    >>> ufs.writefile(testfile, StringIO("MOO"), 9)
-    >>> open(full_testfile).read() == "contents MOO"
-    True
-    >>> ufs.writefile(testfile, StringIO(testfile_contents))
-
-If `start` is specified and the file doesn't exist or is shorter
-than start, the file will contain undefined data before start.
-
-    >>> ufs.writefile("didnt-exist", StringIO("MOO"), 9)
-    >>> open(os.path.join(rootpath, "didnt-exist")).read() == "\x00\x00\x00\x00\x00\x00\x00\x00\x00MOO"
-    True
-    >>> ufs.remove("didnt-exist")
-
-If `end` is not None and there isn't enough data in `instream` to fill
-out the file, then the missing data is undefined.
-
-    >>> ufs.writefile(testfile, StringIO("MOO"), 9, 15)
-    >>> open(full_testfile).read() == "contents MOOthe file"
-    True
-    >>> ufs.writefile(testfile, StringIO(testfile_contents))
-
-If `end` is less than or the same as `start no data is writen to the file.
-
-    >>> ufs.writefile(testfile, StringIO("MOO"), 9, 4)
-    >>> open(full_testfile).read() == "contents of the file"
-    True
-
-    >>> ufs.writefile(testfile, StringIO("MOO"), 9, 9)
-    >>> open(full_testfile).read() == "contents of the file"
-    True
-
-If `append` is true the file is appended to rather than being
-overwritten.
-
-    >>> ufs.writefile(testfile, StringIO("MOO"), append=True)
-    >>> open(full_testfile).read() == "contents of the fileMOO"
-    True
-    >>> ufs.writefile(testfile, StringIO(testfile_contents))
-
-Additionally, if `append` is true, `start` and `end` are ignored.
-
-    >>> ufs.writefile(testfile, StringIO("MOO"), 10, 13, append=True)
-    >>> open(full_testfile).read() == "contents of the fileMOO"
-    True
-    >>> ufs.writefile(testfile, StringIO(testfile_contents))
-
-'writefile' is able to create inexistent directories in a requested
-path:
-
-    >>> os.path.exists(os.path.join(rootpath, "foo"))
-    False
-    >>> ufs.writefile("foo/bar", StringIO("fake")) is None
-    True
-    >>> os.path.exists(os.path.join(rootpath, "foo/bar"))
-    True
-    >>> open(os.path.join(rootpath, "foo/bar")).read()
-    'fake'
-
-
-writable
-========
-
-`writable` returns a boolean indicating whether `path` is writable or
-not.
-
-    >>> ufs.writable(testfile)
-    True
-
-`writable` returns True if `path` is a non-existent file.
-
-    >>> ufs.writable("does-not-exist")
-    True
-
-`writable` returns False if `path` is a directory as we don't allow
-the creation of sub-directories.
-
-    >>> ufs.writable(testdir)
-    False
-
-path checking
-=============
-
-`path` arguments must be normalized.
-
-    >>> ufs.type(os.path.join("non-existent-dir", "..", testfile))
-    'f'
-
-
-Cleanup the server root:
-
-    >>> for leaf in os.listdir(rootpath):
-    ...     full_path = os.path.join(rootpath, leaf)
-    ...     if os.path.isdir(full_path):
-    ...          shutil.rmtree(full_path)
-    ...     else:
-    ...          os.remove(full_path)
-
-
-Dealing with inexistent path:
-
-    >>> ufs.type("foo/bar") is None
-    True
-    >>> ufs.mtime("foo/bar") is None
-    True
-    >>> ufs.size("foo/bar") is None
-    True
-    >>> ufs.writable("foo/bar")
-    True
-    >>> ufs.names("foo/bar")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] foo/bar
-    >>> ufs.ls("foo/bar")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] foo/bar
-    >>> ufs.lsinfo("foo/bar")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] foo/bar
-    >>> ufs.remove("foo/bar")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] foo/bar
-    >>> ufs.rename("foo/bar", "baz")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] foo/bar
-    >>> ufs.rename("baz", "foo/bar")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Not exists:] baz
-
-
-Dealing with paths outside the server root directory:
-
-    >>> ufs.type("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.mtime("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>>	ufs.size("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.writable("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.names("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.ls("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.lsinfo("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.remove("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.rename("..", "baz")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.rename("baz", "..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.mkdir("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-    >>> ufs.rmdir("..")
-    Traceback (most recent call last):
-    ...
-    OSError: [Errno Path not allowed:] ..
-
-
-------------------------------------------------------------------------
-
-Finally, cleanup after ourselves.
-
-    >>> shutil.rmtree(rootpath)
-

=== removed file 'lib/lp/poppy/tests/test_filesystem.py'
--- lib/lp/poppy/tests/test_filesystem.py	2011-12-22 05:09:10 +0000
+++ lib/lp/poppy/tests/test_filesystem.py	1970-01-01 00:00:00 +0000
@@ -1,23 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__metaclass__ = type
-
-import os
-
-from lp.testing.systemdocs import LayeredDocFileSuite
-
-# The setUp() and tearDown() functions ensure that this doctest is not umask
-# dependent.
-def setUp(testobj):
-    testobj._old_umask = os.umask(022)
-
-
-def tearDown(testobj):
-    os.umask(testobj._old_umask)
-
-
-def test_suite():
-    return LayeredDocFileSuite(
-        "filesystem.txt",
-        setUp=setUp, tearDown=tearDown, stdout_logging=False)

=== removed file 'lib/lp/poppy/tests/test_poppy.py'
--- lib/lp/poppy/tests/test_poppy.py	2012-03-26 05:50:20 +0000
+++ lib/lp/poppy/tests/test_poppy.py	1970-01-01 00:00:00 +0000
@@ -1,388 +0,0 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Functional tests for poppy FTP daemon."""
-
-__metaclass__ = type
-
-import os
-import shutil
-import stat
-import StringIO
-import tempfile
-import time
-import unittest
-
-from bzrlib.tests import (
-    condition_id_re,
-    exclude_tests_by_condition,
-    multiply_tests,
-    )
-from bzrlib.transport import get_transport
-from fixtures import (
-    EnvironmentVariableFixture,
-    Fixture,
-    )
-import transaction
-from zope.component import getUtility
-
-from lp.poppy.hooks import Hooks
-from lp.registry.interfaces.ssh import ISSHKeySet
-from lp.services.config import config
-from lp.services.daemons.tachandler import TacTestSetup
-from lp.testing import TestCaseWithFactory
-from lp.testing.layers import (
-    ZopelessAppServerLayer,
-    ZopelessDatabaseLayer,
-    )
-
-
-class FTPServer(Fixture):
-    """This is an abstraction of connecting to an FTP server."""
-
-    def __init__(self, root_dir, factory):
-        self.root_dir = root_dir
-        self.port = 2121
-
-    def setUp(self):
-        super(FTPServer, self).setUp()
-        self.poppytac = self.useFixture(PoppyTac(self.root_dir))
-
-    def getAnonTransport(self):
-        return get_transport(
-            'ftp://anonymous:me@xxxxxxxxxxx@localhost:%s/' % (self.port,))
-
-    def getTransport(self):
-        return get_transport('ftp://ubuntu:@localhost:%s/' % (self.port,))
-
-    def disconnect(self, transport):
-        transport._get_connection().close()
-
-    def waitForStartUp(self):
-        """Wait for the FTP server to start up."""
-        pass
-
-    def waitForClose(self, number=1):
-        """Wait for an FTP connection to close.
-
-        Poppy is configured to echo 'Post-processing finished' to stdout
-        when a connection closes, so we wait for that to appear in its
-        output as a way to tell that the server has finished with the
-        connection.
-        """
-        self.poppytac.waitForPostProcessing(number)
-
-
-class SFTPServer(Fixture):
-    """This is an abstraction of connecting to an SFTP server."""
-
-    def __init__(self, root_dir, factory):
-        self.root_dir = root_dir
-        self._factory = factory
-        self.port = int(config.poppy.port.partition(':')[2])
-
-    def addSSHKey(self, person, public_key_path):
-        f = open(public_key_path, 'r')
-        try:
-            public_key = f.read()
-        finally:
-            f.close()
-        sshkeyset = getUtility(ISSHKeySet)
-        key = sshkeyset.new(person, public_key)
-        transaction.commit()
-        return key
-
-    def setUpUser(self, name):
-        user = self._factory.makePerson(name=name)
-        self.addSSHKey(
-            user, os.path.join(os.path.dirname(__file__), 'poppy-sftp.pub'))
-        # Set up a temporary home directory for Paramiko's sake
-        self._home_dir = tempfile.mkdtemp()
-        self.addCleanup(shutil.rmtree, self._home_dir)
-        os.mkdir(os.path.join(self._home_dir, '.ssh'))
-        os.symlink(
-            os.path.join(os.path.dirname(__file__), 'poppy-sftp'),
-            os.path.join(self._home_dir, '.ssh', 'id_rsa'))
-        self.useFixture(EnvironmentVariableFixture('HOME', self._home_dir))
-        self.useFixture(EnvironmentVariableFixture('SSH_AUTH_SOCK', None))
-        self.useFixture(EnvironmentVariableFixture('BZR_SSH', 'paramiko'))
-
-    def setUp(self):
-        super(SFTPServer, self).setUp()
-        self.setUpUser('joe')
-        self.poppytac = self.useFixture(PoppyTac(self.root_dir))
-
-    def disconnect(self, transport):
-        transport._get_connection().close()
-
-    def waitForStartUp(self):
-        pass
-
-    def waitForClose(self, number=1):
-        self.poppytac.waitForPostProcessing(number)
-
-    def getTransport(self):
-        return get_transport('sftp://joe@localhost:%s/' % (self.port,))
-
-
-class PoppyTac(TacTestSetup):
-    """A SFTP Poppy server fixture.
-
-    This class has two distinct roots:
-     - the POPPY_ROOT where the test looks for uploaded output.
-     - the server root where ssh keys etc go.
-    """
-
-    def __init__(self, fs_root):
-        self.fs_root = fs_root
-        # The setUp check for stale pids races with self._root being assigned,
-        # so store a plausible path temporarily. Once all fixtures use unique
-        # environments this can go.
-        self._root = '/var/does/not/exist'
-
-    def setUp(self):
-        os.environ['POPPY_ROOT'] = self.fs_root
-        super(PoppyTac, self).setUp(umask='0')
-
-    def setUpRoot(self):
-        self._root = tempfile.mkdtemp()
-        self.addCleanup(shutil.rmtree, self.root)
-
-    @property
-    def root(self):
-        return self._root
-
-    @property
-    def tacfile(self):
-        return os.path.abspath(
-            os.path.join(config.root, 'daemons', 'poppy-sftp.tac'))
-
-    @property
-    def logfile(self):
-        return os.path.join(self.root, 'poppy-sftp.log')
-
-    @property
-    def pidfile(self):
-        return os.path.join(self.root, 'poppy-sftp.pid')
-
-    def waitForPostProcessing(self, number=1):
-        now = time.time()
-        deadline = now + 20
-        while now < deadline and not self._hasPostProcessed(number):
-            time.sleep(0.1)
-            now = time.time()
-
-        if now >= deadline:
-            raise Exception("Poppy post-processing did not complete")
-
-    def _hasPostProcessed(self, number):
-        if os.path.exists(self.logfile):
-            with open(self.logfile, "r") as logfile:
-                occurrences = logfile.read().count(Hooks.LOG_MAGIC)
-                return occurrences >= number
-        else:
-            return False
-
-
-class TestPoppy(TestCaseWithFactory):
-    """Test if poppy.py daemon works properly."""
-
-    def setUp(self):
-        """Set up poppy in a temp dir."""
-        super(TestPoppy, self).setUp()
-        self.root_dir = self.makeTemporaryDirectory()
-        self.server = self.server_factory(self.root_dir, self.factory)
-        self.useFixture(self.server)
-
-    def _uploadPath(self, path):
-        """Return system path of specified path inside an upload.
-
-        Only works for a single upload (poppy transaction).
-        """
-        contents = sorted(os.listdir(self.root_dir))
-        upload_dir = contents[1]
-        return os.path.join(self.root_dir, upload_dir, path)
-
-    def test_change_directory_anonymous(self):
-        # Check that FTP access with an anonymous user works.
-        transport = self.server.getAnonTransport()
-        self.test_change_directory(transport)
-
-    def test_change_directory(self, transport=None):
-        """Check automatic creation of directories 'cwd'ed in.
-
-        Also ensure they are created with proper permission (g+rwxs)
-        """
-        self.server.waitForStartUp()
-
-        if transport is None:
-            transport = self.server.getTransport()
-        transport.stat('foo/bar')  # .stat will implicity chdir for us
-
-        self.server.disconnect(transport)
-        self.server.waitForClose()
-
-        wanted_path = self._uploadPath('foo/bar')
-        self.assertTrue(os.path.exists(wanted_path))
-        self.assertEqual(os.stat(wanted_path).st_mode, 042775)
-
-    def test_mkdir(self):
-        # Creating directories on the server makes actual directories where we
-        # expect them, and creates them with g+rwxs
-        self.server.waitForStartUp()
-
-        transport = self.server.getTransport()
-        transport.mkdir('foo/bar', mode=None)
-
-        self.server.disconnect(transport)
-        self.server.waitForClose()
-
-        wanted_path = self._uploadPath('foo/bar')
-        self.assertTrue(os.path.exists(wanted_path))
-        self.assertEqual(os.stat(wanted_path).st_mode, 042775)
-
-    def test_rmdir(self):
-        """Check recursive RMD (aka rmdir)"""
-        self.server.waitForStartUp()
-
-        transport = self.server.getTransport()
-        transport.mkdir('foo/bar')
-        transport.rmdir('foo/bar')
-        transport.rmdir('foo')
-
-        self.server.disconnect(transport)
-        self.server.waitForClose()
-
-        wanted_path = self._uploadPath('foo')
-        self.assertFalse(os.path.exists(wanted_path))
-
-    def test_single_upload(self):
-        """Check if the parent directories are created during file upload.
-
-        The uploaded file permissions are also special (g+rwxs).
-        """
-        self.server.waitForStartUp()
-
-        transport = self.server.getTransport()
-        fake_file = StringIO.StringIO("fake contents")
-
-        transport.put_file('foo/bar/baz', fake_file, mode=None)
-
-        self.server.disconnect(transport)
-        self.server.waitForClose()
-
-        wanted_path = self._uploadPath('foo/bar/baz')
-        fs_content = open(os.path.join(wanted_path)).read()
-        self.assertEqual(fs_content, "fake contents")
-        # Expected mode is -rw-rwSr--.
-        self.assertEqual(
-            os.stat(wanted_path).st_mode,
-            stat.S_IROTH | stat.S_ISGID | stat.S_IRGRP | stat.S_IWGRP
-            | stat.S_IWUSR | stat.S_IRUSR | stat.S_IFREG)
-
-    def test_full_source_upload(self):
-        """Check that the connection will deal with multiple files being
-        uploaded.
-        """
-        self.server.waitForStartUp()
-
-        transport = self.server.getTransport()
-
-        files = ['test-source_0.1.dsc',
-                 'test-source_0.1.orig.tar.gz',
-                 'test-source_0.1.diff.gz',
-                 'test-source_0.1_source.changes']
-
-        for upload in files:
-            fake_file = StringIO.StringIO(upload)
-            file_to_upload = "~ppa-user/ppa/ubuntu/%s" % upload
-            transport.put_file(file_to_upload, fake_file, mode=None)
-
-        self.server.disconnect(transport)
-        self.server.waitForClose()
-
-        upload_path = self._uploadPath('')
-        self.assertEqual(os.stat(upload_path).st_mode, 042770)
-        dir_name = upload_path.split('/')[-2]
-        if transport._user == 'joe':
-            self.assertEqual(dir_name.startswith('upload-sftp-2'), True)
-        elif transport._user == 'ubuntu':
-            self.assertEqual(dir_name.startswith('upload-ftp-2'), True)
-        for upload in files:
-            wanted_path = self._uploadPath(
-                "~ppa-user/ppa/ubuntu/%s" % upload)
-            fs_content = open(os.path.join(wanted_path)).read()
-            self.assertEqual(fs_content, upload)
-            # Expected mode is -rw-rwSr--.
-            self.assertEqual(
-                os.stat(wanted_path).st_mode,
-                stat.S_IROTH | stat.S_ISGID | stat.S_IRGRP | stat.S_IWGRP
-                | stat.S_IWUSR | stat.S_IRUSR | stat.S_IFREG)
-
-    def test_upload_isolation(self):
-        """Check if poppy isolates the uploads properly.
-
-        Upload should be done atomically, i.e., poppy should isolate the
-        context according each connection/session.
-        """
-        # Perform a pair of sessions with distinct connections in time.
-        self.server.waitForStartUp()
-
-        conn_one = self.server.getTransport()
-        fake_file = StringIO.StringIO("ONE")
-        conn_one.put_file('test', fake_file, mode=None)
-        self.server.disconnect(conn_one)
-        self.server.waitForClose(1)
-
-        conn_two = self.server.getTransport()
-        fake_file = StringIO.StringIO("TWO")
-        conn_two.put_file('test', fake_file, mode=None)
-        self.server.disconnect(conn_two)
-        self.server.waitForClose(2)
-
-        # Perform a pair of sessions with simultaneous connections.
-        conn_three = self.server.getTransport()
-        conn_four = self.server.getTransport()
-
-        fake_file = StringIO.StringIO("THREE")
-        conn_three.put_file('test', fake_file, mode=None)
-
-        fake_file = StringIO.StringIO("FOUR")
-        conn_four.put_file('test', fake_file, mode=None)
-
-        self.server.disconnect(conn_three)
-        self.server.waitForClose(3)
-
-        self.server.disconnect(conn_four)
-        self.server.waitForClose(4)
-
-        # Build a list of directories representing the 4 sessions.
-        upload_dirs = [leaf for leaf in sorted(os.listdir(self.root_dir))
-                       if not leaf.startswith(".") and
-                       not leaf.endswith(".distro")]
-        self.assertEqual(len(upload_dirs), 4)
-
-        # Check the contents of files on each session.
-        expected_contents = ['ONE', 'TWO', 'THREE', 'FOUR']
-        for index in range(4):
-            content = open(os.path.join(
-                self.root_dir, upload_dirs[index], "test")).read()
-            self.assertEqual(content, expected_contents[index])
-
-
-def test_suite():
-    tests = unittest.TestLoader().loadTestsFromName(__name__)
-    scenarios = [
-        ('ftp', {'server_factory': FTPServer,
-                 # XXX: In an ideal world, this would be in the UnitTests
-                 # layer. Let's get one step closer to that ideal world.
-                 'layer': ZopelessDatabaseLayer}),
-        ('sftp', {'server_factory': SFTPServer,
-                  'layer': ZopelessAppServerLayer}),
-        ]
-    suite = unittest.TestSuite()
-    multiply_tests(tests, scenarios, suite)
-    # SFTP doesn't have the concept of the server changing directories, since
-    # clients will only send absolute paths, so drop that test.
-    return exclude_tests_by_condition(
-        suite, condition_id_re(r'test_change_directory.*\(sftp\)$'))

=== removed file 'lib/lp/poppy/tests/test_twistedsftp.py'
--- lib/lp/poppy/tests/test_twistedsftp.py	2015-01-06 12:47:59 +0000
+++ lib/lp/poppy/tests/test_twistedsftp.py	1970-01-01 00:00:00 +0000
@@ -1,70 +0,0 @@
-# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for twistedsftp."""
-
-__metaclass__ = type
-
-import os
-
-from fixtures import TempDir
-from lazr.sshserver.sftp import FileIsADirectory
-
-from lp.poppy.twistedsftp import SFTPServer
-from lp.testing import (
-    NestedTempfile,
-    TestCase,
-    )
-
-
-class TestSFTPServer(TestCase):
-
-    def setUp(self):
-        self.useFixture(NestedTempfile())
-        self.fs_root = self.useFixture(TempDir()).path
-        self.sftp_server = SFTPServer(None, self.fs_root)
-        super(TestSFTPServer, self).setUp()
-
-    def assertPermissions(self, expected, file_name):
-        observed = os.stat(file_name).st_mode
-        self.assertEqual(
-            expected, observed, "Expected %07o, got %07o, for %s" % (
-                expected, observed, file_name))
-
-    def test_gotVersion(self):
-        # gotVersion always returns an empty dict, since the server does not
-        # support any extended features. See ISFTPServer.
-        extras = self.sftp_server.gotVersion(None, None)
-        self.assertEquals(extras, {})
-
-    def test_mkdir_and_rmdir(self):
-        self.sftp_server.makeDirectory('foo/bar', None)
-        self.assertEqual(
-            os.listdir(os.path.join(self.sftp_server._current_upload))[0],
-            'foo')
-        dir_name = os.path.join(self.sftp_server._current_upload, 'foo')
-        self.assertEqual(os.listdir(dir_name)[0], 'bar')
-        self.assertPermissions(040775, dir_name)
-        self.sftp_server.removeDirectory('foo/bar')
-        self.assertEqual(
-            os.listdir(os.path.join(self.sftp_server._current_upload,
-            'foo')), [])
-        self.sftp_server.removeDirectory('foo')
-        self.assertEqual(
-            os.listdir(os.path.join(self.sftp_server._current_upload)), [])
-
-    def test_file_creation(self):
-        upload_file = self.sftp_server.openFile('foo/bar', None, None)
-        upload_file.writeChunk(0, "This is a test")
-        file_name = os.path.join(self.sftp_server._current_upload, 'foo/bar')
-        test_file = open(file_name, 'r')
-        self.assertEqual(test_file.read(), "This is a test")
-        test_file.close()
-        self.assertPermissions(0100644, file_name)
-        dir_name = os.path.join(self.sftp_server._current_upload, 'bar/foo')
-        os.makedirs(dir_name)
-        upload_file = self.sftp_server.openFile('bar/foo', None, None)
-        self.assertRaisesWithContent(
-            FileIsADirectory,
-            "File is a directory: '%s'" % dir_name,
-            upload_file.writeChunk, 0, "This is a test")

=== removed file 'lib/lp/poppy/twistedftp.py'
--- lib/lp/poppy/twistedftp.py	2012-03-26 07:14:35 +0000
+++ lib/lp/poppy/twistedftp.py	1970-01-01 00:00:00 +0000
@@ -1,160 +0,0 @@
-# Copyright 2011 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Twisted FTP implementation of the Poppy upload server."""
-
-__metaclass__ = type
-__all__ = [
-    'FTPRealm',
-    'PoppyAnonymousShell',
-    ]
-
-import logging
-import os
-import tempfile
-
-from twisted.application import (
-    service,
-    strports,
-    )
-from twisted.cred import (
-    checkers,
-    credentials,
-    )
-from twisted.cred.portal import (
-    IRealm,
-    Portal,
-    )
-from twisted.internet import defer
-from twisted.protocols import ftp
-from twisted.python import filepath
-from zope.interface import implements
-
-from lp.poppy import get_poppy_root
-from lp.poppy.filesystem import UploadFileSystem
-from lp.poppy.hooks import Hooks
-from lp.services.config import config
-
-
-class PoppyAccessCheck:
-    """An `ICredentialsChecker` for Poppy FTP sessions."""
-    implements(checkers.ICredentialsChecker)
-    credentialInterfaces = (
-        credentials.IUsernamePassword, credentials.IAnonymous)
-
-    def requestAvatarId(self, credentials):
-        # Poppy allows any credentials.  People can use "anonymous" if
-        # they want but anything goes.  Thus, we don't actually *check* the
-        # credentials, and we return the standard avatarId for 'anonymous'.
-        return checkers.ANONYMOUS
-
-
-class PoppyAnonymousShell(ftp.FTPShell):
-    """The 'command' interface for sessions.
-
-    Roughly equivalent to the SFTPServer in the sftp side of things.
-    """
-
-    def __init__(self, fsroot):
-        self._fs_root = fsroot
-        self.uploadfilesystem = UploadFileSystem(tempfile.mkdtemp())
-        self._current_upload = self.uploadfilesystem.rootpath
-        os.chmod(self._current_upload, 0770)
-        self._log = logging.getLogger("poppy-sftp")
-        self.hook = Hooks(
-            self._fs_root, self._log, "ubuntu", perms='g+rws',
-            prefix='-ftp')
-        self.hook.new_client_hook(self._current_upload, 0, 0)
-        self.hook.auth_verify_hook(self._current_upload, None, None)
-        super(PoppyAnonymousShell, self).__init__(
-            filepath.FilePath(self._current_upload))
-
-    def openForWriting(self, file_segments):
-        """Write the uploaded file to disk, safely.
-
-        :param file_segments: A list containing string items, one for each
-            path component of the file being uploaded.  The file referenced
-            is relative to the temporary root for this session.
-
-        If the file path contains directories, we create them.
-        """
-        filename = os.sep.join(file_segments)
-        self._create_missing_directories(filename)
-        return super(PoppyAnonymousShell, self).openForWriting(file_segments)
-
-    def makeDirectory(self, path):
-        """Make a directory using the secure `UploadFileSystem`."""
-        path = os.sep.join(path)
-        return defer.maybeDeferred(self.uploadfilesystem.mkdir, path)
-
-    def access(self, segments):
-        """Permissive CWD that auto-creates target directories."""
-        if segments:
-            path = self._path(segments)
-            path.makedirs()
-        return super(PoppyAnonymousShell, self).access(segments)
-
-    def logout(self):
-        """Called when the client disconnects.
-
-        We need to post-process the upload.
-        """
-        self.hook.client_done_hook(self._current_upload, 0, 0)
-
-    def _create_missing_directories(self, filename):
-        # Same as SFTPServer
-        new_dir, new_file = os.path.split(
-            self.uploadfilesystem._sanitize(filename))
-        if new_dir != '':
-            if not os.path.exists(
-                os.path.join(self._current_upload, new_dir)):
-                self.uploadfilesystem.mkdir(new_dir)
-
-    def list(self, path_segments, attrs):
-        return defer.fail(ftp.CmdNotImplementedError("LIST"))
-
-
-class FTPRealm:
-    """FTP Realm that lets anyone in."""
-    implements(IRealm)
-
-    def __init__(self, root):
-        self.root = root
-
-    def requestAvatar(self, avatarId, mind, *interfaces):
-        """Return a Poppy avatar - that is, an "authorisation".
-
-        Poppy FTP avatars are totally fake, we don't care about credentials.
-        See `PoppyAccessCheck` above.
-        """
-        for iface in interfaces:
-            if iface is ftp.IFTPShell:
-                avatar = PoppyAnonymousShell(self.root)
-                return ftp.IFTPShell, avatar, getattr(
-                    avatar, 'logout', lambda: None)
-        raise NotImplementedError(
-            "Only IFTPShell interface is supported by this realm")
-
-
-class FTPServiceFactory(service.Service):
-    """A factory that makes an `FTPService`"""
-
-    def __init__(self, port):
-        realm = FTPRealm(get_poppy_root())
-        portal = Portal(realm)
-        portal.registerChecker(PoppyAccessCheck())
-        factory = ftp.FTPFactory(portal)
-
-        factory.tld = get_poppy_root()
-        factory.protocol = ftp.FTP
-        factory.welcomeMessage = "Launchpad upload server"
-        factory.timeOut = config.poppy.idle_timeout
-
-        self.ftpfactory = factory
-        self.portno = port
-
-    @staticmethod
-    def makeFTPService(port=2121):
-        strport = "tcp:%s" % port
-        factory = FTPServiceFactory(port)
-        return strports.service(strport, factory.ftpfactory)

=== removed file 'lib/lp/poppy/twistedsftp.py'
--- lib/lp/poppy/twistedsftp.py	2015-01-06 12:47:59 +0000
+++ lib/lp/poppy/twistedsftp.py	1970-01-01 00:00:00 +0000
@@ -1,144 +0,0 @@
-# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Twisted SFTP implementation of the Poppy upload server."""
-
-__metaclass__ = type
-__all__ = [
-    'SFTPFile',
-    'SFTPServer',
-    ]
-
-import errno
-import logging
-import os
-import tempfile
-
-from lazr.sshserver.events import SFTPClosed
-from lazr.sshserver.sftp import FileIsADirectory
-from twisted.conch.interfaces import (
-    ISFTPFile,
-    ISFTPServer,
-    )
-from zope.component import (
-    adapter,
-    provideHandler,
-    )
-from zope.interface import implements
-
-from lp.poppy.filesystem import UploadFileSystem
-from lp.poppy.hooks import Hooks
-
-
-class SFTPServer:
-    """An implementation of `ISFTPServer` that backs onto a Poppy filesystem.
-    """
-
-    implements(ISFTPServer)
-
-    def __init__(self, avatar, fsroot):
-        provideHandler(self.connectionClosed)
-        self._avatar = avatar
-        self._fs_root = fsroot
-        self.uploadfilesystem = UploadFileSystem(tempfile.mkdtemp())
-        self._current_upload = self.uploadfilesystem.rootpath
-        os.chmod(self._current_upload, 0770)
-        self._log = logging.getLogger("poppy-sftp")
-        self.hook = Hooks(
-            self._fs_root, self._log, "ubuntu", perms='g+rws', prefix='-sftp')
-        self.hook.new_client_hook(self._current_upload, 0, 0)
-        self.hook.auth_verify_hook(self._current_upload, None, None)
-
-    def gotVersion(self, other_version, ext_data):
-        return {}
-
-    def openFile(self, filename, flags, attrs):
-        self._create_missing_directories(filename)
-        absfile = self._translate_path(filename)
-        return SFTPFile(absfile)
-
-    def removeFile(self, filename):
-        pass
-
-    def renameFile(self, old_path, new_path):
-        abs_old = self._translate_path(old_path)
-        abs_new = self._translate_path(new_path)
-        os.rename(abs_old, abs_new)
-
-    def makeDirectory(self, path, attrs):
-        # XXX: We ignore attrs here
-        self.uploadfilesystem.mkdir(path)
-
-    def removeDirectory(self, path):
-        self.uploadfilesystem.rmdir(path)
-
-    def openDirectory(self, path):
-        pass
-
-    def getAttrs(self, path, follow_links):
-        pass
-
-    def setAttrs(self, path, attrs):
-        pass
-
-    def readLink(self, path):
-        pass
-
-    def makeLink(self, link_path, target_path):
-        pass
-
-    def realPath(self, path):
-        return path
-
-    def extendedRequest(self, extended_name, extended_data):
-        pass
-
-    def _create_missing_directories(self, filename):
-        new_dir, new_file = os.path.split(
-            self.uploadfilesystem._sanitize(filename))
-        if new_dir != '':
-            if not os.path.exists(
-                os.path.join(self._current_upload, new_dir)):
-                self.uploadfilesystem.mkdir(new_dir)
-
-    def _translate_path(self, filename):
-        return self.uploadfilesystem._full(
-            self.uploadfilesystem._sanitize(filename))
-
-    @adapter(SFTPClosed)
-    def connectionClosed(self, event):
-        if event.avatar is not self._avatar:
-            return
-        self.hook.client_done_hook(self._current_upload, 0, 0)
-
-
-class SFTPFile:
-
-    implements(ISFTPFile)
-
-    def __init__(self, filename):
-        self.filename = filename
-
-    def close(self):
-        pass
-
-    def readChunk(self, offset, length):
-        pass
-
-    def writeChunk(self, offset, data):
-        try:
-            chunk_file = os.open(
-                self.filename, os.O_CREAT | os.O_WRONLY, 0644)
-        except OSError as e:
-            if e.errno != errno.EISDIR:
-                raise
-            raise FileIsADirectory(self.filename)
-        os.lseek(chunk_file, offset, 0)
-        os.write(chunk_file, data)
-        os.close(chunk_file)
-
-    def getAttrs(self):
-        pass
-
-    def setAttrs(self, attr):
-        pass

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2013-05-14 05:29:03 +0000
+++ lib/lp/testing/__init__.py	2015-01-13 15:34:27 +0000
@@ -1398,17 +1398,6 @@
     return launchpad.load(canonical_url(obj, request=api_request))
 
 
-class NestedTempfile(fixtures.Fixture):
-    """Nest all temporary files and directories inside a top-level one."""
-
-    def setUp(self):
-        super(NestedTempfile, self).setUp()
-        tempdir = fixtures.TempDir()
-        self.useFixture(tempdir)
-        patch = fixtures.MonkeyPatch("tempfile.tempdir", tempdir.path)
-        self.useFixture(patch)
-
-
 @contextmanager
 def monkey_patch(context, **kwargs):
     """In the ContextManager scope, monkey-patch values.

=== modified file 'lib/lp/testing/tests/test_testing.py'
--- lib/lp/testing/tests/test_testing.py	2012-06-08 08:54:37 +0000
+++ lib/lp/testing/tests/test_testing.py	2015-01-13 15:34:27 +0000
@@ -6,7 +6,6 @@
 __metaclass__ = type
 
 import os
-import tempfile
 
 from lp.services.config import config
 from lp.services.features import (
@@ -15,7 +14,6 @@
     )
 from lp.testing import (
     feature_flags,
-    NestedTempfile,
     set_feature_flag,
     TestCase,
     YUIUnitTestCase,
@@ -62,31 +60,3 @@
         test_path = os.path.join(config.root, "../bar/baz/../bob.html")
         test.initialize(test_path)
         self.assertEqual("../bar/bob.html", test.id())
-
-
-class NestedTempfileTest(TestCase):
-    """Tests for `NestedTempfile`."""
-
-    def test_normal(self):
-        # The temp directory is removed when the context is exited.
-        starting_tempdir = tempfile.gettempdir()
-        with NestedTempfile():
-            self.assertEqual(tempfile.tempdir, tempfile.gettempdir())
-            self.assertNotEqual(tempfile.tempdir, starting_tempdir)
-            self.assertTrue(os.path.isdir(tempfile.tempdir))
-            nested_tempdir = tempfile.tempdir
-        self.assertEqual(tempfile.tempdir, tempfile.gettempdir())
-        self.assertEqual(starting_tempdir, tempfile.tempdir)
-        self.assertFalse(os.path.isdir(nested_tempdir))
-
-    def test_exception(self):
-        # The temp directory is removed when the context is exited, even if
-        # the code running in context raises an exception.
-        class ContrivedException(Exception):
-            pass
-        try:
-            with NestedTempfile():
-                nested_tempdir = tempfile.tempdir
-                raise ContrivedException
-        except ContrivedException:
-            self.assertFalse(os.path.isdir(nested_tempdir))

=== modified file 'utilities/snakefood/lp-sfood-packages'
--- utilities/snakefood/lp-sfood-packages	2011-12-30 06:47:17 +0000
+++ utilities/snakefood/lp-sfood-packages	2015-01-13 15:34:27 +0000
@@ -5,7 +5,6 @@
 lp/services
 lp/scripts
 lp/registry
-lp/poppy
 lp/hardwaredb
 lp/coop/answersbugs
 lp/codehosting

=== modified file 'utilities/start-dev-soyuz.sh'
--- utilities/start-dev-soyuz.sh	2011-12-08 01:38:24 +0000
+++ utilities/start-dev-soyuz.sh	2015-01-13 15:34:27 +0000
@@ -14,11 +14,24 @@
         -y "$tac" $@
 }
 
+start_twistd_plugin() {
+    # Start twistd for plugin service $1.
+    name=$1
+    plugin=$2
+    shift 2
+    echo "Starting $name."
+    "bin/twistd-for-$name" \
+        --logfile "/var/tmp/development-$name.log" \
+        --pidfile "/var/tmp/development-$name.pid" \
+        "$plugin" "$@"
+}
+
 start_twistd testkeyserver lib/lp/testing/keyserver/testkeyserver.tac
 start_twistd buildd-manager daemons/buildd-manager.tac
-mkdir -p /var/tmp/poppy/incoming
-export POPPY_ROOT=/var/tmp/poppy/incoming
-start_twistd poppy-sftp daemons/poppy-sftp.tac
+mkdir -p /var/tmp/txpkgupload/incoming
+export TXPKGUPLOAD_ROOT=/var/tmp/txpkgupload/incoming
+start_twistd_plugin txpkgupload pkgupload \
+    -c configs/development/txpkgupload.yaml
 
 
 echo "Done."

=== modified file 'versions.cfg'
--- versions.cfg	2015-01-06 12:47:59 +0000
+++ versions.cfg	2015-01-13 15:34:27 +0000
@@ -32,6 +32,7 @@
 FeedParser = 4.1
 feedvalidator = 0.0.0DEV-r1049
 fixtures = 0.3.9
+FormEncode = 1.2.4
 funkload = 1.16.1
 grokcore.component = 1.6
 html5browser = 0.0.9
@@ -104,6 +105,7 @@
 python-openid = 2.2.5-fix1034376
 python-subunit = 0.0.8beta
 python-swiftclient = 1.5.0
+PyYAML = 3.10
 rabbitfixture = 0.3.5
 requests = 1.2.3
 s4 = 0.1.2
@@ -125,6 +127,7 @@
 txfixtures = 0.1.4
 txlongpoll = 0.2.12
 txlongpollfixture = 0.1.3
+txpkgupload = 0.1.1
 unittest2 = 0.5.1
 van.testing = 3.0.0
 wadllib = 1.3.2


Follow ups