blob: 04b462da7188544e0e034195abd648c27657065e [file] [log] [blame]
Alex Klein146d4772019-06-20 13:48:25 -06001# -*- coding: utf-8 -*-
2# Copyright 2019 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
Alex Kleine0fa6422019-06-21 12:01:39 -06006"""Router class for the Build API.
7
8Handles routing requests to the appropriate controller and handles service
9registration.
10"""
Alex Klein146d4772019-06-20 13:48:25 -060011
12from __future__ import print_function
13
Alex Klein92341cd2020-02-27 14:11:04 -070014import collections
Alex Klein146d4772019-06-20 13:48:25 -060015import importlib
16import os
Mike Frysingeref94e4c2020-02-10 23:59:54 -050017import sys
Alex Klein146d4772019-06-20 13:48:25 -060018
Alex Klein146d4772019-06-20 13:48:25 -060019from google.protobuf import symbol_database
20
21from chromite.api import controller
22from chromite.api import field_handler
Alex Klein4de25e82019-08-05 15:58:39 -060023from chromite.api.gen.chromite.api import android_pb2
Alex Klein54e38e32019-06-21 14:54:17 -060024from chromite.api.gen.chromite.api import api_pb2
Alex Klein146d4772019-06-20 13:48:25 -060025from chromite.api.gen.chromite.api import artifacts_pb2
26from chromite.api.gen.chromite.api import binhost_pb2
27from chromite.api.gen.chromite.api import build_api_pb2
28from chromite.api.gen.chromite.api import depgraph_pb2
Jett Rink17ed0f52020-09-25 17:14:31 -060029from chromite.api.gen.chromite.api import firmware_pb2
Alex Klein146d4772019-06-20 13:48:25 -060030from chromite.api.gen.chromite.api import image_pb2
Alex Kleineb77ffa2019-05-28 14:47:44 -060031from chromite.api.gen.chromite.api import packages_pb2
George Engelbrechtfe63c8c2019-08-31 22:51:29 -060032from chromite.api.gen.chromite.api import payload_pb2
Alex Klein146d4772019-06-20 13:48:25 -060033from chromite.api.gen.chromite.api import sdk_pb2
34from chromite.api.gen.chromite.api import sysroot_pb2
35from chromite.api.gen.chromite.api import test_pb2
Tiancong Wangaf050172019-07-10 11:52:03 -070036from chromite.api.gen.chromite.api import toolchain_pb2
Alex Klein146d4772019-06-20 13:48:25 -060037from chromite.lib import cros_build_lib
38from chromite.lib import cros_logging as logging
39from chromite.lib import osutils
Alex Klein92341cd2020-02-27 14:11:04 -070040from chromite.utils import memoize
Alex Klein146d4772019-06-20 13:48:25 -060041
Alex Klein92341cd2020-02-27 14:11:04 -070042MethodData = collections.namedtuple(
43 'MethodData', ('service_descriptor', 'module_name', 'method_descriptor'))
Alex Klein146d4772019-06-20 13:48:25 -060044
Mike Frysingeref94e4c2020-02-10 23:59:54 -050045assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
46
47
Alex Klein146d4772019-06-20 13:48:25 -060048class Error(Exception):
49 """Base error class for the module."""
50
51
Alex Kleind3394c22020-06-16 14:05:06 -060052class InvalidSdkError(Error):
53 """Raised when the SDK is invalid or does not exist."""
54
55
Alex Klein146d4772019-06-20 13:48:25 -060056class CrosSdkNotRunError(Error):
57 """Raised when the cros_sdk command could not be run to enter the chroot."""
58
59
60# API Service Errors.
61class UnknownServiceError(Error):
62 """Error raised when the requested service has not been registered."""
63
64
65class ControllerModuleNotDefinedError(Error):
Alex Klein92341cd2020-02-27 14:11:04 -070066 """Error class for when no controller has been defined for a service."""
Alex Klein146d4772019-06-20 13:48:25 -060067
68
69class ServiceControllerNotFoundError(Error):
70 """Error raised when the service's controller cannot be imported."""
71
72
73# API Method Errors.
74class UnknownMethodError(Error):
Alex Klein92341cd2020-02-27 14:11:04 -070075 """The service has been defined in the proto, but the method has not."""
Alex Klein146d4772019-06-20 13:48:25 -060076
77
78class MethodNotFoundError(Error):
79 """The method's implementation cannot be found in the service's controller."""
80
81
82class Router(object):
83 """Encapsulates the request dispatching logic."""
84
Alex Kleine191ed62020-02-27 15:59:55 -070085 REEXEC_INPUT_FILE = 'input_proto'
86 REEXEC_OUTPUT_FILE = 'output_proto'
87 REEXEC_CONFIG_FILE = 'config_proto'
Alex Kleinbd6edf82019-07-18 10:30:49 -060088
Alex Klein146d4772019-06-20 13:48:25 -060089 def __init__(self):
90 self._services = {}
91 self._aliases = {}
92 # All imported generated messages get added to this symbol db.
93 self._sym_db = symbol_database.Default()
94
Alex Klein92341cd2020-02-27 14:11:04 -070095 # Save the service and method extension info for looking up
96 # configured extension data.
Alex Klein146d4772019-06-20 13:48:25 -060097 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
Alex Klein92341cd2020-02-27 14:11:04 -070098 self._svc_options_ext = extensions['service_options']
99 self._method_options_ext = extensions['method_options']
100
101 @memoize.Memoize
102 def _get_method_data(self, service_name, method_name):
103 """Get the descriptors and module name for the given Service/Method."""
104 try:
105 svc, module_name = self._services[service_name]
106 except KeyError:
107 raise UnknownServiceError(
108 'The %s service has not been registered.' % service_name)
109
110 try:
111 method_desc = svc.methods_by_name[method_name]
112 except KeyError:
113 raise UnknownMethodError('The %s method has not been defined in the %s '
114 'service.' % (method_name, service_name))
115
116 return MethodData(
117 service_descriptor=svc,
118 module_name=module_name,
119 method_descriptor=method_desc)
120
121 def _get_input_message_instance(self, service_name, method_name):
122 """Get an empty input message instance for the specified method."""
123 method_data = self._get_method_data(service_name, method_name)
124 return self._sym_db.GetPrototype(method_data.method_descriptor.input_type)()
125
126 def _get_output_message_instance(self, service_name, method_name):
127 """Get an empty output message instance for the specified method."""
128 method_data = self._get_method_data(service_name, method_name)
129 return self._sym_db.GetPrototype(
130 method_data.method_descriptor.output_type)()
131
132 def _get_module_name(self, service_name, method_name):
133 """Get the name of the module containing the endpoint implementation."""
134 return self._get_method_data(service_name, method_name).module_name
135
136 def _get_service_options(self, service_name, method_name):
137 """Get the configured service options for the endpoint."""
138 method_data = self._get_method_data(service_name, method_name)
139 svc_extensions = method_data.service_descriptor.GetOptions().Extensions
140 return svc_extensions[self._svc_options_ext]
141
142 def _get_method_options(self, service_name, method_name):
143 """Get the configured method options for the endpoint."""
144 method_data = self._get_method_data(service_name, method_name)
145 method_extensions = method_data.method_descriptor.GetOptions().Extensions
146 return method_extensions[self._method_options_ext]
Alex Klein146d4772019-06-20 13:48:25 -0600147
148 def Register(self, proto_module):
149 """Register the services from a generated proto module.
150
151 Args:
Alex Klein92341cd2020-02-27 14:11:04 -0700152 proto_module (module): The generated proto module to register.
Alex Klein146d4772019-06-20 13:48:25 -0600153
154 Raises:
155 ServiceModuleNotDefinedError when the service cannot be found in the
156 provided module.
157 """
158 services = proto_module.DESCRIPTOR.services_by_name
159 for service_name, svc in services.items():
Alex Klein92341cd2020-02-27 14:11:04 -0700160 module_name = svc.GetOptions().Extensions[self._svc_options_ext].module
Alex Klein146d4772019-06-20 13:48:25 -0600161
162 if not module_name:
163 raise ControllerModuleNotDefinedError(
164 'The module must be defined in the service definition: %s.%s' %
165 (proto_module, service_name))
166
167 self._services[svc.full_name] = (svc, module_name)
168
169 def ListMethods(self):
170 """List all methods registered with the router."""
171 services = []
172 for service_name, (svc, _module) in self._services.items():
Alex Klein6cce6f62021-03-02 14:24:05 -0700173 svc_visibility = getattr(
174 svc.GetOptions().Extensions[self._svc_options_ext],
175 'service_visibility', build_api_pb2.LV_VISIBLE)
176 if svc_visibility == build_api_pb2.LV_HIDDEN:
177 continue
178
Alex Klein146d4772019-06-20 13:48:25 -0600179 for method_name in svc.methods_by_name.keys():
Alex Klein6cce6f62021-03-02 14:24:05 -0700180 method_options = self._get_method_options(service_name, method_name)
181 method_visibility = getattr(method_options, 'method_visibility',
182 build_api_pb2.LV_VISIBLE)
183 if method_visibility == build_api_pb2.LV_HIDDEN:
184 continue
185
Alex Klein146d4772019-06-20 13:48:25 -0600186 services.append('%s/%s' % (service_name, method_name))
187
188 return sorted(services)
189
Alex Kleine191ed62020-02-27 15:59:55 -0700190 def Route(self, service_name, method_name, config, input_handler,
191 output_handlers, config_handler):
Alex Klein146d4772019-06-20 13:48:25 -0600192 """Dispatch the request.
193
194 Args:
195 service_name (str): The fully qualified service name.
196 method_name (str): The name of the method being called.
Alex Kleine191ed62020-02-27 15:59:55 -0700197 config (api_config.ApiConfig): The call configs.
198 input_handler (message_util.MessageHandler): The request message handler.
199 output_handlers (list[message_util.MessageHandler]): The response message
200 handlers.
201 config_handler (message_util.MessageHandler): The config message handler.
Alex Klein146d4772019-06-20 13:48:25 -0600202
203 Returns:
204 int: The return code.
205
206 Raises:
207 InvalidInputFileError when the input file cannot be read.
208 InvalidOutputFileError when the output file cannot be written.
209 ServiceModuleNotFoundError when the service module cannot be imported.
210 MethodNotFoundError when the method cannot be retrieved from the module.
211 """
Alex Klein92341cd2020-02-27 14:11:04 -0700212 input_msg = self._get_input_message_instance(service_name, method_name)
Alex Kleine191ed62020-02-27 15:59:55 -0700213 input_handler.read_into(input_msg)
Alex Klein146d4772019-06-20 13:48:25 -0600214
215 # Get an empty output message instance.
Alex Klein92341cd2020-02-27 14:11:04 -0700216 output_msg = self._get_output_message_instance(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600217
Alex Kleinbd6edf82019-07-18 10:30:49 -0600218 # Fetch the method options for chroot and method name overrides.
Alex Klein92341cd2020-02-27 14:11:04 -0700219 method_options = self._get_method_options(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600220
221 # Check the chroot settings before running.
Alex Klein92341cd2020-02-27 14:11:04 -0700222 service_options = self._get_service_options(service_name, method_name)
Alex Kleind1e9e5c2020-12-14 12:32:32 -0700223 if self._ChrootCheck(service_options, method_options, config):
Alex Klein146d4772019-06-20 13:48:25 -0600224 # Run inside the chroot instead.
225 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleine191ed62020-02-27 15:59:55 -0700226 return self._ReexecuteInside(input_msg, output_msg, config, input_handler,
227 output_handlers, config_handler,
228 service_name, method_name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600229
230 # Allow proto-based method name override.
231 if method_options.HasField('implementation_name'):
Alex Klein92341cd2020-02-27 14:11:04 -0700232 implementation_name = method_options.implementation_name
233 else:
234 implementation_name = method_name
Alex Klein146d4772019-06-20 13:48:25 -0600235
236 # Import the module and get the method.
Alex Klein92341cd2020-02-27 14:11:04 -0700237 module_name = self._get_module_name(service_name, method_name)
238 method_impl = self._GetMethod(module_name, implementation_name)
Alex Klein146d4772019-06-20 13:48:25 -0600239
240 # Successfully located; call and return.
Alex Klein69339cc2019-07-22 14:08:35 -0600241 return_code = method_impl(input_msg, output_msg, config)
Alex Klein146d4772019-06-20 13:48:25 -0600242 if return_code is None:
243 return_code = controller.RETURN_CODE_SUCCESS
244
Alex Kleine191ed62020-02-27 15:59:55 -0700245 for h in output_handlers:
246 h.write_from(output_msg)
Alex Klein146d4772019-06-20 13:48:25 -0600247
248 return return_code
249
Alex Kleind1e9e5c2020-12-14 12:32:32 -0700250 def _ChrootCheck(self, service_options, method_options,
251 config: 'api_config.ApiConfig'):
Alex Klein146d4772019-06-20 13:48:25 -0600252 """Check the chroot options, and execute assertion or note reexec as needed.
253
254 Args:
255 service_options (google.protobuf.Message): The service options.
256 method_options (google.protobuf.Message): The method options.
Alex Kleind1e9e5c2020-12-14 12:32:32 -0700257 config: The Build API call config instance.
Alex Klein146d4772019-06-20 13:48:25 -0600258
259 Returns:
260 bool - True iff it needs to be reexeced inside the chroot.
261
262 Raises:
263 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
264 """
Alex Kleind1e9e5c2020-12-14 12:32:32 -0700265 if not config.run_endpoint:
266 # Do not enter the chroot for validate only and mock calls.
267 return False
268
Alex Klein146d4772019-06-20 13:48:25 -0600269 chroot_assert = build_api_pb2.NO_ASSERTION
270 if method_options.HasField('method_chroot_assert'):
271 # Prefer the method option when set.
272 chroot_assert = method_options.method_chroot_assert
273 elif service_options.HasField('service_chroot_assert'):
274 # Fall back to the service option.
275 chroot_assert = service_options.service_chroot_assert
276
277 if chroot_assert == build_api_pb2.INSIDE:
278 return not cros_build_lib.IsInsideChroot()
279 elif chroot_assert == build_api_pb2.OUTSIDE:
280 # If it must be run outside we have to already be outside.
281 cros_build_lib.AssertOutsideChroot()
282
283 return False
284
Alex Kleine191ed62020-02-27 15:59:55 -0700285 def _ReexecuteInside(self, input_msg, output_msg, config, input_handler,
286 output_handlers, config_handler, service_name,
287 method_name):
Alex Klein146d4772019-06-20 13:48:25 -0600288 """Re-execute the service inside the chroot.
289
290 Args:
291 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600292 output_msg (Message): The empty output message instance.
Alex Kleine191ed62020-02-27 15:59:55 -0700293 config (api_config.ApiConfig): The call configs.
294 input_handler (MessageHandler): Input message handler.
295 output_handlers (list[MessageHandler]): Output message handlers.
296 config_handler (MessageHandler): Config message handler.
Alex Klein146d4772019-06-20 13:48:25 -0600297 service_name (str): The name of the service to run.
298 method_name (str): The name of the method to run.
299 """
Alex Kleinc7d647f2020-01-06 12:00:48 -0700300 # Parse the chroot and clear the chroot field in the input message.
301 chroot = field_handler.handle_chroot(input_msg)
Alex Klein915cce92019-12-17 14:19:50 -0700302
Alex Kleind3394c22020-06-16 14:05:06 -0600303 if not chroot.exists():
304 raise InvalidSdkError('Chroot does not exist.')
305
Alex Kleinc7d647f2020-01-06 12:00:48 -0700306 # Use a ContextManagerStack to avoid the deep nesting this many
307 # context managers introduces.
308 with cros_build_lib.ContextManagerStack() as stack:
309 # TempDirs setup.
310 tempdir = stack.Add(chroot.tempdir).tempdir
311 sync_tempdir = stack.Add(chroot.tempdir).tempdir
312 # The copy-paths-in context manager to handle Path messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700313 stack.Add(
314 field_handler.copy_paths_in,
315 input_msg,
316 chroot.tmp,
317 prefix=chroot.path)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700318 # The sync-directories context manager to handle SyncedDir messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700319 stack.Add(
320 field_handler.sync_dirs, input_msg, sync_tempdir, prefix=chroot.path)
Alex Klein146d4772019-06-20 13:48:25 -0600321
Alex Klein4089a492020-06-30 10:59:36 -0600322 # Parse goma.
Alex Kleinc7d647f2020-01-06 12:00:48 -0700323 chroot.goma = field_handler.handle_goma(input_msg, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700324
Alex Klein4089a492020-06-30 10:59:36 -0600325 # Build inside-chroot paths for the input, output, and config messages.
Alex Kleinc7d647f2020-01-06 12:00:48 -0700326 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
327 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
328 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
329 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Kleind815ca62020-01-10 12:21:30 -0700330 new_config = os.path.join(tempdir, self.REEXEC_CONFIG_FILE)
331 chroot_config = '/%s' % os.path.relpath(new_config, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700332
Alex Klein4089a492020-06-30 10:59:36 -0600333 # Setup the inside-chroot message files.
Alex Kleinc7d647f2020-01-06 12:00:48 -0700334 logging.info('Writing input message to: %s', new_input)
Alex Kleine191ed62020-02-27 15:59:55 -0700335 input_handler.write_from(input_msg, path=new_input)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700336 osutils.Touch(new_output)
Alex Kleind815ca62020-01-10 12:21:30 -0700337 logging.info('Writing config message to: %s', new_config)
Alex Kleine191ed62020-02-27 15:59:55 -0700338 config_handler.write_from(config.get_proto(), path=new_config)
Alex Klein146d4772019-06-20 13:48:25 -0600339
Alex Kleine191ed62020-02-27 15:59:55 -0700340 # We can use a single output to write the rest of them. Use the
341 # first one as the reexec output and just translate its output in
342 # the rest of the handlers after.
343 output_handler = output_handlers[0]
344
345 cmd = [
346 'build_api',
347 '%s/%s' % (service_name, method_name),
348 input_handler.input_arg,
349 chroot_input,
350 output_handler.output_arg,
351 chroot_output,
352 config_handler.config_arg,
353 chroot_config,
Alex Klein0b9edda2020-05-20 10:35:01 -0600354 '--debug',
Alex Kleine191ed62020-02-27 15:59:55 -0700355 ]
Alex Klein146d4772019-06-20 13:48:25 -0600356
Alex Kleinc7d647f2020-01-06 12:00:48 -0700357 try:
358 result = cros_build_lib.run(
359 cmd,
360 enter_chroot=True,
361 chroot_args=chroot.get_enter_args(),
362 check=False,
363 extra_env=chroot.env)
364 except cros_build_lib.RunCommandError:
365 # A non-zero return code will not result in an error, but one
366 # is still thrown when the command cannot be run in the first
367 # place. This is known to happen at least when the PATH does
368 # not include the chromite bin dir.
369 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein69339cc2019-07-22 14:08:35 -0600370
Alex Kleinc7d647f2020-01-06 12:00:48 -0700371 logging.info('Endpoint execution completed, return code: %d',
372 result.returncode)
Alex Klein146d4772019-06-20 13:48:25 -0600373
Alex Kleinc7d647f2020-01-06 12:00:48 -0700374 # Transfer result files out of the chroot.
Alex Kleine191ed62020-02-27 15:59:55 -0700375 output_handler.read_into(output_msg, path=new_output)
376 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Klein146d4772019-06-20 13:48:25 -0600377
Alex Kleine191ed62020-02-27 15:59:55 -0700378 # Write out all of the response formats.
379 for handler in output_handlers:
380 handler.write_from(output_msg)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600381
Alex Kleinc7d647f2020-01-06 12:00:48 -0700382 return result.returncode
Alex Klein146d4772019-06-20 13:48:25 -0600383
Alex Klein146d4772019-06-20 13:48:25 -0600384 def _GetMethod(self, module_name, method_name):
385 """Get the implementation of the method for the service module.
386
387 Args:
388 module_name (str): The name of the service module.
389 method_name (str): The name of the method.
390
391 Returns:
392 callable - The method.
393
394 Raises:
395 MethodNotFoundError when the method cannot be found in the module.
396 ServiceModuleNotFoundError when the service module cannot be imported.
397 """
398 try:
399 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
400 except ImportError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400401 raise ServiceControllerNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600402 try:
403 return getattr(module, method_name)
404 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400405 raise MethodNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600406
407
408def RegisterServices(router):
409 """Register all the services.
410
411 Args:
412 router (Router): The router.
413 """
Alex Klein4de25e82019-08-05 15:58:39 -0600414 router.Register(android_pb2)
Alex Klein54e38e32019-06-21 14:54:17 -0600415 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600416 router.Register(artifacts_pb2)
417 router.Register(binhost_pb2)
418 router.Register(depgraph_pb2)
Jett Rink17ed0f52020-09-25 17:14:31 -0600419 router.Register(firmware_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600420 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600421 router.Register(packages_pb2)
George Engelbrechtfe63c8c2019-08-31 22:51:29 -0600422 router.Register(payload_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600423 router.Register(sdk_pb2)
424 router.Register(sysroot_pb2)
425 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700426 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600427 logging.debug('Services registered successfully.')
428
429
430def GetRouter():
431 """Get a router that has had all of the services registered."""
432 router = Router()
433 RegisterServices(router)
434
435 return router