blob: 4a18b7f606042d44ad047affc7a792804103931b [file] [log] [blame]
Alex Kleinf4dc4f52018-12-05 13:55:12 -07001# -*- coding: utf-8 -*-
2# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""The build API entry point."""
7
8from __future__ import print_function
9
10import importlib
Alex Klein5bcb4d22019-03-21 13:51:54 -060011import os
Alex Klein00aa8072019-04-15 16:36:00 -060012import shutil
Alex Kleinf4dc4f52018-12-05 13:55:12 -070013
Alex Kleinf4dc4f52018-12-05 13:55:12 -070014from google.protobuf import json_format
15from google.protobuf import symbol_database
16
Alex Kleinb7cdbe62019-02-22 11:41:32 -070017from chromite.api import controller
Evan Hernandezaeb556a2019-04-03 11:28:49 -060018from chromite.api.gen.chromite.api import artifacts_pb2
19from chromite.api.gen.chromite.api import binhost_pb2
Alex Klein7107bdd2019-03-14 17:14:31 -060020from chromite.api.gen.chromite.api import build_api_pb2
21from chromite.api.gen.chromite.api import depgraph_pb2
22from chromite.api.gen.chromite.api import image_pb2
23from chromite.api.gen.chromite.api import sdk_pb2
Alex Kleind4e1e422019-03-18 16:00:41 -060024from chromite.api.gen.chromite.api import sysroot_pb2
Alex Kleinc5403d62019-04-03 09:34:59 -060025from chromite.api.gen.chromite.api import test_pb2
Alex Klein00aa8072019-04-15 16:36:00 -060026from chromite.api.gen.chromiumos import common_pb2
27from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070028from chromite.lib import commandline
Alex Klein2bfacb22019-02-04 11:42:17 -070029from chromite.lib import cros_build_lib
Alex Klein6db9cdf2019-05-03 14:59:10 -060030from chromite.lib import cros_logging as logging
Alex Kleinf4dc4f52018-12-05 13:55:12 -070031from chromite.lib import osutils
Alex Klein00b1f1e2019-02-08 13:53:42 -070032from chromite.utils import matching
Alex Kleinf4dc4f52018-12-05 13:55:12 -070033
34
35class Error(Exception):
36 """Base error class for the module."""
37
38
Alex Klein5bcb4d22019-03-21 13:51:54 -060039class InvalidInputFileError(Error):
40 """Raised when the input file cannot be read."""
41
42
Alex Klein7a115172019-02-08 14:14:20 -070043class InvalidInputFormatError(Error):
44 """Raised when the passed input protobuf can't be parsed."""
45
46
Alex Klein5bcb4d22019-03-21 13:51:54 -060047class InvalidOutputFileError(Error):
48 """Raised when the output file cannot be written."""
49
50
Alex Klein6db9cdf2019-05-03 14:59:10 -060051class CrosSdkNotRunError(Error):
52 """Raised when the cros_sdk command could not be run to enter the chroot."""
53
54
Alex Kleinf4dc4f52018-12-05 13:55:12 -070055# API Service Errors.
56class UnknownServiceError(Error):
57 """Error raised when the requested service has not been registered."""
58
59
Alex Kleinb7cdbe62019-02-22 11:41:32 -070060class ControllerModuleNotDefinedError(Error):
61 """Error class for when no controller is defined for a service."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070062
63
Alex Kleinb7cdbe62019-02-22 11:41:32 -070064class ServiceControllerNotFoundError(Error):
65 """Error raised when the service's controller cannot be imported."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070066
67
68# API Method Errors.
69class UnknownMethodError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070070 """The service is defined in the proto but the method is not."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070071
72
73class MethodNotFoundError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070074 """The method's implementation cannot be found in the service's controller."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070075
76
77def GetParser():
Alex Klein00b1f1e2019-02-08 13:53:42 -070078 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070079 parser = commandline.ArgumentParser(description=__doc__)
80
81 parser.add_argument('service_method',
82 help='The "chromite.api.Service/Method" that is being '
83 'called.')
84
85 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070086 '--input-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070087 help='Path to the JSON serialized input argument protobuf message.')
88 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070089 '--output-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070090 help='The path to which the result protobuf message should be written.')
91
92 return parser
93
94
Alex Klein00b1f1e2019-02-08 13:53:42 -070095def _ParseArgs(argv, router):
Alex Kleinf4dc4f52018-12-05 13:55:12 -070096 """Parse and validate arguments."""
97 parser = GetParser()
98 opts = parser.parse_args(argv)
99
Alex Klein00b1f1e2019-02-08 13:53:42 -0700100 methods = router.ListMethods()
101 if opts.service_method not in methods:
Alex Klein00aa8072019-04-15 16:36:00 -0600102 # Unknown method, try to match against known methods and make a suggestion.
103 # This is just for developer sanity, e.g. misspellings when testing.
Alex Klein00b1f1e2019-02-08 13:53:42 -0700104 matched = matching.GetMostLikelyMatchedObject(methods, opts.service_method,
105 matched_score_threshold=0.6)
106 error = 'Unrecognized service name.'
107 if matched:
108 error += '\nDid you mean: \n%s' % '\n'.join(matched)
109 parser.error(error)
110
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700111 parts = opts.service_method.split('/')
112
113 if len(parts) != 2:
114 parser.error('Must pass "Service/Method".')
115
116 opts.service = parts[0]
117 opts.method = parts[1]
118
Alex Klein5bcb4d22019-03-21 13:51:54 -0600119 if not os.path.exists(opts.input_json):
120 parser.error('Input file does not exist.')
121
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700122 opts.Freeze()
123 return opts
124
125
126class Router(object):
127 """Encapsulates the request dispatching logic."""
128
129 def __init__(self):
130 self._services = {}
131 self._aliases = {}
132 # All imported generated messages get added to this symbol db.
133 self._sym_db = symbol_database.Default()
134
135 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
136 self._service_options = extensions['service_options']
137 self._method_options = extensions['method_options']
138
139 def Register(self, proto_module):
140 """Register the services from a generated proto module.
141
142 Args:
143 proto_module (module): The generated proto module whose service is being
144 registered.
145
146 Raises:
147 ServiceModuleNotDefinedError when the service cannot be found in the
148 provided module.
149 """
150 services = proto_module.DESCRIPTOR.services_by_name
151 for service_name, svc in services.items():
152 module_name = svc.GetOptions().Extensions[self._service_options].module
153
154 if not module_name:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700155 raise ControllerModuleNotDefinedError(
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700156 'The module must be defined in the service definition: %s.%s' %
157 (proto_module, service_name))
158
159 self._services[svc.full_name] = (svc, module_name)
160
Alex Klein00b1f1e2019-02-08 13:53:42 -0700161 def ListMethods(self):
162 """List all methods registered with the router."""
163 services = []
164 for service_name, (svc, _module) in self._services.items():
165 for method_name in svc.methods_by_name.keys():
166 services.append('%s/%s' % (service_name, method_name))
167
168 return sorted(services)
169
Alex Klein5bcb4d22019-03-21 13:51:54 -0600170 def Route(self, service_name, method_name, input_path, output_path):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700171 """Dispatch the request.
172
173 Args:
174 service_name (str): The fully qualified service name.
175 method_name (str): The name of the method being called.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600176 input_path (str): The path to the input message file.
177 output_path (str): The path where the output message should be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700178
179 Returns:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600180 int: The return code.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700181
182 Raises:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600183 InvalidInputFileError when the input file cannot be read.
184 InvalidOutputFileError when the output file cannot be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700185 ServiceModuleNotFoundError when the service module cannot be imported.
186 MethodNotFoundError when the method cannot be retrieved from the module.
187 """
188 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600189 input_json = osutils.ReadFile(input_path)
190 except IOError as e:
191 raise InvalidInputFileError('Unable to read input file: %s' % e.message)
192
193 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700194 svc, module_name = self._services[service_name]
195 except KeyError:
196 raise UnknownServiceError('The %s service has not been registered.'
197 % service_name)
198
199 try:
200 method_desc = svc.methods_by_name[method_name]
201 except KeyError:
202 raise UnknownMethodError('The %s method has not been defined in the %s '
203 'service.' % (method_name, service_name))
204
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700205 # Parse the input file to build an instance of the input message.
206 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
Alex Klein7a115172019-02-08 14:14:20 -0700207 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700208 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
Alex Klein7a115172019-02-08 14:14:20 -0700209 except json_format.ParseError as e:
210 raise InvalidInputFormatError(
211 'Unable to parse the input json: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700212
213 # Get an empty output message instance.
214 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700215
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700216 # Allow proto-based method name override.
217 method_options = method_desc.GetOptions().Extensions[self._method_options]
218 if method_options.HasField('implementation_name'):
219 method_name = method_options.implementation_name
220
Alex Klein00aa8072019-04-15 16:36:00 -0600221 # Check the chroot settings before running.
Alex Klein2bfacb22019-02-04 11:42:17 -0700222 service_options = svc.GetOptions().Extensions[self._service_options]
Alex Klein00aa8072019-04-15 16:36:00 -0600223 if self._ChrootCheck(service_options, method_options):
224 # Run inside the chroot instead.
Alex Klein6db9cdf2019-05-03 14:59:10 -0600225 logging.info('Re-executing the endpoint inside the chroot.')
Alex Klein00aa8072019-04-15 16:36:00 -0600226 return self._ReexecuteInside(input_msg, output_path, service_name,
227 method_name)
Alex Klein2bfacb22019-02-04 11:42:17 -0700228
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700229 # Import the module and get the method.
230 method_impl = self._GetMethod(module_name, method_name)
231
232 # Successfully located; call and return.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600233 return_code = method_impl(input_msg, output_msg)
234 if return_code is None:
235 return_code = 0
236
237 try:
238 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
239 except IOError as e:
240 raise InvalidOutputFileError('Cannot write output file: %s' % e.message)
241
242 return return_code
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700243
Alex Klein00aa8072019-04-15 16:36:00 -0600244 def _ChrootCheck(self, service_options, method_options):
245 """Check the chroot options, and execute assertion or note reexec as needed.
Alex Klein2bfacb22019-02-04 11:42:17 -0700246
247 Args:
248 service_options (google.protobuf.Message): The service options.
249 method_options (google.protobuf.Message): The method options.
Alex Klein00aa8072019-04-15 16:36:00 -0600250
251 Returns:
252 bool - True iff it needs to be reexeced inside the chroot.
253
254 Raises:
255 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
Alex Klein2bfacb22019-02-04 11:42:17 -0700256 """
257 chroot_assert = build_api_pb2.NO_ASSERTION
258 if method_options.HasField('method_chroot_assert'):
259 # Prefer the method option when set.
260 chroot_assert = method_options.method_chroot_assert
261 elif service_options.HasField('service_chroot_assert'):
262 # Fall back to the service option.
263 chroot_assert = service_options.service_chroot_assert
264
Alex Klein2bfacb22019-02-04 11:42:17 -0700265 if chroot_assert == build_api_pb2.INSIDE:
Alex Klein00aa8072019-04-15 16:36:00 -0600266 return not cros_build_lib.IsInsideChroot()
Alex Klein2bfacb22019-02-04 11:42:17 -0700267 elif chroot_assert == build_api_pb2.OUTSIDE:
Alex Klein00aa8072019-04-15 16:36:00 -0600268 # If it must be run outside we have to already be outside.
Alex Klein2bfacb22019-02-04 11:42:17 -0700269 cros_build_lib.AssertOutsideChroot()
270
Alex Klein00aa8072019-04-15 16:36:00 -0600271 return False
272
Alex Klein6db9cdf2019-05-03 14:59:10 -0600273 def _ReexecuteInside(self, input_msg, output_path, service_name, method_name):
Alex Klein00aa8072019-04-15 16:36:00 -0600274 """Re-execute the service inside the chroot.
275
276 Args:
277 input_msg (Message): The parsed input message.
278 output_path (str): The path for the serialized output.
279 service_name (str): The name of the service to run.
280 method_name (str): The name of the method to run.
281 """
282 chroot_args = []
283 chroot_path = constants.DEFAULT_CHROOT_PATH
284 chroot_field_name = None
Alex Kleinbe0bae42019-05-06 13:01:49 -0600285 chroot_extra_env = None
Alex Klein00aa8072019-04-15 16:36:00 -0600286 # Find the Chroot field. Search for the field by type to prevent it being
287 # tied to a naming convention.
288 for descriptor in input_msg.DESCRIPTOR.fields:
289 field = getattr(input_msg, descriptor.name)
290 if isinstance(field, common_pb2.Chroot):
291 chroot_field_name = descriptor.name
292 chroot = field
Alex Kleine3fc3ca2019-04-30 16:20:55 -0600293 chroot_path = chroot.path or chroot_path
Alex Klein00aa8072019-04-15 16:36:00 -0600294 chroot_args.extend(self._GetChrootArgs(chroot))
Alex Kleinbe0bae42019-05-06 13:01:49 -0600295 chroot_extra_env = self._GetChrootEnv(chroot)
Alex Klein00aa8072019-04-15 16:36:00 -0600296 break
297
298 base_dir = os.path.join(chroot_path, 'tmp')
299 with osutils.TempDir(base_dir=base_dir) as tempdir:
300 new_input = os.path.join(tempdir, 'input.json')
301 chroot_input = '/%s' % os.path.relpath(new_input, chroot_path)
302 new_output = os.path.join(tempdir, 'output.json')
303 chroot_output = '/%s' % os.path.relpath(new_output, chroot_path)
304
305 if chroot_field_name:
306 input_msg.ClearField(chroot_field_name)
Alex Klein6db9cdf2019-05-03 14:59:10 -0600307
308 logging.info('Writing input message to: %s', new_input)
Alex Klein00aa8072019-04-15 16:36:00 -0600309 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
310 osutils.Touch(new_output)
311
312 cmd = ['build_api', '%s/%s' % (service_name, method_name),
313 '--input-json', chroot_input, '--output-json', chroot_output]
Alex Klein6db9cdf2019-05-03 14:59:10 -0600314
Alex Klein6db9cdf2019-05-03 14:59:10 -0600315 try:
316 result = cros_build_lib.RunCommand(cmd, enter_chroot=True,
317 chroot_args=chroot_args,
318 error_code_ok=True,
Alex Kleinbe0bae42019-05-06 13:01:49 -0600319 extra_env=chroot_extra_env)
Alex Klein6db9cdf2019-05-03 14:59:10 -0600320 except cros_build_lib.RunCommandError:
321 # A non-zero return code will not result in an error, but one is still
322 # thrown when the command cannot be run in the first place. This is
323 # known to happen at least when the PATH does not include the chromite
324 # bin dir.
325 raise CrosSdkNotRunError('Unable to enter the chroot.')
326
327 logging.info('Endpoint execution completed, return code: %d',
328 result.returncode)
329
Alex Klein00aa8072019-04-15 16:36:00 -0600330 shutil.move(new_output, output_path)
331
332 return result.returncode
333
334 def _GetChrootArgs(self, chroot):
335 """Translate a Chroot message to chroot enter args.
336
337 Args:
338 chroot (chromiumos.Chroot): A chroot message.
339
340 Returns:
341 list[str]: The cros_sdk args for the chroot.
342 """
343 args = []
344 if chroot.path:
345 args.extend(['--chroot', chroot.path])
346 if chroot.cache_dir:
347 args.extend(['--cache-dir', chroot.cache_dir])
348
349 return args
350
Alex Kleinbe0bae42019-05-06 13:01:49 -0600351 def _GetChrootEnv(self, chroot):
352 """Get chroot environment variables that need to be set."""
353 use_flags = [u.flag for u in chroot.env.use_flags]
354 features = [f.feature for f in chroot.env.features]
355
356 env = {}
357 if use_flags:
358 env['USE'] = ' '.join(use_flags)
Alex Klein40748dc2019-05-13 17:43:23 -0600359
360 # TODO(saklein) Remove the default when fully integrated in recipes.
361 env['FEATURES'] = 'separatedebug'
Alex Kleinbe0bae42019-05-06 13:01:49 -0600362 if features:
Alex Klein40748dc2019-05-13 17:43:23 -0600363 env['FEATURES'] = ' '.join(features)
Alex Kleinbe0bae42019-05-06 13:01:49 -0600364
365 return env
366
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700367 def _GetMethod(self, module_name, method_name):
368 """Get the implementation of the method for the service module.
369
370 Args:
371 module_name (str): The name of the service module.
372 method_name (str): The name of the method.
373
374 Returns:
375 callable - The method.
376
377 Raises:
378 MethodNotFoundError when the method cannot be found in the module.
379 ServiceModuleNotFoundError when the service module cannot be imported.
380 """
381 try:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700382 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700383 except ImportError as e:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700384 raise ServiceControllerNotFoundError(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700385 try:
386 return getattr(module, method_name)
387 except AttributeError as e:
388 raise MethodNotFoundError(e.message)
389
390
391def RegisterServices(router):
392 """Register all the services.
393
394 Args:
395 router (Router): The router.
396 """
Evan Hernandezaeb556a2019-04-03 11:28:49 -0600397 router.Register(artifacts_pb2)
398 router.Register(binhost_pb2)
Ned Nguyen9a7a9052019-02-05 11:04:03 -0700399 router.Register(depgraph_pb2)
Alex Klein2966e302019-01-17 13:29:38 -0700400 router.Register(image_pb2)
Alex Klein19c4cc42019-02-27 14:47:57 -0700401 router.Register(sdk_pb2)
Alex Kleind4e1e422019-03-18 16:00:41 -0600402 router.Register(sysroot_pb2)
Alex Kleinc5403d62019-04-03 09:34:59 -0600403 router.Register(test_pb2)
Alex Klein6db9cdf2019-05-03 14:59:10 -0600404 logging.debug('Services registered successfully.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700405
406
407def main(argv):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700408 router = Router()
409 RegisterServices(router)
410
Alex Klein00b1f1e2019-02-08 13:53:42 -0700411 opts = _ParseArgs(argv, router)
412
Alex Klein7a115172019-02-08 14:14:20 -0700413 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600414 return router.Route(opts.service, opts.method, opts.input_json,
415 opts.output_json)
Alex Klein7a115172019-02-08 14:14:20 -0700416 except Error as e:
417 # Error derivatives are handled nicely, but let anything else bubble up.
418 cros_build_lib.Die(e.message)