From a5a2b03fd2d366ee9955607a3cbf2d1f57a2a1a3 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 29 Oct 2014 18:09:15 +0000 Subject: Allow invert_paths to skip the top level directory We're going to be passing it "$CHROOT/" as a writable directory by default, so it needs to be able to tell that it means to leave everything writable. --- morphlib/fsutils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/morphlib/fsutils.py b/morphlib/fsutils.py index 751f73f6..6d651171 100644 --- a/morphlib/fsutils.py +++ b/morphlib/fsutils.py @@ -104,6 +104,11 @@ def invert_paths(tree_walker, paths): for dirpath, dirnames, filenames in tree_walker: + if any(p == dirpath for p in paths): # pragma: no cover + # Dir is an exact match for a path + # don't recurse any further + # Don't yield it, since we don't return listed paths + continue dn_copy = list(dirnames) for subdir in dn_copy: subdirpath = os.path.join(dirpath, subdir) -- cgit v1.2.1 From e6f8b1bf99eb2bfdbe9a5233237107c8ec36f883 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 29 Oct 2014 18:10:10 +0000 Subject: Move unsharing and containerising logic to util This way the build commands, system integration commands and deployment extension commands can all share the logic. --- morphlib/util.py | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/morphlib/util.py b/morphlib/util.py index cc8ce88d..6f735387 100644 --- a/morphlib/util.py +++ b/morphlib/util.py @@ -18,6 +18,7 @@ import itertools import os import re import subprocess +import textwrap import fs.osfs @@ -512,3 +513,116 @@ def get_data(relative_path): # pragma: no cover with open(get_data_path(relative_path)) as f: return f.read() + + +def unshared_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 + the subprocess cannot see any of the system's mounts other than those + listed in 'mounts', and mounts done by that command can only be seen + by that subprocess and its children. When the subprocess exits all + of its mounts will be unmounted. + + ''' + # We need to do mounts in a different namespace. Unfortunately + # this means we have to in-line the mount commands in the + # command-line. + command = textwrap.dedent(r''' + mount --make-rprivate / + root="$1" + shift + ''') + cmdargs = [root] + + # We need to mount all the specified mounts in the namespace, + # we don't need to unmount them before exiting, as they'll be + # unmounted when the namespace is no longer used. + command += textwrap.dedent(r''' + while true; do + case "$1" in + --) + shift + break + ;; + *) + mount_point="$1" + mount_type="$2" + mount_source="$3" + shift 3 + path="$root/$mount_point" + mount -t "$mount_type" "$mount_source" "$path" + ;; + esac + done + ''') + for mount_point, mount_type, source in mounts: + path = os.path.join(root, mount_point) + if not os.path.exists(path): + os.makedirs(path) + cmdargs.extend((mount_point, mount_type, source)) + cmdargs.append('--') + + command += textwrap.dedent(r''' + exec "$@" + ''') + cmdargs.extend(args) + + # The single - is just a shell convention to fill $0 when using -c, + # since ordinarily $0 contains the program name. + cmdline = ['unshare', '--mount', '--', 'sh', '-ec', command, '-'] + cmdline.extend(cmdargs) + return cmdline + + +def containerised_cmdline(args, cwd='.', root='/', binds=(), + mount_proc=False, unshare_net=False, + writable_paths=None, **kwargs): # pragma: no cover + ''' + Describe how to run 'args' inside a linux-user-chroot container. + + The subprocess will only be permitted to write to the paths we + specifically allow it to write to, listed in 'writeable paths'. All + other locations in the file system will be read-only. + + The 'root' parameter allows running the command in a chroot, allowing + the host file system to be hidden completely except for the paths + below 'root'. + + The 'mount_proc' flag enables mounting of /proc inside 'root'. + Locations from the file system can be bind-mounted inside 'root' by + setting 'binds' to a list of (src, dest) pairs. The 'dest' + directory must be inside 'root'. + + The 'mounts' parameter allows mounting of arbitrary file-systems, + such as tmpfs, before running commands, by setting it to a list of + (mount_point, mount_type, source) triples. + + The subprocess will be run in a separate mount namespace. It can + optionally be run in a separate network namespace too by setting + 'unshare_net'. + + ''' + + if not root.endswith('/'): + root += '/' + if writable_paths is None: + writable_paths = (root,) + + cmdargs = ['linux-user-chroot', '--chdir', cwd] + if unshare_net: + cmdargs.append('--unshare-net') + for src, dst in binds: + # linux-user-chroot's mount target paths are relative to the chroot + cmdargs.extend(('--mount-bind', src, os.path.relpath(dst, root))) + for d in morphlib.fsutils.invert_paths(os.walk(root), writable_paths): + if not os.path.islink(d): + cmdargs.extend(('--mount-readonly', os.path.relpath(d, root))) + if mount_proc: + proc_target = os.path.join(root, 'proc') + if not os.path.exists(proc_target): + os.makedirs(proc_target) + cmdargs.extend(('--mount-proc', 'proc')) + cmdargs.append(root) + cmdargs.extend(args) + return unshared_cmdline(cmdargs, root=root, **kwargs) -- cgit v1.2.1 From 203f34b2b19b1d3dc51a956ae9329d064ea294dd Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 29 Oct 2014 18:13:19 +0000 Subject: Make system-integration commands use containerised_cmdline --- morphlib/builder2.py | 60 +++++----------------------------------------------- 1 file changed, 5 insertions(+), 55 deletions(-) diff --git a/morphlib/builder2.py b/morphlib/builder2.py index 8615ed59..f71f21db 100644 --- a/morphlib/builder2.py +++ b/morphlib/builder2.py @@ -28,7 +28,6 @@ import time import traceback import subprocess import tempfile -import textwrap import gzip import cliapp @@ -647,53 +646,6 @@ class SystemBuilder(BuilderBase): # pragma: no cover os.chmod(os_release_file, 0644) - def _chroot_runcmd(self, rootdir, to_mount, env, *args): - # We need to do mounts in a different namespace. Unfortunately - # this means we have to in-line the mount commands in the - # command-line. - command = textwrap.dedent(r''' - mount --make-rprivate / - rootdir="$1" - shift - ''') - cmdargs = [rootdir] - - # We need to mount all the specified mounts in the namespace, - # we don't need to unmount them before exiting, as they'll be - # unmounted when the namespace is no longer used. - command += textwrap.dedent(r''' - while true; do - case "$1" in - --) - shift - break - ;; - *) - mount_point="$1" - mount_type="$2" - mount_source="$3" - shift 3 - path="$rootdir/$mount_point" - mount -t "$mount_type" "$mount_source" "$path" - ;; - esac - done - ''') - for mount_opts in to_mount: - cmdargs.extend(mount_opts) - cmdargs.append('--') - - command += textwrap.dedent(r''' - exec chroot "$rootdir" "$@" - ''') - cmdargs.extend(args) - - # The single - is just a shell convention to fill $0 when using -c, - # since ordinarily $0 contains the program name. - cmdline = ['unshare', '--mount', '--', 'sh', '-ec', command, '-'] - cmdline.extend(cmdargs) - self.app.runcmd(cmdline, env=env) - def run_system_integration_commands(self, rootdir): # pragma: no cover ''' Run the system integration commands ''' @@ -708,18 +660,16 @@ class SystemBuilder(BuilderBase): # pragma: no cover self.app.status(msg='Running the system integration commands') to_mount = ( - ('proc', 'proc', 'none'), ('dev/shm', 'tmpfs', 'none'), ('tmp', 'tmpfs', 'none'), ) try: - for mount_point, mount_type, source in to_mount: - path = os.path.join(rootdir, mount_point) - if not os.path.exists(path): - os.makedirs(path) for bin in sorted(os.listdir(sys_integration_dir)): - self._chroot_runcmd(rootdir, to_mount, env, - os.path.join(SYSTEM_INTEGRATION_PATH, bin)) + self.app.runcmd( + morphlib.util.containerised_cmdline( + [os.path.join(SYSTEM_INTEGRATION_PATH, bin)], + root=rootdir, mounts=to_mount, mount_proc=True), + env=env) except BaseException, e: self.app.status( msg='Error while running system integration commands', -- cgit v1.2.1 From 64861f8dba8bd038c3ddfeb48dbef140a4332c6a Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 29 Oct 2014 18:13:52 +0000 Subject: Make deployment extensions use unshared_cmdline --- morphlib/extensions.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/morphlib/extensions.py b/morphlib/extensions.py index 6b81e116..b270d304 100644 --- a/morphlib/extensions.py +++ b/morphlib/extensions.py @@ -223,10 +223,7 @@ class ExtensionSubprocess(object): def close_read_end(): os.close(log_read_fd) p = subprocess.Popen( - # We unshare and mount --make-rprivate so mounts done by write - # extensions can't interfere with the rest of the system. - ['unshare', '-m', '--', '/bin/sh', '-c', - 'mount --make-rprivate / && exec "$@"', '-', filename] + args, + morphlib.util.unshared_cmdline([filename] + list(args)), cwd=cwd, env=new_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=close_read_end) -- cgit v1.2.1 From af63ece0a8db78bb5d0b7b67061b305d528fe993 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 29 Oct 2014 18:14:15 +0000 Subject: Make build commands use containerised_cmdline --- morphlib/stagingarea.py | 45 ++++++++++++++------------------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/morphlib/stagingarea.py b/morphlib/stagingarea.py index 25e33b3f..8a9faca6 100644 --- a/morphlib/stagingarea.py +++ b/morphlib/stagingarea.py @@ -267,16 +267,11 @@ class StagingArea(object): def runcmd(self, argv, **kwargs): # pragma: no cover '''Run a command in a chroot in the staging area.''' assert 'env' not in kwargs - kwargs['env'] = self.env + kwargs['env'] = dict(self.env) if 'extra_env' in kwargs: kwargs['env'].update(kwargs['extra_env']) del kwargs['extra_env'] - if 'cwd' in kwargs: - cwd = kwargs['cwd'] - del kwargs['cwd'] - else: - cwd = '/' ccache_dir = kwargs.pop('ccache_dir', None) chroot_dir = self.dirname if self.use_chroot else '/' @@ -289,40 +284,28 @@ class StagingArea(object): 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) - real_argv = ['linux-user-chroot', '--chdir', cwd, '--unshare-net'] - for d in morphlib.fsutils.invert_paths(os.walk(chroot_dir), - do_not_mount_dirs): - if not os.path.islink(d): - real_argv += ['--mount-readonly', self.relative(d)] - - if self.use_chroot: - proc_target = os.path.join(self.dirname, 'proc') - if not os.path.exists(proc_target): - os.makedirs(proc_target) - real_argv += ['--mount-proc', self.relative(proc_target)] - + mount_proc = self.use_chroot if ccache_dir and not self._app.settings['no-ccache']: ccache_target = os.path.join( self.dirname, kwargs['env']['CCACHE_DIR'].lstrip('/')) - real_argv += ['--mount-bind', ccache_dir, - self.relative(ccache_target)] - - real_argv += [chroot_dir] - - real_argv += argv + binds = ((ccache_dir, ccache_target),) + else: + binds = () + cmdline = morphlib.util.containerised_cmdline( + argv, cwd=kwargs.pop('cwd', '/'), + root=chroot_dir, mounts=self.to_mount, + binds=binds, mount_proc=mount_proc, + writable_paths=do_not_mount_dirs) try: - if 'logfile' in kwargs and kwargs['logfile'] != None: - logfile = kwargs['logfile'] - del kwargs['logfile'] - + if kwargs.get('logfile') != None: + logfile = kwargs.pop('logfile') teecmd = ['tee', '-a', logfile] - return self._app.runcmd(real_argv, teecmd, **kwargs) + return self._app.runcmd(cmdline, teecmd, **kwargs) else: - return self._app.runcmd(real_argv, **kwargs) + return self._app.runcmd(cmdline, **kwargs) except cliapp.AppException as e: raise cliapp.AppException('In staging area %s: running ' 'command \'%s\' failed.' % -- cgit v1.2.1 From 53ce07c7062fc5bad7fa4c470b1824393474277d Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 29 Oct 2014 18:28:41 +0000 Subject: stagingarea: Remove vestigial pre-command mounting logic --- morphlib/stagingarea.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/morphlib/stagingarea.py b/morphlib/stagingarea.py index 8a9faca6..b676d4db 100644 --- a/morphlib/stagingarea.py +++ b/morphlib/stagingarea.py @@ -45,7 +45,6 @@ class StagingArea(object): self.dirname = dirname self.builddirname = None self.destdirname = None - self.mounted = [] self._bind_readonly_mount = None self.use_chroot = use_chroot @@ -224,23 +223,6 @@ class StagingArea(object): os.makedirs(ccache_destdir) return ccache_repodir - def do_mounts(self, setup_mounts): # pragma: no cover - if not setup_mounts: - return - for mount_point, mount_type, source in self.to_mount: - logging.debug('Mounting %s in staging area' % mount_point) - path = os.path.join(self.dirname, mount_point) - if not os.path.exists(path): - os.makedirs(path) - morphlib.fsutils.mount(self._app.runcmd, source, path, mount_type) - self.mounted.append(path) - return - - def do_unmounts(self): # pragma: no cover - for path in reversed(self.mounted): - logging.debug('Unmounting %s in staging area' % path) - morphlib.fsutils.unmount(self._app.runcmd, path) - def chroot_open(self, source, setup_mounts): # pragma: no cover '''Setup staging area for use as a chroot.''' @@ -251,8 +233,6 @@ class StagingArea(object): self.builddirname = builddir self.destdirname = destdir - self.do_mounts(setup_mounts) - return builddir, destdir def chroot_close(self): # pragma: no cover @@ -261,8 +241,8 @@ class StagingArea(object): This should be called after the staging area is no longer needed. ''' - - self.do_unmounts() + # No cleanup is currently required + pass def runcmd(self, argv, **kwargs): # pragma: no cover '''Run a command in a chroot in the staging area.''' -- cgit v1.2.1