blob: 691e4206fd1c9918e0bbcef2f334f8a73c9e6a62 [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 Klein54e38e32019-06-21 14:54:17 -060022from chromite.api.gen.chromite.api import api_pb2
Alex Klein146d4772019-06-20 13:48:25 -060023from chromite.api.gen.chromite.api import artifacts_pb2
24from chromite.api.gen.chromite.api import binhost_pb2
25from chromite.api.gen.chromite.api import build_api_pb2
26from chromite.api.gen.chromite.api import depgraph_pb2
27from chromite.api.gen.chromite.api import image_pb2
Alex Kleineb77ffa2019-05-28 14:47:44 -060028from chromite.api.gen.chromite.api import packages_pb2
Alex Klein146d4772019-06-20 13:48:25 -060029from chromite.api.gen.chromite.api import sdk_pb2
30from chromite.api.gen.chromite.api import sysroot_pb2
31from chromite.api.gen.chromite.api import test_pb2
Tiancong Wangaf050172019-07-10 11:52:03 -070032from chromite.api.gen.chromite.api import toolchain_pb2
Alex Klein146d4772019-06-20 13:48:25 -060033from chromite.lib import cros_build_lib
34from chromite.lib import cros_logging as logging
35from chromite.lib import osutils
36
37
38class Error(Exception):
39 """Base error class for the module."""
40
41
42class InvalidInputFileError(Error):
43 """Raised when the input file cannot be read."""
44
45
46class InvalidInputFormatError(Error):
47 """Raised when the passed input protobuf can't be parsed."""
48
49
50class InvalidOutputFileError(Error):
51 """Raised when the output file cannot be written."""
52
53
54class CrosSdkNotRunError(Error):
55 """Raised when the cros_sdk command could not be run to enter the chroot."""
56
57
58# API Service Errors.
59class UnknownServiceError(Error):
60 """Error raised when the requested service has not been registered."""
61
62
63class ControllerModuleNotDefinedError(Error):
64 """Error class for when no controller is defined for a service."""
65
66
67class ServiceControllerNotFoundError(Error):
68 """Error raised when the service's controller cannot be imported."""
69
70
71# API Method Errors.
72class UnknownMethodError(Error):
73 """The service is defined in the proto but the method is not."""
74
75
76class MethodNotFoundError(Error):
77 """The method's implementation cannot be found in the service's controller."""
78
79
80class Router(object):
81 """Encapsulates the request dispatching logic."""
82
Alex Kleinbd6edf82019-07-18 10:30:49 -060083 REEXEC_INPUT_FILE = 'input.json'
84 REEXEC_OUTPUT_FILE = 'output.json'
85
Alex Klein146d4772019-06-20 13:48:25 -060086 def __init__(self):
87 self._services = {}
88 self._aliases = {}
89 # All imported generated messages get added to this symbol db.
90 self._sym_db = symbol_database.Default()
91
92 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
93 self._service_options = extensions['service_options']
94 self._method_options = extensions['method_options']
95
96 def Register(self, proto_module):
97 """Register the services from a generated proto module.
98
99 Args:
100 proto_module (module): The generated proto module whose service is being
101 registered.
102
103 Raises:
104 ServiceModuleNotDefinedError when the service cannot be found in the
105 provided module.
106 """
107 services = proto_module.DESCRIPTOR.services_by_name
108 for service_name, svc in services.items():
109 module_name = svc.GetOptions().Extensions[self._service_options].module
110
111 if not module_name:
112 raise ControllerModuleNotDefinedError(
113 'The module must be defined in the service definition: %s.%s' %
114 (proto_module, service_name))
115
116 self._services[svc.full_name] = (svc, module_name)
117
118 def ListMethods(self):
119 """List all methods registered with the router."""
120 services = []
121 for service_name, (svc, _module) in self._services.items():
122 for method_name in svc.methods_by_name.keys():
123 services.append('%s/%s' % (service_name, method_name))
124
125 return sorted(services)
126
Alex Klein69339cc2019-07-22 14:08:35 -0600127 def Route(self, service_name, method_name, input_path, output_path, config):
Alex Klein146d4772019-06-20 13:48:25 -0600128 """Dispatch the request.
129
130 Args:
131 service_name (str): The fully qualified service name.
132 method_name (str): The name of the method being called.
133 input_path (str): The path to the input message file.
134 output_path (str): The path where the output message should be written.
Alex Klein69339cc2019-07-22 14:08:35 -0600135 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600136
137 Returns:
138 int: The return code.
139
140 Raises:
141 InvalidInputFileError when the input file cannot be read.
142 InvalidOutputFileError when the output file cannot be written.
143 ServiceModuleNotFoundError when the service module cannot be imported.
144 MethodNotFoundError when the method cannot be retrieved from the module.
145 """
146 try:
147 input_json = osutils.ReadFile(input_path).strip()
148 except IOError as e:
149 raise InvalidInputFileError('Unable to read input file: %s' % e.message)
150
151 try:
152 svc, module_name = self._services[service_name]
153 except KeyError:
154 raise UnknownServiceError('The %s service has not been registered.'
155 % service_name)
156
157 try:
158 method_desc = svc.methods_by_name[method_name]
159 except KeyError:
160 raise UnknownMethodError('The %s method has not been defined in the %s '
161 'service.' % (method_name, service_name))
162
163 # Parse the input file to build an instance of the input message.
164 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
165 try:
166 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
167 except json_format.ParseError as e:
168 raise InvalidInputFormatError(
169 'Unable to parse the input json: %s' % e.message)
170
171 # Get an empty output message instance.
172 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
173
Alex Kleinbd6edf82019-07-18 10:30:49 -0600174 # Fetch the method options for chroot and method name overrides.
Alex Klein146d4772019-06-20 13:48:25 -0600175 method_options = method_desc.GetOptions().Extensions[self._method_options]
Alex Klein146d4772019-06-20 13:48:25 -0600176
177 # Check the chroot settings before running.
178 service_options = svc.GetOptions().Extensions[self._service_options]
179 if self._ChrootCheck(service_options, method_options):
180 # Run inside the chroot instead.
181 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleinbd6edf82019-07-18 10:30:49 -0600182 return self._ReexecuteInside(input_msg, output_msg, output_path,
Alex Klein69339cc2019-07-22 14:08:35 -0600183 service_name, method_name, config)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600184
185 # Allow proto-based method name override.
186 if method_options.HasField('implementation_name'):
187 method_name = method_options.implementation_name
Alex Klein146d4772019-06-20 13:48:25 -0600188
189 # Import the module and get the method.
190 method_impl = self._GetMethod(module_name, method_name)
191
192 # Successfully located; call and return.
Alex Klein69339cc2019-07-22 14:08:35 -0600193 return_code = method_impl(input_msg, output_msg, config)
Alex Klein146d4772019-06-20 13:48:25 -0600194 if return_code is None:
195 return_code = controller.RETURN_CODE_SUCCESS
196
197 try:
198 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
199 except IOError as e:
200 raise InvalidOutputFileError('Cannot write output file: %s' % e.message)
201
202 return return_code
203
204 def _ChrootCheck(self, service_options, method_options):
205 """Check the chroot options, and execute assertion or note reexec as needed.
206
207 Args:
208 service_options (google.protobuf.Message): The service options.
209 method_options (google.protobuf.Message): The method options.
210
211 Returns:
212 bool - True iff it needs to be reexeced inside the chroot.
213
214 Raises:
215 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
216 """
217 chroot_assert = build_api_pb2.NO_ASSERTION
218 if method_options.HasField('method_chroot_assert'):
219 # Prefer the method option when set.
220 chroot_assert = method_options.method_chroot_assert
221 elif service_options.HasField('service_chroot_assert'):
222 # Fall back to the service option.
223 chroot_assert = service_options.service_chroot_assert
224
225 if chroot_assert == build_api_pb2.INSIDE:
226 return not cros_build_lib.IsInsideChroot()
227 elif chroot_assert == build_api_pb2.OUTSIDE:
228 # If it must be run outside we have to already be outside.
229 cros_build_lib.AssertOutsideChroot()
230
231 return False
232
Alex Kleinbd6edf82019-07-18 10:30:49 -0600233 def _ReexecuteInside(self, input_msg, output_msg, output_path, service_name,
Alex Klein69339cc2019-07-22 14:08:35 -0600234 method_name, config):
Alex Klein146d4772019-06-20 13:48:25 -0600235 """Re-execute the service inside the chroot.
236
237 Args:
238 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600239 output_msg (Message): The empty output message instance.
Alex Klein146d4772019-06-20 13:48:25 -0600240 output_path (str): The path for the serialized output.
241 service_name (str): The name of the service to run.
242 method_name (str): The name of the method to run.
Alex Klein69339cc2019-07-22 14:08:35 -0600243 config (api_config.ApiConfig): The optional call configs.
Alex Klein146d4772019-06-20 13:48:25 -0600244 """
245 # Parse the chroot and clear the chroot field in the input message.
246 chroot = field_handler.handle_chroot(input_msg)
247
Alex Kleinaae49772019-07-26 10:20:50 -0600248 with field_handler.copy_paths_in(input_msg, chroot.tmp, prefix=chroot.path):
249 with chroot.tempdir() as tempdir:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600250 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
Alex Klein146d4772019-06-20 13:48:25 -0600251 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600252 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
Alex Klein146d4772019-06-20 13:48:25 -0600253 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
254
255 logging.info('Writing input message to: %s', new_input)
256 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
257 osutils.Touch(new_output)
258
259 cmd = ['build_api', '%s/%s' % (service_name, method_name),
260 '--input-json', chroot_input, '--output-json', chroot_output]
261
Alex Klein69339cc2019-07-22 14:08:35 -0600262 if config.validate_only:
263 cmd.append('--validate-only')
264
Alex Klein146d4772019-06-20 13:48:25 -0600265 try:
266 result = cros_build_lib.RunCommand(cmd, enter_chroot=True,
267 chroot_args=chroot.GetEnterArgs(),
268 error_code_ok=True,
269 extra_env=chroot.env)
270 except cros_build_lib.RunCommandError:
271 # A non-zero return code will not result in an error, but one is still
272 # thrown when the command cannot be run in the first place. This is
273 # known to happen at least when the PATH does not include the chromite
274 # bin dir.
275 raise CrosSdkNotRunError('Unable to enter the chroot.')
276
277 logging.info('Endpoint execution completed, return code: %d',
278 result.returncode)
279
Alex Kleinbd6edf82019-07-18 10:30:49 -0600280 # Transfer result files out of the chroot.
281 output_content = osutils.ReadFile(new_output)
282 if output_content:
283 json_format.Parse(output_content, output_msg)
Alex Kleinaae49772019-07-26 10:20:50 -0600284 field_handler.extract_results(input_msg, output_msg, chroot)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600285
286 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
Alex Klein146d4772019-06-20 13:48:25 -0600287
288 return result.returncode
289
290 def _GetChrootArgs(self, chroot):
291 """Translate a Chroot message to chroot enter args.
292
293 Args:
294 chroot (chromiumos.Chroot): A chroot message.
295
296 Returns:
297 list[str]: The cros_sdk args for the chroot.
298 """
299 args = []
300 if chroot.path:
301 args.extend(['--chroot', chroot.path])
302 if chroot.cache_dir:
303 args.extend(['--cache-dir', chroot.cache_dir])
304
305 return args
306
307 def _GetChrootEnv(self, chroot):
308 """Get chroot environment variables that need to be set."""
309 use_flags = [u.flag for u in chroot.env.use_flags]
310 features = [f.feature for f in chroot.env.features]
311
312 env = {}
313 if use_flags:
314 env['USE'] = ' '.join(use_flags)
315
316 # TODO(saklein) Remove the default when fully integrated in recipes.
317 env['FEATURES'] = 'separatedebug'
318 if features:
319 env['FEATURES'] = ' '.join(features)
320
321 return env
322
323 def _GetMethod(self, module_name, method_name):
324 """Get the implementation of the method for the service module.
325
326 Args:
327 module_name (str): The name of the service module.
328 method_name (str): The name of the method.
329
330 Returns:
331 callable - The method.
332
333 Raises:
334 MethodNotFoundError when the method cannot be found in the module.
335 ServiceModuleNotFoundError when the service module cannot be imported.
336 """
337 try:
338 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
339 except ImportError as e:
340 raise ServiceControllerNotFoundError(e.message)
341 try:
342 return getattr(module, method_name)
343 except AttributeError as e:
344 raise MethodNotFoundError(e.message)
345
346
347def RegisterServices(router):
348 """Register all the services.
349
350 Args:
351 router (Router): The router.
352 """
Alex Klein54e38e32019-06-21 14:54:17 -0600353 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600354 router.Register(artifacts_pb2)
355 router.Register(binhost_pb2)
356 router.Register(depgraph_pb2)
357 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600358 router.Register(packages_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600359 router.Register(sdk_pb2)
360 router.Register(sysroot_pb2)
361 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700362 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600363 logging.debug('Services registered successfully.')
364
365
366def GetRouter():
367 """Get a router that has had all of the services registered."""
368 router = Router()
369 RegisterServices(router)
370
371 return router