summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@gmail.com>2014-10-29 18:10:10 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2014-10-30 14:30:10 +0000
commite6f8b1bf99eb2bfdbe9a5233237107c8ec36f883 (patch)
tree3c619cfd2273a1b486e16252a7bb91f670501b40
parenta5a2b03fd2d366ee9955607a3cbf2d1f57a2a1a3 (diff)
downloadmorph-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.py114
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)