launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29376
[Merge] ~andrey-fedoseev/launchpad-buildd:backend-open into launchpad-buildd:master
Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad-buildd:backend-open into launchpad-buildd:master.
Commit message:
Use `Backend.open` to modify files in target environments
instead of using a local temporary file and `copy_out` / `copy_in` methods
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad-buildd/+git/launchpad-buildd/+merge/432731
A new method `open` is added to the base `Backend` class
The method access to the files in the target environment via a file-like object.
Under the hood it uses a temporary file on the host system and `copy_in` / `copy_out` methods to transfer the file to / from the target environment.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad-buildd:backend-open into launchpad-buildd:master.
diff --git a/lpbuildd/binarypackage.py b/lpbuildd/binarypackage.py
index c3fef09..3498e58 100644
--- a/lpbuildd/binarypackage.py
+++ b/lpbuildd/binarypackage.py
@@ -156,11 +156,11 @@ class BinaryPackageBuildManager(DebianBuildManager):
self.archive_purpose))
if self.build_debug_symbols:
currently_building_contents += 'Build-Debug-Symbols: yes\n'
- with tempfile.NamedTemporaryFile(mode='w+') as currently_building:
+ with self.backend.open(
+ '/CurrentlyBuilding', mode='w+'
+ ) as currently_building:
currently_building.write(currently_building_contents)
- currently_building.flush()
os.fchmod(currently_building.fileno(), 0o644)
- self.backend.copy_in(currently_building.name, '/CurrentlyBuilding')
args = ["sbuild-package", self._buildid, self.arch_tag]
args.append(self.suite)
diff --git a/lpbuildd/ci.py b/lpbuildd/ci.py
index 04aa5f8..601d8b4 100644
--- a/lpbuildd/ci.py
+++ b/lpbuildd/ci.py
@@ -6,7 +6,6 @@ from configparser import (
NoSectionError,
)
import os
-import tempfile
import yaml
from twisted.internet import defer
@@ -168,14 +167,10 @@ class CIBuildManager(BuildManagerProxyMixin, DebianBuildManager):
["--plugin-setting", f"{key}={value}"])
if self.secrets is not None:
text = yaml.dump(self.secrets)
- with tempfile.NamedTemporaryFile(mode="w") as f:
+ with self.backend.open(
+ "/build/.launchpad-secrets.yaml", mode="w"
+ ) as f:
f.write(text)
- f.flush()
- path_to_secrets = f.name
- self.backend.copy_in(
- source_path=path_to_secrets,
- target_path="/build/.launchpad-secrets.yaml"
- )
args.extend(
["--secrets", "/build/.launchpad-secrets.yaml"])
if self.scan_malware:
diff --git a/lpbuildd/target/apt.py b/lpbuildd/target/apt.py
index ebd4683..a411806 100644
--- a/lpbuildd/target/apt.py
+++ b/lpbuildd/target/apt.py
@@ -5,7 +5,6 @@ import logging
import os
import subprocess
import sys
-import tempfile
from textwrap import dedent
import time
@@ -30,49 +29,44 @@ class OverrideSourcesList(Operation):
def run(self):
logger.info("Overriding sources.list in build-%s", self.args.build_id)
- with tempfile.NamedTemporaryFile(mode="w+") as sources_list:
+ with self.backend.open(
+ "/etc/apt/sources.list", mode="w+"
+ ) as sources_list:
for archive in self.args.archives:
print(archive, file=sources_list)
- sources_list.flush()
os.fchmod(sources_list.fileno(), 0o644)
- self.backend.copy_in(sources_list.name, "/etc/apt/sources.list")
- with tempfile.NamedTemporaryFile(mode="w+") as apt_retries_conf:
+ with self.backend.open(
+ "/etc/apt/apt.conf.d/99retries", mode="w+"
+ ) as apt_retries_conf:
print('Acquire::Retries "3";', file=apt_retries_conf)
- apt_retries_conf.flush()
os.fchmod(apt_retries_conf.fileno(), 0o644)
- self.backend.copy_in(
- apt_retries_conf.name, "/etc/apt/apt.conf.d/99retries")
# Versions of APT that support phased updates do this automatically
# if running in a chroot, but builds may be running in a LXD
# container instead.
- with tempfile.NamedTemporaryFile(mode="w+") as apt_phasing_conf:
+ with self.backend.open(
+ "/etc/apt/apt.conf.d/99phasing", mode="w+"
+ ) as apt_phasing_conf:
print('APT::Get::Always-Include-Phased-Updates "true";',
file=apt_phasing_conf)
- apt_phasing_conf.flush()
os.fchmod(apt_phasing_conf.fileno(), 0o644)
- self.backend.copy_in(
- apt_phasing_conf.name, "/etc/apt/apt.conf.d/99phasing")
if self.args.apt_proxy_url is not None:
- with tempfile.NamedTemporaryFile(mode="w+") as apt_proxy_conf:
+ with self.backend.open(
+ "/etc/apt/apt.conf.d/99proxy", mode="w+"
+ ) as apt_proxy_conf:
print(
f'Acquire::http::Proxy "{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")
for pocket in ("proposed", "backports"):
- with tempfile.NamedTemporaryFile(mode="w+") as preferences:
+ with self.backend.open(
+ f"/etc/apt/preferences.d/{pocket}.pref", mode="w+"
+ ) as preferences:
print(dedent(f"""\
Package: *
Pin: release a=*-{pocket}
Pin-Priority: 500
"""), file=preferences, end="")
- preferences.flush()
os.fchmod(preferences.fileno(), 0o644)
- self.backend.copy_in(
- preferences.name,
- f"/etc/apt/preferences.d/{pocket}.pref")
return 0
@@ -91,7 +85,9 @@ class AddTrustedKeys(Operation):
gpg_cmd = [
"gpg", "--ignore-time-conflict", "--no-options", "--no-keyring",
]
- with tempfile.NamedTemporaryFile(mode="wb+") as keyring:
+ with self.backend.open(
+ "/etc/apt/trusted.gpg.d/launchpad-buildd.gpg", mode="wb+"
+ ) as keyring:
subprocess.check_call(
gpg_cmd + ["--dearmor"], stdin=self.input_file, stdout=keyring)
keyring.seek(0)
@@ -100,8 +96,6 @@ class AddTrustedKeys(Operation):
["--show-keys", "--keyid-format", "long", "--fingerprint"],
stdin=keyring, stdout=self.show_keys_file)
os.fchmod(keyring.fileno(), 0o644)
- self.backend.copy_in(
- keyring.name, "/etc/apt/trusted.gpg.d/launchpad-buildd.gpg")
return 0
diff --git a/lpbuildd/target/backend.py b/lpbuildd/target/backend.py
index 69746bb..a920cf1 100644
--- a/lpbuildd/target/backend.py
+++ b/lpbuildd/target/backend.py
@@ -3,6 +3,10 @@
import os.path
import subprocess
+import tempfile
+from contextlib import contextmanager
+from pathlib import Path
+from shutil import rmtree
class BackendException(Exception):
@@ -189,6 +193,27 @@ class Backend:
"""Remove the backend."""
subprocess.check_call(["sudo", "rm", "-rf", self.build_path])
+ @contextmanager
+ def open(self, path: str, mode="r", **kwargs):
+ """
+ Provides access to the files in the target environment via a
+ file-like object.
+
+ The arguments are the same as those of the built-in `open` function.
+ """
+ tmp_dir = tempfile.mkdtemp()
+ tmp_path = os.path.join(tmp_dir, Path(path).name)
+ if self.path_exists(path):
+ self.copy_out(path, tmp_path)
+ tmp_file = open(tmp_path, mode=mode, **kwargs)
+ try:
+ yield tmp_file
+ finally:
+ tmp_file.close()
+ if mode not in ("r", "rb", "rt"):
+ self.copy_in(tmp_path, path)
+ rmtree(tmp_dir)
+
def make_backend(name, build_id, series=None, arch=None):
if name == "chroot":
diff --git a/lpbuildd/target/build_oci.py b/lpbuildd/target/build_oci.py
index 5241f9f..ecbda0d 100644
--- a/lpbuildd/target/build_oci.py
+++ b/lpbuildd/target/build_oci.py
@@ -3,7 +3,6 @@
import logging
import os.path
-import tempfile
from textwrap import dedent
from lpbuildd.target.backend import check_path_escape
@@ -56,10 +55,8 @@ class BuildOCI(BuilderProxyOperationMixin, VCSOperationMixin,
Environment="{setting.upper()}={self.args.proxy_url}"
""")
file_path = f"/etc/systemd/system/docker.service.d/{setting}.conf"
- with tempfile.NamedTemporaryFile(mode="w+") as systemd_file:
+ with self.backend.open(file_path, mode="w+") as systemd_file:
systemd_file.write(contents)
- systemd_file.flush()
- self.backend.copy_in(systemd_file.name, file_path)
def install(self):
logger.info("Running install phase...")
diff --git a/lpbuildd/target/build_snap.py b/lpbuildd/target/build_snap.py
index 30c5ecc..615c2d9 100644
--- a/lpbuildd/target/build_snap.py
+++ b/lpbuildd/target/build_snap.py
@@ -5,7 +5,6 @@ import argparse
import json
import logging
import os.path
-import tempfile
from textwrap import dedent
from urllib.parse import urlparse
@@ -92,13 +91,12 @@ class BuildSnap(BuilderProxyOperationMixin, VCSOperationMixin,
svn_servers += f"http-proxy-username = {proxy.username}\n"
if proxy.password:
svn_servers += f"http-proxy-password = {proxy.password}\n"
- with tempfile.NamedTemporaryFile(mode="w+") as svn_servers_file:
+ self.backend.run(["mkdir", "-p", "/root/.subversion"])
+ with self.backend.open(
+ "/root/.subversion/servers", mode="w+"
+ ) as svn_servers_file:
svn_servers_file.write(svn_servers)
- svn_servers_file.flush()
os.fchmod(svn_servers_file.fileno(), 0o644)
- self.backend.run(["mkdir", "-p", "/root/.subversion"])
- self.backend.copy_in(
- svn_servers_file.name, "/root/.subversion/servers")
def install(self):
logger.info("Running install phase...")
diff --git a/lpbuildd/target/lxd.py b/lpbuildd/target/lxd.py
index d58d9ab..63f36d5 100644
--- a/lpbuildd/target/lxd.py
+++ b/lpbuildd/target/lxd.py
@@ -9,7 +9,6 @@ import re
import stat
import subprocess
import tarfile
-import tempfile
from textwrap import dedent
import time
@@ -376,22 +375,16 @@ class LXD(Backend):
["hostname"], universal_newlines=True).rstrip("\n")
fqdn = subprocess.check_output(
["hostname", "--fqdn"], universal_newlines=True).rstrip("\n")
- with tempfile.NamedTemporaryFile(mode="w+") as hosts_file:
- try:
- self.copy_out("/etc/hosts", hosts_file.name)
- except LXDException:
- hosts_file.seek(0, os.SEEK_SET)
- hosts_file.write(fallback_hosts)
+ with self.open("/etc/hosts", mode="a") as hosts_file:
hosts_file.seek(0, os.SEEK_END)
+ if not hosts_file.tell():
+ # /etc/hosts is missing or empty
+ hosts_file.write(fallback_hosts)
print(f"\n127.0.1.1\t{fqdn} {hostname}", file=hosts_file)
- hosts_file.flush()
os.fchmod(hosts_file.fileno(), 0o644)
- self.copy_in(hosts_file.name, "/etc/hosts")
- with tempfile.NamedTemporaryFile(mode="w+") as hostname_file:
+ with self.open("/etc/hostname", mode="w+") as hostname_file:
print(hostname, file=hostname_file)
- hostname_file.flush()
os.fchmod(hostname_file.fileno(), 0o644)
- self.copy_in(hostname_file.name, "/etc/hostname")
resolv_conf = "/etc/resolv.conf"
@@ -403,23 +396,17 @@ class LXD(Backend):
self.copy_in(resolv_conf, "/etc/resolv.conf")
- with tempfile.NamedTemporaryFile(mode="w+") as policy_rc_d_file:
+ with self.open(
+ "/usr/local/sbin/policy-rc.d", mode="w+"
+ ) as policy_rc_d_file:
policy_rc_d_file.write(policy_rc_d)
- 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")
# 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 LXDException:
- pass
- else:
- mounted_dev_file.seek(0, os.SEEK_SET)
+ if self.path_exists("/etc/init/mounted-dev.conf"):
+ with self.open("/etc/init/mounted-dev.conf") as mounted_dev_file:
script = ""
in_script = False
for line in mounted_dev_file:
@@ -431,15 +418,13 @@ class LXD(Backend):
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")
+
+ if script:
+ with self.open(
+ "/etc/init/mounted-dev.override", mode="w"
+ ) as mounted_dev_override_file:
+ mounted_dev_override_file.write(script)
+ os.fchmod(mounted_dev_override_file.fileno(), 0o644)
# Start the container and wait for it to start.
container.start(wait=True)
@@ -481,16 +466,14 @@ class LXD(Backend):
# directory until the container has started. We can get away with
# this for the time being because snapd isn't in the buildd chroots.
self.run(["mkdir", "-p", "/etc/systemd/system/snapd.service.d"])
- with tempfile.NamedTemporaryFile(mode="w+") as no_cdn_file:
+ with self.open(
+ "/etc/systemd/system/snapd.service.d/no-cdn.conf", mode="w+"
+ ) as no_cdn_file:
print(dedent("""\
[Service]
Environment=SNAPPY_STORE_NO_CDN=1
"""), file=no_cdn_file, end="")
- no_cdn_file.flush()
os.fchmod(no_cdn_file.fileno(), 0o644)
- self.copy_in(
- no_cdn_file.name,
- "/etc/systemd/system/snapd.service.d/no-cdn.conf")
# Refreshing snaps from a timer unit during a build isn't
# appropriate. Mask this, but manually so that we don't depend on
diff --git a/lpbuildd/target/run_ci.py b/lpbuildd/target/run_ci.py
index 4ebf765..28dc2e7 100644
--- a/lpbuildd/target/run_ci.py
+++ b/lpbuildd/target/run_ci.py
@@ -3,7 +3,6 @@
import logging
import os
-import tempfile
from lpbuildd.target.build_snap import SnapChannelsAction
from lpbuildd.target.operation import Operation
@@ -77,16 +76,12 @@ class RunCIPrepare(BuilderProxyOperationMixin, VCSOperationMixin,
# services, which is convenient since it allows us to ensure
# that ClamAV's database is up to date before proceeding.
if self.args.clamav_database_url:
- freshclam_path = "/etc/clamav/freshclam.conf"
- with tempfile.NamedTemporaryFile(mode="w+") as freshclam_file:
- self.backend.copy_out(freshclam_path, freshclam_file.name)
- freshclam_file.seek(0, os.SEEK_END)
- print(
- f"PrivateMirror {self.args.clamav_database_url}",
- file=freshclam_file,
+ with self.backend.open(
+ "/etc/clamav/freshclam.conf", mode="a"
+ ) as freshclam_file:
+ freshclam_file.write(
+ f"PrivateMirror {self.args.clamav_database_url}\n"
)
- freshclam_file.flush()
- self.backend.copy_in(freshclam_file.name, freshclam_path)
kwargs = {}
env = self.build_proxy_environment(proxy_url=self.args.proxy_url)
if env:
diff --git a/lpbuildd/target/tests/test_backend.py b/lpbuildd/target/tests/test_backend.py
new file mode 100644
index 0000000..575f564
--- /dev/null
+++ b/lpbuildd/target/tests/test_backend.py
@@ -0,0 +1,37 @@
+from unittest.mock import patch, ANY
+
+from testtools import TestCase
+from fixtures import TempDir
+
+from lpbuildd.tests.fakebuilder import UncontainedBackend
+
+
+class TestBackend(TestCase):
+
+ def test_open(self):
+ backend = UncontainedBackend("1")
+ backend_root = self.useFixture(TempDir())
+ target_path = backend_root.join("test.txt")
+
+ with patch.object(
+ backend, "copy_in", wraps=backend.copy_in
+ ) as copy_in, patch.object(
+ backend, "copy_out", wraps=backend.copy_out
+ ) as copy_out:
+
+ with backend.open(target_path, "w") as f:
+ f.write("text")
+
+ copy_out.assert_not_called()
+ copy_in.assert_called_once_with(ANY, target_path)
+
+ self.assertTrue(backend.path_exists(target_path))
+
+ copy_in.reset_mock()
+ copy_out.reset_mock()
+
+ with backend.open(target_path, "r") as f:
+ self.assertEqual(f.read(), "text")
+
+ copy_in.assert_not_called()
+ copy_out.assert_called_once_with(target_path, ANY)
diff --git a/lpbuildd/target/tests/test_lxd.py b/lpbuildd/target/tests/test_lxd.py
index ecf3ddd..73f4d94 100644
--- a/lpbuildd/target/tests/test_lxd.py
+++ b/lpbuildd/target/tests/test_lxd.py
@@ -391,9 +391,10 @@ class TestLXD(TestCase):
lambda wait=False: setattr(container, "status_code", LXD_RUNNING))
files_api = container.api.files
files_api._api_endpoint = f"/1.0/containers/lp-xenial-{arch}/files"
- files_api.session.get.side_effect = FakeSessionGet({
+ existing_files = {
"/etc/hosts": [b"127.0.0.1\tlocalhost\n"],
- })
+ }
+ files_api.session.get.side_effect = FakeSessionGet(existing_files)
processes_fixture = self.useFixture(FakeProcesses())
def fake_sudo(args):
@@ -414,7 +415,13 @@ class TestLXD(TestCase):
processes_fixture.add(lambda _: {}, name="lxc")
processes_fixture.add(
FakeHostname("example", "example.buildd"), name="hostname")
- LXD("1", "xenial", arch).start()
+
+ with mock.patch.object(
+ LXD,
+ "path_exists",
+ side_effect=lambda path: path in existing_files
+ ):
+ LXD("1", "xenial", arch).start()
self.assert_correct_profile()
@@ -515,9 +522,6 @@ class TestLXD(TestCase):
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.session.get.assert_any_call(
- f"/1.0/containers/lp-xenial-{arch}/files",
- params={"path": "/etc/init/mounted-dev.conf"}, stream=True)
self.assertNotIn(
"/etc/init/mounted-dev.override",
[kwargs["params"]["path"]
@@ -551,11 +555,10 @@ class TestLXD(TestCase):
processes_fixture.add(lambda _: {}, name="lxc")
processes_fixture.add(
FakeHostname("example", "example.buildd"), name="hostname")
- LXD("1", "xenial", "amd64").start()
- files_api.session.get.assert_any_call(
- "/1.0/containers/lp-xenial-amd64/files",
- params={"path": "/etc/hosts"}, stream=True)
+ with mock.patch.object(LXD, "path_exists", return_value=False):
+ LXD("1", "xenial", "amd64").start()
+
files_api.post.assert_any_call(
params={"path": "/etc/hosts"},
data=(
@@ -576,7 +579,7 @@ class TestLXD(TestCase):
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({
+ existing_files = {
"/etc/init/mounted-dev.conf": [dedent("""\
start on mounted MOUNTPOINT=/dev
script
@@ -584,11 +587,18 @@ class TestLXD(TestCase):
/sbin/MAKEDEV std fd ppp tun
end script
task
- """).encode("UTF-8")]})
+ """).encode("UTF-8")]}
+ files_api.session.get.side_effect = FakeSessionGet(existing_files)
processes_fixture = self.useFixture(FakeProcesses())
processes_fixture.add(lambda _: {}, name="sudo")
processes_fixture.add(lambda _: {}, name="lxc")
- LXD("1", "trusty", "amd64").start()
+
+ with mock.patch.object(
+ LXD,
+ "path_exists",
+ side_effect=lambda path: path in existing_files
+ ):
+ LXD("1", "trusty", "amd64").start()
files_api.session.get.assert_any_call(
"/1.0/containers/lp-trusty-amd64/files",