blob: 25c8a3d87916637dfc9620170c3dbf0f8b346148 [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
19from google.protobuf import json_format
20from google.protobuf import symbol_database
21
22from chromite.api import controller
23from chromite.api import field_handler
Alex Klein4de25e82019-08-05 15:58:39 -060024from chromite.api.gen.chromite.api import android_pb2
Alex Klein54e38e32019-06-21 14:54:17 -060025from chromite.api.gen.chromite.api import api_pb2
Alex Klein146d4772019-06-20 13:48:25 -060026from chromite.api.gen.chromite.api import artifacts_pb2
27from chromite.api.gen.chromite.api import binhost_pb2
28from chromite.api.gen.chromite.api import build_api_pb2
29from chromite.api.gen.chromite.api import depgraph_pb2
30from 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
52class InvalidInputFileError(Error):
53 """Raised when the input file cannot be read."""
54
55
56class InvalidInputFormatError(Error):
57 """Raised when the passed input protobuf can't be parsed."""
58
59
60class InvalidOutputFileError(Error):
61 """Raised when the output file cannot be written."""
62
63
64class CrosSdkNotRunError(Error):
65 """Raised when the cros_sdk command could not be run to enter the chroot."""
66
67
68# API Service Errors.
69class UnknownServiceError(Error):
70 """Error raised when the requested service has not been registered."""
71
72
73class ControllerModuleNotDefinedError(Error):
Alex Klein92341cd2020-02-27 14:11:04 -070074 """Error class for when no controller has been defined for a service."""
Alex Klein146d4772019-06-20 13:48:25 -060075
76
77class ServiceControllerNotFoundError(Error):
78 """Error raised when the service's controller cannot be imported."""
79
80
81# API Method Errors.
82class UnknownMethodError(Error):
Alex Klein92341cd2020-02-27 14:11:04 -070083 """The service has been defined in the proto, but the method has not."""
Alex Klein146d4772019-06-20 13:48:25 -060084
85
86class MethodNotFoundError(Error):
87 """The method's implementation cannot be found in the service's controller."""
88
89
90class Router(object):
91 """Encapsulates the request dispatching logic."""
92
Alex Kleinbd6edf82019-07-18 10:30:49 -060093 REEXEC_INPUT_FILE = 'input.json'
94 REEXEC_OUTPUT_FILE = 'output.json'
Alex Kleind815ca62020-01-10 12:21:30 -070095 REEXEC_CONFIG_FILE = 'config.json'
Alex Kleinbd6edf82019-07-18 10:30:49 -060096
Alex Klein146d4772019-06-20 13:48:25 -060097 def __init__(self):
98 self._services = {}
99 self._aliases = {}
100 # All imported generated messages get added to this symbol db.
101 self._sym_db = symbol_database.Default()
102
Alex Klein92341cd2020-02-27 14:11:04 -0700103 # Save the service and method extension info for looking up
104 # configured extension data.
Alex Klein146d4772019-06-20 13:48:25 -0600105 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
Alex Klein92341cd2020-02-27 14:11:04 -0700106 self._svc_options_ext = extensions['service_options']
107 self._method_options_ext = extensions['method_options']
108
109 @memoize.Memoize
110 def _get_method_data(self, service_name, method_name):
111 """Get the descriptors and module name for the given Service/Method."""
112 try:
113 svc, module_name = self._services[service_name]
114 except KeyError:
115 raise UnknownServiceError(
116 'The %s service has not been registered.' % service_name)
117
118 try:
119 method_desc = svc.methods_by_name[method_name]
120 except KeyError:
121 raise UnknownMethodError('The %s method has not been defined in the %s '
122 'service.' % (method_name, service_name))
123
124 return MethodData(
125 service_descriptor=svc,
126 module_name=module_name,
127 method_descriptor=method_desc)
128
129 def _get_input_message_instance(self, service_name, method_name):
130 """Get an empty input message instance for the specified method."""
131 method_data = self._get_method_data(service_name, method_name)
132 return self._sym_db.GetPrototype(method_data.method_descriptor.input_type)()
133
134 def _get_output_message_instance(self, service_name, method_name):
135 """Get an empty output message instance for the specified method."""
136 method_data = self._get_method_data(service_name, method_name)
137 return self._sym_db.GetPrototype(
138 method_data.method_descriptor.output_type)()
139
140 def _get_module_name(self, service_name, method_name):
141 """Get the name of the module containing the endpoint implementation."""
142 return self._get_method_data(service_name, method_name).module_name
143
144 def _get_service_options(self, service_name, method_name):
145 """Get the configured service options for the endpoint."""
146 method_data = self._get_method_data(service_name, method_name)
147 svc_extensions = method_data.service_descriptor.GetOptions().Extensions
148 return svc_extensions[self._svc_options_ext]
149
150 def _get_method_options(self, service_name, method_name):
151 """Get the configured method options for the endpoint."""
152 method_data = self._get_method_data(service_name, method_name)
153 method_extensions = method_data.method_descriptor.GetOptions().Extensions
154 return method_extensions[self._method_options_ext]
Alex Klein146d4772019-06-20 13:48:25 -0600155
156 def Register(self, proto_module):
157 """Register the services from a generated proto module.
158
159 Args:
Alex Klein92341cd2020-02-27 14:11:04 -0700160 proto_module (module): The generated proto module to register.
Alex Klein146d4772019-06-20 13:48:25 -0600161
162 Raises:
163 ServiceModuleNotDefinedError when the service cannot be found in the
164 provided module.
165 """
166 services = proto_module.DESCRIPTOR.services_by_name
167 for service_name, svc in services.items():
Alex Klein92341cd2020-02-27 14:11:04 -0700168 module_name = svc.GetOptions().Extensions[self._svc_options_ext].module
Alex Klein146d4772019-06-20 13:48:25 -0600169
170 if not module_name:
171 raise ControllerModuleNotDefinedError(
172 'The module must be defined in the service definition: %s.%s' %
173 (proto_module, service_name))
174
175 self._services[svc.full_name] = (svc, module_name)
176
177 def ListMethods(self):
178 """List all methods registered with the router."""
179 services = []
180 for service_name, (svc, _module) in self._services.items():
181 for method_name in svc.methods_by_name.keys():
182 services.append('%s/%s' % (service_name, method_name))
183
184 return sorted(services)
185
Alex Klein69339cc2019-07-22 14:08:35 -0600186 def Route(self, service_name, method_name, input_path, output_path, config):
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.
192 input_path (str): The path to the input message file.
193 output_path (str): The path where the output message should be written.
Alex Klein69339cc2019-07-22 14:08:35 -0600194 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600195
196 Returns:
197 int: The return code.
198
199 Raises:
200 InvalidInputFileError when the input file cannot be read.
201 InvalidOutputFileError when the output file cannot be written.
202 ServiceModuleNotFoundError when the service module cannot be imported.
203 MethodNotFoundError when the method cannot be retrieved from the module.
204 """
205 try:
206 input_json = osutils.ReadFile(input_path).strip()
207 except IOError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400208 raise InvalidInputFileError('Unable to read input file: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600209
Alex Klein92341cd2020-02-27 14:11:04 -0700210 input_msg = self._get_input_message_instance(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600211 try:
212 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
213 except json_format.ParseError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400214 raise InvalidInputFormatError('Unable to parse the input json: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600215
216 # Get an empty output message instance.
Alex Klein92341cd2020-02-27 14:11:04 -0700217 output_msg = self._get_output_message_instance(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600218
Alex Kleinbd6edf82019-07-18 10:30:49 -0600219 # Fetch the method options for chroot and method name overrides.
Alex Klein92341cd2020-02-27 14:11:04 -0700220 method_options = self._get_method_options(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600221
222 # Check the chroot settings before running.
Alex Klein92341cd2020-02-27 14:11:04 -0700223 service_options = self._get_service_options(service_name, method_name)
Alex Klein146d4772019-06-20 13:48:25 -0600224 if self._ChrootCheck(service_options, method_options):
225 # Run inside the chroot instead.
226 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleinbd6edf82019-07-18 10:30:49 -0600227 return self._ReexecuteInside(input_msg, output_msg, output_path,
Alex Klein69339cc2019-07-22 14:08:35 -0600228 service_name, method_name, config)
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
245 try:
246 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
247 except IOError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400248 raise InvalidOutputFileError('Cannot write output file: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600249
250 return return_code
251
252 def _ChrootCheck(self, service_options, method_options):
253 """Check the chroot options, and execute assertion or note reexec as needed.
254
255 Args:
256 service_options (google.protobuf.Message): The service options.
257 method_options (google.protobuf.Message): The method options.
258
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 """
265 chroot_assert = build_api_pb2.NO_ASSERTION
266 if method_options.HasField('method_chroot_assert'):
267 # Prefer the method option when set.
268 chroot_assert = method_options.method_chroot_assert
269 elif service_options.HasField('service_chroot_assert'):
270 # Fall back to the service option.
271 chroot_assert = service_options.service_chroot_assert
272
273 if chroot_assert == build_api_pb2.INSIDE:
274 return not cros_build_lib.IsInsideChroot()
275 elif chroot_assert == build_api_pb2.OUTSIDE:
276 # If it must be run outside we have to already be outside.
277 cros_build_lib.AssertOutsideChroot()
278
279 return False
280
Alex Kleinbd6edf82019-07-18 10:30:49 -0600281 def _ReexecuteInside(self, input_msg, output_msg, output_path, service_name,
Alex Klein69339cc2019-07-22 14:08:35 -0600282 method_name, config):
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 Klein146d4772019-06-20 13:48:25 -0600288 output_path (str): The path for the serialized output.
289 service_name (str): The name of the service to run.
290 method_name (str): The name of the method to run.
Alex Klein69339cc2019-07-22 14:08:35 -0600291 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600292 """
Alex Kleinc7d647f2020-01-06 12:00:48 -0700293 # Parse the chroot and clear the chroot field in the input message.
294 chroot = field_handler.handle_chroot(input_msg)
Alex Klein915cce92019-12-17 14:19:50 -0700295
Alex Kleinc7d647f2020-01-06 12:00:48 -0700296 # Use a ContextManagerStack to avoid the deep nesting this many
297 # context managers introduces.
298 with cros_build_lib.ContextManagerStack() as stack:
299 # TempDirs setup.
300 tempdir = stack.Add(chroot.tempdir).tempdir
301 sync_tempdir = stack.Add(chroot.tempdir).tempdir
302 # The copy-paths-in context manager to handle Path messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700303 stack.Add(
304 field_handler.copy_paths_in,
305 input_msg,
306 chroot.tmp,
307 prefix=chroot.path)
Alex Kleinc7d647f2020-01-06 12:00:48 -0700308 # The sync-directories context manager to handle SyncedDir messages.
Alex Klein92341cd2020-02-27 14:11:04 -0700309 stack.Add(
310 field_handler.sync_dirs, input_msg, sync_tempdir, prefix=chroot.path)
Alex Klein146d4772019-06-20 13:48:25 -0600311
Alex Kleinc7d647f2020-01-06 12:00:48 -0700312 chroot.goma = field_handler.handle_goma(input_msg, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700313
Alex Kleinc7d647f2020-01-06 12:00:48 -0700314 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
315 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
316 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
317 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Kleind815ca62020-01-10 12:21:30 -0700318 new_config = os.path.join(tempdir, self.REEXEC_CONFIG_FILE)
319 chroot_config = '/%s' % os.path.relpath(new_config, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700320
Alex Kleinc7d647f2020-01-06 12:00:48 -0700321 logging.info('Writing input message to: %s', new_input)
322 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
323 osutils.Touch(new_output)
Alex Kleind815ca62020-01-10 12:21:30 -0700324 logging.info('Writing config message to: %s', new_config)
325 osutils.WriteFile(new_config,
326 json_format.MessageToJson(config.get_proto()))
Alex Klein146d4772019-06-20 13:48:25 -0600327
Alex Kleinc7d647f2020-01-06 12:00:48 -0700328 cmd = ['build_api', '%s/%s' % (service_name, method_name),
Alex Kleind815ca62020-01-10 12:21:30 -0700329 '--input-json', chroot_input,
330 '--output-json', chroot_output,
331 '--config-json', chroot_config]
Alex Klein146d4772019-06-20 13:48:25 -0600332
Alex Kleinc7d647f2020-01-06 12:00:48 -0700333 try:
334 result = cros_build_lib.run(
335 cmd,
336 enter_chroot=True,
337 chroot_args=chroot.get_enter_args(),
338 check=False,
339 extra_env=chroot.env)
340 except cros_build_lib.RunCommandError:
341 # A non-zero return code will not result in an error, but one
342 # is still thrown when the command cannot be run in the first
343 # place. This is known to happen at least when the PATH does
344 # not include the chromite bin dir.
345 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein69339cc2019-07-22 14:08:35 -0600346
Alex Kleinc7d647f2020-01-06 12:00:48 -0700347 logging.info('Endpoint execution completed, return code: %d',
348 result.returncode)
Alex Klein146d4772019-06-20 13:48:25 -0600349
Alex Kleinc7d647f2020-01-06 12:00:48 -0700350 # Transfer result files out of the chroot.
351 output_content = osutils.ReadFile(new_output)
352 if output_content:
353 json_format.Parse(output_content, output_msg)
354 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Klein146d4772019-06-20 13:48:25 -0600355
Alex Kleinc7d647f2020-01-06 12:00:48 -0700356 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600357
Alex Kleinc7d647f2020-01-06 12:00:48 -0700358 return result.returncode
Alex Klein146d4772019-06-20 13:48:25 -0600359
Alex Klein146d4772019-06-20 13:48:25 -0600360 def _GetMethod(self, module_name, method_name):
361 """Get the implementation of the method for the service module.
362
363 Args:
364 module_name (str): The name of the service module.
365 method_name (str): The name of the method.
366
367 Returns:
368 callable - The method.
369
370 Raises:
371 MethodNotFoundError when the method cannot be found in the module.
372 ServiceModuleNotFoundError when the service module cannot be imported.
373 """
374 try:
375 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
376 except ImportError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400377 raise ServiceControllerNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600378 try:
379 return getattr(module, method_name)
380 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400381 raise MethodNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600382
383
384def RegisterServices(router):
385 """Register all the services.
386
387 Args:
388 router (Router): The router.
389 """
Alex Klein4de25e82019-08-05 15:58:39 -0600390 router.Register(android_pb2)
Alex Klein54e38e32019-06-21 14:54:17 -0600391 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600392 router.Register(artifacts_pb2)
393 router.Register(binhost_pb2)
394 router.Register(depgraph_pb2)
395 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600396 router.Register(packages_pb2)
George Engelbrechtfe63c8c2019-08-31 22:51:29 -0600397 router.Register(payload_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600398 router.Register(sdk_pb2)
399 router.Register(sysroot_pb2)
400 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700401 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600402 logging.debug('Services registered successfully.')
403
404
405def GetRouter():
406 """Get a router that has had all of the services registered."""
407 router = Router()
408 RegisterServices(router)
409
410 return router