summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTristan Maat <tristan.maat@codethink.co.uk>2018-01-31 14:04:43 +0000
committerTristan Maat <tristan.maat@codethink.co.uk>2018-02-19 14:39:38 +0100
commit85b52097f981c665bfb80df7ddccadf43d3ec8d9 (patch)
tree3f20b20c7f8e599ea3b651864482f656f0cfaa03
parent2643b095c59db6e8cbc2b8620c2ad820c7a5636d (diff)
downloadbuildstream-218-allow-specifying-the-chroot-binary-to-use-for-sandboxes-on-unix-platforms.tar.gz
-rw-r--r--buildstream/_platform/unix.py10
-rw-r--r--buildstream/sandbox/__init__.py1
-rw-r--r--buildstream/sandbox/_sandboxchroot.py139
-rw-r--r--buildstream/sandbox/_sandboxuserchroot.py158
4 files changed, 238 insertions, 70 deletions
diff --git a/buildstream/_platform/unix.py b/buildstream/_platform/unix.py
index 6d7b46374..7c3a805fc 100644
--- a/buildstream/_platform/unix.py
+++ b/buildstream/_platform/unix.py
@@ -22,7 +22,7 @@ import os
from .._artifactcache.tarcache import TarCache
from .._exceptions import PlatformError
-from ..sandbox import SandboxChroot
+from ..sandbox import SandboxChroot, SandboxUserChroot
from . import Platform
@@ -35,12 +35,14 @@ class Unix(Platform):
self._artifact_cache = TarCache(context)
# Not necessarily 100% reliable, but we want to fail early.
- if os.geteuid() != 0:
- raise PlatformError("Root privileges are required to run without bubblewrap.")
+ # if os.geteuid() != 0:
+ # raise PlatformError("Root privileges are required to run without bubblewrap.")
@property
def artifactcache(self):
return self._artifact_cache
def create_sandbox(self, *args, **kwargs):
- return SandboxChroot(*args, **kwargs)
+ # We can optionally create a SandboxUserChroot
+ return SandboxUserChroot(*args, **kwargs)
+ # return SandboxChroot(*args, **kwargs)
diff --git a/buildstream/sandbox/__init__.py b/buildstream/sandbox/__init__.py
index 7ee871cab..c55e53ed1 100644
--- a/buildstream/sandbox/__init__.py
+++ b/buildstream/sandbox/__init__.py
@@ -19,5 +19,6 @@
# Tristan Maat <tristan.maat@codethink.co.uk>
from .sandbox import Sandbox, SandboxFlags
+from ._sandboxuserchroot import SandboxUserChroot
from ._sandboxchroot import SandboxChroot
from ._sandboxbwrap import SandboxBwrap
diff --git a/buildstream/sandbox/_sandboxchroot.py b/buildstream/sandbox/_sandboxchroot.py
index 584f0e116..8ac0ba4f1 100644
--- a/buildstream/sandbox/_sandboxchroot.py
+++ b/buildstream/sandbox/_sandboxchroot.py
@@ -91,24 +91,12 @@ class SandboxChroot(Sandbox):
return status
- # chroot()
+ # popen()
#
- # A helper function to chroot into the rootfs.
+ # A helper function to create and manage a subprocess. We mimic
+ # subprocess.Popen's interface here.
#
- # Args:
- # rootfs (str): The path of the sysroot to chroot into
- # command (list): The command to execute in the chroot env
- # stdin (file): The stdin
- # stdout (file): The stdout
- # stderr (file): The stderr
- # cwd (str): The current working directory
- # env (dict): The environment variables to use while executing the command
- # flags (:class:`SandboxFlags`): The flags to enable on the sandbox
- #
- # Returns:
- # (int): The exit code of the executed command
- #
- def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env, flags):
+ def popen(self, command, **kwargs):
def kill_proc():
if process:
# First attempt to gracefully terminate
@@ -128,55 +116,76 @@ class SandboxChroot(Sandbox):
group_id = os.getpgid(process.pid)
os.killpg(group_id, signal.SIGCONT)
+ with _signals.suspendable(suspend_proc, resume_proc), \
+ _signals.terminator(kill_proc):
+ process = subprocess.Popen(command, **kwargs)
+
+ # Wait for the child process to finish, ensuring that
+ # a SIGINT has exactly the effect the user probably
+ # expects (i.e. let the child process handle it).
+ try:
+ while True:
+ try:
+ _, status = os.waitpid(process.pid, 0)
+ # If the process exits due to a signal, we
+ # brutally murder it to avoid zombies
+ if not os.WIFEXITED(status):
+ utils._kill_process_tree(process.pid)
+
+ # Unlike in the bwrap case, here only the main
+ # process seems to receive the SIGINT. We pass
+ # on the signal to the child and then continue
+ # to wait.
+ except KeyboardInterrupt:
+ process.send_signal(signal.SIGINT)
+ continue
+
+ break
+ # If we can't find the process, it has already died of
+ # its own accord, and therefore we don't need to check
+ # or kill anything.
+ except psutil.NoSuchProcess:
+ pass
+
+ # Return the exit code - see the documentation for
+ # os.WEXITSTATUS to see why this is required.
+ if os.WIFEXITED(status):
+ code = os.WEXITSTATUS(status)
+ else:
+ code = -1
+
+ return code
+
+ # chroot()
+ #
+ # A helper function to chroot into the rootfs.
+ #
+ # Args:
+ # rootfs (str): The path of the sysroot to chroot into
+ # command (list): The command to execute in the chroot env
+ # stdin (file): The stdin
+ # stdout (file): The stdout
+ # stderr (file): The stderr
+ # cwd (str): The current working directory
+ # env (dict): The environment variables to use while executing the command
+ # flags (:class:`SandboxFlags`): The flags to enable on the sandbox
+ #
+ # Returns:
+ # (int): The exit code of the executed command
+ #
+ def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env, flags):
try:
- with _signals.suspendable(suspend_proc, resume_proc), _signals.terminator(kill_proc):
- process = subprocess.Popen(
- command,
- close_fds=True,
- cwd=os.path.join(rootfs, cwd.lstrip(os.sep)),
- env=env,
- stdin=stdin,
- stdout=stdout,
- stderr=stderr,
- # If you try to put gtk dialogs here Tristan (either)
- # will personally scald you
- preexec_fn=lambda: (os.chroot(rootfs), os.chdir(cwd)),
- start_new_session=flags & SandboxFlags.INTERACTIVE
- )
-
- # Wait for the child process to finish, ensuring that
- # a SIGINT has exactly the effect the user probably
- # expects (i.e. let the child process handle it).
- try:
- while True:
- try:
- _, status = os.waitpid(process.pid, 0)
- # If the process exits due to a signal, we
- # brutally murder it to avoid zombies
- if not os.WIFEXITED(status):
- utils._kill_process_tree(process.pid)
-
- # Unlike in the bwrap case, here only the main
- # process seems to receive the SIGINT. We pass
- # on the signal to the child and then continue
- # to wait.
- except KeyboardInterrupt:
- process.send_signal(signal.SIGINT)
- continue
-
- break
- # If we can't find the process, it has already died of
- # its own accord, and therefore we don't need to check
- # or kill anything.
- except psutil.NoSuchProcess:
- pass
-
- # Return the exit code - see the documentation for
- # os.WEXITSTATUS to see why this is required.
- if os.WIFEXITED(status):
- code = os.WEXITSTATUS(status)
- else:
- code = -1
+ return self.popen(command,
+ close_fds=True,
+ cwd=os.path.join(rootfs, cwd.lstrip(os.sep)),
+ env=env,
+ stdin=stdin,
+ stdout=stdout,
+ stderr=stderr,
+ # If you try to put gtk dialogs here Tristan (either)
+ # will personally scald you
+ preexec_fn=lambda: (os.chroot(rootfs), os.chdir(cwd)),
+ start_new_session=flags & SandboxFlags.INTERACTIVE)
except subprocess.SubprocessError as e:
# Exceptions in preexec_fn are simply reported as
@@ -189,8 +198,6 @@ class SandboxChroot(Sandbox):
else:
raise SandboxError('Could not run command {}: {}'.format(command, e)) from e
- return code
-
# create_devices()
#
# Create the nodes in /dev/ usually required for builds (null,
diff --git a/buildstream/sandbox/_sandboxuserchroot.py b/buildstream/sandbox/_sandboxuserchroot.py
new file mode 100644
index 000000000..6e98e8df1
--- /dev/null
+++ b/buildstream/sandbox/_sandboxuserchroot.py
@@ -0,0 +1,158 @@
+import os
+import pwd
+import sys
+import stat
+import subprocess
+from pathlib import Path
+from contextlib import ExitStack, contextmanager
+
+from .. import utils
+from . import SandboxFlags
+from ._mount import MountMap
+from .._exceptions import SandboxError
+from ._sandboxchroot import SandboxChroot
+from .._message import Message, MessageType
+
+
+# The sandbox directory needs to fulfill a few criteria:
+# - Its parents must be owned by root
+# - It and its children must be owned by the user defined in the
+# configuration file
+# - Neither it nor its parents must be more permissive than 755
+# - It cannot be in a directory mounted into the sandbox (duh)
+#
+# If we allow the user to specify this location (we probably should),
+# those criteria would be nice to check for before sandbox execution.
+# Although userchroot itself checks for some, the error messages are
+# not particularly helpful.
+#
+SANDBOX_DIR = '/usr/local/sandboxes'
+
+
+def assert_userchroot_configuration():
+ configured = False
+ user = pwd.getpwuid(os.getuid())[0]
+ userchroot = utils.get_host_tool('userchroot')
+ config = Path(userchroot).parents[1].joinpath('etc/userchroot.conf')
+
+ if config.exists():
+ with open(config, 'r') as configf:
+ for line in configf:
+ if line.rstrip() == '{}:{}'.format(user, SANDBOX_DIR):
+ configured = True
+ break
+
+ if not configured:
+ raise SandboxError("'userchroot' is not configured correctly. "
+ "Please add '{}:{}' to '{}'"
+ .format(user, SANDBOX_DIR, config))
+
+
+class SandboxUserChroot(SandboxChroot):
+ def run(self, command, flags, *, cwd=None, env=None):
+ # Ensure sandbox default configuration
+ if cwd is None:
+ cwd = self._get_work_directory() or '/'
+
+ if env is None:
+ env = self._get_environment()
+
+ if isinstance(command, str):
+ command = [command]
+
+ stdout, stderr = self._get_output()
+
+ # Create the mount map, this will tell us where
+ # each mount point needs to be mounted from and to
+ self.mount_map = MountMap(self, True)
+
+ # Make sure userchroot is configured correctly
+ assert_userchroot_configuration()
+
+ with ExitStack() as stack:
+ # Create sysroot
+ try:
+ os.makedirs(SANDBOX_DIR, exist_ok=True)
+ rootfs = stack.enter_context(utils._tempdir(dir=SANDBOX_DIR))
+ except PermissionError as e:
+ raise SandboxError('Could not create sysroot in {}: {}'
+ .format(SANDBOX_DIR, e)) from e
+
+ stack.enter_context(self.stage_sysroot(rootfs, flags, stdout, stderr))
+
+ # Chroot!
+ if flags & SandboxFlags.INTERACTIVE:
+ stdin = sys.stdin
+ else:
+ stdin = stack.enter_context(open(os.devnull, 'r'))
+
+ status = self.chroot(rootfs, command, stdin, stdout, stderr,
+ cwd, env, flags)
+ return status
+
+ def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env,
+ flags):
+ # Create a script in the root directory of the sysroot to
+ # execute the given commands.
+ script = "\n".join(["#!/bin/sh"] + command)
+ scriptpath = os.path.join(rootfs, 'buildstream-run.sh')
+
+ with open(scriptpath, 'w') as scriptfile:
+ scriptfile.write(script)
+ perms = os.stat(scriptpath).st_mode
+ os.chmod(scriptpath, perms & stat.S_IXUSR)
+
+ # Execute the script with userchroot
+ try:
+ command = [utils.get_host_tool('userchroot'),
+ rootfs,
+ '--install-devices',
+ '/buildstream-run.sh']
+ return self.popen(command,
+ env=env,
+ stdin=stdin,
+ stdout=stdout,
+ stderr=stderr,
+ cwd=os.path.join(rootfs, cwd.lstrip(os.sep)),
+ start_new_session=flags & SandboxFlags.INTERACTIVE)
+
+ except subprocess.SubprocessError as e:
+ raise SandboxError('Could not run command {}: {}'.format(command, e)) from e
+
+ # mount_dirs()
+ #
+ # Since we aren't root we can't arbitrarily mount directories. Yet
+ # we *require* our FUSE filesystem for at least some operations.
+ #
+ # FUSE can be mounted by users, therefore this mount function
+ # attempts to safely mount our FUSE system on top of a copy of our
+ # sandbox files.
+ #
+ # This *does* mean that this platform is significantly slower than
+ # others, unfortunately...
+ #
+ @contextmanager
+ def stage_sysroot(self, rootfs, flags, stdout, stderr):
+ def mount(d):
+ overrides = self._get_mount_sources()
+
+ if d in overrides:
+ src = overrides[d]
+ else:
+ src = self.mount_map.get_mount_source(d)
+
+ dst = os.path.join(rootfs, d.lstrip(os.sep))
+
+ self.info('Mounting {} to {}'.format(src, dst))
+
+ with self.mount_map.mounted(rootfs):
+ yield
+
+ mount('/')
+
+ for mark in self._get_marked_directories():
+ mount(mark['directory'])
+
+ def info(self, message):
+ msg = Message('sandbox', MessageType.INFO, message)
+ self._get_context()._message(msg)