diff options
Diffstat (limited to 'buildstream/_sandboxchroot.py')
-rw-r--r-- | buildstream/_sandboxchroot.py | 450 |
1 files changed, 0 insertions, 450 deletions
diff --git a/buildstream/_sandboxchroot.py b/buildstream/_sandboxchroot.py deleted file mode 100644 index 03bdecff4..000000000 --- a/buildstream/_sandboxchroot.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2017 Codethink Limited -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see <http://www.gnu.org/licenses/>. -# -# Authors: -# Tristan Maat <tristan.maat@codethink.co.uk> -# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> - -import os -import sys -import stat -import shutil -import subprocess -from contextlib import contextmanager, ExitStack - -from . import utils -from . import ElementError -from . import Sandbox, SandboxFlags - - -# A class to wrap the `mount` and `umount` commands -class Mount(): - def __init__(self, platform): - self.platform = platform - - def _mount(self, dest, src=None, mount_type=None, - stdout=sys.stdout, stderr=sys.stderr, options=None, - flags=None): - - argv = [utils.get_host_tool('mount')] - if mount_type: - argv.extend(['-t', mount_type]) - if options: - argv.extend(['-o', options]) - if flags: - argv.extend(flags) - - if src is not None: - argv += [src] - argv += [dest] - - status, _ = utils._call( - argv, - terminate=True, - stdout=stdout, - stderr=stderr - ) - - if status != 0: - raise ElementError('`{}` failed with exit code {}' - .format(' '.join(argv), status)) - - return dest - - def _umount(self, path, stdout=sys.stdout, stderr=sys.stderr): - - cmd = [utils.get_host_tool('umount'), '-R', path] - status, _ = utils._call( - cmd, - terminate=True, - stdout=stdout, - stderr=stderr - ) - - if status != 0: - raise ElementError('`{}` failed with exit code {}' - .format(' '.join(cmd), status)) - - # mount() - # - # A wrapper for the `mount` command. The device is unmounted when - # the context is left. - # - # Args: - # src (str) - The directory to mount - # dest (str) - The directory to mount to - # mount_type (str|None) - The mount type (can be omitted or None) - # kwargs - Arguments to pass to the mount command, such as `ro=True` - # - # Yields: - # (str) The path to the destination - # - @contextmanager - def mount(self, dest, src=None, mount_type=None, - stdout=sys.stdout, stderr=sys.stderr, **kwargs): - - options = ','.join([key for key, val in kwargs.items() if val]) - - yield self._mount(dest, src, mount_type, stdout=stdout, stderr=stderr, options=options) - - self._umount(dest, stdout, stderr) - - # bind_mount() - # - # Mount a directory to a different location (a hardlink for all - # intents and purposes). The directory is unmounted when the - # context is left. - # - # Args: - # src (str) - The directory to mount - # dest (str) - The directory to mount to - # kwargs - Arguments to pass to the mount command, such as `ro=True` - # - # Yields: - # (str) The path to the destination - # - # While this is equivalent to `mount --rbind`, this option may not - # exist and can be dangerous, requiring careful cleanupIt is - # recommended to use this function over a manual mount invocation. - # - @contextmanager - def bind_mount(self, dest, src=None, stdout=sys.stdout, stderr=sys.stderr, - **kwargs): - - kwargs['rbind'] = True - options = ','.join([key for key, val in kwargs.items() if val]) - - path = self._mount(dest, src, None, stdout, stderr, options) - - # Make the rbind a slave to avoid unmounting vital devices in - # /proc - self._mount(dest, flags=['--make-rslave']) - - yield path - - self._umount(dest, stdout, stderr) - - -class SandboxChroot(Sandbox): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.platform = self._get_context()._platform - self.mount = Mount(self.platform) - - def run(self, command, flags, cwd=None, env=None): - - # Default settings - if cwd is None: - cwd = self._get_work_directory() - - if cwd is None: - cwd = '/' - - if env is None: - env = self._get_environment() - - # Command must be a list - if isinstance(command, str): - command = [command] - - stdout, stderr = self._get_output() - - # Create a chroot directory and run the command inside it - with ExitStack() as stack: - if flags & SandboxFlags.INTERACTIVE: - stdin = sys.stdin - status = self.run_in_interactive_sandbox(command, env, flags, cwd) - else: - stdin = stack.enter_context(open(os.devnull, 'r')) - status = self.run_in_sandbox(command, stdin, stdout, - stderr, cwd, env, flags) - - return status - - # run_in_sandbox() - # - # A helper function to pass the command to the chroot. - # - # Args: - # command (list): The command to execute in the chroot - # stdin (file): The stdin - # stdout (file): The stdout - # stderr (file): The stderr - # interactive (bool): Whether the sandbox should be run interactively - # cwd (str): The current working directory - # env (dict): The environment variables to use while executing the command - # - # Returns: - # (int): The exit code of the executed command - # - def run_in_sandbox(self, command, stdin, stdout, stderr, cwd, env, flags): - # Hack to ensure a module required for exception handling is - # not loaded in the chroot. - # - # Propagates from here: - # https://github.com/CodethinkLabs/sandboxlib/blob/7e2a551189b5ffb7a0124db63964bdec69ead3e8/sandboxlib/chroot.py#L231 - _ = "Some Text".encode('unicode-escape') - - with ExitStack() as stack: - stack.enter_context(self.create_devices(flags)) - stack.enter_context(self.mount_dirs(flags, stdout, stderr)) - - try: - code, _ = utils._call( - command, - terminate=True, - close_fds=True, - cwd=os.path.join(self.get_directory(), 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(self.get_directory()), os.chdir(cwd)), - start_new_session=flags & SandboxFlags.INTERACTIVE - ) - except subprocess.SubprocessError as e: - # Exceptions in preexec_fn are simply reported as - # 'Exception occurred in preexec_fn', turn these into - # a more readable message. - if '{}'.format(e) == 'Exception occurred in preexec_fn.': - raise ElementError('Could not chroot into {} or chdir into {}. ' - 'Ensure you are root and that the relevant directory exists.' - .format(self.get_directory(), cwd)) from e - else: - raise ElementError('Could not run command {}: {}'.format(command, e)) from e - - if code != 0: - raise ElementError("{} failed with exit code {}".format(command, code)) - - return code - - # run_in_interactive_sandbox() - # - # Run an interactive command. - # - # - # Args: - # command (list): The command to execute in the chroot - # stdin (file): The stdin - # stdout (file): The stdout - # stderr (file): The stderr - # interactive (bool): Whether the sandbox should be run interactively - # cwd (str): The current working directory - # env (dict): The environment variables to use while executing the command - # - # Returns: - # (int): The exit code of the executed command - # - # The method is similar to SandboxChroot.run_in_sandbox(), but - # does not create a subprocess to allow the interactive session to - # communicate with the frontend. It foregoes changing into `cwd` - # since this is less important in an interactive session. - # - def run_in_interactive_sandbox(self, command, env, flags, cwd): - - with ExitStack() as stack: - stack.enter_context(self.create_devices(flags)) - stack.enter_context(self.mount_dirs(flags, sys.stdout, sys.stderr)) - - process = subprocess.Popen( - command, - cwd=os.path.join(self.get_directory(), cwd.lstrip(os.sep)), - close_fds=True, - preexec_fn=lambda: (os.chroot(self.get_directory()), os.chdir(cwd)), - env=env - ) - process.communicate() - - return process.poll() - - # create_devices() - # - # Create the nodes in /dev/ usually required for builds (null, - # none, etc.) - # - # Args: - # flags (:class:`.SandboxFlags`): The sandbox flags - # - @contextmanager - def create_devices(self, flags): - - devices = [] - # When we are interactive, we'd rather mount /dev due to the - # sheer number of devices - if not flags & SandboxFlags.INTERACTIVE: - os.makedirs('/dev/', exist_ok=True) - - for device in Sandbox.DEVICES: - location = os.path.join(self.get_directory(), device.lstrip(os.sep)) - os.makedirs(os.path.dirname(location), exist_ok=True) - try: - # If the image already contains a device, remove - # it, since the device numbers may be different on - # different systems. - os.remove(location) - - devices.append(self.mknod(device, location)) - except OSError as err: - if err.errno == 1: - raise ElementError("Permission denied while creating device node: {}.".format(err) + - "BuildStream reqiures root permissions for these setttings.") - - yield - - for device in devices: - os.remove(device) - - # mount() - # - # Mount paths required for the command. - # - # Args: - # flags (:class:`.SandboxFlags`): The sandbox flags - # - @contextmanager - def mount_dirs(self, flags, stdout, stderr): - - # FIXME: This should probably keep track of potentially - # already existing files a la _sandboxwrap.py:239 - - # To successfully mount the sysroot RO, we need to: - # - Mount / RO first since solaris can't remount a bind mount - # - Create missing directories and mount points - # - Move marked directories to a scratch directory to keep their RW state - # - Mount / RO - # - Mount marked directories RW - # - Mount system directories (/dev, /proc/, /tmp, ...) - # - # - execute command - # - # - Unmount system and marked directories - # - Unmount / - - # Create missing system directories - if flags & SandboxFlags.INTERACTIVE: - # We mount /dev in interactive sandboxes to give the - # user a working console - dev_src, dev_point = self.get_mount_location('/dev/', '/dev/') - - proc_src, proc_point = self.get_mount_location('/proc/', '/proc/') - tmp_src, tmp_point = self.get_mount_location('/tmp', '/tmp') - - marked_directories = self._get_marked_directories() - marked_locations = [] - - for mark in marked_directories: - host_location = os.path.join(self.get_directory(), - mark['directory'].lstrip(os.sep)) - scratch_location = os.path.join(self._get_scratch_directory(), - mark['directory'].lstrip(os.sep)) - - # On the first invocation, move the marked directories to - # the scratch directory to allow mounting them read-write - - # Create the host location if it does not exist yet - os.makedirs(host_location, exist_ok=True) - - # Move marked directories before mounting / RO - shutil.move(host_location, scratch_location) - - # Record the mount locations - marked_locations.append(self.get_mount_location(scratch_location, mark['directory'])) - - with ExitStack() as stack: - # / mount - root_mount = self.mount.bind_mount(self.get_directory(), self.get_directory(), - stdout, stderr, - ro=flags & SandboxFlags.ROOT_READ_ONLY) - # Since / needs to be unmounted after and before - # everything else has been unmounted, manually enter/exit - # the context. - root_mount.__enter__() - - # System mounts - if flags & SandboxFlags.INTERACTIVE: - stack.enter_context(self.mount.bind_mount(dev_point, dev_src, stdout, stderr)) - - stack.enter_context(self.mount.mount(proc_point, proc_src, 'proc', stdout, stderr, ro=True)) - stack.enter_context(self.mount.bind_mount(tmp_point, tmp_src, stdout, stderr)) - - # Mark mounts - for src, point in marked_locations: - stack.enter_context(self.mount.bind_mount(point, src, stdout, stderr)) - - yield - - root_mount.__exit__(None, None, None) - - # Move back mark mounts - for src, point in marked_locations: - shutil.rmtree(point) - shutil.move(src, point) - - # get_mount_location() - # - # Return a tuple that indicates the locations to mount a host - # directory to and from to mount it to the corresponding path in - # the chroot. - # - # Args: - # host_dir (str): The path to the host dir to mount. - # chroot_dir (str): The dir in the chroot to mount it to. - # - # Returns: - # (str, str) - (source, point) - # - def get_mount_location(self, host_dir, chroot_dir): - point = os.path.join(self.get_directory(), chroot_dir.lstrip(os.sep)) - - os.makedirs(host_dir, exist_ok=True) - os.makedirs(point, exist_ok=True) - - return (host_dir, point) - - # mknod() - # - # Create a device node equivalent to the given source node - # - # Args: - # source (str): Path of the device to mimic (e.g. '/dev/null') - # target (str): Location to create the new device in - # - # Returns: - # target (str): The location of the created node - # - def mknod(self, source, target): - try: - dev = os.stat(source) - - major = os.major(dev.st_rdev) - minor = os.minor(dev.st_rdev) - - target_dev = os.makedev(major, minor) - - os.mknod(target, mode=stat.S_IFCHR | dev.st_mode, device=target_dev) - - except PermissionError as e: - raise ElementError('Could not create device {}, ensure that you have root permissions: {}') - - except OSError as e: - raise ElementError('Could not create device {}: {}' - .format(target, e)) from e - - return target |