Ejemplo n.º 1
0
def test_disabled_cache_failed_actions(cas, context):
    disabled_failed_actions = ActionCache(cas, 50, True, False)
    with mock.patch.object(service, 'remote_execution_pb2_grpc'):
        ac_service = ActionCacheService(server)
        ac_service.add_instance("", disabled_failed_actions)

    failure_action_digest = remote_execution_pb2.Digest(hash='failure', size_bytes=4)

    # Add a non-zero exit code ActionResult to the cache
    action_result = remote_execution_pb2.ActionResult(stdout_raw=b'Failed', exit_code=1)
    request = remote_execution_pb2.UpdateActionResultRequest(action_digest=failure_action_digest,
                                                             action_result=action_result)
    ac_service.UpdateActionResult(request, context)

    # Check that before adding the ActionResult, attempting to fetch it fails
    request = remote_execution_pb2.GetActionResultRequest(instance_name="",
                                                          action_digest=failure_action_digest)
    ac_service.GetActionResult(request, context)
    context.set_code.assert_called_once_with(grpc.StatusCode.NOT_FOUND)

    success_action_digest = remote_execution_pb2.Digest(hash='success', size_bytes=4)

    # Now add a zero exit code Action result to the cache, and check that fetching
    # it is successful
    success_action_result = remote_execution_pb2.ActionResult(stdout_raw=b'Successful')
    request = remote_execution_pb2.UpdateActionResultRequest(action_digest=success_action_digest,
                                                             action_result=success_action_result)
    ac_service.UpdateActionResult(request, context)
    request = remote_execution_pb2.GetActionResultRequest(instance_name="",
                                                          action_digest=success_action_digest)
    fetched_result = ac_service.GetActionResult(request, context)
    assert fetched_result.stdout_raw == success_action_result.stdout_raw
Ejemplo n.º 2
0
def test_checks_cas(acType, cas):
    if acType == 'memory':
        cache = ActionCache(cas, 50)
    elif acType == 's3':
        auth_args = {
            "aws_access_key_id": "access_key",
            "aws_secret_access_key": "secret_key"
        }
        boto3.resource('s3', **auth_args).create_bucket(Bucket='cachebucket')
        cache = S3ActionCache(cas,
                              allow_updates=True,
                              cache_failed_actions=True,
                              bucket='cachebucket',
                              access_key="access_key",
                              secret_key="secret_key")

    action_digest1 = remote_execution_pb2.Digest(hash='alpha', size_bytes=4)
    action_digest2 = remote_execution_pb2.Digest(hash='bravo', size_bytes=4)
    action_digest3 = remote_execution_pb2.Digest(hash='charlie', size_bytes=4)

    # Create a tree that actions digests in CAS
    sample_digest = cas.put_message(
        remote_execution_pb2.Command(arguments=["sample"]))
    tree = remote_execution_pb2.Tree()
    tree.root.files.add().digest.CopyFrom(sample_digest)
    tree.children.add().files.add().digest.CopyFrom(sample_digest)
    tree_digest = cas.put_message(tree)

    # Add an ActionResult that actions real digests to the cache
    action_result1 = remote_execution_pb2.ActionResult()
    action_result1.output_directories.add().tree_digest.CopyFrom(tree_digest)
    action_result1.output_files.add().digest.CopyFrom(sample_digest)
    action_result1.stdout_digest.CopyFrom(sample_digest)
    action_result1.stderr_digest.CopyFrom(sample_digest)
    cache.update_action_result(action_digest1, action_result1)

    # Add ActionResults that action fake digests to the cache
    action_result2 = remote_execution_pb2.ActionResult()
    action_result2.output_directories.add().tree_digest.hash = "nonexistent"
    action_result2.output_directories[0].tree_digest.size_bytes = 8
    cache.update_action_result(action_digest2, action_result2)

    action_result3 = remote_execution_pb2.ActionResult()
    action_result3.stdout_digest.hash = "nonexistent"
    action_result3.stdout_digest.size_bytes = 8
    cache.update_action_result(action_digest3, action_result3)

    # Verify we can get the first ActionResult but not the others
    fetched_result1 = cache.get_action_result(action_digest1)
    assert fetched_result1.output_directories[
        0].tree_digest.hash == tree_digest.hash
    with pytest.raises(NotFoundError):
        cache.get_action_result(action_digest2)
        cache.get_action_result(action_digest3)
Ejemplo n.º 3
0
def test_simple_action_result(cache_instances, context):
    with mock.patch.object(service, 'remote_execution_pb2_grpc'):
        ac_service = ActionCacheService(server)

    for k, v in cache_instances.items():
        ac_service.add_instance(k, v)

    action_digest = remote_execution_pb2.Digest(hash='sample', size_bytes=4)

    # Check that before adding the ActionResult, attempting to fetch it fails
    request = remote_execution_pb2.GetActionResultRequest(instance_name="",
                                                          action_digest=action_digest)
    ac_service.GetActionResult(request, context)
    context.set_code.assert_called_once_with(grpc.StatusCode.NOT_FOUND)

    # Add an ActionResult to the cache
    action_result = remote_execution_pb2.ActionResult(stdout_raw=b'example output')
    request = remote_execution_pb2.UpdateActionResultRequest(action_digest=action_digest,
                                                             action_result=action_result)
    ac_service.UpdateActionResult(request, context)

    # Check that fetching it now works
    request = remote_execution_pb2.GetActionResultRequest(action_digest=action_digest)
    fetched_result = ac_service.GetActionResult(request, context)
    assert fetched_result.stdout_raw == action_result.stdout_raw
Ejemplo n.º 4
0
def work_dummy(lease, context, event):
    """ Just returns lease after some random time
    """
    action_result = remote_execution_pb2.ActionResult()

    lease.result.Clear()

    action_result.execution_metadata.worker = get_hostname()

    # Simulation input-downloading phase:
    action_result.execution_metadata.input_fetch_start_timestamp.GetCurrentTime(
    )
    time.sleep(random.random())
    action_result.execution_metadata.input_fetch_completed_timestamp.GetCurrentTime(
    )

    # Simulation execution phase:
    action_result.execution_metadata.execution_start_timestamp.GetCurrentTime()
    time.sleep(random.random())
    action_result.execution_metadata.execution_completed_timestamp.GetCurrentTime(
    )

    # Simulation output-uploading phase:
    action_result.execution_metadata.output_upload_start_timestamp.GetCurrentTime(
    )
    time.sleep(random.random())
    action_result.execution_metadata.output_upload_completed_timestamp.GetCurrentTime(
    )

    lease.result.Pack(action_result)

    return lease
Ejemplo n.º 5
0
def test_null_cas_action_cache(cas):
    cache = ActionCache(cas, 0)

    action_digest1 = remote_execution_pb2.Digest(hash='alpha', size_bytes=4)
    dummy_result = remote_execution_pb2.ActionResult()

    cache.update_action_result(action_digest1, dummy_result)
    with pytest.raises(NotFoundError):
        cache.get_action_result(action_digest1)
Ejemplo n.º 6
0
    def __test_update_disallowed():
        with serve_cache(['testing'], allow_updates=False) as server:
            channel = grpc.insecure_channel(server.remote)
            cache = RemoteActionCache(channel, 'testing')

            action_digest = remote_execution_pb2.Digest(hash='alpha',
                                                        size_bytes=4)
            result = remote_execution_pb2.ActionResult()
            with pytest.raises(NotImplementedError,
                               match='Updating cache not allowed'):
                cache.update_action_result(action_digest, result)
Ejemplo n.º 7
0
    def __test_update():
        with serve_cache(['testing']) as server:
            channel = grpc.insecure_channel(server.remote)
            cache = RemoteActionCache(channel, 'testing')

            action_digest = remote_execution_pb2.Digest(hash='alpha',
                                                        size_bytes=4)
            result = remote_execution_pb2.ActionResult()
            cache.update_action_result(action_digest, result)

            fetched = cache.get_action_result(action_digest)
            assert result == fetched
Ejemplo n.º 8
0
def update(context, action_digest_string, action_result_digest_string):
    """Entry-point of the ``bgd action-cache update`` CLI command.

    Note:
        Digest strings are expected to be like: ``{hash}/{size_bytes}``.
    """
    action_digest = parse_digest(action_digest_string)
    if action_digest is None:
        click.echo(
            "Error: Invalid digest string '{}'.".format(action_digest_string),
            err=True)
        sys.exit(-1)

    action_result_digest = parse_digest(action_result_digest_string)
    if action_result_digest is None:
        click.echo("Error: Invalid digest string '{}'.".format(
            action_result_digest_string),
                   err=True)
        sys.exit(-1)

    # We have to download the ActionResult message from CAS first...
    with download(context.channel,
                  instance=context.instance_name) as downloader:
        try:
            action_result = downloader.get_message(
                action_result_digest, remote_execution_pb2.ActionResult())
        except ConnectionError as e:
            click.echo('Error: Fetching ActionResult from CAS: {}'.format(e),
                       err=True)
            sys.exit(-1)

        # And only then we can update the action cache for the given digest:
        with query(context.channel,
                   instance=context.instance_name) as action_cache:
            try:
                action_result = action_cache.update(action_digest,
                                                    action_result)
            except ConnectionError as e:
                click.echo('Error: Uploading to ActionCache: {}'.format(e),
                           err=True)
                sys.exit(-1)

            if action_result is None:
                click.echo(
                    "Error: Failed updating cache result for action=[{}/{}].".
                    format(action_digest.hash, action_digest.size_bytes),
                    err=True)
                sys.exit(-1)
Ejemplo n.º 9
0
    def GetActionResult(self, request, context):
        self.__logger.debug("GetActionResult request from [%s]",
                            context.peer())

        try:
            instance = self._get_instance(request.instance_name)
            return instance.get_action_result(request.action_digest)

        except InvalidArgumentError as e:
            self.__logger.error(e)
            context.set_details(str(e))
            context.set_code(grpc.StatusCode.INVALID_ARGUMENT)

        except NotFoundError as e:
            self.__logger.debug(e)
            context.set_code(grpc.StatusCode.NOT_FOUND)

        return remote_execution_pb2.ActionResult()
Ejemplo n.º 10
0
    def UpdateActionResult(self, request, context):
        self.__logger.debug("UpdateActionResult request from [%s]",
                            context.peer())

        try:
            instance = self._get_instance(request.instance_name)
            instance.update_action_result(request.action_digest,
                                          request.action_result)
            return request.action_result

        except InvalidArgumentError as e:
            self.__logger.error(e)
            context.set_details(str(e))
            context.set_code(grpc.StatusCode.INVALID_ARGUMENT)

        except NotImplementedError as e:
            self.__logger.error(e)
            context.set_code(grpc.StatusCode.UNIMPLEMENTED)

        return remote_execution_pb2.ActionResult()
Ejemplo n.º 11
0
def test_expiry(cas):
    cache = ActionCache(cas, 2)

    action_digest1 = remote_execution_pb2.Digest(hash='alpha', size_bytes=4)
    action_digest2 = remote_execution_pb2.Digest(hash='bravo', size_bytes=4)
    action_digest3 = remote_execution_pb2.Digest(hash='charlie', size_bytes=4)
    dummy_result = remote_execution_pb2.ActionResult()

    cache.update_action_result(action_digest1, dummy_result)
    cache.update_action_result(action_digest2, dummy_result)

    # Get digest 1 (making 2 the least recently used)
    assert cache.get_action_result(action_digest1) is not None
    # Add digest 3 (so 2 gets removed from the cache)
    cache.update_action_result(action_digest3, dummy_result)

    assert cache.get_action_result(action_digest1) is not None
    with pytest.raises(NotFoundError):
        cache.get_action_result(action_digest2)

    assert cache.get_action_result(action_digest3) is not None
Ejemplo n.º 12
0
def work_host_tools(lease, context, event):
    """Executes a lease for a build action, using host tools.
    """
    instance_name = context.parent

    logger = logging.getLogger(__name__)

    action_digest = remote_execution_pb2.Digest()
    action_result = remote_execution_pb2.ActionResult()

    lease.payload.Unpack(action_digest)
    lease.result.Clear()

    action_result.execution_metadata.worker = get_hostname()

    with tempfile.TemporaryDirectory() as temp_directory:
        with download(context.cas_channel, instance=instance_name) as downloader:
            action = downloader.get_message(action_digest,
                                            remote_execution_pb2.Action())

            assert action.command_digest.hash

            command = downloader.get_message(action.command_digest,
                                             remote_execution_pb2.Command())

            action_result.execution_metadata.input_fetch_start_timestamp.GetCurrentTime()

            downloader.download_directory(action.input_root_digest, temp_directory)

        logger.debug("Command digest: [{}/{}]"
                     .format(action.command_digest.hash, action.command_digest.size_bytes))
        logger.debug("Input root digest: [{}/{}]"
                     .format(action.input_root_digest.hash, action.input_root_digest.size_bytes))

        action_result.execution_metadata.input_fetch_completed_timestamp.GetCurrentTime()

        environment = os.environ.copy()
        for variable in command.environment_variables:
            if variable.name not in ['PWD']:
                environment[variable.name] = variable.value

        command_line = []
        for argument in command.arguments:
            command_line.append(argument.strip())

        working_directory = None
        if command.working_directory:
            working_directory = os.path.join(temp_directory,
                                             command.working_directory)
            os.makedirs(working_directory, exist_ok=True)
        else:
            working_directory = temp_directory

        # Ensure that output files and directories structure exists:
        for output_path in itertools.chain(command.output_files, command.output_directories):
            parent_path = os.path.join(working_directory,
                                       os.path.dirname(output_path))
            os.makedirs(parent_path, exist_ok=True)

        logger.info("Starting execution: [{}...]".format(command.arguments[0]))

        action_result.execution_metadata.execution_start_timestamp.GetCurrentTime()

        process = subprocess.Popen(command_line,
                                   cwd=working_directory,
                                   env=environment,
                                   stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

        stdout, stderr = process.communicate()
        returncode = process.returncode

        action_result.execution_metadata.execution_completed_timestamp.GetCurrentTime()

        action_result.exit_code = returncode

        logger.info("Execution finished with code: [{}]".format(returncode))

        action_result.execution_metadata.output_upload_start_timestamp.GetCurrentTime()

        with upload(context.cas_channel, instance=instance_name) as uploader:

            for output_path in itertools.chain(command.output_files, command.output_directories):
                file_path = os.path.join(working_directory, output_path)
                # Missing outputs should simply be omitted in ActionResult:
                if not os.path.exists(file_path):
                    continue

                if os.path.isdir(file_path):
                    tree_digest = uploader.upload_tree(file_path, queue=True)
                    output_directory = output_directory_maker(file_path, working_directory,
                                                              tree_digest)
                    action_result.output_directories.append(output_directory)
                    logger.debug("Output tree digest: [{}/{}]"
                                 .format(tree_digest.hash, tree_digest.size_bytes))
                else:
                    file_digest = uploader.upload_file(file_path, queue=True)
                    output_file = output_file_maker(file_path, working_directory,
                                                    file_digest)
                    action_result.output_files.append(output_file)
                    logger.debug("Output file digest: [{}/{}]"
                                 .format(file_digest.hash, file_digest.size_bytes))

            if action_result.ByteSize() + len(stdout) > MAX_REQUEST_SIZE:
                stdout_digest = uploader.put_blob(stdout)
                action_result.stdout_digest.CopyFrom(stdout_digest)

            else:
                action_result.stdout_raw = stdout

            if action_result.ByteSize() + len(stderr) > MAX_REQUEST_SIZE:
                stderr_digest = uploader.put_blob(stderr)
                action_result.stderr_digest.CopyFrom(stderr_digest)

            else:
                action_result.stderr_raw = stderr

        action_result.execution_metadata.output_upload_completed_timestamp.GetCurrentTime()

        lease.result.Pack(action_result)

    return lease
Ejemplo n.º 13
0
def work_buildbox(lease, context, event):
    """Executes a lease for a build action, using buildbox.
    """
    local_cas_directory = context.local_cas
    # instance_name = context.parent

    logger = logging.getLogger(__name__)

    action_digest = remote_execution_pb2.Digest()

    lease.payload.Unpack(action_digest)
    lease.result.Clear()

    with download(context.cas_channel) as downloader:
        action = downloader.get_message(action_digest,
                                        remote_execution_pb2.Action())

        assert action.command_digest.hash

        command = downloader.get_message(action.command_digest,
                                         remote_execution_pb2.Command())

    if command.working_directory:
        working_directory = command.working_directory
    else:
        working_directory = '/'

    logger.debug("Command digest: [{}/{}]"
                 .format(action.command_digest.hash, action.command_digest.size_bytes))
    logger.debug("Input root digest: [{}/{}]"
                 .format(action.input_root_digest.hash, action.input_root_digest.size_bytes))

    os.makedirs(os.path.join(local_cas_directory, 'tmp'), exist_ok=True)
    os.makedirs(context.fuse_dir, exist_ok=True)
    tempdir = os.path.join(local_cas_directory, 'tmp')

    with tempfile.NamedTemporaryFile(dir=tempdir) as input_digest_file:
        # Input hash must be written to disk for BuildBox
        write_file(input_digest_file.name, action.input_root_digest.SerializeToString())

        with tempfile.NamedTemporaryFile(dir=tempdir) as output_digest_file:
            with tempfile.NamedTemporaryFile(dir=tempdir) as timestamps_file:
                command_line = ['buildbox',
                                '--remote={}'.format(context.remote_cas_url),
                                '--input-digest={}'.format(input_digest_file.name),
                                '--output-digest={}'.format(output_digest_file.name),
                                '--chdir={}'.format(working_directory),
                                '--local={}'.format(local_cas_directory),
                                '--output-times={}'.format(timestamps_file.name)]

                if context.cas_client_key:
                    command_line.append('--client-key={}'.format(context.cas_client_key))
                if context.cas_client_cert:
                    command_line.append('--client-cert={}'.format(context.cas_client_cert))
                if context.cas_server_cert:
                    command_line.append('--server-cert={}'.format(context.cas_server_cert))

                command_line.append('--clearenv')
                for variable in command.environment_variables:
                    command_line.append('--setenv')
                    command_line.append(variable.name)
                    command_line.append(variable.value)

                command_line.append(context.fuse_dir)
                command_line.extend(command.arguments)

                logger.info("Starting execution: [{}...]".format(command.arguments[0]))

                command_line = subprocess.Popen(command_line,
                                                stdin=subprocess.PIPE,
                                                stdout=subprocess.PIPE,
                                                stderr=subprocess.PIPE)
                stdout, stderr = command_line.communicate()
                returncode = command_line.returncode

                action_result = remote_execution_pb2.ActionResult()
                action_result.exit_code = returncode

                logger.info("Execution finished with code: [{}]".format(returncode))

                output_digest = remote_execution_pb2.Digest()
                output_digest.ParseFromString(read_file(output_digest_file.name))

                logger.debug("Output root digest: [{}/{}]"
                             .format(output_digest.hash, output_digest.size_bytes))

                metadata = read_file(timestamps_file.name)
                logger.debug("metadata: {}".format(metadata))
                action_result.execution_metadata.ParseFromString(metadata)

                if len(output_digest.hash) != HASH_LENGTH:
                    raise BotError(
                        stdout, detail=stderr, reason="Output root digest too small.")

                # TODO: Have BuildBox helping us creating the Tree instance here
                # See https://gitlab.com/BuildStream/buildbox/issues/7 for details
                with download(context.cas_channel) as downloader:
                    output_tree = _cas_tree_maker(downloader, output_digest)

                with upload(context.cas_channel) as uploader:
                    output_tree_digest = uploader.put_message(output_tree)

                    output_directory = remote_execution_pb2.OutputDirectory()
                    output_directory.tree_digest.CopyFrom(output_tree_digest)
                    output_directory.path = os.path.relpath(working_directory, start='/')

                    action_result.output_directories.extend([output_directory])

                    if action_result.ByteSize() + len(stdout) > MAX_REQUEST_SIZE:
                        stdout_digest = uploader.put_blob(stdout)
                        action_result.stdout_digest.CopyFrom(stdout_digest)

                    else:
                        action_result.stdout_raw = stdout

                    if action_result.ByteSize() + len(stderr) > MAX_REQUEST_SIZE:
                        stderr_digest = uploader.put_blob(stderr)
                        action_result.stderr_digest.CopyFrom(stderr_digest)

                    else:
                        action_result.stderr_raw = stderr

                lease.result.Pack(action_result)

    return lease
Ejemplo n.º 14
0
    def update_lease_state(self,
                           state,
                           status=None,
                           result=None,
                           skip_lease_persistence=False,
                           *,
                           data_store):
        """Operates a state transition for the job's current :class:`Lease`.

        Args:
            state (LeaseState): the lease state to transition to.
            status (google.rpc.Status, optional): the lease execution status,
                only required if `state` is `COMPLETED`.
            result (google.protobuf.Any, optional): the lease execution result,
                only required if `state` is `COMPLETED`.
        """
        if state.value == self._lease.state:
            return

        job_changes = {}
        lease_changes = {}

        self._lease.state = state.value
        lease_changes["state"] = state.value

        self.__logger.debug("State changed for job [%s]: [%s] (lease)",
                            self._name, state.name)

        if self._lease.state == LeaseState.PENDING.value:
            self.__worker_start_timestamp.Clear()
            self.__worker_completed_timestamp.Clear()
            job_changes[
                "worker_start_timestamp"] = self.__worker_start_timestamp.ToDatetime(
                )
            job_changes[
                "worker_completed_timestamp"] = self.__worker_completed_timestamp.ToDatetime(
                )

            self._lease.status.Clear()
            self._lease.result.Clear()
            lease_changes["status"] = self._lease.status.code

        elif self._lease.state == LeaseState.COMPLETED.value:
            self.__worker_completed_timestamp.GetCurrentTime()
            job_changes[
                "worker_completed_timestamp"] = self.__worker_completed_timestamp.ToDatetime(
                )

            action_result = remote_execution_pb2.ActionResult()

            # TODO: Make a distinction between build and bot failures!
            if status.code != code_pb2.OK:
                self._do_not_cache = True
                job_changes["do_not_cache"] = True

            lease_changes["status"] = status.code

            if result is not None and result.Is(action_result.DESCRIPTOR):
                result.Unpack(action_result)

            action_metadata = action_result.execution_metadata
            action_metadata.queued_timestamp.CopyFrom(self.__queued_timestamp)
            action_metadata.worker_start_timestamp.CopyFrom(
                self.__worker_start_timestamp)
            action_metadata.worker_completed_timestamp.CopyFrom(
                self.__worker_completed_timestamp)

            self.__execute_response.result.CopyFrom(action_result)
            self.__execute_response.cached_result = False
            self.__execute_response.status.CopyFrom(status)

        data_store.update_job(self.name, job_changes)
        if not skip_lease_persistence:
            data_store.update_lease(self.name, lease_changes)
Ejemplo n.º 15
0
import grpc
import pytest

from buildgrid.client.cas import download, upload
from buildgrid._protos.build.bazel.remote.execution.v2 import remote_execution_pb2
from buildgrid.utils import create_digest

from ..utils.cas import serve_cas
from ..utils.utils import run_in_subprocess

INTANCES = ['', 'instance']
BLOBS = [(b'', ), (b'test-string', ), (b'test', b'string')]
MESSAGES = [(remote_execution_pb2.Directory(), ),
            (remote_execution_pb2.SymlinkNode(name='name', target='target'), ),
            (remote_execution_pb2.Action(do_not_cache=True),
             remote_execution_pb2.ActionResult(exit_code=12))]
DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data')
FILES = [(os.path.join(DATA_DIR, 'void'), ),
         (os.path.join(DATA_DIR, 'hello.cc'), ),
         (os.path.join(DATA_DIR, 'hello',
                       'hello.c'), os.path.join(DATA_DIR, 'hello', 'hello.h'),
          os.path.join(DATA_DIR, 'hello', 'hello.sh')),
         (os.path.join(DATA_DIR, 'hello', 'docs', 'reference', 'api.xml'), )]
FOLDERS = [(DATA_DIR, ), (os.path.join(DATA_DIR, 'hello'), ),
           (os.path.join(DATA_DIR, 'hello', 'docs'), ),
           (os.path.join(DATA_DIR, 'hello', 'utils'), ),
           (os.path.join(DATA_DIR, 'hello', 'docs', 'reference'), )]
DIRECTORIES = [(DATA_DIR, ), (os.path.join(DATA_DIR, 'hello'), )]


@pytest.mark.parametrize('blobs', BLOBS)