diff options
author | Sam Thursfield <sam.thursfield@codethink.co.uk> | 2015-05-27 16:46:47 +0000 |
---|---|---|
committer | Sam Thursfield <sam.thursfield@codethink.co.uk> | 2015-06-10 18:21:42 +0100 |
commit | 46158b0a486223eb31b6fb329ac348940f27c310 (patch) | |
tree | 2a2846fbf05559241c751c0ab584ea9ffc8e5fe4 | |
parent | 7a81a02b3a533a00740a3932b2e10d6377f45ede (diff) | |
download | morph-sam/sandboxlib.tar.gz |
WIP: Use 'sandboxlib' library where possible.sam/sandboxlib
This doesn't quite work because of the need to use 'tee' in a new
way. Really sandboxlib needs to handle tee-ing stdout and stderr itself.
Change-Id: Ia5c9182beb486f6223670eab8dc429e30cd4f94a
-rw-r--r-- | morphlib/builder.py | 52 | ||||
-rw-r--r-- | morphlib/extensions.py | 11 | ||||
-rw-r--r-- | morphlib/stagingarea.py | 94 | ||||
-rw-r--r-- | morphlib/util.py | 3 |
4 files changed, 91 insertions, 69 deletions
diff --git a/morphlib/builder.py b/morphlib/builder.py index 8caaaf39..66d980bb 100644 --- a/morphlib/builder.py +++ b/morphlib/builder.py @@ -17,6 +17,7 @@ import json import logging import os from os.path import relpath +import pipes import shutil import stat import tarfile @@ -26,10 +27,10 @@ import subprocess import tempfile import cliapp +import sandboxlib import morphlib from morphlib.artifactcachereference import ArtifactCacheReference -from morphlib.util import error_message_for_containerised_commandline import morphlib.gitversion SYSTEM_INTEGRATION_PATH = os.path.join('baserock', 'system-integration') @@ -365,12 +366,28 @@ class ChunkBuilder(BuilderBase): if stdout: stdout.flush() - self.runcmd(['sh', '-c', cmd], + # Nasty hack to allow both capturing stdout and writing + # it to a file. It'd be better to do this in-process. + # Adding the equivalent of 'tee' to sandboxlib would be + # the best option. + # + # Just writing stdout and stderr to the log file + # after the command finishes isn't ideal because the + # command might take a long time, and it's good to be + # able to watch the output 'live'. + # + # The 'pipefail' option is available in Busybox sh, + # GNU Bash, ksh and zsh, but is not in POSIX. It is + # needed so that we get the exit code of the actual + # command, not the 'tee' that follows it. + argv = ['sh', '-o', 'pipefail', '-c', "%s | tee -a %s" % + (cmd, pipes.quote(logfilepath))] + + self.runcmd(argv, extra_env=extra_env, cwd=relative_builddir, stdout=stdout or subprocess.PIPE, stderr=subprocess.STDOUT, - logfile=logfilepath, ccache_dir=ccache_dir) if stdout: @@ -655,23 +672,28 @@ class SystemBuilder(BuilderBase): # pragma: no cover self.app.status(msg='Running the system integration commands') to_mount = ( - ('dev/shm', 'tmpfs', 'none'), - ('tmp', 'tmpfs', 'none'), + (None, '/dev/shm', 'tmpfs'), + (None, '/proc', 'proc'), + (None, '/tmp', 'tmpfs'), ) try: for bin in sorted(os.listdir(sys_integration_dir)): argv = [os.path.join(SYSTEM_INTEGRATION_PATH, bin)] - container_config = dict( - root=rootdir, mounts=to_mount, mount_proc=True) - cmdline = morphlib.util.containerised_cmdline( - argv, **container_config) - exit, out, err = self.app.runcmd_unchecked( - cmdline, env=env) + + sandbox_config = dict( + env=env, + filesystem_root=rootdir, + mounts='isolated', + network='isolated', + extra_mounts=to_mount) + + exit, out, err = sandboxlib.linux_user_chroot.run_sandbox( + argv, **sandbox_config) + if exit != 0: - logging.debug('Command returned code %i', exit) - msg = error_message_for_containerised_commandline( - argv, err, container_config) - raise cliapp.AppException(msg) + message = ('Command %s returned code %i, error output %s' % + (argv, exit, err)) + raise cliapp.AppException(message) except BaseException as e: self.app.status( msg='Error while running system integration commands', diff --git a/morphlib/extensions.py b/morphlib/extensions.py index 59281d66..dc2acc15 100644 --- a/morphlib/extensions.py +++ b/morphlib/extensions.py @@ -227,8 +227,15 @@ class ExtensionSubprocess(object): cmdline = [filename] + list(args) if separate_mount_namespace: - cmdline = morphlib.util.unshared_cmdline(cmdline) - + cmdline = morphlib.util.separate_mount_namespace_cmdline( + cmdline) + + # FIXME: It would be good to use 'sandboxlib' to run the extensions + # in a sandbox. However, this might break some of them, because + # they may assume that stuff from the host system can be accessed + # in certain ways. It needs to be done carefully. Probably defining + # 'a system in which to run the deployment' would be a good first + # step, so tools aren't needed to be included in the host system. p = subprocess.Popen( cmdline, cwd=cwd, env=new_env, diff --git a/morphlib/stagingarea.py b/morphlib/stagingarea.py index 76bb3a18..416c46a4 100644 --- a/morphlib/stagingarea.py +++ b/morphlib/stagingarea.py @@ -17,10 +17,12 @@ import logging import os import shutil import stat -import cliapp from urlparse import urlparse import tempfile +import cliapp +import sandboxlib + import morphlib @@ -44,7 +46,6 @@ class StagingArea(object): self.dirname = dirname self.builddirname = None self.destdirname = None - self._bind_readonly_mount = None self.use_chroot = use_chroot self.env = build_env.env @@ -187,7 +188,8 @@ class StagingArea(object): shutil.rmtree(self.dirname) to_mount_in_staging = ( - ('dev/shm', 'tmpfs', 'none'), + (None, '/dev/shm', 'tmpfs'), + (None, '/proc', 'proc') ) to_mount_in_bootstrap = () @@ -241,69 +243,59 @@ class StagingArea(object): # No cleanup is currently required pass - def runcmd(self, argv, **kwargs): # pragma: no cover + def runcmd(self, argv, cwd=None, extra_env=None, ccache_dir=None, + stdout=sandboxlib.CAPTURE, + stderr=sandboxlib.CAPTURE): # pragma: no cover '''Run a command in a chroot in the staging area.''' - assert 'env' not in kwargs - kwargs['env'] = dict(self.env) - if 'extra_env' in kwargs: - kwargs['env'].update(kwargs['extra_env']) - del kwargs['extra_env'] - - ccache_dir = kwargs.pop('ccache_dir', None) + env = dict(self.env) + if extra_env is not None: + env.update(extra_env) chroot_dir = self.dirname if self.use_chroot else '/' - temp_dir = kwargs["env"].get("TMPDIR", "/tmp") - staging_dirs = [self.builddirname, self.destdirname] + temp_dir = env.get("TMPDIR", "/tmp") + if self.use_chroot: - staging_dirs += ["dev", "proc", temp_dir.lstrip('/')] - do_not_mount_dirs = [os.path.join(self.dirname, d) - for d in staging_dirs] - if not self.use_chroot: - do_not_mount_dirs += [temp_dir] - logging.debug("Not mounting dirs %r" % do_not_mount_dirs) + writable_paths = [ + self.relative(self.builddirname), + self.relative(self.destdirname), + temp_dir, "dev", "proc"] + else: + writable_paths = [self.builddirname, self.destdirname, temp_dir] + + logging.debug( + "The following directories will be writable in the sandbox: %r" % + writable_paths) if self.use_chroot: - mounts = self.to_mount_in_staging + mounts = list(self.to_mount_in_staging) else: - mounts = [(os.path.join(self.dirname, target), type, source) - for target, type, source in self.to_mount_in_bootstrap] - mount_proc = self.use_chroot + mounts = list(self.to_mount_in_bootstrap) if ccache_dir and not self._app.settings['no-ccache']: - ccache_target = os.path.join( - self.dirname, kwargs['env']['CCACHE_DIR'].lstrip('/')) - binds = ((ccache_dir, ccache_target),) - else: - binds = () - - container_config=dict( - cwd=kwargs.pop('cwd', '/'), - root=chroot_dir, - mounts=mounts, - mount_proc=mount_proc, - binds=binds, - writable_paths=do_not_mount_dirs) - - cmdline = morphlib.util.containerised_cmdline( - argv, **container_config) - - if kwargs.get('logfile') != None: - logfile = kwargs.pop('logfile') - teecmd = ['tee', '-a', logfile] - exit, out, err = self._app.runcmd_unchecked( - cmdline, teecmd, **kwargs) - else: - exit, out, err = self._app.runcmd_unchecked(cmdline, **kwargs) + ccache_target = env['CCACHE_DIR'] + mounts.append([ccache_dir, ccache_target, None, 'bind']) + + sandbox_config=dict( + cwd=cwd, + env=env, + filesystem_root=chroot_dir, + filesystem_writable_paths=writable_paths, + mounts='isolated', + network='isolated', + extra_mounts=mounts, + ) + + exit, out, err = sandboxlib.linux_user_chroot.run_sandbox( + argv, stdout=stdout, stderr=stderr, **sandbox_config) if exit == 0: return out else: - logging.debug('Command returned code %i', exit) - msg = morphlib.util.error_message_for_containerised_commandline( - argv, err, container_config) + logging.debug('Command %s returned code %i', argv, exit) raise cliapp.AppException( - 'In staging area %s: %s' % (self._failed_location(), msg)) + 'In staging area %s: command %s failed. Error output: %s' % + (self._failed_location(), argv, err or out)) def _failed_location(self): # pragma: no cover '''Path this staging area will be moved to if an error occurs.''' diff --git a/morphlib/util.py b/morphlib/util.py index fb9fba0a..652e6508 100644 --- a/morphlib/util.py +++ b/morphlib/util.py @@ -535,7 +535,8 @@ def get_data(relative_path): # pragma: no cover return f.read() -def unshared_cmdline(args, root='/', mounts=()): # pragma: no cover +def separate_mount_namespace_cmdline(args, root='/', + mounts=()): # pragma: no cover '''Describe how to run 'args' inside a separate mount namespace. This function wraps 'args' in a rather long commandline that ensures |