blob: a5c0e139ae4af91d98454b66c5f4030fd4e85722 [file] [log] [blame]
Alex Klein146d4772019-06-20 13:48:25 -06001# Copyright 2019 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Kleine0fa6422019-06-21 12:01:39 -06005"""Router class for the Build API.
6
7Handles routing requests to the appropriate controller and handles service
8registration.
9"""
Alex Klein146d4772019-06-20 13:48:25 -060010
Alex Klein92341cd2020-02-27 14:11:04 -070011import collections
Alex Klein146d4772019-06-20 13:48:25 -060012import importlib
Chris McDonald1672ddb2021-07-21 11:48:23 -060013import logging
Alex Klein146d4772019-06-20 13:48:25 -060014import os
Alex Klein146d4772019-06-20 13:48:25 -060015
Mike Frysinger2c024062021-05-22 15:43:22 -040016from chromite.third_party.google.protobuf import symbol_database
Alex Klein146d4772019-06-20 13:48:25 -060017
18from chromite.api import controller
19from chromite.api import field_handler
Alex Klein4de25e82019-08-05 15:58:39 -060020from chromite.api.gen.chromite.api import android_pb2
Alex Klein54e38e32019-06-21 14:54:17 -060021from chromite.api.gen.chromite.api import api_pb2
Alex Klein146d4772019-06-20 13:48:25 -060022from chromite.api.gen.chromite.api import artifacts_pb2
23from chromite.api.gen.chromite.api import binhost_pb2
24from chromite.api.gen.chromite.api import build_api_pb2
25from chromite.api.gen.chromite.api import depgraph_pb2
Jett Rink17ed0f52020-09-25 17:14:31 -060026from chromite.api.gen.chromite.api import firmware_pb2
Alex Klein146d4772019-06-20 13:48:25 -060027from chromite.api.gen.chromite.api import image_pb2
Alex Kleineb77ffa2019-05-28 14:47:44 -060028from chromite.api.gen.chromite.api import packages_pb2
George Engelbrechtfe63c8c2019-08-31 22:51:29 -060029from chromite.api.gen.chromite.api import payload_pb2
Alex Klein146d4772019-06-20 13:48:25 -060030from chromite.api.gen.chromite.api import sdk_pb2
31from chromite.api.gen.chromite.api import sysroot_pb2
32from chromite.api.gen.chromite.api import test_pb2
Tiancong Wangaf050172019-07-10 11:52:03 -070033from chromite.api.gen.chromite.api import toolchain_pb2
Alex Klein146d4772019-06-20 13:48:25 -060034from chromite.lib import cros_build_lib
Alex Klein146d4772019-06-20 13:48:25 -060035from chromite.lib import osutils
Alex Klein92341cd2020-02-27 14:11:04 -070036from chromite.utils import memoize
Alex Klein146d4772019-06-20 13:48:25 -060037
Mike Frysinger88770ef2021-05-21 11:04:00 -040038
Alex Klein92341cd2020-02-27 14:11:04 -070039MethodData = collections.namedtuple(
40 'MethodData', ('service_descriptor', 'module_name', 'method_descriptor'))
Alex Klein146d4772019-06-20 13:48:25 -060041
Mike Frysingeref94e4c2020-02-10 23:59:54 -050042
Alex Klein146d4772019-06-20 13:48:25 -060043class Error(Exception):
44 """Base error class for the module."""
45
46
Alex Kleind3394c22020-06-16 14:05:06 -060047class InvalidSdkError(Error):
48 """Raised when the SDK is invalid or does not exist."""
49
50
Alex Klein146d4772019-06-20 13:48:25 -060051class CrosSdkNotRunError(Error):
52 """Raised when the cros_sdk command could not be run to enter the chroot."""
53
54
55# API Service Errors.
56class UnknownServiceError(Error):
57 """Error raised when the requested service has not been registered."""
58
59
60class ControllerModuleNotDefinedError(Error):
Alex Klein92341cd2020-02-27 14:11:04 -070061 """Error class for when no controller has been defined for a service."""
Alex Klein146d4772019-06-20 13:48:25 -060062
63
64class ServiceControllerNotFoundError(Error):
65 """Error raised when the service's controller cannot be imported."""
66
67
68# API Method Errors.
69class UnknownMethodError(Error):
Alex Klein92341cd2020-02-27 14:11:04 -070070 """The service has been defined in the proto, but the method has not."""
Alex Klein146d4772019-06-20 13:48:25 -060071
72
73class MethodNotFoundError(Error):
74 """The method's implementation cannot be found in the service's controller."""
75
76
77class Router(object):
78 """Encapsulates the request dispatching logic."""
79
Alex Kleine191ed62020-02-27 15:59:55 -070080 REEXEC_INPUT_FILE = 'input_proto'
81 REEXEC_OUTPUT_FILE = 'output_proto'
82 REEXEC_CONFIG_FILE = 'config_proto'
Alex Kleinbd6edf82019-07-18 10:30:49 -060083
Alex Klein146d4772019-06-20 13:48:25 -060084 def __init__(self):
85 self._services = {}
86 self._aliases = {}
87 # All imported generated messages get added to this symbol db.
88 self._sym_db = symbol_database.Default()
89
Alex Klein92341cd2020-02-27 14:11:04 -070090 # Save the service and method extension info for looking up
91 # configured extension data.
Alex Klein146d4772019-06-20 13:48:25 -060092 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
Alex Klein92341cd2020-02-27 14:11:04 -070093 self._svc_options_ext = extensions['service_options']
94 self._method_options_ext = extensions['method_options']
95
96 @memoize.Memoize
97 def _get_method_data(self, service_name, method_name):
98 """Get the descriptors and module name for the given Service/Method."""
99 try:
100 svc, module_name = self._services[service_name]
101 except KeyError:
102 raise UnknownServiceError(
103 'The %s service has not been registered.' % service_name)
104
105 try:
106 method_desc = svc.methods_by_name[method_name]
107 except KeyError:
108 raise UnknownMethodError('The %s method has not been defined in the %s '
109 'service.' % (method_name, service_name))
110
111 return MethodData(
112 service_descriptor=svc,
113 module_name=module_name,
114 method_descriptor=method_desc)
115
116 def _get_input_message_instance(self, service_name, method_name):
117 """Get an empty input message instance for the specified method."""
118 method_data = self._get_method_data(service_name, method_name)
119 return self._sym_db.GetPrototype(method_data.method_descriptor.input_type)()
120
121 def _get_output_message_instance(self, service_name, method_name):
122 """Get an empty output message instance for the specified method."""
123 method_data = self._get_method_data(service_name, method_name)
124 return self._sym_db.GetPrototype(
125 method_data.method_descriptor.output_type)()
126
127 def _get_module_name(self, service_name, method_name):
128 """Get the name of the module containing the endpoint implementation."""
129 return self._get_method_data(service_name, method_name).module_name
130
131 def _get_service_options(self, service_name, method_name):
132 """Get the configured service options for the endpoint."""
133 method_data = self._get_method_data(service_name, method_name)
134 svc_extensions = method_data.service_descriptor.GetOptions().Extensions
135 return svc_extensions[self._svc_options_ext]
136
137 def _get_method_options(self, service_name, method_name):
138 """Get the configured method options for the endpoint."""
139 method_data = self._get_method_data(service_name, method_name)
140 method_extensions = method_data.method_descriptor.GetOptions().Extensions
141 return method_extensions[self._method_options_ext]
Alex Klein146d4772019-06-20 13:48:25 -0600142
143 def Register(self, proto_module):
144 """Register the services from a generated proto module.
145
146 Args:
Alex Klein92341cd2020-02-27 14:11:04 -0700147 proto_module (module): The generated proto module to register.
Alex Klein146d4772019-06-20 13:48:25 -0600148
149 Raises:
150 ServiceModuleNotDefinedError when the service cannot be found in the
151 provided module.
152 """
153 services = proto_module.DESCRIPTOR.services_by_name
154 for service_name, svc in services.items():
Alex Klein92341cd2020-02-27 14:11:04 -0700155 module_name = svc.GetOptions().Extensions[self._svc_options_ext].module
Alex Klein146d4772019-06-20 13:48:25 -0600156
157 if not module_name:
158 raise ControllerModuleNotDefinedError(
159 'The module must be defined in the service definition: %s.%s' %
160 (proto_module, service_name))
161
162 self._services[svc.full_name] = (svc, module_name)
163
164 def ListMethods(self):
165 """List all methods registered with the router."""
166 services = []
167 for service_name, (svc, _module) in self._services.items():
Alex Klein6cce6f62021-03-02 14:24:05 -0700168 svc_visibility = getattr(
169 svc.GetOptions().Extensions[self._svc_options_ext],
170 'service_visibility', build_api_pb2.LV_VISIBLE)
171 if svc_visibility == build_api_pb2.LV_HIDDEN:
172 continue
173
Alex Klein146d4772019-06-20 13:48:25 -0600174 for method_name in svc.methods_by_name.keys():
Alex Klein6cce6f62021-03-02 14:24:05 -0700175 method_options = self._get_method_options(service_name, method_name)
176 method_visibility = getattr(method_options, 'method_visibility',
177 build_api_pb2.LV_VISIBLE)
178 if method_visibility == build_api_pb2.LV_HIDDEN:
179 continue
180
Alex Klein146d4772019-06-20 13:48:25 -0600181 services.append('%s/%s' % (service_name, method_name))
182
183 return sorted(services)
184
Alex Kleine191ed62020-02-27 15:59:55 -0700185 def Route(self, service_name, method_name, config, input_handler,
186 output_handlers, config_handler):
Alex Klein146d4772019-06-20 13:48:25 -0600187 """Dispatch the request.
188
189 Args:
190 service_name (str): The fully qualified service name.
191 method_name (str): The name of the method being called.
Alex Kleine191ed62020-02-27 15:59:55 -0700192 config (api_config.ApiConfig): The call configs.
193 input_handler (message_util.MessageHandler): The request message handler.
194 output_handlers (list[message_util.MessageHandler]): The response message
195 handlers.
196 config_handler (message_util.MessageHandler): The config message handler.
Alex Klein146d4772019-06-20 13:48:25 -0600197
198 Returns:
199 int: The return code.
200
201 Raises:
202 InvalidInputFileError when the input file cannot be read.
203 InvalidOutputFileError when the output file cannot be written.
204 ServiceModuleNotFoundError when the service module cannot be imported.
205 MethodNotFoundError when the method cannot be retrieved from the module.
206 """
Alex Klein92341cd2020-02-27 14:11:04 -0700207 input_msg = self._get_input_message_instance(service_name, method_name)
Alex Kleine191ed62020-02-27 15:59:55 -0700208 input_handler.read_into(input_msg)
Alex Klein146d4772019-06-20 13:48:25 -0600209
210 # Get an empty output message instance.
Alex Klein92341cd2020-02-27 14:11:04 -0700211 output_msg = self._get_output_message_instance(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600212
Alex Kleinbd6edf82019-07-18 10:30:49 -0600213 # Fetch the method options for chroot and method name overrides.
Alex Klein92341cd2020-02-27 14:11:04 -0700214 method_options = self._get_method_options(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600215
216 # Check the chroot settings before running.
Alex Klein92341cd2020-02-27 14:11:04 -0700217 service_options = self._get_service_options(service_name, method_name)
Alex Kleind1e9e5c2020-12-14 12:32:32 -0700218 if self._ChrootCheck(service_options, method_options, config):
Alex Klein146d4772019-06-20 13:48:25 -0600219 # Run inside the chroot instead.
220 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleine191ed62020-02-27 15:59:55 -0700221 return self._ReexecuteInside(input_msg, output_msg, config, input_handler,
222 output_handlers, config_handler,
223 service_name, method_name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600224
225 # Allow proto-based method name override.
226 if method_options.HasField('implementation_name'):
Alex Klein92341cd2020-02-27 14:11:04 -0700227 implementation_name = method_options.implementation_name
228 else:
229 implementation_name = method_name
Alex Klein146d4772019-06-20 13:48:25 -0600230
231 # Import the module and get the method.
Alex Klein92341cd2020-02-27 14:11:04 -0700232 module_name = self._get_module_name(service_name, method_name)
233 method_impl = self._GetMethod(module_name, implementation_name)
Alex Klein146d4772019-06-20 13:48:25 -0600234
235 # Successfully located; call and return.
Alex Klein69339cc2019-07-22 14:08:35 -0600236 return_code = method_impl(input_msg, output_msg, config)
Alex Klein146d4772019-06-20 13:48:25 -0600237 if return_code is None:
238 return_code = controller.RETURN_CODE_SUCCESS
239
Alex Kleine191ed62020-02-27 15:59:55 -0700240 for h in output_handlers:
241 h.write_from(output_msg)
Alex Klein146d4772019-06-20 13:48:25 -0600242
243 return return_code
244
Alex Kleind1e9e5c2020-12-14 12:32:32 -0700245 def _ChrootCheck(self, service_options, method_options,
246 config: 'api_config.ApiConfig'):
Alex Klein146d4772019-06-20 13:48:25 -0600247 """Check the chroot options, and execute assertion or note reexec as needed.
248
249 Args:
250 service_options (google.protobuf.Message): The service options.
251 method_options (google.protobuf.Message): The method options.
Alex Kleind1e9e5c2020-12-14 12:32:32 -0700252 config: The Build API call config instance.
Alex Klein146d4772019-06-20 13:48:25 -0600253
254 Returns:
255 bool - True iff it needs to be reexeced inside the chroot.
256
257 Raises:
258 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
259 """
Alex Kleind1e9e5c2020-12-14 12:32:32 -0700260 if not config.run_endpoint:
261 # Do not enter the chroot for validate only and mock calls.
262 return False
263
Alex Klein146d4772019-06-20 13:48:25 -0600264 chroot_assert = build_api_pb2.NO_ASSERTION
265 if method_options.HasField('method_chroot_assert'):
266 # Prefer the method option when set.
267 chroot_assert = method_options.method_chroot_assert
268 elif service_options.HasField('service_chroot_assert'):
269 # Fall back to the service option.
270 chroot_assert = service_options.service_chroot_assert
271
272 if chroot_assert == build_api_pb2.INSIDE:
273 return not cros_build_lib.IsInsideChroot()
274 elif chroot_assert == build_api_pb2.OUTSIDE:
275 # If it must be run outside we have to already be outside.
276 cros_build_lib.AssertOutsideChroot()
277
278 return False
279
Alex Kleine191ed62020-02-27 15:59:55 -0700280 def _ReexecuteInside(self, input_msg, output_msg, config, input_handler,
281 output_handlers, config_handler, service_name,
282 method_name):
Alex Klein146d4772019-06-20 13:48:25 -0600283 """Re-execute the service inside the chroot.
284
285 Args:
286 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600287 output_msg (Message): The empty output message instance.
Alex Kleine191ed62020-02-27 15:59:55 -0700288 config (api_config.ApiConfig): The call configs.
289 input_handler (MessageHandler): Input message handler.
290 output_handlers (list[MessageHandler]): Output message handlers.
291 config_handler (MessageHandler): Config message handler.
Alex Klein146d4772019-06-20 13:48:25 -0600292 service_name (str): The name of the service to run.
293 method_name (str): The name of the method to run.
294 """
Alex Kleinc7d647f2020-01-06 12:00:48 -0700295 # Parse the chroot and clear the chroot field in the input message.
296 chroot = field_handler.handle_chroot(input_msg)
Alex Klein915cce92019-12-17 14:19:50 -0700297
Alex Kleind3394c22020-06-16 14:05:06 -0600298 if not chroot.exists():
299 raise InvalidSdkError('Chroot does not exist.')
300
Alex Kleinc7d647f2020-01-06 12:00:48 -0700301 # Use a ContextManagerStack to avoid the deep nesting this many
302 # context managers introduces.
303 with cros_build_lib.ContextManagerStack() as stack:
304 # TempDirs setup.
305 tempdir = stack.Add(chroot.tempdir).tempdir
306 sync_tempdir = stack.Add(chroot.tempdir).tempdir
307 # The copy-paths-in context manager to handle Path messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700308 stack.Add(
309 field_handler.copy_paths_in,
310 input_msg,
311 chroot.tmp,
312 prefix=chroot.path)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700313 # The sync-directories context manager to handle SyncedDir messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700314 stack.Add(
315 field_handler.sync_dirs, input_msg, sync_tempdir, prefix=chroot.path)
Alex Klein146d4772019-06-20 13:48:25 -0600316
Alex Klein4089a492020-06-30 10:59:36 -0600317 # Parse goma.
Alex Kleinc7d647f2020-01-06 12:00:48 -0700318 chroot.goma = field_handler.handle_goma(input_msg, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700319
Alex Klein4089a492020-06-30 10:59:36 -0600320 # Build inside-chroot paths for the input, output, and config messages.
Alex Kleinc7d647f2020-01-06 12:00:48 -0700321 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
322 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
323 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
324 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Kleind815ca62020-01-10 12:21:30 -0700325 new_config = os.path.join(tempdir, self.REEXEC_CONFIG_FILE)
326 chroot_config = '/%s' % os.path.relpath(new_config, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700327
Alex Klein4089a492020-06-30 10:59:36 -0600328 # Setup the inside-chroot message files.
Alex Kleinc7d647f2020-01-06 12:00:48 -0700329 logging.info('Writing input message to: %s', new_input)
Alex Kleine191ed62020-02-27 15:59:55 -0700330 input_handler.write_from(input_msg, path=new_input)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700331 osutils.Touch(new_output)
Alex Kleind815ca62020-01-10 12:21:30 -0700332 logging.info('Writing config message to: %s', new_config)
Alex Kleine191ed62020-02-27 15:59:55 -0700333 config_handler.write_from(config.get_proto(), path=new_config)
Alex Klein146d4772019-06-20 13:48:25 -0600334
Alex Kleine191ed62020-02-27 15:59:55 -0700335 # We can use a single output to write the rest of them. Use the
336 # first one as the reexec output and just translate its output in
337 # the rest of the handlers after.
338 output_handler = output_handlers[0]
339
340 cmd = [
341 'build_api',
342 '%s/%s' % (service_name, method_name),
343 input_handler.input_arg,
344 chroot_input,
345 output_handler.output_arg,
346 chroot_output,
347 config_handler.config_arg,
348 chroot_config,
Alex Klein0b9edda2020-05-20 10:35:01 -0600349 '--debug',
Alex Kleine191ed62020-02-27 15:59:55 -0700350 ]
Alex Klein146d4772019-06-20 13:48:25 -0600351
Alex Kleinc7d647f2020-01-06 12:00:48 -0700352 try:
353 result = cros_build_lib.run(
354 cmd,
355 enter_chroot=True,
356 chroot_args=chroot.get_enter_args(),
357 check=False,
358 extra_env=chroot.env)
359 except cros_build_lib.RunCommandError:
360 # A non-zero return code will not result in an error, but one
361 # is still thrown when the command cannot be run in the first
362 # place. This is known to happen at least when the PATH does
363 # not include the chromite bin dir.
364 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein69339cc2019-07-22 14:08:35 -0600365
Alex Kleinc7d647f2020-01-06 12:00:48 -0700366 logging.info('Endpoint execution completed, return code: %d',
367 result.returncode)
Alex Klein146d4772019-06-20 13:48:25 -0600368
Alex Kleinc7d647f2020-01-06 12:00:48 -0700369 # Transfer result files out of the chroot.
Alex Kleine191ed62020-02-27 15:59:55 -0700370 output_handler.read_into(output_msg, path=new_output)
371 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Klein146d4772019-06-20 13:48:25 -0600372
Alex Kleine191ed62020-02-27 15:59:55 -0700373 # Write out all of the response formats.
374 for handler in output_handlers:
375 handler.write_from(output_msg)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600376
Alex Kleinc7d647f2020-01-06 12:00:48 -0700377 return result.returncode
Alex Klein146d4772019-06-20 13:48:25 -0600378
Alex Klein146d4772019-06-20 13:48:25 -0600379 def _GetMethod(self, module_name, method_name):
380 """Get the implementation of the method for the service module.
381
382 Args:
383 module_name (str): The name of the service module.
384 method_name (str): The name of the method.
385
386 Returns:
387 callable - The method.
388
389 Raises:
390 MethodNotFoundError when the method cannot be found in the module.
391 ServiceModuleNotFoundError when the service module cannot be imported.
392 """
393 try:
394 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
395 except ImportError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400396 raise ServiceControllerNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600397 try:
398 return getattr(module, method_name)
399 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400400 raise MethodNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600401
402
403def RegisterServices(router):
404 """Register all the services.
405
406 Args:
407 router (Router): The router.
408 """
Alex Klein4de25e82019-08-05 15:58:39 -0600409 router.Register(android_pb2)
Alex Klein54e38e32019-06-21 14:54:17 -0600410 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600411 router.Register(artifacts_pb2)
412 router.Register(binhost_pb2)
413 router.Register(depgraph_pb2)
Jett Rink17ed0f52020-09-25 17:14:31 -0600414 router.Register(firmware_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600415 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600416 router.Register(packages_pb2)
George Engelbrechtfe63c8c2019-08-31 22:51:29 -0600417 router.Register(payload_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600418 router.Register(sdk_pb2)
419 router.Register(sysroot_pb2)
420 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700421 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600422 logging.debug('Services registered successfully.')
423
424
425def GetRouter():
426 """Get a router that has had all of the services registered."""
427 router = Router()
428 RegisterServices(router)
429
430 return router