← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad-buildd/override-sources-list-python into lp:launchpad-buildd

 

Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/override-sources-list-python into lp:launchpad-buildd with lp:~cjwatson/launchpad-buildd/update-chroot-python as a prerequisite.

Commit message:
Rewrite override-sources-list in Python, allowing it to have unit tests.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/override-sources-list-python/+merge/328254
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/override-sources-list-python into lp:launchpad-buildd.
=== modified file 'bin/override-sources-list'
--- bin/override-sources-list	2017-07-25 22:11:19 +0000
+++ bin/override-sources-list	2017-07-28 20:36:04 +0000
@@ -1,31 +1,34 @@
-#!/bin/sh
+#! /usr/bin/python -u
 #
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-# Buildd Slave tool to override sources.list in the chroot with a list of
-# archives
-
-# Expects build id as arg 1
-# Expects sources.list lines as subsequent args
-
-# Needs SUDO to be set to a sudo instance for passwordless access
-
-set -e
-exec 2>&1
-
-SUDO=/usr/bin/sudo
-
-BUILDID="$1"
-shift
-
-cd $HOME
-cd "build-$BUILDID/chroot-autobuild/etc/apt"
-
-echo "Overriding sources.list in build-$BUILDID"
-
-$SUDO rm -f sources.list.new
-(for archive; do
-  echo "$archive"
-done) | $SUDO tee sources.list.new >/dev/null
-$SUDO mv sources.list.new sources.list
+"""Override sources.list in the chroot with a list of archives."""
+
+from __future__ import print_function
+
+__metaclass__ = type
+
+from argparse import ArgumentParser
+import sys
+
+from lpbuildd.target.chroot import ChrootSetup
+
+
+def main():
+    parser = ArgumentParser(
+        description="Override sources.list in the chroot.")
+    parser.add_argument(
+        "build_id", metavar="ID", help="update chroot for build ID")
+    parser.add_argument(
+        "archives", metavar="ARCHIVE", nargs="+", help="sources.list lines")
+    args = parser.parse_args()
+
+    print("Overriding sources.list in build-%s" % args.build_id)
+    setup = ChrootSetup(args.build_id)
+    setup.override_sources_list(args.archives)
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

=== modified file 'bin/update-debian-chroot'
--- bin/update-debian-chroot	2017-07-28 20:36:04 +0000
+++ bin/update-debian-chroot	2017-07-28 20:36:04 +0000
@@ -12,7 +12,7 @@
 from argparse import ArgumentParser
 import sys
 
-from lpbuildd.target.chroot import ChrootUpdater
+from lpbuildd.target.chroot import ChrootSetup
 
 
 def main():
@@ -26,8 +26,8 @@
     args = parser.parse_args()
 
     print("Updating debian chroot for build %s" % args.build_id)
-    updater = ChrootUpdater(args.build_id, args.series, args.arch)
-    updater.update()
+    setup = ChrootSetup(args.build_id, args.series, args.arch)
+    setup.update()
     return 0
 
 

=== modified file 'debian/changelog'
--- debian/changelog	2017-07-28 20:36:04 +0000
+++ debian/changelog	2017-07-28 20:36:04 +0000
@@ -10,6 +10,7 @@
     dpkg-architecture instead.
   * Rewrite update-debian-chroot in Python, allowing it to use lpbuildd.util
     and to have unit tests.
+  * Rewrite override-sources-list in Python, allowing it to have unit tests.
 
  -- Colin Watson <cjwatson@xxxxxxxxxx>  Tue, 25 Jul 2017 23:07:58 +0100
 

=== modified file 'debian/python-lpbuildd.install'
--- debian/python-lpbuildd.install	2011-11-11 06:48:32 +0000
+++ debian/python-lpbuildd.install	2017-07-28 20:36:04 +0000
@@ -1,4 +1,5 @@
 buildd-slave.tac 		usr/lib/launchpad-buildd
 lpbuildd/*py			usr/lib/launchpad-buildd/lpbuildd
 lpbuildd/pottery/*py		usr/lib/launchpad-buildd/lpbuildd/pottery
+lpbuildd/target/*py		usr/lib/launchpad-buildd/lpbuildd/target
 lpbuildd/tests/*		usr/lib/launchpad-buildd/lpbuildd/tests

=== modified file 'lpbuildd/target/chroot.py'
--- lpbuildd/target/chroot.py	2017-07-28 20:36:04 +0000
+++ lpbuildd/target/chroot.py	2017-07-28 20:36:04 +0000
@@ -8,6 +8,7 @@
 import os.path
 import subprocess
 import sys
+import tempfile
 import time
 
 from lpbuildd.util import (
@@ -16,17 +17,17 @@
     )
 
 
-class ChrootUpdater:
-    """Updates a chroot."""
+class ChrootSetup:
+    """Sets up a chroot."""
 
-    def __init__(self, build_id, series, arch):
+    def __init__(self, build_id, series=None, arch=None):
         self.build_id = build_id
         self.series = series
         self.arch = arch
         self.chroot_path = os.path.join(
             os.environ["HOME"], "build-" + build_id, "chroot-autobuild")
 
-    def chroot(self, args, env=None, **kwargs):
+    def chroot(self, args, env=None, input_text=None, **kwargs):
         """Run a command in the chroot.
 
         :param args: the command and arguments to run.
@@ -35,9 +36,32 @@
             args = ["env"] + [
                 "%s=%s" % (key, shell_escape(value))
                 for key, value in env.items()] + args
-        args = set_personality(args, self.arch, series=self.series)
+        if self.arch is not None:
+            args = set_personality(args, self.arch, series=self.series)
         cmd = ["/usr/bin/sudo", "/usr/sbin/chroot", self.chroot_path] + args
-        subprocess.check_call(cmd, **kwargs)
+        if input_text is None:
+            subprocess.check_call(cmd, **kwargs)
+        else:
+            proc = subprocess.Popen(
+                cmd, stdin=subprocess.PIPE, universal_newlines=True, **kwargs)
+            proc.communicate(input_text)
+            if proc.returncode:
+                raise subprocess.CalledProcessError(proc.returncode, cmd)
+
+    def insert_file(self, source_path, target_path,
+                    owner="root", group="root", mode=0o644):
+        """Insert a file into the chroot.
+
+        :param source_path: the path to the file outside the chroot.
+        :param target_path: the path where the file should be installed
+            inside the chroot.
+        """
+        full_target_path = os.path.join(
+            self.chroot_path, target_path.lstrip("/"))
+        subprocess.check_call(
+            ["/usr/bin/sudo", "install",
+             "-o", owner, "-g", group, "-m", "%o" % mode,
+             source_path, full_target_path])
 
     def update(self):
         with open("/dev/null", "r") as devnull:
@@ -60,3 +84,10 @@
                 "--purge", "dist-upgrade",
                 ]
             self.chroot(upgrade_args, env=env, stdin=devnull)
+
+    def override_sources_list(self, archives):
+        with tempfile.NamedTemporaryFile() as sources_list:
+            for archive in archives:
+                print(archive, file=sources_list)
+            sources_list.flush()
+            self.insert_file(sources_list.name, "/etc/apt/sources.list")

=== modified file 'lpbuildd/target/tests/test_chroot.py'
--- lpbuildd/target/tests/test_chroot.py	2017-07-28 20:36:04 +0000
+++ lpbuildd/target/tests/test_chroot.py	2017-07-28 20:36:04 +0000
@@ -5,10 +5,12 @@
 
 import sys
 import time
+from textwrap import dedent
 
 from fixtures import (
     EnvironmentVariable,
     MockPatch,
+    MockPatchObject,
     )
 from systemfixtures import (
     FakeProcesses,
@@ -16,16 +18,30 @@
     )
 from testtools import TestCase
 
-from lpbuildd.target.chroot import ChrootUpdater
-
-
-class TestChrootUpdater(TestCase):
+from lpbuildd.target.chroot import ChrootSetup
+from lpbuildd.tests.fakeslave import FakeMethod
+
+
+class MockInsertFile(FakeMethod):
+
+    def __init__(self, *args, **kwargs):
+        super(MockInsertFile, self).__init__(*args, **kwargs)
+        self.source_bytes = None
+
+    def __call__(self, source_path, *args, **kwargs):
+        with open(source_path, "rb") as source:
+            self.source_bytes = source.read()
+        return super(MockInsertFile, self).__call__(
+            source_path, *args, **kwargs)
+
+
+class TestChrootSetup(TestCase):
 
     def test_chroot(self):
         self.useFixture(EnvironmentVariable("HOME", "/expected/home"))
         processes_fixture = self.useFixture(FakeProcesses())
         processes_fixture.add(lambda _: {}, name="/usr/bin/sudo")
-        ChrootUpdater("1", "xenial", "amd64").chroot(
+        ChrootSetup("1", "xenial", "amd64").chroot(
             ["apt-get", "update"], env={"LANG": "C"})
 
         expected_args = [
@@ -37,13 +53,33 @@
             expected_args,
             [proc._args["args"] for proc in processes_fixture.procs])
 
+    def test_insert_file(self):
+        self.useFixture(EnvironmentVariable("HOME", "/expected/home"))
+        processes_fixture = self.useFixture(FakeProcesses())
+        processes_fixture.add(lambda _: {}, name="/usr/bin/sudo")
+        source_path = "/path/to/source"
+        target_path = "/path/to/target"
+        ChrootSetup("1", "xenial", "amd64").insert_file(
+            source_path, target_path)
+
+        expected_target_path = (
+            "/expected/home/build-1/chroot-autobuild/path/to/target")
+        expected_args = [
+            ["/usr/bin/sudo", "install",
+             "-o", "root", "-g", "root", "-m", "644",
+             source_path, expected_target_path],
+            ]
+        self.assertEqual(
+            expected_args,
+            [proc._args["args"] for proc in processes_fixture.procs])
+
     def test_update_succeeds(self):
         self.useFixture(EnvironmentVariable("HOME", "/expected/home"))
         processes_fixture = self.useFixture(FakeProcesses())
         processes_fixture.add(lambda _: {}, name="/usr/bin/sudo")
         self.useFixture(FakeTime())
         start_time = time.time()
-        ChrootUpdater("1", "xenial", "amd64").update()
+        ChrootSetup("1", "xenial", "amd64").update()
 
         apt_get_args = [
             "/usr/bin/sudo", "/usr/sbin/chroot",
@@ -75,7 +111,7 @@
         mock_print = self.useFixture(MockPatch("__builtin__.print")).mock
         self.useFixture(FakeTime())
         start_time = time.time()
-        ChrootUpdater("1", "xenial", "amd64").update()
+        ChrootSetup("1", "xenial", "amd64").update()
 
         apt_get_args = [
             "/usr/bin/sudo", "/usr/sbin/chroot",
@@ -100,3 +136,21 @@
         mock_print.assert_called_once_with(
             "Waiting 15 seconds and trying again ...", file=sys.stderr)
         self.assertEqual(start_time + 15, time.time())
+
+    def test_override_sources_list(self):
+        self.useFixture(EnvironmentVariable("HOME", "/expected/home"))
+        setup = ChrootSetup("1")
+        mock_insert_file = self.useFixture(
+            MockPatchObject(setup, "insert_file", new=MockInsertFile())).mock
+        setup.override_sources_list([
+            "deb http://archive.ubuntu.com/ubuntu xenial main",
+            "deb http://ppa.launchpad.net/launchpad/ppa/ubuntu xenial main",
+            ])
+
+        self.assertEqual(dedent("""\
+            deb http://archive.ubuntu.com/ubuntu xenial main
+            deb http://ppa.launchpad.net/launchpad/ppa/ubuntu xenial main
+            """).encode("UTF-8"), mock_insert_file.source_bytes)
+        self.assertEqual(
+            ("/etc/apt/sources.list",),
+            mock_insert_file.extract_args()[0][1:])