summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJürg Billeter <j@bitron.ch>2019-12-10 12:34:19 +0000
committerJürg Billeter <j@bitron.ch>2019-12-10 12:34:19 +0000
commit4ad66cd51f36319feef0ed0824e303f3f64c7422 (patch)
tree0a71cc445f0fdd3f0cfa14466a0ff7d11adc3743
parent393fd2d458181cb9cbf30133dc5bbac8659db84b (diff)
parent9fe6c9e63a50edd80c792608b6c6bc63ff2e2e1f (diff)
downloadbuildstream-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.py1
-rw-r--r--src/buildstream/_platform/platform.py27
-rw-r--r--src/buildstream/sandbox/_sandboxbuildboxrun.py148
-rw-r--r--src/buildstream/sandbox/_sandboxreapi.py4
-rw-r--r--src/buildstream/sandbox/_sandboxremote.py2
-rw-r--r--tests/integration/interactive_build.py2
-rw-r--r--tests/integration/manual.py1
-rw-r--r--tests/integration/messages.py2
-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.py10
-rw-r--r--tests/integration/sandbox.py43
-rw-r--r--tests/integration/script.py5
-rw-r--r--tests/integration/shell.py8
-rw-r--r--tests/integration/workspace.py1
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")