← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad-buildd/loop-mounted-dev into lp:launchpad-buildd

 

Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/loop-mounted-dev into lp:launchpad-buildd.

Commit message:
The previous patch was labouring under mistaken assumptions: it's actually the mounted-dev Upstart job that we race with in trusty containers, so neuter that instead.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/loop-mounted-dev/+merge/332409

This is nicer than the previous approach in that we get to use a proper override file and will cope with the /etc/init/ directory not existing, but more horrible in that we have to produce a mangled shell script.

This time, I've confirmed that if I start a container with the mounted-dev neutering but not with launchpad-buildd's own loop device creation, I get a container with no loop devices; so I think it actually works this time.

A few device nodes go missing in trusty containers as a result of this:

  /dev/kmem
  /dev/mem
  /dev/port
  /dev/ppp
  /dev/ram*

We can add these back in if we need to, but it gets more complicated.  Most of these are deprecated (not present in xenial) or fairly useless in this context, so I think it'll be OK.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/loop-mounted-dev into lp:launchpad-buildd.
=== modified file 'debian/changelog'
--- debian/changelog	2017-10-18 06:58:31 +0000
+++ debian/changelog	2017-10-18 12:02:06 +0000
@@ -1,3 +1,11 @@
+launchpad-buildd (154) UNRELEASED; urgency=medium
+
+  * The previous patch was labouring under mistaken assumptions: it's
+    actually the mounted-dev Upstart job that we race with in trusty
+    containers, so neuter that instead.
+
+ -- Colin Watson <cjwatson@xxxxxxxxxx>  Wed, 18 Oct 2017 12:52:26 +0100
+
 launchpad-buildd (153) xenial; urgency=medium
 
   * Defend against racing with udev to create loop devices in trusty

=== modified file 'lpbuildd/target/lxd.py'
--- lpbuildd/target/lxd.py	2017-10-17 16:20:21 +0000
+++ lpbuildd/target/lxd.py	2017-10-18 12:02:06 +0000
@@ -9,6 +9,7 @@
 import io
 import json
 import os
+import re
 import stat
 import subprocess
 import tarfile
@@ -331,26 +332,38 @@
             policy_rc_d_file.flush()
             os.fchmod(policy_rc_d_file.fileno(), 0o755)
             self.copy_in(policy_rc_d_file.name, "/usr/local/sbin/policy-rc.d")
-        # Ensure that loop devices are not created, even if the target
-        # system's udev rules would ordinarily do so.  We can't do it the
-        # other way round (ensure that udev always creates them) because not
-        # all buildd chroots have udev.
-        # Poking this into /lib is wrong, but /etc/udev/rules.d/ doesn't
-        # exist in all buildd chroots, and xenial's LXD doesn't support
-        # creating directories when pushing files.  The containers won't be
-        # upgraded in ways that make this be a problem anyway.
-        with tempfile.NamedTemporaryFile(mode="w+") as udev_rules_file:
-            # Copied from systemd 234.
-            print(
-                'SUBSYSTEM=="block", KERNEL=="loop[0-9]*", '
-                'ENV{DEVTYPE}=="disk", TEST!="loop/backing_file", '
-                'ENV{SYSTEMD_READY}="0"',
-                file=udev_rules_file)
-            udev_rules_file.flush()
-            os.fchmod(udev_rules_file.fileno(), 0o644)
-            self.copy_in(
-                udev_rules_file.name,
-                "/lib/udev/rules.d/99-zz-buildd-loop.rules")
+        # For targets that use Upstart, prevent the mounted-dev job from
+        # creating devices.  Most of the devices it creates are unnecessary
+        # in a container, and creating loop devices will race with our own
+        # code to do so.
+        with tempfile.NamedTemporaryFile(mode="w+") as mounted_dev_file:
+            try:
+                self.copy_out(
+                    "/etc/init/mounted-dev.conf", mounted_dev_file.name)
+            except LXDAPIException:
+                pass
+            else:
+                mounted_dev_file.seek(0, os.SEEK_SET)
+                script = ""
+                in_script = False
+                for line in mounted_dev_file:
+                    if in_script:
+                        script += re.sub(
+                            r"^(\s*)(.*MAKEDEV)", r"\1: # \2", line)
+                        if line.strip() == "end script":
+                            in_script = False
+                    elif line.strip() == "script":
+                        script += line
+                        in_script = True
+                if script:
+                    mounted_dev_file.seek(0, os.SEEK_SET)
+                    mounted_dev_file.truncate()
+                    mounted_dev_file.write(script)
+                    mounted_dev_file.flush()
+                    os.fchmod(mounted_dev_file.fileno(), 0o644)
+                    self.copy_in(
+                        mounted_dev_file.name,
+                        "/etc/init/mounted-dev.override")
 
         # Start the container and wait for it to start.
         container.start(wait=True)

=== modified file 'lpbuildd/target/tests/test_lxd.py'
--- lpbuildd/target/tests/test_lxd.py	2017-10-17 17:48:48 +0000
+++ lpbuildd/target/tests/test_lxd.py	2017-10-18 12:02:06 +0000
@@ -56,6 +56,23 @@
         return "Fake LXD exception"
 
 
+class FakeSessionGet:
+
+    def __init__(self, file_contents):
+        self.file_contents = file_contents
+
+    def __call__(self, *args, **kwargs):
+        params = kwargs["params"]
+        response = mock.MagicMock()
+        if params["path"] in self.file_contents:
+            response.status_code = 200
+            response.iter_content.return_value = iter(
+                self.file_contents[params["path"]])
+        else:
+            response.json.return_value = {"error": "not found"}
+        return response
+
+
 class TestLXD(TestCase):
 
     def make_chroot_tarball(self, output_path):
@@ -184,9 +201,9 @@
             lambda wait=False: setattr(container, "status_code", LXD_RUNNING))
         files_api = container.api.files
         files_api._api_endpoint = "/1.0/containers/lp-xenial-amd64/files"
-        files_api.session.get.return_value.status_code = 200
-        files_api.session.get.return_value.iter_content.return_value = (
-            iter([b"127.0.0.1\tlocalhost\n"]))
+        files_api.session.get.side_effect = FakeSessionGet({
+            "/etc/hosts": [b"127.0.0.1\tlocalhost\n"],
+            })
         processes_fixture = self.useFixture(FakeProcesses())
         processes_fixture.add(lambda _: {}, name="sudo")
         processes_fixture.add(lambda _: {}, name="lxc")
@@ -248,7 +265,7 @@
             "profiles": ["lpbuildd"],
             "source": {"type": "image", "alias": "lp-xenial-amd64"},
             }, wait=True)
-        files_api.session.get.assert_called_once_with(
+        files_api.session.get.assert_any_call(
             "/1.0/containers/lp-xenial-amd64/files",
             params={"path": "/etc/hosts"}, stream=True)
         files_api.post.assert_any_call(
@@ -267,13 +284,13 @@
             params={"path": "/usr/local/sbin/policy-rc.d"},
             data=policy_rc_d.encode("UTF-8"),
             headers={"X-LXD-uid": 0, "X-LXD-gid": 0, "X-LXD-mode": "0755"})
-        files_api.post.assert_any_call(
-            params={"path": "/lib/udev/rules.d/99-zz-buildd-loop.rules"},
-            data=(
-                b'SUBSYSTEM=="block", KERNEL=="loop[0-9]*", '
-                b'ENV{DEVTYPE}=="disk", TEST!="loop/backing_file", '
-                b'ENV{SYSTEMD_READY}="0"\n'),
-            headers={"X-LXD-uid": 0, "X-LXD-gid": 0, "X-LXD-mode": "0644"})
+        files_api.session.get.assert_any_call(
+            "/1.0/containers/lp-xenial-amd64/files",
+            params={"path": "/etc/init/mounted-dev.conf"}, stream=True)
+        self.assertNotIn(
+            "/etc/init/mounted-dev.override",
+            [kwargs["params"]["path"]
+             for _, kwargs in files_api.post.call_args_list])
         files_api.post.assert_any_call(
             params={"path": "/etc/systemd/system/snapd.service.d/no-cdn.conf"},
             data=b"[Service]\nEnvironment=SNAPPY_STORE_NO_CDN=1\n",
@@ -300,16 +317,13 @@
             lambda wait=False: setattr(container, "status_code", LXD_RUNNING))
         files_api = container.api.files
         files_api._api_endpoint = "/1.0/containers/lp-xenial-amd64/files"
-        files_api.session.get.return_value.status_code = 404
-        files_api.session.get.return_value.json.return_value = {
-            "error": "not found",
-            }
+        files_api.session.get.side_effect = FakeSessionGet({})
         processes_fixture = self.useFixture(FakeProcesses())
         processes_fixture.add(lambda _: {}, name="sudo")
         processes_fixture.add(lambda _: {}, name="lxc")
         LXD("1", "xenial", "amd64").start()
 
-        files_api.session.get.assert_called_once_with(
+        files_api.session.get.assert_any_call(
             "/1.0/containers/lp-xenial-amd64/files",
             params={"path": "/etc/hosts"}, stream=True)
         files_api.post.assert_any_call(
@@ -319,6 +333,52 @@
                 "\n127.0.1.1\tlp-xenial-amd64\n").encode("UTF-8"),
             headers={"X-LXD-uid": 0, "X-LXD-gid": 0, "X-LXD-mode": "0644"})
 
+    def test_start_with_mounted_dev_conf(self):
+        fs_fixture = self.useFixture(FakeFilesystem())
+        fs_fixture.add("/sys")
+        fs_fixture.add("/run")
+        os.makedirs("/run/launchpad-buildd")
+        fs_fixture.add("/etc")
+        os.mkdir("/etc")
+        with open("/etc/resolv.conf", "w") as f:
+            print("host resolv.conf", file=f)
+        os.chmod("/etc/resolv.conf", 0o644)
+        self.useFixture(MockPatch("pylxd.Client"))
+        client = pylxd.Client()
+        client.profiles.get.side_effect = FakeLXDAPIException
+        container = client.containers.create.return_value
+        client.containers.get.return_value = container
+        container.start.side_effect = (
+            lambda wait=False: setattr(container, "status_code", LXD_RUNNING))
+        files_api = container.api.files
+        files_api._api_endpoint = "/1.0/containers/lp-trusty-amd64/files"
+        files_api.session.get.side_effect = FakeSessionGet({
+            "/etc/init/mounted-dev.conf": dedent("""\
+                start on mounted MOUNTPOINT=/dev
+                script
+                    [ -e /dev/shm ] || ln -s /run/shm /dev/shm
+                    /sbin/MAKEDEV std fd ppp tun
+                end script
+                task
+                """)})
+        processes_fixture = self.useFixture(FakeProcesses())
+        processes_fixture.add(lambda _: {}, name="sudo")
+        processes_fixture.add(lambda _: {}, name="lxc")
+        LXD("1", "trusty", "amd64").start()
+
+        files_api.session.get.assert_any_call(
+            "/1.0/containers/lp-trusty-amd64/files",
+            params={"path": "/etc/init/mounted-dev.conf"}, stream=True)
+        files_api.post.assert_any_call(
+            params={"path": "/etc/init/mounted-dev.override"},
+            data=dedent("""\
+                script
+                    [ -e /dev/shm ] || ln -s /run/shm /dev/shm
+                    : # /sbin/MAKEDEV std fd ppp tun
+                end script
+                """).encode("UTF-8"),
+            headers={"X-LXD-uid": 0, "X-LXD-gid": 0, "X-LXD-mode": "0644"})
+
     def test_run(self):
         processes_fixture = self.useFixture(FakeProcesses())
         processes_fixture.add(lambda _: {}, name="lxc")
@@ -375,13 +435,13 @@
         client = pylxd.Client()
         container = mock.MagicMock()
         client.containers.get.return_value = container
-        files_api = container.api.files
-        files_api._api_endpoint = "/1.0/containers/lp-xenial-amd64/files"
-        files_api.session.get.return_value.status_code = 200
-        files_api.session.get.return_value.iter_content.return_value = (
-            iter([b"hello\n", b"world\n"]))
         source_path = "/path/to/source"
         target_path = os.path.join(target_dir, "target")
+        files_api = container.api.files
+        files_api._api_endpoint = "/1.0/containers/lp-xenial-amd64/files"
+        files_api.session.get.side_effect = FakeSessionGet({
+            source_path: [b"hello\n", b"world\n"],
+            })
         LXD("1", "xenial", "amd64").copy_out(source_path, target_path)
 
         client.containers.get.assert_called_once_with("lp-xenial-amd64")