diff options
author | Jürg Billeter <j@bitron.ch> | 2019-12-10 12:34:19 +0000 |
---|---|---|
committer | Jürg Billeter <j@bitron.ch> | 2019-12-10 12:34:19 +0000 |
commit | 4ad66cd51f36319feef0ed0824e303f3f64c7422 (patch) | |
tree | 0a71cc445f0fdd3f0cfa14466a0ff7d11adc3743 | |
parent | 393fd2d458181cb9cbf30133dc5bbac8659db84b (diff) | |
parent | 9fe6c9e63a50edd80c792608b6c6bc63ff2e2e1f (diff) | |
download | buildstream-4ad66cd51f36319feef0ed0824e303f3f64c7422.tar.gz |
Merge branch 'juerg/buildbox-run' into 'master'
Add buildbox-run sandboxing backend
See merge request BuildStream/buildstream!1738
-rw-r--r-- | src/buildstream/_platform/linux.py | 1 | ||||
-rw-r--r-- | src/buildstream/_platform/platform.py | 27 | ||||
-rw-r--r-- | src/buildstream/sandbox/_sandboxbuildboxrun.py | 148 | ||||
-rw-r--r-- | src/buildstream/sandbox/_sandboxreapi.py | 4 | ||||
-rw-r--r-- | src/buildstream/sandbox/_sandboxremote.py | 2 | ||||
-rw-r--r-- | tests/integration/interactive_build.py | 2 | ||||
-rw-r--r-- | tests/integration/manual.py | 1 | ||||
-rw-r--r-- | tests/integration/messages.py | 2 | ||||
-rw-r--r-- | tests/integration/project/elements/sandbox/test-dev-shm.bst (renamed from tests/integration/project/elements/sandbox-bwrap/test-dev-shm.bst) | 0 | ||||
-rw-r--r-- | tests/integration/sandbox-bwrap.py | 10 | ||||
-rw-r--r-- | tests/integration/sandbox.py | 43 | ||||
-rw-r--r-- | tests/integration/script.py | 5 | ||||
-rw-r--r-- | tests/integration/shell.py | 8 | ||||
-rw-r--r-- | tests/integration/workspace.py | 1 |
14 files changed, 232 insertions, 22 deletions
diff --git a/src/buildstream/_platform/linux.py b/src/buildstream/_platform/linux.py index bdc2e0df1..a6215c90f 100644 --- a/src/buildstream/_platform/linux.py +++ b/src/buildstream/_platform/linux.py @@ -32,6 +32,7 @@ class Linux(Platform): sandbox_setups = { "bwrap": self._setup_bwrap_sandbox, "buildbox": self._setup_buildbox_sandbox, + "buildbox-run": self.setup_buildboxrun_sandbox, "chroot": self._setup_chroot_sandbox, "dummy": self._setup_dummy_sandbox, } diff --git a/src/buildstream/_platform/platform.py b/src/buildstream/_platform/platform.py index 1fddbe82c..e4fa1b9f2 100644 --- a/src/buildstream/_platform/platform.py +++ b/src/buildstream/_platform/platform.py @@ -45,7 +45,8 @@ class Platform: self._setup_sandbox(force_sandbox) def _setup_sandbox(self, force_sandbox): - sandbox_setups = {"dummy": self._setup_dummy_sandbox} + # The buildbox-run interface is not platform-specific + sandbox_setups = {"buildbox-run": self.setup_buildboxrun_sandbox, "dummy": self._setup_dummy_sandbox} preferred_sandboxes = [] self._try_sandboxes(force_sandbox, sandbox_setups, preferred_sandboxes) @@ -209,10 +210,10 @@ class Platform: # Returns: # (Sandbox) A sandbox # - def create_sandbox(self, *args, **kwargs): + def create_sandbox(self, *args, **kwargs): # pylint: disable=method-hidden raise ImplError("Platform {platform} does not implement create_sandbox()".format(platform=type(self).__name__)) - def check_sandbox_config(self, config): + def check_sandbox_config(self, config): # pylint: disable=method-hidden raise ImplError( "Platform {platform} does not implement check_sandbox_config()".format(platform=type(self).__name__) ) @@ -237,3 +238,23 @@ class Platform: raise ImplError( "Platform {platform} does not implement _setup_dummy_sandbox()".format(platform=type(self).__name__) ) + + # Buildbox run sandbox methods + def _check_sandbox_config_buildboxrun(self, config): + from ..sandbox._sandboxbuildboxrun import SandboxBuildBoxRun + + return SandboxBuildBoxRun.check_sandbox_config(self, config) + + @staticmethod + def _create_buildboxrun_sandbox(*args, **kwargs): + from ..sandbox._sandboxbuildboxrun import SandboxBuildBoxRun + + return SandboxBuildBoxRun(*args, **kwargs) + + def setup_buildboxrun_sandbox(self): + from ..sandbox._sandboxbuildboxrun import SandboxBuildBoxRun + + self._check_sandbox(SandboxBuildBoxRun) + self.check_sandbox_config = self._check_sandbox_config_buildboxrun + self.create_sandbox = self._create_buildboxrun_sandbox + return True diff --git a/src/buildstream/sandbox/_sandboxbuildboxrun.py b/src/buildstream/sandbox/_sandboxbuildboxrun.py new file mode 100644 index 000000000..d542d39f8 --- /dev/null +++ b/src/buildstream/sandbox/_sandboxbuildboxrun.py @@ -0,0 +1,148 @@ +# +# Copyright (C) 2018-2019 Bloomberg Finance 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 signal +import subprocess +import sys + +import psutil + +from .. import utils, _signals +from . import SandboxFlags +from .._exceptions import SandboxError +from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2 +from ._sandboxreapi import SandboxREAPI + + +# SandboxBuildBoxRun() +# +# BuildBox-based sandbox implementation. +# +class SandboxBuildBoxRun(SandboxREAPI): + def __init__(self, context, project, directory, **kwargs): + kwargs["allow_real_directory"] = False + super().__init__(context, project, directory, **kwargs) + + @classmethod + def check_available(cls): + try: + utils.get_host_tool("buildbox-run") + except utils.ProgramNotFoundError as Error: + cls._dummy_reasons += ["buildbox-run 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 + 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.") + if config.build_arch != platform.get_host_arch(): + raise SandboxError("Configured and host architecture don't match.") + + return True + + def _execute_action(self, action, flags): + stdout, stderr = self._get_output() + + context = self._get_context() + cascache = context.get_cascache() + casd_process_manager = cascache.get_casd_process_manager() + + with utils._tempnamedfile() as action_file, utils._tempnamedfile() as result_file: + action_file.write(action.SerializeToString()) + action_file.flush() + + buildbox_command = [ + utils.get_host_tool("buildbox-run"), + "--use-localcas", + "--remote={}".format(casd_process_manager._connection_string), + "--action={}".format(action_file.name), + "--action-result={}".format(result_file.name), + ] + + # 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 = subprocess.DEVNULL + + self._run_buildbox( + buildbox_command, stdin, stdout, stderr, interactive=(flags & SandboxFlags.INTERACTIVE), + ) + + return remote_execution_pb2.ActionResult().FromString(result_file.read()) + + def _run_buildbox(self, argv, stdin, stdout, stderr, *, interactive): + 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, stdin=stdin, stdout=stdout, stderr=stderr, 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: + returncode = process.wait() + # If the process exits due to a signal, we + # brutally murder it to avoid zombies + if returncode < 0: + 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 + + if returncode != 0: + raise SandboxError("buildbox-run failed with returncode {}".format(returncode)) diff --git a/src/buildstream/sandbox/_sandboxreapi.py b/src/buildstream/sandbox/_sandboxreapi.py index 834cdd8e0..ec31b97f1 100644 --- a/src/buildstream/sandbox/_sandboxreapi.py +++ b/src/buildstream/sandbox/_sandboxreapi.py @@ -70,7 +70,7 @@ class SandboxREAPI(Sandbox): command_digest = cascache.add_object(buffer=command_proto.SerializeToString()) action = remote_execution_pb2.Action(command_digest=command_digest, input_root_digest=input_root_digest) - action_result = self._execute_action(action) # pylint: disable=assignment-from-no-return + action_result = self._execute_action(action, flags) # pylint: disable=assignment-from-no-return # Get output of build self._process_job_output( @@ -148,7 +148,7 @@ class SandboxREAPI(Sandbox): def _create_batch(self, main_group, flags, *, collect=None): return _SandboxREAPIBatch(self, main_group, flags, collect=collect) - def _execute_action(self, action): + def _execute_action(self, action, flags): raise ImplError("Sandbox of type '{}' does not implement _execute_action()".format(type(self).__name__)) diff --git a/src/buildstream/sandbox/_sandboxremote.py b/src/buildstream/sandbox/_sandboxremote.py index d4ffd64a1..5ec1c974b 100644 --- a/src/buildstream/sandbox/_sandboxremote.py +++ b/src/buildstream/sandbox/_sandboxremote.py @@ -297,7 +297,7 @@ class SandboxRemote(SandboxREAPI): "{} output files are missing on the CAS server".format(len(remote_missing_blobs)) ) - def _execute_action(self, action): + def _execute_action(self, action, flags): context = self._get_context() project = self._get_project() cascache = context.get_cascache() diff --git a/tests/integration/interactive_build.py b/tests/integration/interactive_build.py index 3c20b0f12..84db69ad9 100644 --- a/tests/integration/interactive_build.py +++ b/tests/integration/interactive_build.py @@ -53,7 +53,7 @@ def test_failed_build_quit(element_name, build_session, choice): @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") -@pytest.mark.xfail(HAVE_SANDBOX == "buildbox", reason="Not working with BuildBox") +@pytest.mark.xfail(HAVE_SANDBOX in ["buildbox", "buildbox-run"], reason="Not working with BuildBox") @pytest.mark.datafiles(DATA_DIR) @pytest.mark.parametrize("element_name", ["interactive/failed-build.bst"]) def test_failed_build_log(element_name, build_session): diff --git a/tests/integration/manual.py b/tests/integration/manual.py index c6a84b062..4e80d0dc0 100644 --- a/tests/integration/manual.py +++ b/tests/integration/manual.py @@ -127,6 +127,7 @@ def test_manual_element_noparallel(cli, datafiles): @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") +@pytest.mark.xfail(HAVE_SANDBOX == "buildbox-run", reason="Individual commands are not logged with command batching") def test_manual_element_logging(cli, datafiles): project = str(datafiles) element_path = os.path.join(project, "elements") diff --git a/tests/integration/messages.py b/tests/integration/messages.py index f35b778d6..8bd56b82c 100644 --- a/tests/integration/messages.py +++ b/tests/integration/messages.py @@ -38,6 +38,7 @@ DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "project",) @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") +@pytest.mark.xfail(HAVE_SANDBOX == "buildbox-run", reason="Individual commands are not logged with command batching") def test_disable_message_lines(cli, datafiles): project = str(datafiles) element_path = os.path.join(project, "elements") @@ -66,6 +67,7 @@ def test_disable_message_lines(cli, datafiles): @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") +@pytest.mark.xfail(HAVE_SANDBOX == "buildbox-run", reason="Individual commands are not logged with command batching") def test_disable_error_lines(cli, datafiles): project = str(datafiles) element_path = os.path.join(project, "elements") diff --git a/tests/integration/project/elements/sandbox-bwrap/test-dev-shm.bst b/tests/integration/project/elements/sandbox/test-dev-shm.bst index 03dc74a35..03dc74a35 100644 --- a/tests/integration/project/elements/sandbox-bwrap/test-dev-shm.bst +++ b/tests/integration/project/elements/sandbox/test-dev-shm.bst diff --git a/tests/integration/sandbox-bwrap.py b/tests/integration/sandbox-bwrap.py index 2908752de..a6312914b 100644 --- a/tests/integration/sandbox-bwrap.py +++ b/tests/integration/sandbox-bwrap.py @@ -55,13 +55,3 @@ def test_sandbox_bwrap_return_subprocess(cli, datafiles): result = cli.run(project=project, args=["build", element_name]) result.assert_task_error(error_domain=ErrorDomain.SANDBOX, error_reason="command-failed") assert "sandbox-bwrap/command-exit-42.bst|Command failed with exitcode 42" in result.stderr - - -@pytest.mark.skipif(HAVE_SANDBOX != "bwrap", reason="Only available with bubblewrap") -@pytest.mark.datafiles(DATA_DIR) -def test_sandbox_bwrap_dev_shm(cli, datafiles): - project = str(datafiles) - element_name = "sandbox-bwrap/test-dev-shm.bst" - - result = cli.run(project=project, args=["build", element_name]) - assert result.exit_code == 0 diff --git a/tests/integration/sandbox.py b/tests/integration/sandbox.py new file mode 100644 index 000000000..d772f5437 --- /dev/null +++ b/tests/integration/sandbox.py @@ -0,0 +1,43 @@ +# +# Copyright (C) 2019 Bloomberg Finance 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/>. +# + +# Pylint doesn't play well with fixtures and dependency injection from pytest +# pylint: disable=redefined-outer-name + +import os +import pytest + +from buildstream.testing import cli_integration as cli # pylint: disable=unused-import +from buildstream.testing._utils.site import HAVE_SANDBOX + + +pytestmark = pytest.mark.integration + + +DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "project") + + +@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") +@pytest.mark.xfail(HAVE_SANDBOX == "buildbox", reason="The buildbox sandbox doesn't support shm") +@pytest.mark.xfail(HAVE_SANDBOX == "chroot", reason="The chroot sandbox doesn't support shm") +@pytest.mark.datafiles(DATA_DIR) +def test_sandbox_shm(cli, datafiles): + project = str(datafiles) + element_name = "sandbox/test-dev-shm.bst" + + result = cli.run(project=project, args=["build", element_name]) + assert result.exit_code == 0 diff --git a/tests/integration/script.py b/tests/integration/script.py index b27f1507e..b5e21452c 100644 --- a/tests/integration/script.py +++ b/tests/integration/script.py @@ -92,6 +92,9 @@ def test_script_root(cli, datafiles): @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") @pytest.mark.xfail(HAVE_SANDBOX == "buildbox", reason="Not working with BuildBox") +@pytest.mark.xfail( + HAVE_SANDBOX == "buildbox-run", reason="Read-only root directory not supported by buildbox-run", +) def test_script_no_root(cli, datafiles): project = str(datafiles) element_path = os.path.join(project, "elements") @@ -113,7 +116,7 @@ def test_script_no_root(cli, datafiles): res = cli.run(project=project, args=["build", element_name]) assert res.exit_code != 0 - assert "/test: Read-only file system" in res.stderr + assert "/test: Read-only file system" in res.stderr or "/test: Permission denied" in res.stderr @pytest.mark.datafiles(DATA_DIR) diff --git a/tests/integration/shell.py b/tests/integration/shell.py index e331a5fe0..394edeb1d 100644 --- a/tests/integration/shell.py +++ b/tests/integration/shell.py @@ -158,7 +158,7 @@ def test_no_shell(cli, datafiles): @pytest.mark.parametrize("path", [("/etc/pony.conf"), ("/usr/share/pony/pony.txt")]) @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") -@pytest.mark.xfail(HAVE_SANDBOX == "buildbox", reason="Not working with BuildBox") +@pytest.mark.xfail(HAVE_SANDBOX in ["buildbox", "buildbox-run"], reason="Not working with BuildBox") def test_host_files(cli, datafiles, path): project = str(datafiles) ponyfile = os.path.join(project, "files", "shell-mount", "pony.txt") @@ -173,7 +173,7 @@ def test_host_files(cli, datafiles, path): @pytest.mark.parametrize("path", [("/etc"), ("/usr/share/pony")]) @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") -@pytest.mark.xfail(HAVE_SANDBOX == "buildbox", reason="Not working with BuildBox") +@pytest.mark.xfail(HAVE_SANDBOX in ["buildbox", "buildbox-run"], reason="Not working with BuildBox") def test_host_files_expand_environ(cli, datafiles, path): project = str(datafiles) hostpath = os.path.join(project, "files", "shell-mount") @@ -246,7 +246,7 @@ def test_host_files_missing(cli, datafiles, optional): @pytest.mark.parametrize("path", [("/etc/pony.conf"), ("/usr/share/pony/pony.txt")]) @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") -@pytest.mark.xfail(HAVE_SANDBOX == "buildbox", reason="Not working with BuildBox") +@pytest.mark.xfail(HAVE_SANDBOX in ["buildbox", "buildbox-run"], reason="Not working with BuildBox") def test_cli_mount(cli, datafiles, path): project = str(datafiles) ponyfile = os.path.join(project, "files", "shell-mount", "pony.txt") @@ -294,7 +294,7 @@ def test_workspace_visible(cli, datafiles): # Test that '--sysroot' works @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") -@pytest.mark.xfail(HAVE_SANDBOX == "buildbox", reason="Not working with BuildBox") +@pytest.mark.xfail(HAVE_SANDBOX in ["buildbox", "buildbox-run"], reason="Not working with BuildBox") def test_sysroot(cli, tmpdir, datafiles): project = str(datafiles) base_element = "base/base-alpine.bst" diff --git a/tests/integration/workspace.py b/tests/integration/workspace.py index 8dfe92279..8b4baddef 100644 --- a/tests/integration/workspace.py +++ b/tests/integration/workspace.py @@ -344,6 +344,7 @@ def test_workspace_missing_last_successful(cli, datafiles): # Check that we can still read failed workspace logs @pytest.mark.datafiles(DATA_DIR) @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") +@pytest.mark.xfail(HAVE_SANDBOX == "buildbox-run", reason="Individual commands are not logged with command batching") def test_workspace_failed_logs(cli, datafiles): project = str(datafiles) workspace = os.path.join(cli.directory, "failing_amhello") |