finn pushed to branch finn/80-remote-parser at BuildGrid / buildgrid
Commits:
-
d7ef832c
by Martin Blanchard at 2018-09-11T16:51:58Z
-
2d44c601
by Martin Blanchard at 2018-09-11T16:51:58Z
-
a1dd6783
by Martin Blanchard at 2018-09-11T16:51:58Z
-
aa688609
by finnball at 2018-09-12T09:13:18Z
-
8d947213
by finnball at 2018-09-12T09:13:18Z
-
1dc1bb60
by finnball at 2018-09-12T09:13:18Z
8 changed files:
- buildgrid/_app/bots/buildbox.py
- buildgrid/_app/commands/cmd_server.py
- + buildgrid/_app/settings/__init__.py
- + buildgrid/_app/settings/cas.yml
- buildgrid/_app/settings/default.yml
- buildgrid/_app/settings/parser.py
- + buildgrid/_app/settings/remote-storage.yml
- buildgrid/utils.py
Changes:
| ... | ... | @@ -19,83 +19,126 @@ import tempfile |
| 19 | 19 |
|
| 20 | 20 |
from google.protobuf import any_pb2
|
| 21 | 21 |
|
| 22 |
+from buildgrid.client.cas import upload
|
|
| 22 | 23 |
from buildgrid._protos.build.bazel.remote.execution.v2 import remote_execution_pb2
|
| 23 | 24 |
from buildgrid._protos.google.bytestream import bytestream_pb2_grpc
|
| 24 |
-from buildgrid.utils import parse_to_pb2_from_fetch
|
|
| 25 |
+from buildgrid.utils import read_file, write_file, parse_to_pb2_from_fetch
|
|
| 25 | 26 |
|
| 26 | 27 |
|
| 27 | 28 |
def work_buildbox(context, lease):
|
| 29 |
+ """Executes a lease for a build action, using buildbox.
|
|
| 30 |
+ """
|
|
| 31 |
+ |
|
| 32 |
+ stub_bytestream = bytestream_pb2_grpc.ByteStreamStub(context.cas_channel)
|
|
| 33 |
+ local_cas_directory = context.local_cas
|
|
| 28 | 34 |
logger = context.logger
|
| 29 | 35 |
|
| 30 |
- action_digest_any = lease.payload
|
|
| 31 | 36 |
action_digest = remote_execution_pb2.Digest()
|
| 32 |
- action_digest_any.Unpack(action_digest)
|
|
| 37 |
+ lease.payload.Unpack(action_digest)
|
|
| 38 |
+ |
|
| 39 |
+ action = parse_to_pb2_from_fetch(remote_execution_pb2.Action(),
|
|
| 40 |
+ stub_bytestream, action_digest)
|
|
| 33 | 41 |
|
| 34 |
- stub = bytestream_pb2_grpc.ByteStreamStub(context.cas_channel)
|
|
| 42 |
+ command = parse_to_pb2_from_fetch(remote_execution_pb2.Command(),
|
|
| 43 |
+ stub_bytestream, action.command_digest)
|
|
| 35 | 44 |
|
| 36 |
- action = remote_execution_pb2.Action()
|
|
| 37 |
- parse_to_pb2_from_fetch(action, stub, action_digest)
|
|
| 45 |
+ environment = dict()
|
|
| 46 |
+ for variable in command.environment_variables:
|
|
| 47 |
+ if variable.name not in ['PWD']:
|
|
| 48 |
+ environment[variable.name] = variable.value
|
|
| 38 | 49 |
|
| 39 |
- casdir = context.local_cas
|
|
| 40 |
- remote_command = remote_execution_pb2.Command()
|
|
| 41 |
- parse_to_pb2_from_fetch(remote_command, stub, action.command_digest)
|
|
| 50 |
+ if command.working_directory:
|
|
| 51 |
+ working_directory = command.working_directory
|
|
| 52 |
+ else:
|
|
| 53 |
+ working_directory = '/'
|
|
| 42 | 54 |
|
| 43 |
- environment = dict((x.name, x.value) for x in remote_command.environment_variables)
|
|
| 44 | 55 |
logger.debug("command hash: {}".format(action.command_digest.hash))
|
| 45 | 56 |
logger.debug("vdir hash: {}".format(action.input_root_digest.hash))
|
| 46 |
- logger.debug("\n{}".format(' '.join(remote_command.arguments)))
|
|
| 47 |
- |
|
| 48 |
- # Input hash must be written to disk for buildbox.
|
|
| 49 |
- os.makedirs(os.path.join(casdir, 'tmp'), exist_ok=True)
|
|
| 50 |
- with tempfile.NamedTemporaryFile(dir=os.path.join(casdir, 'tmp')) as input_digest_file:
|
|
| 51 |
- with open(input_digest_file.name, 'wb') as f:
|
|
| 52 |
- f.write(action.input_root_digest.SerializeToString())
|
|
| 53 |
- f.flush()
|
|
| 54 |
- |
|
| 55 |
- with tempfile.NamedTemporaryFile(dir=os.path.join(casdir, 'tmp')) as output_digest_file:
|
|
| 56 |
- command = ['buildbox',
|
|
| 57 |
- '--remote={}'.format(context.remote_cas_url),
|
|
| 58 |
- '--input-digest={}'.format(input_digest_file.name),
|
|
| 59 |
- '--output-digest={}'.format(output_digest_file.name),
|
|
| 60 |
- '--local={}'.format(casdir)]
|
|
| 57 |
+ logger.debug("\n{}".format(' '.join(command.arguments)))
|
|
| 58 |
+ |
|
| 59 |
+ os.makedirs(os.path.join(local_cas_directory, 'tmp'), exist_ok=True)
|
|
| 60 |
+ os.makedirs(context.fuse_dir, exist_ok=True)
|
|
| 61 |
+ |
|
| 62 |
+ with tempfile.NamedTemporaryFile(dir=os.path.join(local_cas_directory, 'tmp')) as input_digest_file:
|
|
| 63 |
+ # Input hash must be written to disk for BuildBox
|
|
| 64 |
+ write_file(input_digest_file.name, action.input_root_digest.SerializeToString())
|
|
| 65 |
+ |
|
| 66 |
+ with tempfile.NamedTemporaryFile(dir=os.path.join(local_cas_directory, 'tmp')) as output_digest_file:
|
|
| 67 |
+ command_line = ['buildbox',
|
|
| 68 |
+ '--remote={}'.format(context.remote_cas_url),
|
|
| 69 |
+ '--input-digest={}'.format(input_digest_file.name),
|
|
| 70 |
+ '--output-digest={}'.format(output_digest_file.name),
|
|
| 71 |
+ '--chdir={}'.format(working_directory),
|
|
| 72 |
+ '--local={}'.format(local_cas_directory)]
|
|
| 61 | 73 |
|
| 62 | 74 |
if context.cas_client_key:
|
| 63 |
- command.append('--client-key={}'.format(context.cas_client_key))
|
|
| 75 |
+ command_line.append('--client-key={}'.format(context.cas_client_key))
|
|
| 64 | 76 |
if context.cas_client_cert:
|
| 65 |
- command.append('--client-cert={}'.format(context.cas_client_cert))
|
|
| 77 |
+ command_line.append('--client-cert={}'.format(context.cas_client_cert))
|
|
| 66 | 78 |
if context.cas_server_cert:
|
| 67 |
- command.append('--server-cert={}'.format(context.cas_server_cert))
|
|
| 68 |
- |
|
| 69 |
- if 'PWD' in environment and environment['PWD']:
|
|
| 70 |
- command.append('--chdir={}'.format(environment['PWD']))
|
|
| 79 |
+ command_line.append('--server-cert={}'.format(context.cas_server_cert))
|
|
| 71 | 80 |
|
| 72 |
- command.append(context.fuse_dir)
|
|
| 73 |
- command.extend(remote_command.arguments)
|
|
| 81 |
+ command_line.append(context.fuse_dir)
|
|
| 82 |
+ command_line.extend(command.arguments)
|
|
| 74 | 83 |
|
| 75 |
- logger.debug(' '.join(command))
|
|
| 84 |
+ logger.debug(' '.join(command_line))
|
|
| 76 | 85 |
logger.debug("Input root digest:\n{}".format(action.input_root_digest))
|
| 77 | 86 |
logger.info("Launching process")
|
| 78 | 87 |
|
| 79 |
- proc = subprocess.Popen(command,
|
|
| 80 |
- stdin=subprocess.PIPE,
|
|
| 81 |
- stdout=subprocess.PIPE)
|
|
| 82 |
- proc.communicate()
|
|
| 88 |
+ command_line = subprocess.Popen(command_line,
|
|
| 89 |
+ stdin=subprocess.PIPE,
|
|
| 90 |
+ stdout=subprocess.PIPE)
|
|
| 91 |
+ # TODO: Should return the stdout and stderr to the user.
|
|
| 92 |
+ command_line.communicate()
|
|
| 83 | 93 |
|
| 84 |
- output_root_digest = remote_execution_pb2.Digest()
|
|
| 85 |
- with open(output_digest_file.name, 'rb') as f:
|
|
| 86 |
- output_root_digest.ParseFromString(f.read())
|
|
| 87 |
- logger.debug("Output root digest: {}".format(output_root_digest))
|
|
| 94 |
+ output_digest = remote_execution_pb2.Digest()
|
|
| 95 |
+ output_digest.ParseFromString(read_file(output_digest_file.name))
|
|
| 88 | 96 |
|
| 89 |
- if len(output_root_digest.hash) < 64:
|
|
| 97 |
+ logger.debug("Output root digest: {}".format(output_digest))
|
|
| 98 |
+ |
|
| 99 |
+ if len(output_digest.hash) < 64:
|
|
| 90 | 100 |
logger.warning("Buildbox command failed - no output root digest present.")
|
| 91 |
- output_file = remote_execution_pb2.OutputDirectory(tree_digest=output_root_digest)
|
|
| 92 | 101 |
|
| 93 |
- action_result = remote_execution_pb2.ActionResult()
|
|
| 94 |
- action_result.output_directories.extend([output_file])
|
|
| 102 |
+ # TODO: Have BuildBox helping us creating the Tree instance here
|
|
| 103 |
+ # See https://gitlab.com/BuildStream/buildbox/issues/7 for details
|
|
| 104 |
+ output_tree = _cas_tree_maker(stub_bytestream, output_digest)
|
|
| 105 |
+ |
|
| 106 |
+ with upload(context.cas_channel) as cas:
|
|
| 107 |
+ output_tree_digest = cas.send_message(output_tree)
|
|
| 95 | 108 |
|
| 96 |
- action_result_any = any_pb2.Any()
|
|
| 97 |
- action_result_any.Pack(action_result)
|
|
| 109 |
+ output_directory = remote_execution_pb2.OutputDirectory()
|
|
| 110 |
+ output_directory.tree_digest.CopyFrom(output_tree_digest)
|
|
| 111 |
+ output_directory.path = os.path.relpath(working_directory, start='/')
|
|
| 98 | 112 |
|
| 99 |
- lease.result.CopyFrom(action_result_any)
|
|
| 113 |
+ action_result = remote_execution_pb2.ActionResult()
|
|
| 114 |
+ action_result.output_directories.extend([output_directory])
|
|
| 115 |
+ |
|
| 116 |
+ action_result_any = any_pb2.Any()
|
|
| 117 |
+ action_result_any.Pack(action_result)
|
|
| 118 |
+ |
|
| 119 |
+ lease.result.CopyFrom(action_result_any)
|
|
| 100 | 120 |
|
| 101 | 121 |
return lease
|
| 122 |
+ |
|
| 123 |
+ |
|
| 124 |
+def _cas_tree_maker(stub_bytestream, directory_digest):
|
|
| 125 |
+ # Generates and stores a Tree for a given Directory. This is very inefficient
|
|
| 126 |
+ # and only temporary. See https://gitlab.com/BuildStream/buildbox/issues/7.
|
|
| 127 |
+ output_tree = remote_execution_pb2.Tree()
|
|
| 128 |
+ |
|
| 129 |
+ def list_directories(parent_directory):
|
|
| 130 |
+ directory_list = list()
|
|
| 131 |
+ for directory_node in parent_directory.directories:
|
|
| 132 |
+ directory = parse_to_pb2_from_fetch(remote_execution_pb2.Directory(),
|
|
| 133 |
+ stub_bytestream, directory_node.digest)
|
|
| 134 |
+ directory_list.extend(list_directories(directory))
|
|
| 135 |
+ directory_list.append(directory)
|
|
| 136 |
+ |
|
| 137 |
+ return directory_list
|
|
| 138 |
+ |
|
| 139 |
+ root_directory = parse_to_pb2_from_fetch(remote_execution_pb2.Directory(),
|
|
| 140 |
+ stub_bytestream, directory_digest)
|
|
| 141 |
+ output_tree.children.extend(list_directories(root_directory))
|
|
| 142 |
+ output_tree.root.CopyFrom(root_directory)
|
|
| 143 |
+ |
|
| 144 |
+ return output_tree
|
| ... | ... | @@ -53,10 +53,16 @@ def start(context, config): |
| 53 | 53 |
insecure_mode = server_settings['insecure-mode']
|
| 54 | 54 |
|
| 55 | 55 |
credentials = None
|
| 56 |
+ credentials_settings = server_settings.get('credentials')
|
|
| 56 | 57 |
if not insecure_mode:
|
| 57 |
- server_key = server_settings['tls-server-key']
|
|
| 58 |
- server_cert = server_settings['tls-server-cert']
|
|
| 59 |
- client_certs = server_settings['tls-client-certs']
|
|
| 58 |
+ if not credentials_settings:
|
|
| 59 |
+ click.echo("ERROR: no TLS keys were specified and no defaults could be found.\n" +
|
|
| 60 |
+ "Set `insecure-mode: false` in order to deactivate TLS encryption.\n", err=True)
|
|
| 61 |
+ sys.exit(-1)
|
|
| 62 |
+ |
|
| 63 |
+ server_key = credentials_settings['tls-server-key']
|
|
| 64 |
+ server_cert = credentials_settings['tls-server-cert']
|
|
| 65 |
+ client_certs = credentials_settings['tls-client-certs']
|
|
| 60 | 66 |
credentials = context.load_server_credentials(server_key, server_cert, client_certs)
|
| 61 | 67 |
|
| 62 | 68 |
if not credentials:
|
| 1 |
+server:
|
|
| 2 |
+ port: 50052
|
|
| 3 |
+ insecure-mode: true
|
|
| 4 |
+ tls-server-key: null
|
|
| 5 |
+ tls-server-cert: null
|
|
| 6 |
+ tls-client-certs: null
|
|
| 7 |
+ |
|
| 8 |
+description: |
|
|
| 9 |
+ Just a CAS.
|
|
| 10 |
+ |
|
| 11 |
+instances:
|
|
| 12 |
+ - name: main
|
|
| 13 |
+ description: |
|
|
| 14 |
+ The main server
|
|
| 15 |
+ |
|
| 16 |
+ storages:
|
|
| 17 |
+ - !disk-storage &main-storage
|
|
| 18 |
+ path: ~/cas/
|
|
| 19 |
+ |
|
| 20 |
+ services:
|
|
| 21 |
+ - !cas
|
|
| 22 |
+ storage: *main-storage
|
| 1 | 1 |
server:
|
| 2 | 2 |
port: 50051
|
| 3 |
+ insecure-mode: true
|
|
| 3 | 4 |
tls-server-key: null
|
| 4 | 5 |
tls-server-cert: null
|
| 5 | 6 |
tls-client-certs: null
|
| 6 |
- insecure-mode: true
|
|
| 7 | 7 |
|
| 8 | 8 |
description: |
|
| 9 | 9 |
A single default instance
|
| ... | ... | @@ -14,7 +14,11 @@ |
| 14 | 14 |
|
| 15 | 15 |
|
| 16 | 16 |
import os
|
| 17 |
+import sys
|
|
| 18 |
+from urllib.parse import urlparse
|
|
| 17 | 19 |
|
| 20 |
+import click
|
|
| 21 |
+import grpc
|
|
| 18 | 22 |
import yaml
|
| 19 | 23 |
|
| 20 | 24 |
from buildgrid.server.controller import ExecutionController
|
| ... | ... | @@ -22,9 +26,12 @@ from buildgrid.server.actioncache.storage import ActionCache |
| 22 | 26 |
from buildgrid.server.cas.instance import ByteStreamInstance, ContentAddressableStorageInstance
|
| 23 | 27 |
from buildgrid.server.cas.storage.disk import DiskStorage
|
| 24 | 28 |
from buildgrid.server.cas.storage.lru_memory_cache import LRUMemoryCache
|
| 29 |
+from buildgrid.server.cas.storage.remote import RemoteStorage
|
|
| 25 | 30 |
from buildgrid.server.cas.storage.s3 import S3Storage
|
| 26 | 31 |
from buildgrid.server.cas.storage.with_cache import WithCacheStorage
|
| 27 | 32 |
|
| 33 |
+from ..cli import Context
|
|
| 34 |
+ |
|
| 28 | 35 |
|
| 29 | 36 |
class YamlFactory(yaml.YAMLObject):
|
| 30 | 37 |
@classmethod
|
| ... | ... | @@ -58,6 +65,45 @@ class S3(YamlFactory): |
| 58 | 65 |
return S3Storage(bucket, endpoint_url=endpoint)
|
| 59 | 66 |
|
| 60 | 67 |
|
| 68 |
+class Remote(YamlFactory):
|
|
| 69 |
+ |
|
| 70 |
+ yaml_tag = u'!remote-storage'
|
|
| 71 |
+ |
|
| 72 |
+ def __new__(cls, url, credentials=None):
|
|
| 73 |
+ # TODO: Context could be passed into the parser.
|
|
| 74 |
+ context = Context()
|
|
| 75 |
+ |
|
| 76 |
+ url = urlparse(url)
|
|
| 77 |
+ remote = '{}:{}'.format(url.hostname, url.port or 50051)
|
|
| 78 |
+ |
|
| 79 |
+ channel = None
|
|
| 80 |
+ if url.scheme == 'http':
|
|
| 81 |
+ channel = grpc.insecure_channel(remote)
|
|
| 82 |
+ |
|
| 83 |
+ else:
|
|
| 84 |
+ if not credentials:
|
|
| 85 |
+ click.echo("ERROR: no TLS keys were specified and no defaults could be found.\n" +
|
|
| 86 |
+ "Set remote url scheme to `http` in order to deactivate" +
|
|
| 87 |
+ "TLS encryption.\n", err=True)
|
|
| 88 |
+ sys.exit(-1)
|
|
| 89 |
+ |
|
| 90 |
+ client_key = credentials['tls-client-key']
|
|
| 91 |
+ client_cert = credentials['tls-client-cert']
|
|
| 92 |
+ server_cert = credentials['tls-server-cert']
|
|
| 93 |
+ credentials = context.load_client_credentials(client_key,
|
|
| 94 |
+ client_cert,
|
|
| 95 |
+ server_cert)
|
|
| 96 |
+ if not credentials:
|
|
| 97 |
+ click.echo("ERROR: no TLS keys were specified and no defaults could be found.\n" +
|
|
| 98 |
+ "Set remote url scheme to `http` in order to deactivate" +
|
|
| 99 |
+ "TLS encryption.\n", err=True)
|
|
| 100 |
+ sys.exit(-1)
|
|
| 101 |
+ |
|
| 102 |
+ channel = grpc.secure_channel(remote, credentials)
|
|
| 103 |
+ |
|
| 104 |
+ return RemoteStorage(channel)
|
|
| 105 |
+ |
|
| 106 |
+ |
|
| 61 | 107 |
class WithCache(YamlFactory):
|
| 62 | 108 |
|
| 63 | 109 |
yaml_tag = u'!with-cache-storage'
|
| ... | ... | @@ -118,6 +164,7 @@ def get_parser(): |
| 118 | 164 |
yaml.SafeLoader.add_constructor(Disk.yaml_tag, Disk.from_yaml)
|
| 119 | 165 |
yaml.SafeLoader.add_constructor(LRU.yaml_tag, LRU.from_yaml)
|
| 120 | 166 |
yaml.SafeLoader.add_constructor(S3.yaml_tag, S3.from_yaml)
|
| 167 |
+ yaml.SafeLoader.add_constructor(Remote.yaml_tag, Remote.from_yaml)
|
|
| 121 | 168 |
yaml.SafeLoader.add_constructor(WithCache.yaml_tag, WithCache.from_yaml)
|
| 122 | 169 |
yaml.SafeLoader.add_constructor(CAS.yaml_tag, CAS.from_yaml)
|
| 123 | 170 |
yaml.SafeLoader.add_constructor(ByteStream.yaml_tag, ByteStream.from_yaml)
|
| 1 |
+server:
|
|
| 2 |
+ port: 50051
|
|
| 3 |
+ insecure-mode: false
|
|
| 4 |
+ tls-server-key: null
|
|
| 5 |
+ tls-server-cert: null
|
|
| 6 |
+ tls-client-certs: null
|
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+description: |
|
|
| 10 |
+ A single default instance with remote storage.
|
|
| 11 |
+ |
|
| 12 |
+instances:
|
|
| 13 |
+ - name: main
|
|
| 14 |
+ description: |
|
|
| 15 |
+ The main server
|
|
| 16 |
+ |
|
| 17 |
+ storages:
|
|
| 18 |
+ - !remote-storage &main-storage
|
|
| 19 |
+ url: "https://localhost:50052"
|
|
| 20 |
+ credentials:
|
|
| 21 |
+ tls-client-key: null
|
|
| 22 |
+ tls-client-cert: null
|
|
| 23 |
+ tls-server-cert: null
|
|
| 24 |
+ |
|
| 25 |
+ services:
|
|
| 26 |
+ - !action-cache &main-action
|
|
| 27 |
+ storage: *main-storage
|
|
| 28 |
+ max_cached_refs: 256
|
|
| 29 |
+ allow_updates: true
|
|
| 30 |
+ |
|
| 31 |
+ - !execution
|
|
| 32 |
+ storage: *main-storage
|
|
| 33 |
+ action_cache: *main-action
|
|
| 34 |
+ |
|
| 35 |
+ - !cas
|
|
| 36 |
+ storage: *main-storage
|
|
| 37 |
+ |
|
| 38 |
+ - !bytestream
|
|
| 39 |
+ storage: *main-storage
|
| ... | ... | @@ -304,6 +304,21 @@ def read_file(file_path): |
| 304 | 304 |
return byte_file.read()
|
| 305 | 305 |
|
| 306 | 306 |
|
| 307 |
+def write_file(file_path, content):
|
|
| 308 |
+ """Dumps raw memory content to a file.
|
|
| 309 |
+ |
|
| 310 |
+ Args:
|
|
| 311 |
+ file_path (str): path to the target file.
|
|
| 312 |
+ content (bytes): raw file's content.
|
|
| 313 |
+ |
|
| 314 |
+ Raises:
|
|
| 315 |
+ OSError: If `file_path` does not exist or is not writable.
|
|
| 316 |
+ """
|
|
| 317 |
+ with open(file_path, 'wb') as byte_file:
|
|
| 318 |
+ byte_file.write(content)
|
|
| 319 |
+ byte_file.flush()
|
|
| 320 |
+ |
|
| 321 |
+ |
|
| 307 | 322 |
def output_file_maker(file_path, input_path, cas=None):
|
| 308 | 323 |
"""Creates an :obj:`OutputFile` from a local file and possibly upload it.
|
| 309 | 324 |
|
