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