blob: 98d9d4162e9a095fe11e4bce00844f6a56762a3d [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
14import importlib
15import os
Mike Frysingeref94e4c2020-02-10 23:59:54 -050016import sys
Alex Klein146d4772019-06-20 13:48:25 -060017
18from google.protobuf import json_format
19from 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
39
40
Mike Frysingeref94e4c2020-02-10 23:59:54 -050041assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
42
43
Alex Klein146d4772019-06-20 13:48:25 -060044class Error(Exception):
45 """Base error class for the module."""
46
47
48class InvalidInputFileError(Error):
49 """Raised when the input file cannot be read."""
50
51
52class InvalidInputFormatError(Error):
53 """Raised when the passed input protobuf can't be parsed."""
54
55
56class InvalidOutputFileError(Error):
57 """Raised when the output file cannot be written."""
58
59
60class CrosSdkNotRunError(Error):
61 """Raised when the cros_sdk command could not be run to enter the chroot."""
62
63
64# API Service Errors.
65class UnknownServiceError(Error):
66 """Error raised when the requested service has not been registered."""
67
68
69class ControllerModuleNotDefinedError(Error):
70 """Error class for when no controller is defined for a service."""
71
72
73class ServiceControllerNotFoundError(Error):
74 """Error raised when the service's controller cannot be imported."""
75
76
77# API Method Errors.
78class UnknownMethodError(Error):
79 """The service is defined in the proto but the method is not."""
80
81
82class MethodNotFoundError(Error):
83 """The method's implementation cannot be found in the service's controller."""
84
85
86class Router(object):
87 """Encapsulates the request dispatching logic."""
88
Alex Kleinbd6edf82019-07-18 10:30:49 -060089 REEXEC_INPUT_FILE = 'input.json'
90 REEXEC_OUTPUT_FILE = 'output.json'
Alex Kleind815ca62020-01-10 12:21:30 -070091 REEXEC_CONFIG_FILE = 'config.json'
Alex Kleinbd6edf82019-07-18 10:30:49 -060092
Alex Klein146d4772019-06-20 13:48:25 -060093 def __init__(self):
94 self._services = {}
95 self._aliases = {}
96 # All imported generated messages get added to this symbol db.
97 self._sym_db = symbol_database.Default()
98
99 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
100 self._service_options = extensions['service_options']
101 self._method_options = extensions['method_options']
102
103 def Register(self, proto_module):
104 """Register the services from a generated proto module.
105
106 Args:
107 proto_module (module): The generated proto module whose service is being
108 registered.
109
110 Raises:
111 ServiceModuleNotDefinedError when the service cannot be found in the
112 provided module.
113 """
114 services = proto_module.DESCRIPTOR.services_by_name
115 for service_name, svc in services.items():
116 module_name = svc.GetOptions().Extensions[self._service_options].module
117
118 if not module_name:
119 raise ControllerModuleNotDefinedError(
120 'The module must be defined in the service definition: %s.%s' %
121 (proto_module, service_name))
122
123 self._services[svc.full_name] = (svc, module_name)
124
125 def ListMethods(self):
126 """List all methods registered with the router."""
127 services = []
128 for service_name, (svc, _module) in self._services.items():
129 for method_name in svc.methods_by_name.keys():
130 services.append('%s/%s' % (service_name, method_name))
131
132 return sorted(services)
133
Alex Klein69339cc2019-07-22 14:08:35 -0600134 def Route(self, service_name, method_name, input_path, output_path, config):
Alex Klein146d4772019-06-20 13:48:25 -0600135 """Dispatch the request.
136
137 Args:
138 service_name (str): The fully qualified service name.
139 method_name (str): The name of the method being called.
140 input_path (str): The path to the input message file.
141 output_path (str): The path where the output message should be written.
Alex Klein69339cc2019-07-22 14:08:35 -0600142 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600143
144 Returns:
145 int: The return code.
146
147 Raises:
148 InvalidInputFileError when the input file cannot be read.
149 InvalidOutputFileError when the output file cannot be written.
150 ServiceModuleNotFoundError when the service module cannot be imported.
151 MethodNotFoundError when the method cannot be retrieved from the module.
152 """
153 try:
154 input_json = osutils.ReadFile(input_path).strip()
155 except IOError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400156 raise InvalidInputFileError('Unable to read input file: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600157
158 try:
159 svc, module_name = self._services[service_name]
160 except KeyError:
161 raise UnknownServiceError('The %s service has not been registered.'
162 % service_name)
163
164 try:
165 method_desc = svc.methods_by_name[method_name]
166 except KeyError:
167 raise UnknownMethodError('The %s method has not been defined in the %s '
168 'service.' % (method_name, service_name))
169
170 # Parse the input file to build an instance of the input message.
171 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
172 try:
173 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
174 except json_format.ParseError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400175 raise InvalidInputFormatError('Unable to parse the input json: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600176
177 # Get an empty output message instance.
178 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
179
Alex Kleinbd6edf82019-07-18 10:30:49 -0600180 # Fetch the method options for chroot and method name overrides.
Alex Klein146d4772019-06-20 13:48:25 -0600181 method_options = method_desc.GetOptions().Extensions[self._method_options]
Alex Klein146d4772019-06-20 13:48:25 -0600182
183 # Check the chroot settings before running.
184 service_options = svc.GetOptions().Extensions[self._service_options]
185 if self._ChrootCheck(service_options, method_options):
186 # Run inside the chroot instead.
187 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleinbd6edf82019-07-18 10:30:49 -0600188 return self._ReexecuteInside(input_msg, output_msg, output_path,
Alex Klein69339cc2019-07-22 14:08:35 -0600189 service_name, method_name, config)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600190
191 # Allow proto-based method name override.
192 if method_options.HasField('implementation_name'):
193 method_name = method_options.implementation_name
Alex Klein146d4772019-06-20 13:48:25 -0600194
195 # Import the module and get the method.
196 method_impl = self._GetMethod(module_name, method_name)
197
198 # Successfully located; call and return.
Alex Klein69339cc2019-07-22 14:08:35 -0600199 return_code = method_impl(input_msg, output_msg, config)
Alex Klein146d4772019-06-20 13:48:25 -0600200 if return_code is None:
201 return_code = controller.RETURN_CODE_SUCCESS
202
203 try:
204 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
205 except IOError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400206 raise InvalidOutputFileError('Cannot write output file: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600207
208 return return_code
209
210 def _ChrootCheck(self, service_options, method_options):
211 """Check the chroot options, and execute assertion or note reexec as needed.
212
213 Args:
214 service_options (google.protobuf.Message): The service options.
215 method_options (google.protobuf.Message): The method options.
216
217 Returns:
218 bool - True iff it needs to be reexeced inside the chroot.
219
220 Raises:
221 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
222 """
223 chroot_assert = build_api_pb2.NO_ASSERTION
224 if method_options.HasField('method_chroot_assert'):
225 # Prefer the method option when set.
226 chroot_assert = method_options.method_chroot_assert
227 elif service_options.HasField('service_chroot_assert'):
228 # Fall back to the service option.
229 chroot_assert = service_options.service_chroot_assert
230
231 if chroot_assert == build_api_pb2.INSIDE:
232 return not cros_build_lib.IsInsideChroot()
233 elif chroot_assert == build_api_pb2.OUTSIDE:
234 # If it must be run outside we have to already be outside.
235 cros_build_lib.AssertOutsideChroot()
236
237 return False
238
Alex Kleinbd6edf82019-07-18 10:30:49 -0600239 def _ReexecuteInside(self, input_msg, output_msg, output_path, service_name,
Alex Klein69339cc2019-07-22 14:08:35 -0600240 method_name, config):
Alex Klein146d4772019-06-20 13:48:25 -0600241 """Re-execute the service inside the chroot.
242
243 Args:
244 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600245 output_msg (Message): The empty output message instance.
Alex Klein146d4772019-06-20 13:48:25 -0600246 output_path (str): The path for the serialized output.
247 service_name (str): The name of the service to run.
248 method_name (str): The name of the method to run.
Alex Klein69339cc2019-07-22 14:08:35 -0600249 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600250 """
Alex Kleinc7d647f2020-01-06 12:00:48 -0700251 # Parse the chroot and clear the chroot field in the input message.
252 chroot = field_handler.handle_chroot(input_msg)
Alex Klein915cce92019-12-17 14:19:50 -0700253
Alex Kleinc7d647f2020-01-06 12:00:48 -0700254 # Use a ContextManagerStack to avoid the deep nesting this many
255 # context managers introduces.
256 with cros_build_lib.ContextManagerStack() as stack:
257 # TempDirs setup.
258 tempdir = stack.Add(chroot.tempdir).tempdir
259 sync_tempdir = stack.Add(chroot.tempdir).tempdir
260 # The copy-paths-in context manager to handle Path messages.
261 stack.Add(field_handler.copy_paths_in, input_msg, chroot.tmp,
262 prefix=chroot.path)
263 # The sync-directories context manager to handle SyncedDir messages.
264 stack.Add(field_handler.sync_dirs, input_msg, sync_tempdir,
265 prefix=chroot.path)
Alex Klein146d4772019-06-20 13:48:25 -0600266
Alex Kleinc7d647f2020-01-06 12:00:48 -0700267 chroot.goma = field_handler.handle_goma(input_msg, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700268
Alex Kleinc7d647f2020-01-06 12:00:48 -0700269 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
270 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
271 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
272 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Kleind815ca62020-01-10 12:21:30 -0700273 new_config = os.path.join(tempdir, self.REEXEC_CONFIG_FILE)
274 chroot_config = '/%s' % os.path.relpath(new_config, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700275
Alex Kleinc7d647f2020-01-06 12:00:48 -0700276 logging.info('Writing input message to: %s', new_input)
277 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
278 osutils.Touch(new_output)
Alex Kleind815ca62020-01-10 12:21:30 -0700279 logging.info('Writing config message to: %s', new_config)
280 osutils.WriteFile(new_config,
281 json_format.MessageToJson(config.get_proto()))
Alex Klein146d4772019-06-20 13:48:25 -0600282
Alex Kleinc7d647f2020-01-06 12:00:48 -0700283 cmd = ['build_api', '%s/%s' % (service_name, method_name),
Alex Kleind815ca62020-01-10 12:21:30 -0700284 '--input-json', chroot_input,
285 '--output-json', chroot_output,
286 '--config-json', chroot_config]
Alex Klein146d4772019-06-20 13:48:25 -0600287
Alex Kleinc7d647f2020-01-06 12:00:48 -0700288 try:
289 result = cros_build_lib.run(
290 cmd,
291 enter_chroot=True,
292 chroot_args=chroot.get_enter_args(),
293 check=False,
294 extra_env=chroot.env)
295 except cros_build_lib.RunCommandError:
296 # A non-zero return code will not result in an error, but one
297 # is still thrown when the command cannot be run in the first
298 # place. This is known to happen at least when the PATH does
299 # not include the chromite bin dir.
300 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein69339cc2019-07-22 14:08:35 -0600301
Alex Kleinc7d647f2020-01-06 12:00:48 -0700302 logging.info('Endpoint execution completed, return code: %d',
303 result.returncode)
Alex Klein146d4772019-06-20 13:48:25 -0600304
Alex Kleinc7d647f2020-01-06 12:00:48 -0700305 # Transfer result files out of the chroot.
306 output_content = osutils.ReadFile(new_output)
307 if output_content:
308 json_format.Parse(output_content, output_msg)
309 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Klein146d4772019-06-20 13:48:25 -0600310
Alex Kleinc7d647f2020-01-06 12:00:48 -0700311 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600312
Alex Kleinc7d647f2020-01-06 12:00:48 -0700313 return result.returncode
Alex Klein146d4772019-06-20 13:48:25 -0600314
Alex Klein146d4772019-06-20 13:48:25 -0600315 def _GetMethod(self, module_name, method_name):
316 """Get the implementation of the method for the service module.
317
318 Args:
319 module_name (str): The name of the service module.
320 method_name (str): The name of the method.
321
322 Returns:
323 callable - The method.
324
325 Raises:
326 MethodNotFoundError when the method cannot be found in the module.
327 ServiceModuleNotFoundError when the service module cannot be imported.
328 """
329 try:
330 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
331 except ImportError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400332 raise ServiceControllerNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600333 try:
334 return getattr(module, method_name)
335 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400336 raise MethodNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600337
338
339def RegisterServices(router):
340 """Register all the services.
341
342 Args:
343 router (Router): The router.
344 """
Alex Klein4de25e82019-08-05 15:58:39 -0600345 router.Register(android_pb2)
Alex Klein54e38e32019-06-21 14:54:17 -0600346 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600347 router.Register(artifacts_pb2)
348 router.Register(binhost_pb2)
349 router.Register(depgraph_pb2)
350 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600351 router.Register(packages_pb2)
George Engelbrechtfe63c8c2019-08-31 22:51:29 -0600352 router.Register(payload_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600353 router.Register(sdk_pb2)
354 router.Register(sysroot_pb2)
355 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700356 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600357 logging.debug('Services registered successfully.')
358
359
360def GetRouter():
361 """Get a router that has had all of the services registered."""
362 router = Router()
363 RegisterServices(router)
364
365 return router