blob: 5d6de98783ce7b8070ea8ac6c9a2600057e52433 [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 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():
168 for method_name in svc.methods_by_name.keys():
169 services.append('%s/%s' % (service_name, method_name))
170
171 return sorted(services)
172
Alex Kleine191ed62020-02-27 15:59:55 -0700173 def Route(self, service_name, method_name, config, input_handler,
174 output_handlers, config_handler):
Alex Klein146d4772019-06-20 13:48:25 -0600175 """Dispatch the request.
176
177 Args:
178 service_name (str): The fully qualified service name.
179 method_name (str): The name of the method being called.
Alex Kleine191ed62020-02-27 15:59:55 -0700180 config (api_config.ApiConfig): The call configs.
181 input_handler (message_util.MessageHandler): The request message handler.
182 output_handlers (list[message_util.MessageHandler]): The response message
183 handlers.
184 config_handler (message_util.MessageHandler): The config message handler.
Alex Klein146d4772019-06-20 13:48:25 -0600185
186 Returns:
187 int: The return code.
188
189 Raises:
190 InvalidInputFileError when the input file cannot be read.
191 InvalidOutputFileError when the output file cannot be written.
192 ServiceModuleNotFoundError when the service module cannot be imported.
193 MethodNotFoundError when the method cannot be retrieved from the module.
194 """
Alex Klein92341cd2020-02-27 14:11:04 -0700195 input_msg = self._get_input_message_instance(service_name, method_name)
Alex Kleine191ed62020-02-27 15:59:55 -0700196 input_handler.read_into(input_msg)
Alex Klein146d4772019-06-20 13:48:25 -0600197
198 # Get an empty output message instance.
Alex Klein92341cd2020-02-27 14:11:04 -0700199 output_msg = self._get_output_message_instance(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600200
Alex Kleinbd6edf82019-07-18 10:30:49 -0600201 # Fetch the method options for chroot and method name overrides.
Alex Klein92341cd2020-02-27 14:11:04 -0700202 method_options = self._get_method_options(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600203
204 # Check the chroot settings before running.
Alex Klein92341cd2020-02-27 14:11:04 -0700205 service_options = self._get_service_options(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600206 if self._ChrootCheck(service_options, method_options):
207 # Run inside the chroot instead.
208 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleine191ed62020-02-27 15:59:55 -0700209 return self._ReexecuteInside(input_msg, output_msg, config, input_handler,
210 output_handlers, config_handler,
211 service_name, method_name)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600212
213 # Allow proto-based method name override.
214 if method_options.HasField('implementation_name'):
Alex Klein92341cd2020-02-27 14:11:04 -0700215 implementation_name = method_options.implementation_name
216 else:
217 implementation_name = method_name
Alex Klein146d4772019-06-20 13:48:25 -0600218
219 # Import the module and get the method.
Alex Klein92341cd2020-02-27 14:11:04 -0700220 module_name = self._get_module_name(service_name, method_name)
221 method_impl = self._GetMethod(module_name, implementation_name)
Alex Klein146d4772019-06-20 13:48:25 -0600222
223 # Successfully located; call and return.
Alex Klein69339cc2019-07-22 14:08:35 -0600224 return_code = method_impl(input_msg, output_msg, config)
Alex Klein146d4772019-06-20 13:48:25 -0600225 if return_code is None:
226 return_code = controller.RETURN_CODE_SUCCESS
227
Alex Kleine191ed62020-02-27 15:59:55 -0700228 for h in output_handlers:
229 h.write_from(output_msg)
Alex Klein146d4772019-06-20 13:48:25 -0600230
231 return return_code
232
233 def _ChrootCheck(self, service_options, method_options):
234 """Check the chroot options, and execute assertion or note reexec as needed.
235
236 Args:
237 service_options (google.protobuf.Message): The service options.
238 method_options (google.protobuf.Message): The method options.
239
240 Returns:
241 bool - True iff it needs to be reexeced inside the chroot.
242
243 Raises:
244 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
245 """
246 chroot_assert = build_api_pb2.NO_ASSERTION
247 if method_options.HasField('method_chroot_assert'):
248 # Prefer the method option when set.
249 chroot_assert = method_options.method_chroot_assert
250 elif service_options.HasField('service_chroot_assert'):
251 # Fall back to the service option.
252 chroot_assert = service_options.service_chroot_assert
253
254 if chroot_assert == build_api_pb2.INSIDE:
255 return not cros_build_lib.IsInsideChroot()
256 elif chroot_assert == build_api_pb2.OUTSIDE:
257 # If it must be run outside we have to already be outside.
258 cros_build_lib.AssertOutsideChroot()
259
260 return False
261
Alex Kleine191ed62020-02-27 15:59:55 -0700262 def _ReexecuteInside(self, input_msg, output_msg, config, input_handler,
263 output_handlers, config_handler, service_name,
264 method_name):
Alex Klein146d4772019-06-20 13:48:25 -0600265 """Re-execute the service inside the chroot.
266
267 Args:
268 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600269 output_msg (Message): The empty output message instance.
Alex Kleine191ed62020-02-27 15:59:55 -0700270 config (api_config.ApiConfig): The call configs.
271 input_handler (MessageHandler): Input message handler.
272 output_handlers (list[MessageHandler]): Output message handlers.
273 config_handler (MessageHandler): Config message handler.
Alex Klein146d4772019-06-20 13:48:25 -0600274 service_name (str): The name of the service to run.
275 method_name (str): The name of the method to run.
276 """
Alex Kleinc7d647f2020-01-06 12:00:48 -0700277 # Parse the chroot and clear the chroot field in the input message.
278 chroot = field_handler.handle_chroot(input_msg)
Alex Klein915cce92019-12-17 14:19:50 -0700279
Alex Kleinc7d647f2020-01-06 12:00:48 -0700280 # Use a ContextManagerStack to avoid the deep nesting this many
281 # context managers introduces.
282 with cros_build_lib.ContextManagerStack() as stack:
283 # TempDirs setup.
284 tempdir = stack.Add(chroot.tempdir).tempdir
285 sync_tempdir = stack.Add(chroot.tempdir).tempdir
286 # The copy-paths-in context manager to handle Path messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700287 stack.Add(
288 field_handler.copy_paths_in,
289 input_msg,
290 chroot.tmp,
291 prefix=chroot.path)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700292 # The sync-directories context manager to handle SyncedDir messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700293 stack.Add(
294 field_handler.sync_dirs, input_msg, sync_tempdir, prefix=chroot.path)
Alex Klein146d4772019-06-20 13:48:25 -0600295
Alex Kleinc7d647f2020-01-06 12:00:48 -0700296 chroot.goma = field_handler.handle_goma(input_msg, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700297
Alex Kleinc7d647f2020-01-06 12:00:48 -0700298 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
299 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
300 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
301 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Kleind815ca62020-01-10 12:21:30 -0700302 new_config = os.path.join(tempdir, self.REEXEC_CONFIG_FILE)
303 chroot_config = '/%s' % os.path.relpath(new_config, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700304
Alex Kleinc7d647f2020-01-06 12:00:48 -0700305 logging.info('Writing input message to: %s', new_input)
Alex Kleine191ed62020-02-27 15:59:55 -0700306 input_handler.write_from(input_msg, path=new_input)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700307 osutils.Touch(new_output)
Alex Kleind815ca62020-01-10 12:21:30 -0700308 logging.info('Writing config message to: %s', new_config)
Alex Kleine191ed62020-02-27 15:59:55 -0700309 config_handler.write_from(config.get_proto(), path=new_config)
Alex Klein146d4772019-06-20 13:48:25 -0600310
Alex Kleine191ed62020-02-27 15:59:55 -0700311 # We can use a single output to write the rest of them. Use the
312 # first one as the reexec output and just translate its output in
313 # the rest of the handlers after.
314 output_handler = output_handlers[0]
315
316 cmd = [
317 'build_api',
318 '%s/%s' % (service_name, method_name),
319 input_handler.input_arg,
320 chroot_input,
321 output_handler.output_arg,
322 chroot_output,
323 config_handler.config_arg,
324 chroot_config,
325 ]
Alex Klein146d4772019-06-20 13:48:25 -0600326
Alex Kleinc7d647f2020-01-06 12:00:48 -0700327 try:
328 result = cros_build_lib.run(
329 cmd,
330 enter_chroot=True,
331 chroot_args=chroot.get_enter_args(),
332 check=False,
333 extra_env=chroot.env)
334 except cros_build_lib.RunCommandError:
335 # A non-zero return code will not result in an error, but one
336 # is still thrown when the command cannot be run in the first
337 # place. This is known to happen at least when the PATH does
338 # not include the chromite bin dir.
339 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein69339cc2019-07-22 14:08:35 -0600340
Alex Kleinc7d647f2020-01-06 12:00:48 -0700341 logging.info('Endpoint execution completed, return code: %d',
342 result.returncode)
Alex Klein146d4772019-06-20 13:48:25 -0600343
Alex Kleinc7d647f2020-01-06 12:00:48 -0700344 # Transfer result files out of the chroot.
Alex Kleine191ed62020-02-27 15:59:55 -0700345 output_handler.read_into(output_msg, path=new_output)
346 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Klein146d4772019-06-20 13:48:25 -0600347
Alex Kleine191ed62020-02-27 15:59:55 -0700348 # Write out all of the response formats.
349 for handler in output_handlers:
350 handler.write_from(output_msg)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600351
Alex Kleinc7d647f2020-01-06 12:00:48 -0700352 return result.returncode
Alex Klein146d4772019-06-20 13:48:25 -0600353
Alex Klein146d4772019-06-20 13:48:25 -0600354 def _GetMethod(self, module_name, method_name):
355 """Get the implementation of the method for the service module.
356
357 Args:
358 module_name (str): The name of the service module.
359 method_name (str): The name of the method.
360
361 Returns:
362 callable - The method.
363
364 Raises:
365 MethodNotFoundError when the method cannot be found in the module.
366 ServiceModuleNotFoundError when the service module cannot be imported.
367 """
368 try:
369 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
370 except ImportError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400371 raise ServiceControllerNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600372 try:
373 return getattr(module, method_name)
374 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400375 raise MethodNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600376
377
378def RegisterServices(router):
379 """Register all the services.
380
381 Args:
382 router (Router): The router.
383 """
Alex Klein4de25e82019-08-05 15:58:39 -0600384 router.Register(android_pb2)
Alex Klein54e38e32019-06-21 14:54:17 -0600385 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600386 router.Register(artifacts_pb2)
387 router.Register(binhost_pb2)
388 router.Register(depgraph_pb2)
389 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600390 router.Register(packages_pb2)
George Engelbrechtfe63c8c2019-08-31 22:51:29 -0600391 router.Register(payload_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600392 router.Register(sdk_pb2)
393 router.Register(sysroot_pb2)
394 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700395 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600396 logging.debug('Services registered successfully.')
397
398
399def GetRouter():
400 """Get a router that has had all of the services registered."""
401 router = Router()
402 RegisterServices(router)
403
404 return router