blob: 84d75992f4f47a7095d1fa31f68c9eb5a24d794f [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'
Alex Kleind815ca62020-01-10 12:21:30 -070087 REEXEC_CONFIG_FILE = 'config.json'
Alex Kleinbd6edf82019-07-18 10:30:49 -060088
Alex Klein146d4772019-06-20 13:48:25 -060089 def __init__(self):
90 self._services = {}
91 self._aliases = {}
92 # All imported generated messages get added to this symbol db.
93 self._sym_db = symbol_database.Default()
94
95 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
96 self._service_options = extensions['service_options']
97 self._method_options = extensions['method_options']
98
99 def Register(self, proto_module):
100 """Register the services from a generated proto module.
101
102 Args:
103 proto_module (module): The generated proto module whose service is being
104 registered.
105
106 Raises:
107 ServiceModuleNotDefinedError when the service cannot be found in the
108 provided module.
109 """
110 services = proto_module.DESCRIPTOR.services_by_name
111 for service_name, svc in services.items():
112 module_name = svc.GetOptions().Extensions[self._service_options].module
113
114 if not module_name:
115 raise ControllerModuleNotDefinedError(
116 'The module must be defined in the service definition: %s.%s' %
117 (proto_module, service_name))
118
119 self._services[svc.full_name] = (svc, module_name)
120
121 def ListMethods(self):
122 """List all methods registered with the router."""
123 services = []
124 for service_name, (svc, _module) in self._services.items():
125 for method_name in svc.methods_by_name.keys():
126 services.append('%s/%s' % (service_name, method_name))
127
128 return sorted(services)
129
Alex Klein69339cc2019-07-22 14:08:35 -0600130 def Route(self, service_name, method_name, input_path, output_path, config):
Alex Klein146d4772019-06-20 13:48:25 -0600131 """Dispatch the request.
132
133 Args:
134 service_name (str): The fully qualified service name.
135 method_name (str): The name of the method being called.
136 input_path (str): The path to the input message file.
137 output_path (str): The path where the output message should be written.
Alex Klein69339cc2019-07-22 14:08:35 -0600138 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600139
140 Returns:
141 int: The return code.
142
143 Raises:
144 InvalidInputFileError when the input file cannot be read.
145 InvalidOutputFileError when the output file cannot be written.
146 ServiceModuleNotFoundError when the service module cannot be imported.
147 MethodNotFoundError when the method cannot be retrieved from the module.
148 """
149 try:
150 input_json = osutils.ReadFile(input_path).strip()
151 except IOError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400152 raise InvalidInputFileError('Unable to read input file: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600153
154 try:
155 svc, module_name = self._services[service_name]
156 except KeyError:
157 raise UnknownServiceError('The %s service has not been registered.'
158 % service_name)
159
160 try:
161 method_desc = svc.methods_by_name[method_name]
162 except KeyError:
163 raise UnknownMethodError('The %s method has not been defined in the %s '
164 'service.' % (method_name, service_name))
165
166 # Parse the input file to build an instance of the input message.
167 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
168 try:
169 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
170 except json_format.ParseError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400171 raise InvalidInputFormatError('Unable to parse the input json: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600172
173 # Get an empty output message instance.
174 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
175
Alex Kleinbd6edf82019-07-18 10:30:49 -0600176 # Fetch the method options for chroot and method name overrides.
Alex Klein146d4772019-06-20 13:48:25 -0600177 method_options = method_desc.GetOptions().Extensions[self._method_options]
Alex Klein146d4772019-06-20 13:48:25 -0600178
179 # Check the chroot settings before running.
180 service_options = svc.GetOptions().Extensions[self._service_options]
181 if self._ChrootCheck(service_options, method_options):
182 # Run inside the chroot instead.
183 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleinbd6edf82019-07-18 10:30:49 -0600184 return self._ReexecuteInside(input_msg, output_msg, output_path,
Alex Klein69339cc2019-07-22 14:08:35 -0600185 service_name, method_name, config)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600186
187 # Allow proto-based method name override.
188 if method_options.HasField('implementation_name'):
189 method_name = method_options.implementation_name
Alex Klein146d4772019-06-20 13:48:25 -0600190
191 # Import the module and get the method.
192 method_impl = self._GetMethod(module_name, method_name)
193
194 # Successfully located; call and return.
Alex Klein69339cc2019-07-22 14:08:35 -0600195 return_code = method_impl(input_msg, output_msg, config)
Alex Klein146d4772019-06-20 13:48:25 -0600196 if return_code is None:
197 return_code = controller.RETURN_CODE_SUCCESS
198
199 try:
200 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
201 except IOError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400202 raise InvalidOutputFileError('Cannot write output file: %s' % e)
Alex Klein146d4772019-06-20 13:48:25 -0600203
204 return return_code
205
206 def _ChrootCheck(self, service_options, method_options):
207 """Check the chroot options, and execute assertion or note reexec as needed.
208
209 Args:
210 service_options (google.protobuf.Message): The service options.
211 method_options (google.protobuf.Message): The method options.
212
213 Returns:
214 bool - True iff it needs to be reexeced inside the chroot.
215
216 Raises:
217 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
218 """
219 chroot_assert = build_api_pb2.NO_ASSERTION
220 if method_options.HasField('method_chroot_assert'):
221 # Prefer the method option when set.
222 chroot_assert = method_options.method_chroot_assert
223 elif service_options.HasField('service_chroot_assert'):
224 # Fall back to the service option.
225 chroot_assert = service_options.service_chroot_assert
226
227 if chroot_assert == build_api_pb2.INSIDE:
228 return not cros_build_lib.IsInsideChroot()
229 elif chroot_assert == build_api_pb2.OUTSIDE:
230 # If it must be run outside we have to already be outside.
231 cros_build_lib.AssertOutsideChroot()
232
233 return False
234
Alex Kleinbd6edf82019-07-18 10:30:49 -0600235 def _ReexecuteInside(self, input_msg, output_msg, output_path, service_name,
Alex Klein69339cc2019-07-22 14:08:35 -0600236 method_name, config):
Alex Klein146d4772019-06-20 13:48:25 -0600237 """Re-execute the service inside the chroot.
238
239 Args:
240 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600241 output_msg (Message): The empty output message instance.
Alex Klein146d4772019-06-20 13:48:25 -0600242 output_path (str): The path for the serialized output.
243 service_name (str): The name of the service to run.
244 method_name (str): The name of the method to run.
Alex Klein69339cc2019-07-22 14:08:35 -0600245 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600246 """
Alex Kleinc7d647f2020-01-06 12:00:48 -0700247 # Parse the chroot and clear the chroot field in the input message.
248 chroot = field_handler.handle_chroot(input_msg)
Alex Klein915cce92019-12-17 14:19:50 -0700249
Alex Kleinc7d647f2020-01-06 12:00:48 -0700250 # Use a ContextManagerStack to avoid the deep nesting this many
251 # context managers introduces.
252 with cros_build_lib.ContextManagerStack() as stack:
253 # TempDirs setup.
254 tempdir = stack.Add(chroot.tempdir).tempdir
255 sync_tempdir = stack.Add(chroot.tempdir).tempdir
256 # The copy-paths-in context manager to handle Path messages.
257 stack.Add(field_handler.copy_paths_in, input_msg, chroot.tmp,
258 prefix=chroot.path)
259 # The sync-directories context manager to handle SyncedDir messages.
260 stack.Add(field_handler.sync_dirs, input_msg, sync_tempdir,
261 prefix=chroot.path)
Alex Klein146d4772019-06-20 13:48:25 -0600262
Alex Kleinc7d647f2020-01-06 12:00:48 -0700263 chroot.goma = field_handler.handle_goma(input_msg, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700264
Alex Kleinc7d647f2020-01-06 12:00:48 -0700265 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
266 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
267 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
268 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
Alex Kleind815ca62020-01-10 12:21:30 -0700269 new_config = os.path.join(tempdir, self.REEXEC_CONFIG_FILE)
270 chroot_config = '/%s' % os.path.relpath(new_config, chroot.path)
Alex Klein9b7331e2019-12-30 14:37:21 -0700271
Alex Kleinc7d647f2020-01-06 12:00:48 -0700272 logging.info('Writing input message to: %s', new_input)
273 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
274 osutils.Touch(new_output)
Alex Kleind815ca62020-01-10 12:21:30 -0700275 logging.info('Writing config message to: %s', new_config)
276 osutils.WriteFile(new_config,
277 json_format.MessageToJson(config.get_proto()))
Alex Klein146d4772019-06-20 13:48:25 -0600278
Alex Kleinc7d647f2020-01-06 12:00:48 -0700279 cmd = ['build_api', '%s/%s' % (service_name, method_name),
Alex Kleind815ca62020-01-10 12:21:30 -0700280 '--input-json', chroot_input,
281 '--output-json', chroot_output,
282 '--config-json', chroot_config]
Alex Klein146d4772019-06-20 13:48:25 -0600283
Alex Kleinc7d647f2020-01-06 12:00:48 -0700284 try:
285 result = cros_build_lib.run(
286 cmd,
287 enter_chroot=True,
288 chroot_args=chroot.get_enter_args(),
289 check=False,
290 extra_env=chroot.env)
291 except cros_build_lib.RunCommandError:
292 # A non-zero return code will not result in an error, but one
293 # is still thrown when the command cannot be run in the first
294 # place. This is known to happen at least when the PATH does
295 # not include the chromite bin dir.
296 raise CrosSdkNotRunError('Unable to enter the chroot.')
Alex Klein69339cc2019-07-22 14:08:35 -0600297
Alex Kleinc7d647f2020-01-06 12:00:48 -0700298 logging.info('Endpoint execution completed, return code: %d',
299 result.returncode)
Alex Klein146d4772019-06-20 13:48:25 -0600300
Alex Kleinc7d647f2020-01-06 12:00:48 -0700301 # Transfer result files out of the chroot.
302 output_content = osutils.ReadFile(new_output)
303 if output_content:
304 json_format.Parse(output_content, output_msg)
305 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Klein146d4772019-06-20 13:48:25 -0600306
Alex Kleinc7d647f2020-01-06 12:00:48 -0700307 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
Alex Kleinbd6edf82019-07-18 10:30:49 -0600308
Alex Kleinc7d647f2020-01-06 12:00:48 -0700309 return result.returncode
Alex Klein146d4772019-06-20 13:48:25 -0600310
Alex Klein146d4772019-06-20 13:48:25 -0600311 def _GetMethod(self, module_name, method_name):
312 """Get the implementation of the method for the service module.
313
314 Args:
315 module_name (str): The name of the service module.
316 method_name (str): The name of the method.
317
318 Returns:
319 callable - The method.
320
321 Raises:
322 MethodNotFoundError when the method cannot be found in the module.
323 ServiceModuleNotFoundError when the service module cannot be imported.
324 """
325 try:
326 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
327 except ImportError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400328 raise ServiceControllerNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600329 try:
330 return getattr(module, method_name)
331 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400332 raise MethodNotFoundError(str(e))
Alex Klein146d4772019-06-20 13:48:25 -0600333
334
335def RegisterServices(router):
336 """Register all the services.
337
338 Args:
339 router (Router): The router.
340 """
Alex Klein4de25e82019-08-05 15:58:39 -0600341 router.Register(android_pb2)
Alex Klein54e38e32019-06-21 14:54:17 -0600342 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600343 router.Register(artifacts_pb2)
344 router.Register(binhost_pb2)
345 router.Register(depgraph_pb2)
346 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600347 router.Register(packages_pb2)
George Engelbrechtfe63c8c2019-08-31 22:51:29 -0600348 router.Register(payload_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600349 router.Register(sdk_pb2)
350 router.Register(sysroot_pb2)
351 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700352 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600353 logging.debug('Services registered successfully.')
354
355
356def GetRouter():
357 """Get a router that has had all of the services registered."""
358 router = Router()
359 RegisterServices(router)
360
361 return router