blob: 981eef9a5960f805c83d682e071165d797a15523 [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
29from chromite.api.gen.chromite.api import image_pb2
Alex Kleineb77ffa2019-05-28 14:47:44 -060030from chromite.api.gen.chromite.api import packages_pb2
George Engelbrechtfe63c8c2019-08-31 22:51:29 -060031from chromite.api.gen.chromite.api import payload_pb2
Alex Klein146d4772019-06-20 13:48:25 -060032from chromite.api.gen.chromite.api import sdk_pb2
33from chromite.api.gen.chromite.api import sysroot_pb2
34from chromite.api.gen.chromite.api import test_pb2
Tiancong Wangaf050172019-07-10 11:52:03 -070035from chromite.api.gen.chromite.api import toolchain_pb2
Alex Klein146d4772019-06-20 13:48:25 -060036from chromite.lib import cros_build_lib
37from chromite.lib import cros_logging as logging
38from chromite.lib import osutils
Alex Klein92341cd2020-02-27 14:11:04 -070039from chromite.utils import memoize
Alex Klein146d4772019-06-20 13:48:25 -060040
Alex Klein92341cd2020-02-27 14:11:04 -070041MethodData = collections.namedtuple(
42 'MethodData', ('service_descriptor', 'module_name', 'method_descriptor'))
Alex Klein146d4772019-06-20 13:48:25 -060043
Mike Frysingeref94e4c2020-02-10 23:59:54 -050044assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
45
46
Alex Klein146d4772019-06-20 13:48:25 -060047class Error(Exception):
48 """Base error class for the module."""
49
50
Alex Kleind3394c22020-06-16 14:05:06 -060051class InvalidSdkError(Error):
52 """Raised when the SDK is invalid or does not exist."""
53
54
Alex Klein146d4772019-06-20 13:48:25 -060055class CrosSdkNotRunError(Error):
56 """Raised when the cros_sdk command could not be run to enter the chroot."""
57
58
59# API Service Errors.
60class UnknownServiceError(Error):
61 """Error raised when the requested service has not been registered."""
62
63
64class ControllerModuleNotDefinedError(Error):
Alex Klein92341cd2020-02-27 14:11:04 -070065 """Error class for when no controller has been defined for a service."""
Alex Klein146d4772019-06-20 13:48:25 -060066
67
68class ServiceControllerNotFoundError(Error):
69 """Error raised when the service's controller cannot be imported."""
70
71
72# API Method Errors.
73class UnknownMethodError(Error):
Alex Klein92341cd2020-02-27 14:11:04 -070074 """The service has been defined in the proto, but the method has not."""
Alex Klein146d4772019-06-20 13:48:25 -060075
76
77class MethodNotFoundError(Error):
78 """The method's implementation cannot be found in the service's controller."""
79
80
81class Router(object):
82 """Encapsulates the request dispatching logic."""
83
Alex Kleine191ed62020-02-27 15:59:55 -070084 REEXEC_INPUT_FILE = 'input_proto'
85 REEXEC_OUTPUT_FILE = 'output_proto'
86 REEXEC_CONFIG_FILE = 'config_proto'
Alex Kleinbd6edf82019-07-18 10:30:49 -060087
Alex Klein146d4772019-06-20 13:48:25 -060088 def __init__(self):
89 self._services = {}
90 self._aliases = {}
91 # All imported generated messages get added to this symbol db.
92 self._sym_db = symbol_database.Default()
93
Alex Klein92341cd2020-02-27 14:11:04 -070094 # Save the service and method extension info for looking up
95 # configured extension data.
Alex Klein146d4772019-06-20 13:48:25 -060096 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
Alex Klein92341cd2020-02-27 14:11:04 -070097 self._svc_options_ext = extensions['service_options']
98 self._method_options_ext = extensions['method_options']
99
100 @memoize.Memoize
101 def _get_method_data(self, service_name, method_name):
102 """Get the descriptors and module name for the given Service/Method."""
103 try:
104 svc, module_name = self._services[service_name]
105 except KeyError:
106 raise UnknownServiceError(
107 'The %s service has not been registered.' % service_name)
108
109 try:
110 method_desc = svc.methods_by_name[method_name]
111 except KeyError:
112 raise UnknownMethodError('The %s method has not been defined in the %s '
113 'service.' % (method_name, service_name))
114
115 return MethodData(
116 service_descriptor=svc,
117 module_name=module_name,
118 method_descriptor=method_desc)
119
120 def _get_input_message_instance(self, service_name, method_name):
121 """Get an empty input message instance for the specified method."""
122 method_data = self._get_method_data(service_name, method_name)
123 return self._sym_db.GetPrototype(method_data.method_descriptor.input_type)()
124
125 def _get_output_message_instance(self, service_name, method_name):
126 """Get an empty output message instance for the specified method."""
127 method_data = self._get_method_data(service_name, method_name)
128 return self._sym_db.GetPrototype(
129 method_data.method_descriptor.output_type)()
130
131 def _get_module_name(self, service_name, method_name):
132 """Get the name of the module containing the endpoint implementation."""
133 return self._get_method_data(service_name, method_name).module_name
134
135 def _get_service_options(self, service_name, method_name):
136 """Get the configured service options for the endpoint."""
137 method_data = self._get_method_data(service_name, method_name)
138 svc_extensions = method_data.service_descriptor.GetOptions().Extensions
139 return svc_extensions[self._svc_options_ext]
140
141 def _get_method_options(self, service_name, method_name):
142 """Get the configured method options for the endpoint."""
143 method_data = self._get_method_data(service_name, method_name)
144 method_extensions = method_data.method_descriptor.GetOptions().Extensions
145 return method_extensions[self._method_options_ext]
Alex Klein146d4772019-06-20 13:48:25 -0600146
147 def Register(self, proto_module):
148 """Register the services from a generated proto module.
149
150 Args:
Alex Klein92341cd2020-02-27 14:11:04 -0700151 proto_module (module): The generated proto module to register.
Alex Klein146d4772019-06-20 13:48:25 -0600152
153 Raises:
154 ServiceModuleNotDefinedError when the service cannot be found in the
155 provided module.
156 """
157 services = proto_module.DESCRIPTOR.services_by_name
158 for service_name, svc in services.items():
Alex Klein92341cd2020-02-27 14:11:04 -0700159 module_name = svc.GetOptions().Extensions[self._svc_options_ext].module
Alex Klein146d4772019-06-20 13:48:25 -0600160
161 if not module_name:
162 raise ControllerModuleNotDefinedError(
163 'The module must be defined in the service definition: %s.%s' %
164 (proto_module, service_name))
165
166 self._services[svc.full_name] = (svc, module_name)
167
168 def ListMethods(self):
169 """List all methods registered with the router."""
170 services = []
171 for service_name, (svc, _module) in self._services.items():
172 for method_name in svc.methods_by_name.keys():
173 services.append('%s/%s' % (service_name, method_name))
174
175 return sorted(services)
176
Alex Kleine191ed62020-02-27 15:59:55 -0700177 def Route(self, service_name, method_name, config, input_handler,
178 output_handlers, config_handler):
Alex Klein146d4772019-06-20 13:48:25 -0600179 """Dispatch the request.
180
181 Args:
182 service_name (str): The fully qualified service name.
183 method_name (str): The name of the method being called.
Alex Kleine191ed62020-02-27 15:59:55 -0700184 config (api_config.ApiConfig): The call configs.
185 input_handler (message_util.MessageHandler): The request message handler.
186 output_handlers (list[message_util.MessageHandler]): The response message
187 handlers.
188 config_handler (message_util.MessageHandler): The config message handler.
Alex Klein146d4772019-06-20 13:48:25 -0600189
190 Returns:
191 int: The return code.
192
193 Raises:
194 InvalidInputFileError when the input file cannot be read.
195 InvalidOutputFileError when the output file cannot be written.
196 ServiceModuleNotFoundError when the service module cannot be imported.
197 MethodNotFoundError when the method cannot be retrieved from the module.
198 """
Alex Klein92341cd2020-02-27 14:11:04 -0700199 input_msg = self._get_input_message_instance(service_name, method_name)
Alex Kleine191ed62020-02-27 15:59:55 -0700200 input_handler.read_into(input_msg)
Alex Klein146d4772019-06-20 13:48:25 -0600201
202 # Get an empty output message instance.
Alex Klein92341cd2020-02-27 14:11:04 -0700203 output_msg = self._get_output_message_instance(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600204
Alex Kleinbd6edf82019-07-18 10:30:49 -0600205 # Fetch the method options for chroot and method name overrides.
Alex Klein92341cd2020-02-27 14:11:04 -0700206 method_options = self._get_method_options(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600207
208 # Check the chroot settings before running.
Alex Klein92341cd2020-02-27 14:11:04 -0700209 service_options = self._get_service_options(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600210 if self._ChrootCheck(service_options, method_options):
211 # Run inside the chroot instead.
212 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleine191ed62020-02-27 15:59:55 -0700213 return self._ReexecuteInside(input_msg, output_msg, config, input_handler,
214 output_handlers, config_handler,
215 service_name, method_name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600216
217 # Allow proto-based method name override.
218 if method_options.HasField('implementation_name'):
Alex Klein92341cd2020-02-27 14:11:04 -0700219 implementation_name = method_options.implementation_name
220 else:
221 implementation_name = method_name
Alex Klein146d4772019-06-20 13:48:25 -0600222
223 # Import the module and get the method.
Alex Klein92341cd2020-02-27 14:11:04 -0700224 module_name = self._get_module_name(service_name, method_name)
225 method_impl = self._GetMethod(module_name, implementation_name)
Alex Klein146d4772019-06-20 13:48:25 -0600226
227 # Successfully located; call and return.
Alex Klein69339cc2019-07-22 14:08:35 -0600228 return_code = method_impl(input_msg, output_msg, config)
Alex Klein146d4772019-06-20 13:48:25 -0600229 if return_code is None:
230 return_code = controller.RETURN_CODE_SUCCESS
231
Alex Kleine191ed62020-02-27 15:59:55 -0700232 for h in output_handlers:
233 h.write_from(output_msg)
Alex Klein146d4772019-06-20 13:48:25 -0600234
235 return return_code
236
237 def _ChrootCheck(self, service_options, method_options):
238 """Check the chroot options, and execute assertion or note reexec as needed.
239
240 Args:
241 service_options (google.protobuf.Message): The service options.
242 method_options (google.protobuf.Message): The method options.
243
244 Returns:
245 bool - True iff it needs to be reexeced inside the chroot.
246
247 Raises:
248 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
249 """
250 chroot_assert = build_api_pb2.NO_ASSERTION
251 if method_options.HasField('method_chroot_assert'):
252 # Prefer the method option when set.
253 chroot_assert = method_options.method_chroot_assert
254 elif service_options.HasField('service_chroot_assert'):
255 # Fall back to the service option.
256 chroot_assert = service_options.service_chroot_assert
257
258 if chroot_assert == build_api_pb2.INSIDE:
259 return not cros_build_lib.IsInsideChroot()
260 elif chroot_assert == build_api_pb2.OUTSIDE:
261 # If it must be run outside we have to already be outside.
262 cros_build_lib.AssertOutsideChroot()
263
264 return False
265
Alex Kleine191ed62020-02-27 15:59:55 -0700266 def _ReexecuteInside(self, input_msg, output_msg, config, input_handler,
267 output_handlers, config_handler, service_name,
268 method_name):
Alex Klein146d4772019-06-20 13:48:25 -0600269 """Re-execute the service inside the chroot.
270
271 Args:
272 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600273 output_msg (Message): The empty output message instance.
Alex Kleine191ed62020-02-27 15:59:55 -0700274 config (api_config.ApiConfig): The call configs.
275 input_handler (MessageHandler): Input message handler.
276 output_handlers (list[MessageHandler]): Output message handlers.
277 config_handler (MessageHandler): Config message handler.
Alex Klein146d4772019-06-20 13:48:25 -0600278 service_name (str): The name of the service to run.
279 method_name (str): The name of the method to run.
280 """
Alex Kleinc7d647f2020-01-06 12:00:48 -0700281 # Parse the chroot and clear the chroot field in the input message.
282 chroot = field_handler.handle_chroot(input_msg)
Alex Klein915cce92019-12-17 14:19:50 -0700283
Alex Kleind3394c22020-06-16 14:05:06 -0600284 if not chroot.exists():
285 raise InvalidSdkError('Chroot does not exist.')
286
Alex Kleinc7d647f2020-01-06 12:00:48 -0700287 # Use a ContextManagerStack to avoid the deep nesting this many
288 # context managers introduces.
289 with cros_build_lib.ContextManagerStack() as stack:
290 # TempDirs setup.
291 tempdir = stack.Add(chroot.tempdir).tempdir
292 sync_tempdir = stack.Add(chroot.tempdir).tempdir
293 # The copy-paths-in context manager to handle Path messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700294 stack.Add(
295 field_handler.copy_paths_in,
296 input_msg,
297 chroot.tmp,
298 prefix=chroot.path)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700299 # The sync-directories context manager to handle SyncedDir messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700300 stack.Add(
301 field_handler.sync_dirs, input_msg, sync_tempdir, prefix=chroot.path)
Alex Klein146d4772019-06-20 13:48:25 -0600302
Alex Kleinc7d647f2020-01-06 12:00:48 -0700303 chroot.goma = field_handler.handle_goma(input_msg, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700304
Alex Kleinc7d647f2020-01-06 12:00:48 -0700305 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
306 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
307 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
308 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Kleind815ca62020-01-10 12:21:30 -0700309 new_config = os.path.join(tempdir, self.REEXEC_CONFIG_FILE)
310 chroot_config = '/%s' % os.path.relpath(new_config, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700311
Alex Kleinc7d647f2020-01-06 12:00:48 -0700312 logging.info('Writing input message to: %s', new_input)
Alex Kleine191ed62020-02-27 15:59:55 -0700313 input_handler.write_from(input_msg, path=new_input)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700314 osutils.Touch(new_output)
Alex Kleind815ca62020-01-10 12:21:30 -0700315 logging.info('Writing config message to: %s', new_config)
Alex Kleine191ed62020-02-27 15:59:55 -0700316 config_handler.write_from(config.get_proto(), path=new_config)
Alex Klein146d4772019-06-20 13:48:25 -0600317
Alex Kleine191ed62020-02-27 15:59:55 -0700318 # We can use a single output to write the rest of them. Use the
319 # first one as the reexec output and just translate its output in
320 # the rest of the handlers after.
321 output_handler = output_handlers[0]
322
323 cmd = [
324 'build_api',
325 '%s/%s' % (service_name, method_name),
326 input_handler.input_arg,
327 chroot_input,
328 output_handler.output_arg,
329 chroot_output,
330 config_handler.config_arg,
331 chroot_config,
Alex Klein0b9edda2020-05-20 10:35:01 -0600332 '--debug',
Alex Kleine191ed62020-02-27 15:59:55 -0700333 ]
Alex Klein146d4772019-06-20 13:48:25 -0600334
Alex Kleinc7d647f2020-01-06 12:00:48 -0700335 try:
336 result = cros_build_lib.run(
337 cmd,
338 enter_chroot=True,
339 chroot_args=chroot.get_enter_args(),
340 check=False,
341 extra_env=chroot.env)
342 except cros_build_lib.RunCommandError:
343 # A non-zero return code will not result in an error, but one
344 # is still thrown when the command cannot be run in the first
345 # place. This is known to happen at least when the PATH does
346 # not include the chromite bin dir.
347 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein69339cc2019-07-22 14:08:35 -0600348
Alex Kleinc7d647f2020-01-06 12:00:48 -0700349 logging.info('Endpoint execution completed, return code: %d',
350 result.returncode)
Alex Klein146d4772019-06-20 13:48:25 -0600351
Alex Kleinc7d647f2020-01-06 12:00:48 -0700352 # Transfer result files out of the chroot.
Alex Kleine191ed62020-02-27 15:59:55 -0700353 output_handler.read_into(output_msg, path=new_output)
354 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Klein146d4772019-06-20 13:48:25 -0600355
Alex Kleine191ed62020-02-27 15:59:55 -0700356 # Write out all of the response formats.
357 for handler in output_handlers:
358 handler.write_from(output_msg)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600359
Alex Kleinc7d647f2020-01-06 12:00:48 -0700360 return result.returncode
Alex Klein146d4772019-06-20 13:48:25 -0600361
Alex Klein146d4772019-06-20 13:48:25 -0600362 def _GetMethod(self, module_name, method_name):
363 """Get the implementation of the method for the service module.
364
365 Args:
366 module_name (str): The name of the service module.
367 method_name (str): The name of the method.
368
369 Returns:
370 callable - The method.
371
372 Raises:
373 MethodNotFoundError when the method cannot be found in the module.
374 ServiceModuleNotFoundError when the service module cannot be imported.
375 """
376 try:
377 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
378 except ImportError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400379 raise ServiceControllerNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600380 try:
381 return getattr(module, method_name)
382 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400383 raise MethodNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600384
385
386def RegisterServices(router):
387 """Register all the services.
388
389 Args:
390 router (Router): The router.
391 """
Alex Klein4de25e82019-08-05 15:58:39 -0600392 router.Register(android_pb2)
Alex Klein54e38e32019-06-21 14:54:17 -0600393 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600394 router.Register(artifacts_pb2)
395 router.Register(binhost_pb2)
396 router.Register(depgraph_pb2)
397 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600398 router.Register(packages_pb2)
George Engelbrechtfe63c8c2019-08-31 22:51:29 -0600399 router.Register(payload_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600400 router.Register(sdk_pb2)
401 router.Register(sysroot_pb2)
402 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700403 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600404 logging.debug('Services registered successfully.')
405
406
407def GetRouter():
408 """Get a router that has had all of the services registered."""
409 router = Router()
410 RegisterServices(router)
411
412 return router