You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@buildstream.apache.org by ro...@apache.org on 2020/12/29 13:38:01 UTC

[buildstream] 06/08: sandbox: Add initial SandboxBuildBox

This is an automated email from the ASF dual-hosted git repository.

root pushed a commit to branch willsalmon/buildbox
in repository https://gitbox.apache.org/repos/asf/buildstream.git

commit f2426abed88209afc5ca9f06ef2095c784ae326e
Author: Jürg Billeter <j...@bitron.ch>
AuthorDate: Wed Aug 8 14:35:25 2018 +0200

    sandbox: Add initial SandboxBuildBox
---
 src/buildstream/_platform/linux.py          |  25 +++
 src/buildstream/sandbox/_sandboxbuildbox.py | 247 ++++++++++++++++++++++++++++
 src/buildstream/sandbox/_sandboxbwrap.py    |   2 +-
 src/buildstream/sandbox/sandbox.py          |  16 ++
 4 files changed, 289 insertions(+), 1 deletion(-)

diff --git a/src/buildstream/_platform/linux.py b/src/buildstream/_platform/linux.py
index b69dd45..b400bfa 100644
--- a/src/buildstream/_platform/linux.py
+++ b/src/buildstream/_platform/linux.py
@@ -24,6 +24,7 @@ from .. import utils
 from ..sandbox import SandboxDummy
 
 from .platform import Platform
+from .._exceptions import PlatformError
 
 
 class Linux(Platform):
@@ -31,6 +32,7 @@ class Linux(Platform):
     def _setup_sandbox(self, force_sandbox):
         sandbox_setups = {
             'bwrap': self._setup_bwrap_sandbox,
+            'buildbox': self._setup_buildbox_sandbox,
             'chroot': self._setup_chroot_sandbox,
             'dummy': self._setup_dummy_sandbox,
         }
@@ -67,6 +69,7 @@ class Linux(Platform):
     #              Private Methods                 #
     ################################################
 
+    # Dummy sandbox methods
     @staticmethod
     def _check_dummy_sandbox_config(config):
         return True
@@ -81,6 +84,7 @@ class Linux(Platform):
         self.create_sandbox = self._create_dummy_sandbox
         return True
 
+    # Bubble-wrap sandbox methods
     def _check_sandbox_config_bwrap(self, config):
         from ..sandbox._sandboxbwrap import SandboxBwrap
         return SandboxBwrap.check_sandbox_config(self, config)
@@ -103,6 +107,7 @@ class Linux(Platform):
         self.create_sandbox = self._create_bwrap_sandbox
         return True
 
+    # Chroot sandbox methods
     def _check_sandbox_config_chroot(self, config):
         from ..sandbox._sandboxchroot import SandboxChroot
         return SandboxChroot.check_sandbox_config(self, config)
@@ -118,3 +123,23 @@ class Linux(Platform):
         self.check_sandbox_config = self._check_sandbox_config_chroot
         self.create_sandbox = Linux._create_chroot_sandbox
         return True
+
+    # Buildbox sandbox methods
+    def _check_sandbox_config_buildbox(self, config):
+        from ..sandbox._sandboxbuildbox import SandboxBuildBox
+        return SandboxBuildBox.check_sandbox_config(self, config)
+
+    @staticmethod
+    def _create_buildbox_sandbox(*args, **kwargs):
+        from ..sandbox._sandboxbuildbox import SandboxBuildBox
+        if kwargs.get('allow_real_directory'):
+            raise PlatformError("The BuildBox Sandbox does not support real directories.",
+                                reason="You are using BuildBox sandbox because BST_FORCE_SANBOX=buildbox")
+        return SandboxBuildBox(*args, **kwargs)
+
+    def _setup_buildbox_sandbox(self):
+        from ..sandbox._sandboxbuildbox import SandboxBuildBox
+        self._check_sandbox(SandboxBuildBox)
+        self.check_sandbox_config = self._check_sandbox_config_buildbox
+        self.create_sandbox = self._create_buildbox_sandbox
+        return True
diff --git a/src/buildstream/sandbox/_sandboxbuildbox.py b/src/buildstream/sandbox/_sandboxbuildbox.py
new file mode 100644
index 0000000..417d222
--- /dev/null
+++ b/src/buildstream/sandbox/_sandboxbuildbox.py
@@ -0,0 +1,247 @@
+#
+#  Copyright (C) 2018 Bloomberg LP
+#
+#  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/>.
+
+import os
+import sys
+import signal
+import subprocess
+from contextlib import ExitStack
+
+import psutil
+
+from .. import utils, _signals, ProgramNotFoundError
+from . import Sandbox, SandboxFlags
+from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2
+from ..storage._casbaseddirectory import CasBasedDirectory
+from .._exceptions import SandboxError
+
+
+# SandboxBuidBox()
+#
+# BuildBox-based sandbox implementation.
+#
+class SandboxBuildBox(Sandbox):
+
+    def __init__(self, context, project, directory, **kwargs):
+        if kwargs.get('allow_real_directory'):
+            raise SandboxError("BuildBox does not support real directories")
+        else:
+            kwargs['allow_real_directory'] = False
+        super().__init__(context, project, directory, **kwargs)
+
+    @classmethod
+    def check_available(cls):
+        try:
+            utils.get_host_tool('buildbox')
+        except utils.ProgramNotFoundError as Error:
+            cls._dummy_reasons += ["buildbox not found"]
+            raise SandboxError(" and ".join(cls._dummy_reasons),
+                               reason="unavailable-local-sandbox") from Error
+
+    @classmethod
+    def check_sandbox_config(cls, platform, config):
+        # Report error for elements requiring non-0 UID/GID
+        # TODO
+        if config.build_uid != 0 or config.build_gid != 0:
+            return False
+
+        # Check host os and architecture match
+        if config.build_os != platform.get_host_os():
+            raise SandboxError("Configured and host OS don't match.")
+        elif config.build_arch != platform.get_host_arch():
+            raise SandboxError("Configured and host architecture don't match.")
+
+        return True
+
+    def _run(self, command, flags, *, cwd, env):
+        stdout, stderr = self._get_output()
+
+        root_directory = self.get_virtual_directory()
+        scratch_directory = self._get_scratch_directory()
+
+        if not self._has_command(command[0], env):
+            raise SandboxError("Staged artifacts do not provide command "
+                               "'{}'".format(command[0]),
+                               reason='missing-command')
+
+        # Grab the full path of the buildbox binary
+        try:
+            buildbox_command = [utils.get_host_tool('buildbox')]
+        except ProgramNotFoundError as Err:
+            raise SandboxError(("BuildBox not on path, you are using the BuildBox sandbox because "
+                                "BST_FORCE_SANDBOX=buildbox")) from Err
+
+        for mark in self._get_marked_directories():
+            path = mark['directory']
+            assert path.startswith('/') and len(path) > 1
+            root_directory.descend(*path[1:].split(os.path.sep), create=True)
+
+        digest = root_directory._get_digest()
+        with open(os.path.join(scratch_directory, 'in'), 'wb') as input_digest_file:
+            input_digest_file.write(digest.SerializeToString())
+
+        buildbox_command += ["--local=" + root_directory.cas_cache.casdir]
+        buildbox_command += ["--input-digest=in"]
+        buildbox_command += ["--output-digest=out"]
+
+        common_details = ("BuildBox is a experimental sandbox and does not support the requested feature.\n"
+                          "You are using this feature because BST_FORCE_SANDBOX=buildbox.")
+
+        if not flags & SandboxFlags.NETWORK_ENABLED:
+            # TODO
+            self._issue_warning(
+                "BuildBox sandbox does not have Networking yet",
+                detail=common_details
+            )
+
+        if cwd is not None:
+            buildbox_command += ['--chdir=' + cwd]
+
+        # In interactive mode, we want a complete devpts inside
+        # the container, so there is a /dev/console and such. In
+        # the regular non-interactive sandbox, we want to hand pick
+        # a minimal set of devices to expose to the sandbox.
+        #
+        if flags & SandboxFlags.INTERACTIVE:
+            # TODO
+            self._issue_warning(
+                "BuildBox sandbox does not fully support BuildStream shells yet",
+                detail=common_details
+            )
+
+        if flags & SandboxFlags.ROOT_READ_ONLY:
+            # TODO
+            self._issue_warning(
+                "BuildBox sandbox does not fully support BuildStream `Read only Root`",
+                detail=common_details
+            )
+
+        # Set UID and GID
+        if not flags & SandboxFlags.INHERIT_UID:
+            # TODO
+            self._issue_warning(
+                "BuildBox sandbox does not fully support BuildStream Inherit UID",
+                detail=common_details
+            )
+
+        os.makedirs(os.path.join(scratch_directory, 'mnt'), exist_ok=True)
+        buildbox_command += ['mnt']
+
+        # Add the command
+        buildbox_command += command
+
+        # Use the MountMap context manager to ensure that any redirected
+        # mounts through fuse layers are in context and ready for buildbox
+        # to mount them from.
+        #
+        with ExitStack() as stack:
+            # Ensure the cwd exists
+            if cwd is not None and len(cwd) > 1:
+                assert cwd.startswith('/')
+                root_directory.descend(*cwd[1:].split(os.path.sep), create=True)
+
+            # If we're interactive, we want to inherit our stdin,
+            # otherwise redirect to /dev/null, ensuring process
+            # disconnected from terminal.
+            if flags & SandboxFlags.INTERACTIVE:
+                stdin = sys.stdin
+            else:
+                stdin = stack.enter_context(open(os.devnull, "r"))
+
+            # Run buildbox !
+            exit_code = self.run_buildbox(buildbox_command, stdin, stdout, stderr, env,
+                                          interactive=(flags & SandboxFlags.INTERACTIVE),
+                                          cwd=scratch_directory)
+
+            if exit_code == 0:
+                with open(os.path.join(scratch_directory, 'out'), 'rb') as output_digest_file:
+                    output_digest = remote_execution_pb2.Digest()
+                    output_digest.ParseFromString(output_digest_file.read())
+                    self._vdir = CasBasedDirectory(root_directory.cas_cache, digest=output_digest)
+
+        return exit_code
+
+    def run_buildbox(self, argv, stdin, stdout, stderr, env, *, interactive, cwd):
+        def kill_proc():
+            if process:
+                # First attempt to gracefully terminate
+                proc = psutil.Process(process.pid)
+                proc.terminate()
+
+                try:
+                    proc.wait(20)
+                except psutil.TimeoutExpired:
+                    utils._kill_process_tree(process.pid)
+
+        def suspend_proc():
+            group_id = os.getpgid(process.pid)
+            os.killpg(group_id, signal.SIGSTOP)
+
+        def resume_proc():
+            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(
+                argv,
+                close_fds=True,
+                env=env,
+                stdin=stdin,
+                stdout=stdout,
+                stderr=stderr,
+                cwd=cwd,
+                start_new_session=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):
+                exit_code = os.WEXITSTATUS(status)
+            else:
+                exit_code = -1
+
+        return exit_code
+
+    def _use_cas_based_directory(self):
+        # Always use CasBasedDirectory for BuildBox
+        return True
diff --git a/src/buildstream/sandbox/_sandboxbwrap.py b/src/buildstream/sandbox/_sandboxbwrap.py
index 1155793..81e9f34 100644
--- a/src/buildstream/sandbox/_sandboxbwrap.py
+++ b/src/buildstream/sandbox/_sandboxbwrap.py
@@ -336,7 +336,7 @@ class SandboxBwrap(Sandbox):
                 # The only message relevant to us now is the exit-code of the subprocess.
                 for line in json_status_file:
                     with suppress(json.decoder.JSONDecodeError):
-                        o = json.loads(line)
+                        o = json.loads(line.decode())
                         if isinstance(o, collections.abc.Mapping) and 'exit-code' in o:
                             child_exit_code = o['exit-code']
                             break
diff --git a/src/buildstream/sandbox/sandbox.py b/src/buildstream/sandbox/sandbox.py
index 3229b2d..ece15c9 100644
--- a/src/buildstream/sandbox/sandbox.py
+++ b/src/buildstream/sandbox/sandbox.py
@@ -613,6 +613,22 @@ class Sandbox():
         self._build_directory = directory
         self._build_directory_always = always
 
+    # _issue_warning()
+    #
+    # Issue warning with __context that is not available with subclasses
+    #
+    # Args:
+    #    message (str): A message to issue
+    #    details (str): optional, more detatils
+    def _issue_warning(self, message, detail=None):
+        self.__context.messenger.message(
+            Message(None,
+                    MessageType.WARN,
+                    message,
+                    detail=detail
+                    )
+        )
+
 
 # _SandboxBatch()
 #