blob: 4a6f900f4ac279d95a378fb39d751746682d1d65 [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
Alex Klein146d4772019-06-20 13:48:25 -060016
17from google.protobuf import json_format
18from google.protobuf import symbol_database
19
20from chromite.api import controller
21from chromite.api import field_handler
Alex Klein4de25e82019-08-05 15:58:39 -060022from chromite.api.gen.chromite.api import android_pb2
Alex Klein54e38e32019-06-21 14:54:17 -060023from chromite.api.gen.chromite.api import api_pb2
Alex Klein146d4772019-06-20 13:48:25 -060024from chromite.api.gen.chromite.api import artifacts_pb2
25from chromite.api.gen.chromite.api import binhost_pb2
26from chromite.api.gen.chromite.api import build_api_pb2
27from chromite.api.gen.chromite.api import depgraph_pb2
28from chromite.api.gen.chromite.api import image_pb2
Alex Kleineb77ffa2019-05-28 14:47:44 -060029from chromite.api.gen.chromite.api import packages_pb2
George Engelbrechtfe63c8c2019-08-31 22:51:29 -060030from chromite.api.gen.chromite.api import payload_pb2
Alex Klein146d4772019-06-20 13:48:25 -060031from chromite.api.gen.chromite.api import sdk_pb2
32from chromite.api.gen.chromite.api import sysroot_pb2
33from chromite.api.gen.chromite.api import test_pb2
Tiancong Wangaf050172019-07-10 11:52:03 -070034from chromite.api.gen.chromite.api import toolchain_pb2
Alex Klein146d4772019-06-20 13:48:25 -060035from chromite.lib import cros_build_lib
36from chromite.lib import cros_logging as logging
37from chromite.lib import osutils
38
39
40class Error(Exception):
41 """Base error class for the module."""
42
43
44class InvalidInputFileError(Error):
45 """Raised when the input file cannot be read."""
46
47
48class InvalidInputFormatError(Error):
49 """Raised when the passed input protobuf can't be parsed."""
50
51
52class InvalidOutputFileError(Error):
53 """Raised when the output file cannot be written."""
54
55
56class 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):
66 """Error class for when no controller is defined for a service."""
67
68
69class ServiceControllerNotFoundError(Error):
70 """Error raised when the service's controller cannot be imported."""
71
72
73# API Method Errors.
74class UnknownMethodError(Error):
75 """The service is defined in the proto but the method is not."""
76
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 Kleinbd6edf82019-07-18 10:30:49 -060085 REEXEC_INPUT_FILE = 'input.json'
86 REEXEC_OUTPUT_FILE = 'output.json'
87
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
94 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
95 self._service_options = extensions['service_options']
96 self._method_options = extensions['method_options']
97
98 def Register(self, proto_module):
99 """Register the services from a generated proto module.
100
101 Args:
102 proto_module (module): The generated proto module whose service is being
103 registered.
104
105 Raises:
106 ServiceModuleNotDefinedError when the service cannot be found in the
107 provided module.
108 """
109 services = proto_module.DESCRIPTOR.services_by_name
110 for service_name, svc in services.items():
111 module_name = svc.GetOptions().Extensions[self._service_options].module
112
113 if not module_name:
114 raise ControllerModuleNotDefinedError(
115 'The module must be defined in the service definition: %s.%s' %
116 (proto_module, service_name))
117
118 self._services[svc.full_name] = (svc, module_name)
119
120 def ListMethods(self):
121 """List all methods registered with the router."""
122 services = []
123 for service_name, (svc, _module) in self._services.items():
124 for method_name in svc.methods_by_name.keys():
125 services.append('%s/%s' % (service_name, method_name))
126
127 return sorted(services)
128
Alex Klein69339cc2019-07-22 14:08:35 -0600129 def Route(self, service_name, method_name, input_path, output_path, config):
Alex Klein146d4772019-06-20 13:48:25 -0600130 """Dispatch the request.
131
132 Args:
133 service_name (str): The fully qualified service name.
134 method_name (str): The name of the method being called.
135 input_path (str): The path to the input message file.
136 output_path (str): The path where the output message should be written.
Alex Klein69339cc2019-07-22 14:08:35 -0600137 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600138
139 Returns:
140 int: The return code.
141
142 Raises:
143 InvalidInputFileError when the input file cannot be read.
144 InvalidOutputFileError when the output file cannot be written.
145 ServiceModuleNotFoundError when the service module cannot be imported.
146 MethodNotFoundError when the method cannot be retrieved from the module.
147 """
148 try:
149 input_json = osutils.ReadFile(input_path).strip()
150 except IOError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400151 raise InvalidInputFileError('Unable to read input file: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600152
153 try:
154 svc, module_name = self._services[service_name]
155 except KeyError:
156 raise UnknownServiceError('The %s service has not been registered.'
157 % service_name)
158
159 try:
160 method_desc = svc.methods_by_name[method_name]
161 except KeyError:
162 raise UnknownMethodError('The %s method has not been defined in the %s '
163 'service.' % (method_name, service_name))
164
165 # Parse the input file to build an instance of the input message.
166 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
167 try:
168 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
169 except json_format.ParseError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400170 raise InvalidInputFormatError('Unable to parse the input json: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600171
172 # Get an empty output message instance.
173 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
174
Alex Kleinbd6edf82019-07-18 10:30:49 -0600175 # Fetch the method options for chroot and method name overrides.
Alex Klein146d4772019-06-20 13:48:25 -0600176 method_options = method_desc.GetOptions().Extensions[self._method_options]
Alex Klein146d4772019-06-20 13:48:25 -0600177
178 # Check the chroot settings before running.
179 service_options = svc.GetOptions().Extensions[self._service_options]
180 if self._ChrootCheck(service_options, method_options):
181 # Run inside the chroot instead.
182 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleinbd6edf82019-07-18 10:30:49 -0600183 return self._ReexecuteInside(input_msg, output_msg, output_path,
Alex Klein69339cc2019-07-22 14:08:35 -0600184 service_name, method_name, config)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600185
186 # Allow proto-based method name override.
187 if method_options.HasField('implementation_name'):
188 method_name = method_options.implementation_name
Alex Klein146d4772019-06-20 13:48:25 -0600189
190 # Import the module and get the method.
191 method_impl = self._GetMethod(module_name, method_name)
192
193 # Successfully located; call and return.
Alex Klein69339cc2019-07-22 14:08:35 -0600194 return_code = method_impl(input_msg, output_msg, config)
Alex Klein146d4772019-06-20 13:48:25 -0600195 if return_code is None:
196 return_code = controller.RETURN_CODE_SUCCESS
197
198 try:
199 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
200 except IOError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400201 raise InvalidOutputFileError('Cannot write output file: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600202
203 return return_code
204
205 def _ChrootCheck(self, service_options, method_options):
206 """Check the chroot options, and execute assertion or note reexec as needed.
207
208 Args:
209 service_options (google.protobuf.Message): The service options.
210 method_options (google.protobuf.Message): The method options.
211
212 Returns:
213 bool - True iff it needs to be reexeced inside the chroot.
214
215 Raises:
216 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
217 """
218 chroot_assert = build_api_pb2.NO_ASSERTION
219 if method_options.HasField('method_chroot_assert'):
220 # Prefer the method option when set.
221 chroot_assert = method_options.method_chroot_assert
222 elif service_options.HasField('service_chroot_assert'):
223 # Fall back to the service option.
224 chroot_assert = service_options.service_chroot_assert
225
226 if chroot_assert == build_api_pb2.INSIDE:
227 return not cros_build_lib.IsInsideChroot()
228 elif chroot_assert == build_api_pb2.OUTSIDE:
229 # If it must be run outside we have to already be outside.
230 cros_build_lib.AssertOutsideChroot()
231
232 return False
233
Alex Kleinbd6edf82019-07-18 10:30:49 -0600234 def _ReexecuteInside(self, input_msg, output_msg, output_path, service_name,
Alex Klein69339cc2019-07-22 14:08:35 -0600235 method_name, config):
Alex Klein146d4772019-06-20 13:48:25 -0600236 """Re-execute the service inside the chroot.
237
238 Args:
239 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600240 output_msg (Message): The empty output message instance.
Alex Klein146d4772019-06-20 13:48:25 -0600241 output_path (str): The path for the serialized output.
242 service_name (str): The name of the service to run.
243 method_name (str): The name of the method to run.
Alex Klein69339cc2019-07-22 14:08:35 -0600244 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600245 """
Alex Klein915cce92019-12-17 14:19:50 -0700246 # TODO(saklein): Fix the chroot/goma handling.
247
248 # First parse is just the chroot itself so we can use the tempdir.
249 chroot = field_handler.handle_chroot(input_msg, clear_field=False,
250 parse_goma=False)
Alex Klein146d4772019-06-20 13:48:25 -0600251
Alex Kleinaae49772019-07-26 10:20:50 -0600252 with field_handler.copy_paths_in(input_msg, chroot.tmp, prefix=chroot.path):
Alex Kleinf0717a62019-12-06 09:45:00 -0700253 with chroot.tempdir() as tempdir, chroot.tempdir() as sync_tmp:
254 with field_handler.sync_dirs(input_msg, sync_tmp, prefix=chroot.path):
Alex Klein915cce92019-12-17 14:19:50 -0700255 # Parse the chroot and clear the chroot field in the input message.
256 chroot = field_handler.handle_chroot(input_msg)
Alex Klein9b7331e2019-12-30 14:37:21 -0700257
258 if not chroot.goma:
259 goma = field_handler.handle_goma(input_msg, chroot.path)
260 chroot.goma = goma
261
Alex Kleinf0717a62019-12-06 09:45:00 -0700262 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
263 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
264 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
265 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Klein146d4772019-06-20 13:48:25 -0600266
Alex Kleinf0717a62019-12-06 09:45:00 -0700267 logging.info('Writing input message to: %s', new_input)
268 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
269 osutils.Touch(new_output)
Alex Klein146d4772019-06-20 13:48:25 -0600270
Alex Kleinf0717a62019-12-06 09:45:00 -0700271 cmd = ['build_api', '%s/%s' % (service_name, method_name),
272 '--input-json', chroot_input, '--output-json', chroot_output]
Alex Klein146d4772019-06-20 13:48:25 -0600273
Alex Kleinf0717a62019-12-06 09:45:00 -0700274 if config.validate_only:
275 cmd.append('--validate-only')
Alex Klein69339cc2019-07-22 14:08:35 -0600276
Alex Kleinf0717a62019-12-06 09:45:00 -0700277 try:
278 result = cros_build_lib.run(
279 cmd,
280 enter_chroot=True,
281 chroot_args=chroot.get_enter_args(),
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500282 check=False,
Alex Kleinf0717a62019-12-06 09:45:00 -0700283 extra_env=chroot.env)
284 except cros_build_lib.RunCommandError:
285 # A non-zero return code will not result in an error, but one
286 # is still thrown when the command cannot be run in the first
287 # place. This is known to happen at least when the PATH does
288 # not include the chromite bin dir.
289 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein146d4772019-06-20 13:48:25 -0600290
Alex Kleinf0717a62019-12-06 09:45:00 -0700291 logging.info('Endpoint execution completed, return code: %d',
292 result.returncode)
Alex Klein146d4772019-06-20 13:48:25 -0600293
Alex Kleinf0717a62019-12-06 09:45:00 -0700294 # Transfer result files out of the chroot.
295 output_content = osutils.ReadFile(new_output)
296 if output_content:
297 json_format.Parse(output_content, output_msg)
298 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600299
Alex Kleinf0717a62019-12-06 09:45:00 -0700300 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
Alex Klein146d4772019-06-20 13:48:25 -0600301
Alex Kleinf0717a62019-12-06 09:45:00 -0700302 return result.returncode
Alex Klein146d4772019-06-20 13:48:25 -0600303
Alex Klein146d4772019-06-20 13:48:25 -0600304 def _GetMethod(self, module_name, method_name):
305 """Get the implementation of the method for the service module.
306
307 Args:
308 module_name (str): The name of the service module.
309 method_name (str): The name of the method.
310
311 Returns:
312 callable - The method.
313
314 Raises:
315 MethodNotFoundError when the method cannot be found in the module.
316 ServiceModuleNotFoundError when the service module cannot be imported.
317 """
318 try:
319 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
320 except ImportError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400321 raise ServiceControllerNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600322 try:
323 return getattr(module, method_name)
324 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400325 raise MethodNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600326
327
328def RegisterServices(router):
329 """Register all the services.
330
331 Args:
332 router (Router): The router.
333 """
Alex Klein4de25e82019-08-05 15:58:39 -0600334 router.Register(android_pb2)
Alex Klein54e38e32019-06-21 14:54:17 -0600335 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600336 router.Register(artifacts_pb2)
337 router.Register(binhost_pb2)
338 router.Register(depgraph_pb2)
339 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600340 router.Register(packages_pb2)
George Engelbrechtfe63c8c2019-08-31 22:51:29 -0600341 router.Register(payload_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600342 router.Register(sdk_pb2)
343 router.Register(sysroot_pb2)
344 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700345 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600346 logging.debug('Services registered successfully.')
347
348
349def GetRouter():
350 """Get a router that has had all of the services registered."""
351 router = Router()
352 RegisterServices(router)
353
354 return router