launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #21797
[Merge] lp:~cjwatson/launchpad-buildd/build-livefs-operation into lp:launchpad-buildd
Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/build-livefs-operation into lp:launchpad-buildd with lp:~cjwatson/launchpad-buildd/gather-results-via-backend as a prerequisite.
Commit message:
Convert buildlivefs to the new Operation framework and add unit tests.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/build-livefs-operation/+merge/328657
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/build-livefs-operation into lp:launchpad-buildd.
=== added file 'bin/buildlivefs'
--- bin/buildlivefs 1970-01-01 00:00:00 +0000
+++ bin/buildlivefs 2017-08-07 10:26:10 +0000
@@ -0,0 +1,24 @@
+#! /usr/bin/python -u
+#
+# Copyright 2013-2017 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Build a live file system."""
+
+from __future__ import print_function
+
+__metaclass__ = type
+
+import sys
+
+from lpbuildd.target.build_livefs import BuildLiveFS
+from lpbuildd.target.operation import configure_logging
+
+
+def main():
+ configure_logging()
+ return BuildLiveFS().run()
+
+
+if __name__ == "__main__":
+ sys.exit(main())
=== modified file 'debian/changelog'
--- debian/changelog 2017-08-07 10:26:10 +0000
+++ debian/changelog 2017-08-07 10:26:10 +0000
@@ -20,6 +20,7 @@
* Rewrite mount-chroot and umount-chroot in Python, allowing them to have
unit tests.
* Rewrite scan-for-processes in Python, allowing it to have unit tests.
+ * Convert buildlivefs to the new Operation framework and add unit tests.
-- Colin Watson <cjwatson@xxxxxxxxxx> Tue, 25 Jul 2017 23:07:58 +0100
=== modified file 'lpbuildd/livefs.py'
--- lpbuildd/livefs.py 2017-08-07 10:26:10 +0000
+++ lpbuildd/livefs.py 2017-08-07 10:26:10 +0000
@@ -45,17 +45,12 @@
def doRunBuild(self):
"""Run the process to build the live filesystem."""
- args = [
- "buildlivefs",
- "--build-id", self._buildid,
- "--arch", self.arch_tag,
- ]
+ args = ["buildlivefs"]
if self.subarch:
args.extend(["--subarch", self.subarch])
args.extend(["--project", self.project])
if self.subproject:
args.extend(["--subproject", self.subproject])
- args.extend(["--series", self.series])
if self.datestamp:
args.extend(["--datestamp", self.datestamp])
if self.image_format:
@@ -66,7 +61,7 @@
args.extend(["--locale", self.locale])
for ppa in self.extra_ppas:
args.extend(["--extra-ppa", ppa])
- self.runSubProcess(self.build_livefs_path, args)
+ self.runTargetSubProcess(self.build_livefs_path, args)
def iterate_BUILD_LIVEFS(self, retcode):
"""Finished building the live filesystem."""
=== modified file 'lpbuildd/target/backend.py'
--- lpbuildd/target/backend.py 2017-08-07 10:26:10 +0000
+++ lpbuildd/target/backend.py 2017-08-07 10:26:10 +0000
@@ -50,13 +50,14 @@
raise NotImplementedError
def run(self, args, env=None, input_text=None, get_output=False,
- **kwargs):
+ echo=False, **kwargs):
"""Run a command in the target environment.
:param args: the command and arguments to run.
:param env: additional environment variables to set.
:param input_text: input text to pass on the command's stdin.
:param get_output: if True, return the output from the command.
+ :param echo: if True, print the command before executing it.
:param kwargs: additional keyword arguments for `subprocess.Popen`.
"""
raise NotImplementedError
=== renamed file 'bin/buildlivefs' => 'lpbuildd/target/build_livefs.py' (properties changed: +x to -x)
--- bin/buildlivefs 2017-07-28 11:15:51 +0000
+++ lpbuildd/target/build_livefs.py 2017-08-07 10:26:10 +0000
@@ -1,30 +1,25 @@
-#! /usr/bin/python -u
# Copyright 2013-2017 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""A script that builds a live file system."""
-
from __future__ import print_function
__metaclass__ = type
-from optparse import OptionParser
+from collections import OrderedDict
+import logging
import os
-import subprocess
-import sys
-import traceback
-
-from lpbuildd.util import (
- set_personality,
- shell_escape,
- )
-
-
-RETCODE_SUCCESS = 0
+
+from lpbuildd.target.operation import Operation
+from lpbuildd.util import shell_escape
+
+
RETCODE_FAILURE_INSTALL = 200
RETCODE_FAILURE_BUILD = 201
+logger = logging.getLogger(__name__)
+
+
def get_build_path(build_id, *extra):
"""Generate a path within the build directory.
@@ -35,27 +30,34 @@
return os.path.join(os.environ["HOME"], "build-" + build_id, *extra)
-class LiveFSBuilder:
- """Builds a live file system."""
-
- def __init__(self, options):
- self.options = options
- self.chroot_path = get_build_path(
- self.options.build_id, 'chroot-autobuild')
-
- def chroot(self, args, echo=False):
- """Run a command in the chroot.
-
- :param args: the command and arguments to run.
- """
- args = set_personality(
- args, self.options.arch, series=self.options.series)
- if echo:
- print("Running in chroot: %s" %
- ' '.join("'%s'" % arg for arg in args))
- sys.stdout.flush()
- subprocess.check_call([
- "/usr/bin/sudo", "/usr/sbin/chroot", self.chroot_path] + args)
+class BuildLiveFS(Operation):
+
+ description = "Build a live file system."
+
+ def make_parser(self):
+ parser = super(BuildLiveFS, self).make_parser()
+ parser.add_argument(
+ "--subarch", metavar="SUBARCH",
+ help="build for subarchitecture SUBARCH")
+ parser.add_argument(
+ "--project", metavar="PROJECT", help="build for project PROJECT")
+ parser.add_argument(
+ "--subproject", metavar="SUBPROJECT",
+ help="build for subproject SUBPROJECT")
+ parser.add_argument("--datestamp", help="date stamp")
+ parser.add_argument(
+ "--image-format", metavar="FORMAT",
+ help="produce an image in FORMAT")
+ parser.add_argument(
+ "--proposed", default=False, action="store_true",
+ help="enable use of -proposed pocket")
+ parser.add_argument(
+ "--locale", metavar="LOCALE",
+ help="use ubuntu-defaults-image to build an image for LOCALE")
+ parser.add_argument(
+ "--extra-ppa", dest="extra_ppas", default=[], action="append",
+ help="use this additional PPA")
+ return parser
def run_build_command(self, args, env=None, echo=False):
"""Run a build command in the chroot.
@@ -67,6 +69,7 @@
:param args: the command and arguments to run.
:param env: dictionary of additional environment variables to set.
+ :param echo: if True, print the command before executing it.
"""
args = [shell_escape(arg) for arg in args]
if env:
@@ -74,28 +77,28 @@
"%s=%s" % (key, shell_escape(value))
for key, value in env.items()] + args
command = "cd /build && %s" % " ".join(args)
- self.chroot(["/bin/sh", "-c", command], echo=echo)
+ self.backend.run(["/bin/sh", "-c", command], echo=echo)
def install(self):
- self.chroot(["apt-get", "-y", "install", "livecd-rootfs"])
- if self.options.arch == "i386":
- self.chroot([
+ self.backend.run(["apt-get", "-y", "install", "livecd-rootfs"])
+ if self.args.arch == "i386":
+ self.backend.run([
"apt-get", "-y", "--no-install-recommends", "install",
"ltsp-server",
])
- if self.options.locale is not None:
- self.chroot([
+ if self.args.locale is not None:
+ self.backend.run([
"apt-get", "-y", "--install-recommends", "install",
"ubuntu-defaults-builder",
])
def build(self):
- if self.options.locale is not None:
+ if self.args.locale is not None:
self.run_build_command([
"ubuntu-defaults-image",
- "--locale", self.options.locale,
- "--arch", self.options.arch,
- "--release", self.options.series,
+ "--locale", self.args.locale,
+ "--arch", self.args.arch,
+ "--release", self.args.series,
])
else:
self.run_build_command(["rm", "-rf", "auto"])
@@ -106,70 +109,35 @@
self.run_build_command(["ln", "-s", lb_script_path, "auto/"])
self.run_build_command(["lb", "clean", "--purge"])
- base_lb_env = {
- "PROJECT": self.options.project,
- "ARCH": self.options.arch,
- }
- if self.options.subproject is not None:
- base_lb_env["SUBPROJECT"] = self.options.subproject
- if self.options.subarch is not None:
- base_lb_env["SUBARCH"] = self.options.subarch
- lb_env = dict(base_lb_env)
- lb_env["SUITE"] = self.options.series
- if self.options.datestamp is not None:
- lb_env["NOW"] = self.options.datestamp
- if self.options.image_format is not None:
- lb_env["IMAGEFORMAT"] = self.options.image_format
- if self.options.proposed:
+ base_lb_env = OrderedDict()
+ base_lb_env["PROJECT"] = self.args.project
+ base_lb_env["ARCH"] = self.args.arch
+ if self.args.subproject is not None:
+ base_lb_env["SUBPROJECT"] = self.args.subproject
+ if self.args.subarch is not None:
+ base_lb_env["SUBARCH"] = self.args.subarch
+ lb_env = base_lb_env.copy()
+ lb_env["SUITE"] = self.args.series
+ if self.args.datestamp is not None:
+ lb_env["NOW"] = self.args.datestamp
+ if self.args.image_format is not None:
+ lb_env["IMAGEFORMAT"] = self.args.image_format
+ if self.args.proposed:
lb_env["PROPOSED"] = "1"
- if self.options.extra_ppas:
- lb_env["EXTRA_PPAS"] = " ".join(self.options.extra_ppas)
+ if self.args.extra_ppas:
+ lb_env["EXTRA_PPAS"] = " ".join(self.args.extra_ppas)
self.run_build_command(["lb", "config"], env=lb_env)
self.run_build_command(["lb", "build"], env=base_lb_env)
-
-def main():
- parser = OptionParser()
- parser.add_option("--build-id", help="build identifier")
- parser.add_option(
- "--arch", metavar="ARCH", help="build for architecture ARCH")
- parser.add_option(
- "--subarch", metavar="SUBARCH",
- help="build for subarchitecture SUBARCH")
- parser.add_option(
- "--project", metavar="PROJECT", help="build for project PROJECT")
- parser.add_option(
- "--subproject", metavar="SUBPROJECT",
- help="build for subproject SUBPROJECT")
- parser.add_option(
- "--series", metavar="SERIES", help="build for series SERIES")
- parser.add_option("--datestamp", help="date stamp")
- parser.add_option(
- "--image-format", metavar="FORMAT", help="produce an image in FORMAT")
- parser.add_option(
- "--proposed", default=False, action="store_true",
- help="enable use of -proposed pocket")
- parser.add_option(
- "--locale", metavar="LOCALE",
- help="use ubuntu-defaults-image to build an image for LOCALE")
- parser.add_option(
- "--extra-ppa", dest="extra_ppas", default=[], action="append",
- help="use this additional PPA")
- options, _ = parser.parse_args()
-
- builder = LiveFSBuilder(options)
- try:
- builder.install()
- except Exception:
- traceback.print_exc()
- return RETCODE_FAILURE_INSTALL
- try:
- builder.build()
- except Exception:
- traceback.print_exc()
- return RETCODE_FAILURE_BUILD
- return RETCODE_SUCCESS
-
-
-if __name__ == "__main__":
- sys.exit(main())
+ def run(self):
+ try:
+ self.install()
+ except Exception:
+ logger.exception('Install failed')
+ return RETCODE_FAILURE_INSTALL
+ try:
+ self.build()
+ except Exception:
+ logger.exception('Build failed')
+ return RETCODE_FAILURE_BUILD
+ return 0
=== modified file 'lpbuildd/target/chroot.py'
--- lpbuildd/target/chroot.py 2017-08-07 10:26:10 +0000
+++ lpbuildd/target/chroot.py 2017-08-07 10:26:10 +0000
@@ -53,7 +53,7 @@
self.copy_in(path, path)
def run(self, args, env=None, input_text=None, get_output=False,
- **kwargs):
+ echo=False, **kwargs):
"""See `Backend`."""
if env:
args = ["env"] + [
@@ -61,6 +61,9 @@
for key, value in env.items()] + args
if self.arch is not None:
args = set_personality(args, self.arch, series=self.series)
+ if echo:
+ print("Running in chroot: %s" % ' '.join(
+ shell_escape(arg) for arg in args))
cmd = ["sudo", "/usr/sbin/chroot", self.chroot_path] + args
if input_text is None and not get_output:
subprocess.check_call(cmd, cwd=self.chroot_path, **kwargs)
=== added file 'lpbuildd/target/tests/test_build_livefs.py'
--- lpbuildd/target/tests/test_build_livefs.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/target/tests/test_build_livefs.py 2017-08-07 10:26:10 +0000
@@ -0,0 +1,175 @@
+# Copyright 2017 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import subprocess
+
+from testtools import TestCase
+from testtools.matchers import (
+ AnyMatch,
+ Equals,
+ Is,
+ MatchesAll,
+ MatchesDict,
+ MatchesListwise,
+ )
+
+from lpbuildd.target.build_livefs import (
+ BuildLiveFS,
+ RETCODE_FAILURE_BUILD,
+ RETCODE_FAILURE_INSTALL,
+ )
+from lpbuildd.tests.fakeslave import FakeMethod
+
+
+class RanCommand(MatchesListwise):
+
+ def __init__(self, args, echo=None, **env):
+ kwargs_matcher = {}
+ if echo is not None:
+ kwargs_matcher["echo"] = Is(echo)
+ if env:
+ kwargs_matcher["env"] = MatchesDict(env)
+ super(RanCommand, self).__init__(
+ [Equals((args,)), MatchesDict(kwargs_matcher)])
+
+
+class RanAptGet(RanCommand):
+
+ def __init__(self, *args):
+ super(RanAptGet, self).__init__(["apt-get", "-y"] + list(args))
+
+
+class RanBuildCommand(RanCommand):
+
+ def __init__(self, command):
+ super(RanBuildCommand, self).__init__(
+ ["/bin/sh", "-c", "cd /build && " + command], echo=False)
+
+
+class TestBuildLiveFS(TestCase):
+
+ def test_run_build_command_no_env(self):
+ args = ["--backend=fake", "--series=xenial", "--arch=amd64", "1"]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.run_build_command(["echo", "hello world"])
+ self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
+ RanBuildCommand("echo 'hello world'"),
+ ]))
+
+ def test_run_build_command_env(self):
+ args = ["--backend=fake", "--series=xenial", "--arch=amd64", "1"]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.run_build_command(
+ ["echo", "hello world"], env={"FOO": "bar baz"})
+ self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
+ RanBuildCommand("env FOO='bar baz' echo 'hello world'"),
+ ]))
+
+ def test_install(self):
+ args = ["--backend=fake", "--series=xenial", "--arch=amd64", "1"]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.install()
+ self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
+ RanAptGet("install", "livecd-rootfs"),
+ ]))
+
+ def test_install_i386(self):
+ args = ["--backend=fake", "--series=xenial", "--arch=i386", "1"]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.install()
+ self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
+ RanAptGet("install", "livecd-rootfs"),
+ RanAptGet("--no-install-recommends", "install", "ltsp-server"),
+ ]))
+
+ def test_install_locale(self):
+ args = [
+ "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+ "--locale=zh_CN",
+ ]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.install()
+ self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
+ RanAptGet("install", "livecd-rootfs"),
+ RanAptGet(
+ "--install-recommends", "install", "ubuntu-defaults-builder"),
+ ]))
+
+ def test_build(self):
+ args = [
+ "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+ "--project=ubuntu",
+ ]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.build()
+ self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
+ RanBuildCommand("rm -rf auto"),
+ RanBuildCommand("mkdir -p auto"),
+ RanBuildCommand(
+ "ln -s /usr/share/livecd-rootfs/live-build/auto/config auto/"),
+ RanBuildCommand(
+ "ln -s /usr/share/livecd-rootfs/live-build/auto/build auto/"),
+ RanBuildCommand(
+ "ln -s /usr/share/livecd-rootfs/live-build/auto/clean auto/"),
+ RanBuildCommand("lb clean --purge"),
+ RanBuildCommand(
+ "env PROJECT=ubuntu ARCH=amd64 SUITE=xenial lb config"),
+ RanBuildCommand("env PROJECT=ubuntu ARCH=amd64 lb build"),
+ ]))
+
+ def test_build_locale(self):
+ args = [
+ "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+ "--locale=zh_CN",
+ ]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.build()
+ self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
+ RanBuildCommand(
+ "ubuntu-defaults-image --locale zh_CN --arch amd64 "
+ "--release xenial"),
+ ]))
+
+ def test_run_succeeds(self):
+ args = [
+ "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+ "--project=ubuntu",
+ ]
+ build_livefs = BuildLiveFS(args=args)
+ self.assertEqual(0, build_livefs.run())
+ self.assertThat(build_livefs.backend.run.calls, MatchesAll(
+ AnyMatch(RanAptGet("install", "livecd-rootfs")),
+ AnyMatch(RanBuildCommand(
+ "env PROJECT=ubuntu ARCH=amd64 lb build"))))
+
+ def test_run_install_fails(self):
+ class FailInstall(FakeMethod):
+ def __call__(self, run_args, *args, **kwargs):
+ super(FailInstall, self).__call__(run_args, *args, **kwargs)
+ if run_args[0] == "apt-get":
+ raise subprocess.CalledProcessError(1, run_args)
+
+ args = [
+ "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+ "--project=ubuntu",
+ ]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.backend.run = FailInstall()
+ self.assertEqual(RETCODE_FAILURE_INSTALL, build_livefs.run())
+
+ def test_run_build_fails(self):
+ class FailBuild(FakeMethod):
+ def __call__(self, run_args, *args, **kwargs):
+ super(FailBuild, self).__call__(run_args, *args, **kwargs)
+ if run_args[0] == "/bin/sh":
+ raise subprocess.CalledProcessError(1, run_args)
+
+ args = [
+ "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+ "--project=ubuntu",
+ ]
+ build_livefs = BuildLiveFS(args=args)
+ build_livefs.backend.run = FailBuild()
+ self.assertEqual(RETCODE_FAILURE_BUILD, build_livefs.run())
=== modified file 'lpbuildd/tests/test_livefs.py'
--- lpbuildd/tests/test_livefs.py 2017-08-07 10:26:10 +0000
+++ lpbuildd/tests/test_livefs.py 2017-08-07 10:26:10 +0000
@@ -75,9 +75,10 @@
self.assertEqual(
LiveFilesystemBuildState.BUILD_LIVEFS, self.getState())
expected_command = [
- "sharepath/slavebin/buildlivefs", "buildlivefs", "--build-id",
- self.buildid, "--arch", "i386", "--project", "ubuntu",
- "--series", "saucy",
+ "sharepath/slavebin/buildlivefs", "buildlivefs",
+ "--backend=chroot", "--series=saucy", "--arch=i386",
+ self.buildid,
+ "--project", "ubuntu",
]
self.assertEqual(expected_command, self.buildmanager.commands[-1])
self.assertEqual(