blob: 0a7f1fd8e938fe3f728000947c8cf73335bbebef [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
Alex Kleinc05f3d12019-05-29 14:16:21 -060018from chromite.api import field_handler
Evan Hernandezaeb556a2019-04-03 11:28:49 -060019from chromite.api.gen.chromite.api import artifacts_pb2
20from chromite.api.gen.chromite.api import binhost_pb2
Alex Klein7107bdd2019-03-14 17:14:31 -060021from chromite.api.gen.chromite.api import build_api_pb2
22from chromite.api.gen.chromite.api import depgraph_pb2
23from chromite.api.gen.chromite.api import image_pb2
24from chromite.api.gen.chromite.api import sdk_pb2
Alex Kleind4e1e422019-03-18 16:00:41 -060025from chromite.api.gen.chromite.api import sysroot_pb2
Alex Kleinc5403d62019-04-03 09:34:59 -060026from chromite.api.gen.chromite.api import test_pb2
Alex Kleinf4dc4f52018-12-05 13:55:12 -070027from chromite.lib import commandline
Alex Klein2bfacb22019-02-04 11:42:17 -070028from chromite.lib import cros_build_lib
Alex Klein6db9cdf2019-05-03 14:59:10 -060029from chromite.lib import cros_logging as logging
Alex Kleinf4dc4f52018-12-05 13:55:12 -070030from chromite.lib import osutils
Alex Klein00b1f1e2019-02-08 13:53:42 -070031from chromite.utils import matching
Alex Kleinf4dc4f52018-12-05 13:55:12 -070032
33
34class Error(Exception):
35 """Base error class for the module."""
36
37
Alex Klein5bcb4d22019-03-21 13:51:54 -060038class InvalidInputFileError(Error):
39 """Raised when the input file cannot be read."""
40
41
Alex Klein7a115172019-02-08 14:14:20 -070042class InvalidInputFormatError(Error):
43 """Raised when the passed input protobuf can't be parsed."""
44
45
Alex Klein5bcb4d22019-03-21 13:51:54 -060046class InvalidOutputFileError(Error):
47 """Raised when the output file cannot be written."""
48
49
Alex Klein6db9cdf2019-05-03 14:59:10 -060050class CrosSdkNotRunError(Error):
51 """Raised when the cros_sdk command could not be run to enter the chroot."""
52
53
Alex Kleinf4dc4f52018-12-05 13:55:12 -070054# API Service Errors.
55class UnknownServiceError(Error):
56 """Error raised when the requested service has not been registered."""
57
58
Alex Kleinb7cdbe62019-02-22 11:41:32 -070059class ControllerModuleNotDefinedError(Error):
60 """Error class for when no controller is defined for a service."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070061
62
Alex Kleinb7cdbe62019-02-22 11:41:32 -070063class ServiceControllerNotFoundError(Error):
64 """Error raised when the service's controller cannot be imported."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070065
66
67# API Method Errors.
68class UnknownMethodError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070069 """The service is defined in the proto but the method is not."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070070
71
72class MethodNotFoundError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070073 """The method's implementation cannot be found in the service's controller."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070074
75
76def GetParser():
Alex Klein00b1f1e2019-02-08 13:53:42 -070077 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070078 parser = commandline.ArgumentParser(description=__doc__)
79
80 parser.add_argument('service_method',
81 help='The "chromite.api.Service/Method" that is being '
82 'called.')
83
84 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070085 '--input-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070086 help='Path to the JSON serialized input argument protobuf message.')
87 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070088 '--output-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070089 help='The path to which the result protobuf message should be written.')
90
91 return parser
92
93
Alex Klein00b1f1e2019-02-08 13:53:42 -070094def _ParseArgs(argv, router):
Alex Kleinf4dc4f52018-12-05 13:55:12 -070095 """Parse and validate arguments."""
96 parser = GetParser()
97 opts = parser.parse_args(argv)
98
Alex Klein00b1f1e2019-02-08 13:53:42 -070099 methods = router.ListMethods()
100 if opts.service_method not in methods:
Alex Klein00aa8072019-04-15 16:36:00 -0600101 # Unknown method, try to match against known methods and make a suggestion.
102 # This is just for developer sanity, e.g. misspellings when testing.
Alex Klein00b1f1e2019-02-08 13:53:42 -0700103 matched = matching.GetMostLikelyMatchedObject(methods, opts.service_method,
104 matched_score_threshold=0.6)
105 error = 'Unrecognized service name.'
106 if matched:
107 error += '\nDid you mean: \n%s' % '\n'.join(matched)
108 parser.error(error)
109
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700110 parts = opts.service_method.split('/')
111
112 if len(parts) != 2:
113 parser.error('Must pass "Service/Method".')
114
115 opts.service = parts[0]
116 opts.method = parts[1]
117
Alex Klein5bcb4d22019-03-21 13:51:54 -0600118 if not os.path.exists(opts.input_json):
119 parser.error('Input file does not exist.')
120
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700121 opts.Freeze()
122 return opts
123
124
125class Router(object):
126 """Encapsulates the request dispatching logic."""
127
128 def __init__(self):
129 self._services = {}
130 self._aliases = {}
131 # All imported generated messages get added to this symbol db.
132 self._sym_db = symbol_database.Default()
133
134 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
135 self._service_options = extensions['service_options']
136 self._method_options = extensions['method_options']
137
138 def Register(self, proto_module):
139 """Register the services from a generated proto module.
140
141 Args:
142 proto_module (module): The generated proto module whose service is being
143 registered.
144
145 Raises:
146 ServiceModuleNotDefinedError when the service cannot be found in the
147 provided module.
148 """
149 services = proto_module.DESCRIPTOR.services_by_name
150 for service_name, svc in services.items():
151 module_name = svc.GetOptions().Extensions[self._service_options].module
152
153 if not module_name:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700154 raise ControllerModuleNotDefinedError(
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700155 'The module must be defined in the service definition: %s.%s' %
156 (proto_module, service_name))
157
158 self._services[svc.full_name] = (svc, module_name)
159
Alex Klein00b1f1e2019-02-08 13:53:42 -0700160 def ListMethods(self):
161 """List all methods registered with the router."""
162 services = []
163 for service_name, (svc, _module) in self._services.items():
164 for method_name in svc.methods_by_name.keys():
165 services.append('%s/%s' % (service_name, method_name))
166
167 return sorted(services)
168
Alex Klein5bcb4d22019-03-21 13:51:54 -0600169 def Route(self, service_name, method_name, input_path, output_path):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700170 """Dispatch the request.
171
172 Args:
173 service_name (str): The fully qualified service name.
174 method_name (str): The name of the method being called.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600175 input_path (str): The path to the input message file.
176 output_path (str): The path where the output message should be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700177
178 Returns:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600179 int: The return code.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700180
181 Raises:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600182 InvalidInputFileError when the input file cannot be read.
183 InvalidOutputFileError when the output file cannot be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700184 ServiceModuleNotFoundError when the service module cannot be imported.
185 MethodNotFoundError when the method cannot be retrieved from the module.
186 """
187 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600188 input_json = osutils.ReadFile(input_path)
189 except IOError as e:
190 raise InvalidInputFileError('Unable to read input file: %s' % e.message)
191
192 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700193 svc, module_name = self._services[service_name]
194 except KeyError:
195 raise UnknownServiceError('The %s service has not been registered.'
196 % service_name)
197
198 try:
199 method_desc = svc.methods_by_name[method_name]
200 except KeyError:
201 raise UnknownMethodError('The %s method has not been defined in the %s '
202 'service.' % (method_name, service_name))
203
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700204 # Parse the input file to build an instance of the input message.
205 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
Alex Klein7a115172019-02-08 14:14:20 -0700206 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700207 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
Alex Klein7a115172019-02-08 14:14:20 -0700208 except json_format.ParseError as e:
209 raise InvalidInputFormatError(
210 'Unable to parse the input json: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700211
212 # Get an empty output message instance.
213 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700214
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700215 # Allow proto-based method name override.
216 method_options = method_desc.GetOptions().Extensions[self._method_options]
217 if method_options.HasField('implementation_name'):
218 method_name = method_options.implementation_name
219
Alex Klein00aa8072019-04-15 16:36:00 -0600220 # Check the chroot settings before running.
Alex Klein2bfacb22019-02-04 11:42:17 -0700221 service_options = svc.GetOptions().Extensions[self._service_options]
Alex Klein00aa8072019-04-15 16:36:00 -0600222 if self._ChrootCheck(service_options, method_options):
223 # Run inside the chroot instead.
Alex Klein6db9cdf2019-05-03 14:59:10 -0600224 logging.info('Re-executing the endpoint inside the chroot.')
Alex Klein00aa8072019-04-15 16:36:00 -0600225 return self._ReexecuteInside(input_msg, output_path, service_name,
226 method_name)
Alex Klein2bfacb22019-02-04 11:42:17 -0700227
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700228 # Import the module and get the method.
229 method_impl = self._GetMethod(module_name, method_name)
230
231 # Successfully located; call and return.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600232 return_code = method_impl(input_msg, output_msg)
233 if return_code is None:
Alex Klein8cb365a2019-05-15 16:24:53 -0600234 return_code = controller.RETURN_CODE_SUCCESS
Alex Klein5bcb4d22019-03-21 13:51:54 -0600235
236 try:
237 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
238 except IOError as e:
239 raise InvalidOutputFileError('Cannot write output file: %s' % e.message)
240
241 return return_code
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700242
Alex Klein00aa8072019-04-15 16:36:00 -0600243 def _ChrootCheck(self, service_options, method_options):
244 """Check the chroot options, and execute assertion or note reexec as needed.
Alex Klein2bfacb22019-02-04 11:42:17 -0700245
246 Args:
247 service_options (google.protobuf.Message): The service options.
248 method_options (google.protobuf.Message): The method options.
Alex Klein00aa8072019-04-15 16:36:00 -0600249
250 Returns:
251 bool - True iff it needs to be reexeced inside the chroot.
252
253 Raises:
254 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
Alex Klein2bfacb22019-02-04 11:42:17 -0700255 """
256 chroot_assert = build_api_pb2.NO_ASSERTION
257 if method_options.HasField('method_chroot_assert'):
258 # Prefer the method option when set.
259 chroot_assert = method_options.method_chroot_assert
260 elif service_options.HasField('service_chroot_assert'):
261 # Fall back to the service option.
262 chroot_assert = service_options.service_chroot_assert
263
Alex Klein2bfacb22019-02-04 11:42:17 -0700264 if chroot_assert == build_api_pb2.INSIDE:
Alex Klein00aa8072019-04-15 16:36:00 -0600265 return not cros_build_lib.IsInsideChroot()
Alex Klein2bfacb22019-02-04 11:42:17 -0700266 elif chroot_assert == build_api_pb2.OUTSIDE:
Alex Klein00aa8072019-04-15 16:36:00 -0600267 # If it must be run outside we have to already be outside.
Alex Klein2bfacb22019-02-04 11:42:17 -0700268 cros_build_lib.AssertOutsideChroot()
269
Alex Klein00aa8072019-04-15 16:36:00 -0600270 return False
271
Alex Klein6db9cdf2019-05-03 14:59:10 -0600272 def _ReexecuteInside(self, input_msg, output_path, service_name, method_name):
Alex Klein00aa8072019-04-15 16:36:00 -0600273 """Re-execute the service inside the chroot.
274
275 Args:
276 input_msg (Message): The parsed input message.
277 output_path (str): The path for the serialized output.
278 service_name (str): The name of the service to run.
279 method_name (str): The name of the method to run.
280 """
Alex Kleinc05f3d12019-05-29 14:16:21 -0600281 # Parse the chroot and clear the chroot field in the input message.
282 chroot = field_handler.handle_chroot(input_msg)
Alex Klein00aa8072019-04-15 16:36:00 -0600283
Alex Kleinc05f3d12019-05-29 14:16:21 -0600284 base_dir = os.path.join(chroot.path, 'tmp')
285 with field_handler.handle_paths(input_msg, base_dir):
286 with osutils.TempDir(base_dir=base_dir) as tempdir:
287 new_input = os.path.join(tempdir, 'input.json')
288 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
289 new_output = os.path.join(tempdir, 'output.json')
290 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Klein00aa8072019-04-15 16:36:00 -0600291
Alex Kleinc05f3d12019-05-29 14:16:21 -0600292 logging.info('Writing input message to: %s', new_input)
293 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
294 osutils.Touch(new_output)
Alex Klein6db9cdf2019-05-03 14:59:10 -0600295
Alex Kleinc05f3d12019-05-29 14:16:21 -0600296 cmd = ['build_api', '%s/%s' % (service_name, method_name),
297 '--input-json', chroot_input, '--output-json', chroot_output]
Alex Klein00aa8072019-04-15 16:36:00 -0600298
Alex Kleinc05f3d12019-05-29 14:16:21 -0600299 try:
300 result = cros_build_lib.RunCommand(cmd, enter_chroot=True,
301 chroot_args=chroot.GetEnterArgs(),
302 error_code_ok=True,
303 extra_env=chroot.env)
304 except cros_build_lib.RunCommandError:
305 # A non-zero return code will not result in an error, but one is still
306 # thrown when the command cannot be run in the first place. This is
307 # known to happen at least when the PATH does not include the chromite
308 # bin dir.
309 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein6db9cdf2019-05-03 14:59:10 -0600310
Alex Kleinc05f3d12019-05-29 14:16:21 -0600311 logging.info('Endpoint execution completed, return code: %d',
312 result.returncode)
Alex Klein6db9cdf2019-05-03 14:59:10 -0600313
Alex Kleinc05f3d12019-05-29 14:16:21 -0600314 shutil.move(new_output, output_path)
Alex Klein6db9cdf2019-05-03 14:59:10 -0600315
Alex Kleinc05f3d12019-05-29 14:16:21 -0600316 return result.returncode
Alex Klein00aa8072019-04-15 16:36:00 -0600317
318 def _GetChrootArgs(self, chroot):
319 """Translate a Chroot message to chroot enter args.
320
321 Args:
322 chroot (chromiumos.Chroot): A chroot message.
323
324 Returns:
325 list[str]: The cros_sdk args for the chroot.
326 """
327 args = []
328 if chroot.path:
329 args.extend(['--chroot', chroot.path])
330 if chroot.cache_dir:
331 args.extend(['--cache-dir', chroot.cache_dir])
332
333 return args
334
Alex Kleinbe0bae42019-05-06 13:01:49 -0600335 def _GetChrootEnv(self, chroot):
336 """Get chroot environment variables that need to be set."""
337 use_flags = [u.flag for u in chroot.env.use_flags]
338 features = [f.feature for f in chroot.env.features]
339
340 env = {}
341 if use_flags:
342 env['USE'] = ' '.join(use_flags)
Alex Klein40748dc2019-05-13 17:43:23 -0600343
344 # TODO(saklein) Remove the default when fully integrated in recipes.
345 env['FEATURES'] = 'separatedebug'
Alex Kleinbe0bae42019-05-06 13:01:49 -0600346 if features:
Alex Klein40748dc2019-05-13 17:43:23 -0600347 env['FEATURES'] = ' '.join(features)
Alex Kleinbe0bae42019-05-06 13:01:49 -0600348
349 return env
350
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700351 def _GetMethod(self, module_name, method_name):
352 """Get the implementation of the method for the service module.
353
354 Args:
355 module_name (str): The name of the service module.
356 method_name (str): The name of the method.
357
358 Returns:
359 callable - The method.
360
361 Raises:
362 MethodNotFoundError when the method cannot be found in the module.
363 ServiceModuleNotFoundError when the service module cannot be imported.
364 """
365 try:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700366 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700367 except ImportError as e:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700368 raise ServiceControllerNotFoundError(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700369 try:
370 return getattr(module, method_name)
371 except AttributeError as e:
372 raise MethodNotFoundError(e.message)
373
374
375def RegisterServices(router):
376 """Register all the services.
377
378 Args:
379 router (Router): The router.
380 """
Evan Hernandezaeb556a2019-04-03 11:28:49 -0600381 router.Register(artifacts_pb2)
382 router.Register(binhost_pb2)
Ned Nguyen9a7a9052019-02-05 11:04:03 -0700383 router.Register(depgraph_pb2)
Alex Klein2966e302019-01-17 13:29:38 -0700384 router.Register(image_pb2)
Alex Klein19c4cc42019-02-27 14:47:57 -0700385 router.Register(sdk_pb2)
Alex Kleind4e1e422019-03-18 16:00:41 -0600386 router.Register(sysroot_pb2)
Alex Kleinc5403d62019-04-03 09:34:59 -0600387 router.Register(test_pb2)
Alex Klein6db9cdf2019-05-03 14:59:10 -0600388 logging.debug('Services registered successfully.')
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700389
390
391def main(argv):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700392 router = Router()
393 RegisterServices(router)
394
Alex Klein00b1f1e2019-02-08 13:53:42 -0700395 opts = _ParseArgs(argv, router)
396
Alex Klein7a115172019-02-08 14:14:20 -0700397 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600398 return router.Route(opts.service, opts.method, opts.input_json,
399 opts.output_json)
Alex Klein7a115172019-02-08 14:14:20 -0700400 except Error as e:
401 # Error derivatives are handled nicely, but let anything else bubble up.
402 cros_build_lib.Die(e.message)