[Notes] [Git][BuildStream/buildstream][juerg/buildbox] WIP: SandboxBuildBox



Title: GitLab

Jürg Billeter pushed to branch juerg/buildbox at BuildStream / buildstream

Commits:

5 changed files:

Changes:

  • buildstream/_platform/linux.py
    ... ... @@ -23,7 +23,7 @@ from .. import _site
    23 23
     from .. import utils
    
    24 24
     from .._artifactcache.cascache import CASCache
    
    25 25
     from .._message import Message, MessageType
    
    26
    -from ..sandbox import SandboxBwrap
    
    26
    +from ..sandbox import SandboxBuildBox
    
    27 27
     
    
    28 28
     from . import Platform
    
    29 29
     
    
    ... ... @@ -34,50 +34,11 @@ class Linux(Platform):
    34 34
     
    
    35 35
             super().__init__(context)
    
    36 36
     
    
    37
    -        self._die_with_parent_available = _site.check_bwrap_version(0, 1, 8)
    
    38
    -        self._user_ns_available = self._check_user_ns_available(context)
    
    39
    -        self._artifact_cache = CASCache(context, enable_push=self._user_ns_available)
    
    37
    +        self._artifact_cache = CASCache(context, enable_push=True)
    
    40 38
     
    
    41 39
         @property
    
    42 40
         def artifactcache(self):
    
    43 41
             return self._artifact_cache
    
    44 42
     
    
    45 43
         def create_sandbox(self, *args, **kwargs):
    
    46
    -        # Inform the bubblewrap sandbox as to whether it can use user namespaces or not
    
    47
    -        kwargs['user_ns_available'] = self._user_ns_available
    
    48
    -        kwargs['die_with_parent_available'] = self._die_with_parent_available
    
    49
    -        return SandboxBwrap(*args, **kwargs)
    
    50
    -
    
    51
    -    ################################################
    
    52
    -    #              Private Methods                 #
    
    53
    -    ################################################
    
    54
    -    def _check_user_ns_available(self, context):
    
    55
    -
    
    56
    -        # Here, lets check if bwrap is able to create user namespaces,
    
    57
    -        # issue a warning if it's not available, and save the state
    
    58
    -        # locally so that we can inform the sandbox to not try it
    
    59
    -        # later on.
    
    60
    -        bwrap = utils.get_host_tool('bwrap')
    
    61
    -        whoami = utils.get_host_tool('whoami')
    
    62
    -        try:
    
    63
    -            output = subprocess.check_output([
    
    64
    -                bwrap,
    
    65
    -                '--ro-bind', '/', '/',
    
    66
    -                '--unshare-user',
    
    67
    -                '--uid', '0', '--gid', '0',
    
    68
    -                whoami,
    
    69
    -            ])
    
    70
    -            output = output.decode('UTF-8').strip()
    
    71
    -        except subprocess.CalledProcessError:
    
    72
    -            output = ''
    
    73
    -
    
    74
    -        if output == 'root':
    
    75
    -            return True
    
    76
    -
    
    77
    -        else:
    
    78
    -            context.message(
    
    79
    -                Message(None, MessageType.WARN,
    
    80
    -                        "Unable to create user namespaces with bubblewrap, resorting to fallback",
    
    81
    -                        detail="Some builds may not function due to lack of uid / gid 0, " +
    
    82
    -                        "artifacts created will not be trusted for push purposes."))
    
    83
    -            return False
    44
    +        return SandboxBuildBox(*args, **kwargs)

  • buildstream/element.py
    ... ... @@ -1496,8 +1496,6 @@ class Element(Plugin):
    1496 1496
                 with _signals.terminator(cleanup_rootdir), \
    
    1497 1497
                     self.__sandbox(rootdir, output_file, output_file, self.__sandbox_config) as sandbox:  # nopep8
    
    1498 1498
     
    
    1499
    -                sandbox_vroot = sandbox.get_virtual_directory()
    
    1500
    -
    
    1501 1499
                     # By default, the dynamic public data is the same as the static public data.
    
    1502 1500
                     # The plugin's assemble() method may modify this, though.
    
    1503 1501
                     self.__dynamic_public = _yaml.node_copy(self.__public)
    
    ... ... @@ -1543,6 +1541,8 @@ class Element(Plugin):
    1543 1541
                         self.__set_build_result(success=False, description=str(e), detail=e.detail)
    
    1544 1542
                         raise
    
    1545 1543
                     finally:
    
    1544
    +                    sandbox_vroot = sandbox.get_virtual_directory()
    
    1545
    +
    
    1546 1546
                         if collect is not None:
    
    1547 1547
                             try:
    
    1548 1548
                                 collectvdir = sandbox_vroot.descend(collect.lstrip(os.sep).split(os.sep))
    

  • buildstream/sandbox/__init__.py
    ... ... @@ -20,3 +20,4 @@
    20 20
     from .sandbox import Sandbox, SandboxFlags
    
    21 21
     from ._sandboxchroot import SandboxChroot
    
    22 22
     from ._sandboxbwrap import SandboxBwrap
    
    23
    +from ._sandboxbuildbox import SandboxBuildBox

  • buildstream/sandbox/_sandboxbuildbox.py
    1
    +#
    
    2
    +#  Copyright (C) 2016 Codethink Limited
    
    3
    +#
    
    4
    +#  This program is free software; you can redistribute it and/or
    
    5
    +#  modify it under the terms of the GNU Lesser General Public
    
    6
    +#  License as published by the Free Software Foundation; either
    
    7
    +#  version 2 of the License, or (at your option) any later version.
    
    8
    +#
    
    9
    +#  This library is distributed in the hope that it will be useful,
    
    10
    +#  but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    11
    +#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
    
    12
    +#  Lesser General Public License for more details.
    
    13
    +#
    
    14
    +#  You should have received a copy of the GNU Lesser General Public
    
    15
    +#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
    
    16
    +#
    
    17
    +#  Authors:
    
    18
    +#        Andrew Leeming <andrew leeming codethink co uk>
    
    19
    +#        Tristan Van Berkom <tristan vanberkom codethink co uk>
    
    20
    +import os
    
    21
    +import sys
    
    22
    +import time
    
    23
    +import errno
    
    24
    +import signal
    
    25
    +import subprocess
    
    26
    +import shutil
    
    27
    +from contextlib import ExitStack
    
    28
    +
    
    29
    +import psutil
    
    30
    +
    
    31
    +from .._exceptions import SandboxError
    
    32
    +from .. import utils, _signals
    
    33
    +from ._mount import MountMap
    
    34
    +from . import Sandbox, SandboxFlags
    
    35
    +from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2
    
    36
    +from ..storage._casbaseddirectory import CasBasedDirectory
    
    37
    +
    
    38
    +
    
    39
    +# SandboxBuidBox()
    
    40
    +#
    
    41
    +# BuildBox-based sandbox implementation.
    
    42
    +#
    
    43
    +class SandboxBuildBox(Sandbox):
    
    44
    +
    
    45
    +    def __init__(self, *args, **kwargs):
    
    46
    +        super().__init__(*args, **kwargs)
    
    47
    +
    
    48
    +    def run(self, command, flags, *, cwd=None, env=None):
    
    49
    +        stdout, stderr = self._get_output()
    
    50
    +
    
    51
    +        root_directory = self.get_virtual_directory()
    
    52
    +        scratch_directory = self._get_scratch_directory()
    
    53
    +
    
    54
    +        # Fallback to the sandbox default settings for
    
    55
    +        # the cwd and env.
    
    56
    +        #
    
    57
    +        if cwd is None:
    
    58
    +            cwd = self._get_work_directory()
    
    59
    +
    
    60
    +        if env is None:
    
    61
    +            env = self._get_environment()
    
    62
    +
    
    63
    +        # if not self._has_command(command[0], env):
    
    64
    +        #     raise SandboxError("Staged artifacts do not provide command "
    
    65
    +        #                        "'{}'".format(command[0]),
    
    66
    +        #                        reason='missing-command')
    
    67
    +
    
    68
    +        # We want command args as a list of strings
    
    69
    +        if isinstance(command, str):
    
    70
    +            command = [command]
    
    71
    +
    
    72
    +        if cwd is None:
    
    73
    +            cwd = '/'
    
    74
    +
    
    75
    +        # Grab the full path of the buildbox binary
    
    76
    +        buildbox_command = [utils.get_host_tool('buildbox')]
    
    77
    +
    
    78
    +        marked_directories = self._get_marked_directories()
    
    79
    +        for mark in marked_directories:
    
    80
    +            path = mark['directory']
    
    81
    +            assert path.startswith('/') and len(path) > 1
    
    82
    +            root_directory.descend(path[1:].split('/'), create=True)
    
    83
    +
    
    84
    +        root_directory._recalculate_recursing_down()
    
    85
    +        with open(os.path.join(scratch_directory, 'in'), 'wb') as input_digest_file:
    
    86
    +            input_digest_file.write(root_directory.ref.SerializeToString())
    
    87
    +
    
    88
    +        buildbox_command += ["--local=" + root_directory.cas_cache.casdir]
    
    89
    +        buildbox_command += ["--input-digest=in"]
    
    90
    +        buildbox_command += ["--output-digest=out"]
    
    91
    +
    
    92
    +        if not flags & SandboxFlags.NETWORK_ENABLED:
    
    93
    +            # TODO
    
    94
    +            pass
    
    95
    +
    
    96
    +        if cwd is not None:
    
    97
    +            buildbox_command += ['--chdir=' + cwd]
    
    98
    +
    
    99
    +        # In interactive mode, we want a complete devpts inside
    
    100
    +        # the container, so there is a /dev/console and such. In
    
    101
    +        # the regular non-interactive sandbox, we want to hand pick
    
    102
    +        # a minimal set of devices to expose to the sandbox.
    
    103
    +        #
    
    104
    +        if flags & SandboxFlags.INTERACTIVE:
    
    105
    +            # TODO
    
    106
    +            pass
    
    107
    +
    
    108
    +        if flags & SandboxFlags.ROOT_READ_ONLY:
    
    109
    +            # TODO
    
    110
    +            pass
    
    111
    +
    
    112
    +        # Set UID and GUI
    
    113
    +        if not flags & SandboxFlags.INHERIT_UID:
    
    114
    +            # TODO
    
    115
    +            pass
    
    116
    +            # uid = self._get_config().build_uid
    
    117
    +            # gid = self._get_config().build_gid
    
    118
    +            # buildbox_command += ['--uid', str(uid), '--gid', str(gid)]
    
    119
    +
    
    120
    +        os.makedirs(os.path.join(scratch_directory, 'mnt'), exist_ok=True)
    
    121
    +        buildbox_command += ['mnt']
    
    122
    +
    
    123
    +        # Add the command
    
    124
    +        buildbox_command += command
    
    125
    +
    
    126
    +        # Use the MountMap context manager to ensure that any redirected
    
    127
    +        # mounts through fuse layers are in context and ready for buildbox
    
    128
    +        # to mount them from.
    
    129
    +        #
    
    130
    +        with ExitStack() as stack:
    
    131
    +            # Ensure the cwd exists
    
    132
    +            if cwd is not None and len(cwd) > 1:
    
    133
    +                assert cwd.startswith('/')
    
    134
    +                root_directory.descend(cwd[1:].split('/'), create=True)
    
    135
    +
    
    136
    +            # If we're interactive, we want to inherit our stdin,
    
    137
    +            # otherwise redirect to /dev/null, ensuring process
    
    138
    +            # disconnected from terminal.
    
    139
    +            if flags & SandboxFlags.INTERACTIVE:
    
    140
    +                stdin = sys.stdin
    
    141
    +            else:
    
    142
    +                stdin = stack.enter_context(open(os.devnull, "r"))
    
    143
    +
    
    144
    +            # Run buildbox !
    
    145
    +            exit_code = self.run_buildbox(buildbox_command, stdin, stdout, stderr, env,
    
    146
    +                                          interactive=(flags & SandboxFlags.INTERACTIVE),
    
    147
    +                                          cwd=scratch_directory)
    
    148
    +
    
    149
    +            if exit_code == 0:
    
    150
    +                with open(os.path.join(scratch_directory, 'out'), 'rb') as output_digest_file:
    
    151
    +                    output_digest = remote_execution_pb2.Digest()
    
    152
    +                    output_digest.ParseFromString(output_digest_file.read())
    
    153
    +                    self._vdir = CasBasedDirectory(self._get_context(), ref=output_digest)
    
    154
    +
    
    155
    +        return exit_code
    
    156
    +
    
    157
    +    def run_buildbox(self, argv, stdin, stdout, stderr, env, *, interactive, cwd):
    
    158
    +        def kill_proc():
    
    159
    +            if process:
    
    160
    +                # First attempt to gracefully terminate
    
    161
    +                proc = psutil.Process(process.pid)
    
    162
    +                proc.terminate()
    
    163
    +
    
    164
    +                try:
    
    165
    +                    proc.wait(20)
    
    166
    +                except psutil.TimeoutExpired:
    
    167
    +                    utils._kill_process_tree(process.pid)
    
    168
    +
    
    169
    +        def suspend_proc():
    
    170
    +            group_id = os.getpgid(process.pid)
    
    171
    +            os.killpg(group_id, signal.SIGSTOP)
    
    172
    +
    
    173
    +        def resume_proc():
    
    174
    +            group_id = os.getpgid(process.pid)
    
    175
    +            os.killpg(group_id, signal.SIGCONT)
    
    176
    +
    
    177
    +        with _signals.suspendable(suspend_proc, resume_proc), _signals.terminator(kill_proc):
    
    178
    +            process = subprocess.Popen(
    
    179
    +                argv,
    
    180
    +                close_fds=True,
    
    181
    +                env=env,
    
    182
    +                stdin=stdin,
    
    183
    +                stdout=stdout,
    
    184
    +                stderr=stderr,
    
    185
    +                cwd=cwd,
    
    186
    +                start_new_session=interactive
    
    187
    +            )
    
    188
    +
    
    189
    +            # Wait for the child process to finish, ensuring that
    
    190
    +            # a SIGINT has exactly the effect the user probably
    
    191
    +            # expects (i.e. let the child process handle it).
    
    192
    +            try:
    
    193
    +                while True:
    
    194
    +                    try:
    
    195
    +                        _, status = os.waitpid(process.pid, 0)
    
    196
    +                        # If the process exits due to a signal, we
    
    197
    +                        # brutally murder it to avoid zombies
    
    198
    +                        if not os.WIFEXITED(status):
    
    199
    +                            utils._kill_process_tree(process.pid)
    
    200
    +
    
    201
    +                    # Unlike in the bwrap case, here only the main
    
    202
    +                    # process seems to receive the SIGINT. We pass
    
    203
    +                    # on the signal to the child and then continue
    
    204
    +                    # to wait.
    
    205
    +                    except KeyboardInterrupt:
    
    206
    +                        process.send_signal(signal.SIGINT)
    
    207
    +                        continue
    
    208
    +
    
    209
    +                    break
    
    210
    +            # If we can't find the process, it has already died of
    
    211
    +            # its own accord, and therefore we don't need to check
    
    212
    +            # or kill anything.
    
    213
    +            except psutil.NoSuchProcess:
    
    214
    +                pass
    
    215
    +
    
    216
    +            # Return the exit code - see the documentation for
    
    217
    +            # os.WEXITSTATUS to see why this is required.
    
    218
    +            if os.WIFEXITED(status):
    
    219
    +                exit_code = os.WEXITSTATUS(status)
    
    220
    +            else:
    
    221
    +                exit_code = -1
    
    222
    +
    
    223
    +        return exit_code

  • buildstream/sandbox/sandbox.py
    ... ... @@ -138,10 +138,10 @@ class Sandbox():
    138 138
             if not self._vdir:
    
    139 139
                 # BST_CAS_DIRECTORIES is a deliberately hidden environment variable which
    
    140 140
                 # can be used to switch on CAS-based directories for testing.
    
    141
    -            if 'BST_CAS_DIRECTORIES' in os.environ:
    
    142
    -                self._vdir = CasBasedDirectory(self.__context, ref=None)
    
    143
    -            else:
    
    144
    -                self._vdir = FileBasedDirectory(self._root)
    
    141
    +            # if 'BST_CAS_DIRECTORIES' in os.environ:
    
    142
    +            self._vdir = CasBasedDirectory(self.__context, ref=None)
    
    143
    +            # else:
    
    144
    +            #     self._vdir = FileBasedDirectory(self._root)
    
    145 145
             return self._vdir
    
    146 146
     
    
    147 147
         def set_environment(self, environment):
    



  • [Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]