diff options
author | Richard Maw <richard.maw@gmail.com> | 2014-10-29 18:10:10 +0000 |
---|---|---|
committer | Richard Maw <richard.maw@codethink.co.uk> | 2014-10-30 14:30:10 +0000 |
commit | e6f8b1bf99eb2bfdbe9a5233237107c8ec36f883 (patch) | |
tree | 3c619cfd2273a1b486e16252a7bb91f670501b40 | |
parent | a5a2b03fd2d366ee9955607a3cbf2d1f57a2a1a3 (diff) | |
download | morph-e6f8b1bf99eb2bfdbe9a5233237107c8ec36f883.tar.gz |
Move unsharing and containerising logic to util
This way the build commands, system integration commands and deployment
extension commands can all share the logic.
-rw-r--r-- | morphlib/util.py | 114 |
1 files changed, 114 insertions, 0 deletions
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) |