← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad-buildd/proxy-configs into lp:launchpad-buildd

 

Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/proxy-configs into lp:launchpad-buildd.

Commit message:
Allow configuring APT or snap store proxies via a new [proxy] configuration file section.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/proxy-configs/+merge/362332

This makes testing builds behind a slow connection 90% less annoying.

While python-requests and python-six are new direct dependencies in this branch, they were already indirect dependencies.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/proxy-configs into lp:launchpad-buildd.
=== modified file 'debian/changelog'
--- debian/changelog	2019-01-28 13:11:11 +0000
+++ debian/changelog	2019-01-28 15:15:21 +0000
@@ -1,3 +1,10 @@
+launchpad-buildd (167) UNRELEASED; urgency=medium
+
+  * Allow configuring APT or snap store proxies via a new [proxy]
+    configuration file section.
+
+ -- Colin Watson <cjwatson@xxxxxxxxxx>  Mon, 28 Jan 2019 14:23:16 +0000
+
 launchpad-buildd (166) xenial; urgency=medium
 
   [ Colin Watson ]

=== modified file 'debian/control'
--- debian/control	2018-06-07 16:23:18 +0000
+++ debian/control	2019-01-28 15:15:21 +0000
@@ -17,7 +17,10 @@
                python-mock,
                python-netaddr,
                python-pylxd,
+               python-requests,
+               python-responses,
                python-setuptools,
+               python-six,
                python-systemfixtures,
                python-testtools,
                python-twisted,

=== modified file 'lpbuildd/debian.py'
--- lpbuildd/debian.py	2018-10-19 06:41:14 +0000
+++ lpbuildd/debian.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # Authors: Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
@@ -13,6 +13,10 @@
 import re
 import signal
 
+from six.moves.configparser import (
+    NoOptionError,
+    NoSectionError,
+    )
 from twisted.internet import (
     defer,
     threads,
@@ -63,7 +67,14 @@
 
         Mainly used for PPA builds.
         """
-        self.runTargetSubProcess("override-sources-list", *self.sources_list)
+        args = []
+        try:
+            apt_proxy_url = self._slave._config.get("proxy", "apt")
+            args.extend(["--apt-proxy-url", apt_proxy_url])
+        except (NoSectionError, NoOptionError):
+            pass
+        args.extend(self.sources_list)
+        self.runTargetSubProcess("override-sources-list", *args)
 
     def doTrustedKeys(self):
         """Add trusted keys."""

=== modified file 'lpbuildd/livefs.py'
--- lpbuildd/livefs.py	2019-01-28 13:07:53 +0000
+++ lpbuildd/livefs.py	2019-01-28 15:15:21 +0000
@@ -1,10 +1,15 @@
-# Copyright 2013-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2013-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
 
 import os
 
+from six.moves.configparser import (
+    NoOptionError,
+    NoSectionError,
+    )
+
 from lpbuildd.debian import (
     DebianBuildManager,
     DebianBuildState,
@@ -71,6 +76,12 @@
             args.extend(["--repo-snapshot-stamp", self.repo_snapshot_stamp])
         if self.cohort_key:
             args.extend(["--cohort-key", self.cohort_key])
+        try:
+            snap_store_proxy_url = self._slave._config.get(
+                "proxy", "snapstore")
+            args.extend(["--snap-store-proxy-url", snap_store_proxy_url])
+        except (NoSectionError, NoOptionError):
+            pass
         if self.debug:
             args.append("--debug")
         self.runTargetSubProcess("buildlivefs", *args)

=== modified file 'lpbuildd/snap.py'
--- lpbuildd/snap.py	2018-10-19 06:41:14 +0000
+++ lpbuildd/snap.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 from __future__ import print_function
@@ -29,6 +29,10 @@
         )
     from urlparse import urlparse
 
+from six.moves.configparser import (
+    NoOptionError,
+    NoSectionError,
+    )
 from twisted.application import strports
 from twisted.internet import reactor
 from twisted.internet.interfaces import IHalfCloseableProtocol
@@ -348,6 +352,12 @@
             args.extend(["--git-path", self.git_path])
         if self.build_source_tarball:
             args.append("--build-source-tarball")
+        try:
+            snap_store_proxy_url = self._slave._config.get(
+                "proxy", "snapstore")
+            args.extend(["--snap-store-proxy-url", snap_store_proxy_url])
+        except (NoSectionError, NoOptionError):
+            pass
         args.append(self.name)
         self.runTargetSubProcess("buildsnap", *args)
 

=== modified file 'lpbuildd/target/apt.py'
--- lpbuildd/target/apt.py	2017-08-22 15:55:44 +0000
+++ lpbuildd/target/apt.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 from __future__ import print_function
@@ -26,6 +26,8 @@
     def add_arguments(cls, parser):
         super(OverrideSourcesList, cls).add_arguments(parser)
         parser.add_argument(
+            "--apt-proxy-url", metavar="URL", help="APT proxy URL")
+        parser.add_argument(
             "archives", metavar="ARCHIVE", nargs="+",
             help="sources.list lines")
 
@@ -37,6 +39,16 @@
             sources_list.flush()
             os.fchmod(sources_list.fileno(), 0o644)
             self.backend.copy_in(sources_list.name, "/etc/apt/sources.list")
+        if self.args.apt_proxy_url is not None:
+            with tempfile.NamedTemporaryFile() as apt_proxy_conf:
+                print(
+                    'Acquire::http::Proxy "{}";'.format(
+                        self.args.apt_proxy_url),
+                    file=apt_proxy_conf)
+                apt_proxy_conf.flush()
+                os.fchmod(apt_proxy_conf.fileno(), 0o644)
+                self.backend.copy_in(
+                    apt_proxy_conf.name, "/etc/apt/apt.conf.d/99proxy")
         return 0
 
 

=== modified file 'lpbuildd/target/build_livefs.py'
--- lpbuildd/target/build_livefs.py	2019-01-28 13:10:24 +0000
+++ lpbuildd/target/build_livefs.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2013-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2013-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 from __future__ import print_function
@@ -10,6 +10,7 @@
 import os
 
 from lpbuildd.target.operation import Operation
+from lpbuildd.target.snapstore import SnapStoreOperationMixin
 
 
 RETCODE_FAILURE_INSTALL = 200
@@ -29,7 +30,7 @@
     return os.path.join(os.environ["HOME"], "build-" + build_id, *extra)
 
 
-class BuildLiveFS(Operation):
+class BuildLiveFS(SnapStoreOperationMixin, Operation):
 
     description = "Build a live file system."
 
@@ -94,6 +95,8 @@
                 if self.backend.is_package_available(dep):
                     deps.append(dep)
         self.backend.run(["apt-get", "-y", "install"] + deps)
+        if self.args.backend in ("lxd", "fake"):
+            self.snap_store_set_proxy()
         if self.args.arch == "i386":
             self.backend.run([
                 "apt-get", "-y", "--no-install-recommends", "install",

=== modified file 'lpbuildd/target/build_snap.py'
--- lpbuildd/target/build_snap.py	2018-10-11 08:59:22 +0000
+++ lpbuildd/target/build_snap.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 from __future__ import print_function
@@ -18,6 +18,7 @@
     from urlparse import urlparse
 
 from lpbuildd.target.operation import Operation
+from lpbuildd.target.snapstore import SnapStoreOperationMixin
 from lpbuildd.target.vcs import VCSOperationMixin
 
 
@@ -28,7 +29,7 @@
 logger = logging.getLogger(__name__)
 
 
-class BuildSnap(VCSOperationMixin, Operation):
+class BuildSnap(VCSOperationMixin, SnapStoreOperationMixin, Operation):
 
     description = "Build a snap."
 
@@ -126,6 +127,8 @@
         else:
             deps.append("snapcraft")
         self.backend.run(["apt-get", "-y", "install"] + deps)
+        if self.args.backend in ("lxd", "fake"):
+            self.snap_store_set_proxy()
         if self.args.channel_core:
             self.backend.run(
                 ["snap", "install",

=== added file 'lpbuildd/target/snapstore.py'
--- lpbuildd/target/snapstore.py	1970-01-01 00:00:00 +0000
+++ lpbuildd/target/snapstore.py	2019-01-28 15:15:21 +0000
@@ -0,0 +1,54 @@
+# Copyright 2019 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from __future__ import print_function
+
+__metaclass__ = type
+
+import requests
+from six.moves.urllib.parse import (
+    urljoin,
+    urlparse,
+    urlunparse,
+    )
+
+
+class SnapStoreOperationMixin:
+    """Methods supporting operations that interact with the snap store."""
+
+    @classmethod
+    def add_arguments(cls, parser):
+        super(SnapStoreOperationMixin, cls).add_arguments(parser)
+        parser.add_argument(
+            "--snap-store-proxy-url", metavar="URL",
+            help="snap store proxy URL")
+
+    def snap_store_set_proxy(self):
+        if self.args.snap_store_proxy_url is None:
+            return
+        # Canonicalise: proxy registration always sends only the scheme and
+        # domain.
+        parsed_url = urlparse(self.args.snap_store_proxy_url)
+        canonical_url = urlunparse(
+            [parsed_url.scheme, parsed_url.netloc, "", "", "", ""])
+        assertions_response = requests.get(
+            urljoin(canonical_url, "v2/auth/store/assertions"))
+        assertions_response.raise_for_status()
+        self.backend.run(
+            ["snap", "ack", "/dev/stdin"], input_text=assertions_response.text)
+        store_assertion = self.backend.run(
+            ["snap", "known", "store", "url={}".format(canonical_url)],
+            get_output=True)
+        # Very cheap parser.  Not at all robust, but snapd has already
+        # handled validation for us, and if we get more than one assertion
+        # back from "snap known" despite filtering then we only care about
+        # the first.
+        for line in store_assertion.split("\n\n")[0].splitlines():
+            if line.startswith("store: "):
+                store_id = line[len("store: "):]
+                break
+        else:
+            store_id = None
+        if store_id is not None:
+            self.backend.run(
+                ["snap", "set", "core", "proxy.store={}".format(store_id)])

=== modified file 'lpbuildd/target/tests/test_apt.py'
--- lpbuildd/target/tests/test_apt.py	2017-08-22 15:55:44 +0000
+++ lpbuildd/target/tests/test_apt.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2017-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -53,6 +53,27 @@
                 """).encode("UTF-8"), stat.S_IFREG | 0o644),
             override_sources_list.backend.backend_fs["/etc/apt/sources.list"])
 
+    def test_apt_proxy(self):
+        args = [
+            "override-sources-list",
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--apt-proxy-url", "http://apt-proxy.example:3128/";,
+            "deb http://archive.ubuntu.com/ubuntu xenial main",
+            ]
+        override_sources_list = parse_args(args=args).operation
+        self.assertEqual(0, override_sources_list.run())
+        self.assertEqual(
+            (dedent("""\
+                deb http://archive.ubuntu.com/ubuntu xenial main
+                """).encode("UTF-8"), stat.S_IFREG | 0o644),
+            override_sources_list.backend.backend_fs["/etc/apt/sources.list"])
+        self.assertEqual(
+            (dedent("""\
+                Acquire::http::Proxy "http://apt-proxy.example:3128/";;
+                """).encode("UTF-8"), stat.S_IFREG | 0o644),
+            override_sources_list.backend.backend_fs[
+                "/etc/apt/apt.conf.d/99proxy"])
+
 
 class TestAddTrustedKeys(TestCase):
 

=== modified file 'lpbuildd/target/tests/test_build_livefs.py'
--- lpbuildd/target/tests/test_build_livefs.py	2018-01-12 20:01:24 +0000
+++ lpbuildd/target/tests/test_build_livefs.py	2019-01-28 15:15:21 +0000
@@ -1,11 +1,13 @@
-# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2017-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
 
 import subprocess
+from textwrap import dedent
 
 from fixtures import FakeLogger
+import responses
 from testtools import TestCase
 from testtools.matchers import (
     AnyMatch,
@@ -26,12 +28,17 @@
 
 class RanCommand(MatchesListwise):
 
-    def __init__(self, args, echo=None, cwd=None, **env):
+    def __init__(self, args, echo=None, cwd=None, input_text=None,
+                 get_output=None, **env):
         kwargs_matcher = {}
         if echo is not None:
             kwargs_matcher["echo"] = Is(echo)
         if cwd:
             kwargs_matcher["cwd"] = Equals(cwd)
+        if input_text:
+            kwargs_matcher["input_text"] = Equals(input_text)
+        if get_output is not None:
+            kwargs_matcher["get_output"] = Is(get_output)
         if env:
             kwargs_matcher["env"] = MatchesDict(
                 {key: Equals(value) for key, value in env.items()})
@@ -113,6 +120,44 @@
                 "--install-recommends", "install", "ubuntu-defaults-builder"),
             ]))
 
+    @responses.activate
+    def test_install_snap_store_proxy(self):
+        store_assertion = dedent("""\
+            type: store
+            store: store-id
+            url: http://snap-store-proxy.example
+
+            body
+            """)
+
+        class SnapCommands(FakeMethod):
+            def __call__(self, run_args, *args, **kwargs):
+                super(SnapCommands, self).__call__(run_args, *args, **kwargs)
+                if run_args[0] == "snap" and run_args[1] == "known":
+                    return store_assertion
+
+        responses.add(
+            "GET", "http://snap-store-proxy.example/v2/auth/store/assertions";,
+            body=store_assertion)
+        args = [
+            "buildlivefs",
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--snap-store-proxy-url", "http://snap-store-proxy.example/";,
+            ]
+        build_livefs = parse_args(args=args).operation
+        build_livefs.backend.run = SnapCommands()
+        build_livefs.install()
+        self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
+            RanAptGet("install", "livecd-rootfs"),
+            RanCommand(
+                ["snap", "ack", "/dev/stdin"], input_text=store_assertion),
+            RanCommand(
+                ["snap", "known", "store",
+                 "url=http://snap-store-proxy.example";],
+                get_output=True),
+            RanCommand(["snap", "set", "core", "proxy.store=store-id"]),
+            ]))
+
     def test_build(self):
         args = [
             "buildlivefs",

=== modified file 'lpbuildd/target/tests/test_build_snap.py'
--- lpbuildd/target/tests/test_build_snap.py	2018-10-11 08:59:22 +0000
+++ lpbuildd/target/tests/test_build_snap.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2017-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -7,11 +7,13 @@
 import os.path
 import stat
 import subprocess
+from textwrap import dedent
 
 from fixtures import (
     FakeLogger,
     TempDir,
     )
+import responses
 from systemfixtures import FakeFilesystem
 from testtools import TestCase
 from testtools.matchers import (
@@ -33,14 +35,17 @@
 
 class RanCommand(MatchesListwise):
 
-    def __init__(self, args, get_output=None, echo=None, cwd=None, **env):
+    def __init__(self, args, echo=None, cwd=None, input_text=None,
+                 get_output=None, **env):
         kwargs_matcher = {}
-        if get_output is not None:
-            kwargs_matcher["get_output"] = Is(get_output)
         if echo is not None:
             kwargs_matcher["echo"] = Is(echo)
         if cwd:
             kwargs_matcher["cwd"] = Equals(cwd)
+        if input_text:
+            kwargs_matcher["input_text"] = Equals(input_text)
+        if get_output is not None:
+            kwargs_matcher["get_output"] = Is(get_output)
         if env:
             kwargs_matcher["env"] = MatchesDict(
                 {key: Equals(value) for key, value in env.items()})
@@ -132,6 +137,46 @@
             RanAptGet("install", "git", "snapcraft"),
             ]))
 
+    @responses.activate
+    def test_install_snap_store_proxy(self):
+        store_assertion = dedent("""\
+            type: store
+            store: store-id
+            url: http://snap-store-proxy.example
+
+            body
+            """)
+
+        class SnapCommands(FakeMethod):
+            def __call__(self, run_args, *args, **kwargs):
+                super(SnapCommands, self).__call__(run_args, *args, **kwargs)
+                if run_args[0] == "snap" and run_args[1] == "known":
+                    return store_assertion
+
+        responses.add(
+            "GET", "http://snap-store-proxy.example/v2/auth/store/assertions";,
+            body=store_assertion)
+        args = [
+            "buildsnap",
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--git-repository", "lp:foo",
+            "--snap-store-proxy-url", "http://snap-store-proxy.example/";,
+            "test-snap",
+            ]
+        build_snap = parse_args(args=args).operation
+        build_snap.backend.run = SnapCommands()
+        build_snap.install()
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanAptGet("install", "git", "snapcraft"),
+            RanCommand(
+                ["snap", "ack", "/dev/stdin"], input_text=store_assertion),
+            RanCommand(
+                ["snap", "known", "store",
+                 "url=http://snap-store-proxy.example";],
+                get_output=True),
+            RanCommand(["snap", "set", "core", "proxy.store=store-id"]),
+            ]))
+
     def test_install_proxy(self):
         args = [
             "buildsnap",

=== modified file 'lpbuildd/tests/fakeslave.py'
--- lpbuildd/tests/fakeslave.py	2017-11-10 20:55:33 +0000
+++ lpbuildd/tests/fakeslave.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2013-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -9,12 +9,18 @@
     'UncontainedBackend',
     ]
 
+from collections import defaultdict
 import hashlib
 import os
 import shutil
 import stat
 import subprocess
 
+from six.moves.configparser import (
+    NoOptionError,
+    NoSectionError,
+    )
+
 from lpbuildd.target.backend import Backend
 from lpbuildd.util import (
     set_personality,
@@ -78,8 +84,22 @@
 
 
 class FakeConfig:
+    def __init__(self):
+        self._overrides = defaultdict(dict)
+
     def get(self, section, key):
-        return key
+        if key in self._overrides[section]:
+            return self._overrides[section][key]
+        elif section == "proxy":
+            if not self._overrides[section]:
+                raise NoSectionError(section)
+            else:
+                raise NoOptionError(section, key)
+        else:
+            return key
+
+    def set(self, section, key, value):
+        self._overrides[section][key] = value
 
 
 class FakeSlave:

=== modified file 'lpbuildd/tests/test_debian.py'
--- lpbuildd/tests/test_debian.py	2018-05-02 09:00:10 +0000
+++ lpbuildd/tests/test_debian.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2017-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -421,3 +421,33 @@
         self.assertTrue(self.slave.wasCalled('buildOK'))
         self.assertTrue(self.slave.wasCalled('buildComplete'))
 
+    def test_iterate_apt_proxy(self):
+        # The build manager can be configured to use an APT proxy.
+        self.slave._config.set(
+            'proxy', 'apt', 'http://apt-proxy.example:3128/')
+        extra_args = {
+            'arch_tag': 'amd64',
+            'archives': [
+                'deb http://ppa.launchpad.dev/owner/name/ubuntu xenial main',
+                ],
+            'series': 'xenial',
+            }
+        self.startBuild(extra_args)
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.UNPACK, self.getState())
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.MOUNT, self.getState())
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.SOURCES, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/in-target', 'in-target',
+              'override-sources-list',
+              '--backend=chroot', '--series=xenial', '--arch=amd64',
+              self.buildid,
+              '--apt-proxy-url', 'http://apt-proxy.example:3128/',
+              'deb http://ppa.launchpad.dev/owner/name/ubuntu xenial main'],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])

=== modified file 'lpbuildd/tests/test_livefs.py'
--- lpbuildd/tests/test_livefs.py	2018-10-05 15:43:40 +0000
+++ lpbuildd/tests/test_livefs.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2013-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2013-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -58,7 +58,7 @@
         return self.buildmanager._state
 
     @defer.inlineCallbacks
-    def startBuild(self):
+    def startBuild(self, options=None):
         # The build manager's iterate() kicks off the consecutive states
         # after INIT.
         extra_args = {
@@ -86,6 +86,8 @@
             "--backend=lxd", "--series=saucy", "--arch=i386", self.buildid,
             "--project", "ubuntu",
             ]
+        if options is not None:
+            expected_command.extend(options)
         self.assertEqual(expected_command, self.buildmanager.commands[-1])
         self.assertEqual(
             self.buildmanager.iterate, self.buildmanager.iterators[-1])
@@ -134,6 +136,15 @@
         self.assertFalse(self.slave.wasCalled("buildFail"))
 
     @defer.inlineCallbacks
+    def test_iterate_snap_store_proxy(self):
+        # The build manager can be told to use a snap store proxy.
+        self.slave._config.set(
+            "proxy", "snapstore", "http://snap-store-proxy.example/";)
+        expected_options = [
+            "--snap-store-proxy-url", "http://snap-store-proxy.example/";]
+        yield self.startBuild(options=expected_options)
+
+    @defer.inlineCallbacks
     def test_omits_symlinks(self):
         # Symlinks in the build output are not included in gathered results.
         yield self.startBuild()

=== modified file 'lpbuildd/tests/test_snap.py'
--- lpbuildd/tests/test_snap.py	2018-10-05 15:43:40 +0000
+++ lpbuildd/tests/test_snap.py	2019-01-28 15:15:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -252,6 +252,15 @@
             self.buildmanager.iterate, self.buildmanager.iterators[-1])
         self.assertFalse(self.slave.wasCalled("buildFail"))
 
+    @defer.inlineCallbacks
+    def test_iterate_snap_store_proxy(self):
+        # The build manager can be told to use a snap store proxy.
+        self.slave._config.set(
+            "proxy", "snapstore", "http://snap-store-proxy.example/";)
+        expected_options = [
+            "--snap-store-proxy-url", "http://snap-store-proxy.example/";]
+        yield self.startBuild(options=expected_options)
+
     def getListenerURL(self, listener):
         port = listener.getHost().port
         return b"http://localhost:%d/"; % port

=== modified file 'setup.py'
--- setup.py	2018-10-25 09:04:12 +0000
+++ setup.py	2019-01-28 15:15:21 +0000
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-# Copyright 2015 Canonical Ltd.  All rights reserved.
+# Copyright 2015-2019 Canonical Ltd.  All rights reserved.
 #
 # This file is part of launchpad-buildd.
 #
@@ -59,6 +59,8 @@
         # causes problems for Launchpad's build system.
         #'python-apt',
         'python-debian>=0.1.23',
+        'requests',
+        'six',
         'Twisted',
         'zope.interface',
         ],
@@ -69,6 +71,7 @@
     tests_require=[
         'fixtures',
         'mock',
+        'responses',
         'systemfixtures',
         'testtools',
         'txfixtures',