blob: ab983a146ec824aefa043928e8f855ddf94ed4b0 [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
127 def Route(self, service_name, method_name, input_path, output_path):
128 """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.
135
136 Returns:
137 int: The return code.
138
139 Raises:
140 InvalidInputFileError when the input file cannot be read.
141 InvalidOutputFileError when the output file cannot be written.
142 ServiceModuleNotFoundError when the service module cannot be imported.
143 MethodNotFoundError when the method cannot be retrieved from the module.
144 """
145 try:
146 input_json = osutils.ReadFile(input_path).strip()
147 except IOError as e:
148 raise InvalidInputFileError('Unable to read input file: %s' % e.message)
149
150 try:
151 svc, module_name = self._services[service_name]
152 except KeyError:
153 raise UnknownServiceError('The %s service has not been registered.'
154 % service_name)
155
156 try:
157 method_desc = svc.methods_by_name[method_name]
158 except KeyError:
159 raise UnknownMethodError('The %s method has not been defined in the %s '
160 'service.' % (method_name, service_name))
161
162 # Parse the input file to build an instance of the input message.
163 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
164 try:
165 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
166 except json_format.ParseError as e:
167 raise InvalidInputFormatError(
168 'Unable to parse the input json: %s' % e.message)
169
170 # Get an empty output message instance.
171 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
172
Alex Kleinbd6edf82019-07-18 10:30:49 -0600173 # Fetch the method options for chroot and method name overrides.
Alex Klein146d4772019-06-20 13:48:25 -0600174 method_options = method_desc.GetOptions().Extensions[self._method_options]
Alex Klein146d4772019-06-20 13:48:25 -0600175
176 # Check the chroot settings before running.
177 service_options = svc.GetOptions().Extensions[self._service_options]
178 if self._ChrootCheck(service_options, method_options):
179 # Run inside the chroot instead.
180 logging.info('Re-executing the endpoint inside the chroot.')
Alex Kleinbd6edf82019-07-18 10:30:49 -0600181 return self._ReexecuteInside(input_msg, output_msg, output_path,
182 service_name, method_name)
183
184 # Allow proto-based method name override.
185 if method_options.HasField('implementation_name'):
186 method_name = method_options.implementation_name
Alex Klein146d4772019-06-20 13:48:25 -0600187
188 # Import the module and get the method.
189 method_impl = self._GetMethod(module_name, method_name)
190
191 # Successfully located; call and return.
192 return_code = method_impl(input_msg, output_msg)
193 if return_code is None:
194 return_code = controller.RETURN_CODE_SUCCESS
195
196 try:
197 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
198 except IOError as e:
199 raise InvalidOutputFileError('Cannot write output file: %s' % e.message)
200
201 return return_code
202
203 def _ChrootCheck(self, service_options, method_options):
204 """Check the chroot options, and execute assertion or note reexec as needed.
205
206 Args:
207 service_options (google.protobuf.Message): The service options.
208 method_options (google.protobuf.Message): The method options.
209
210 Returns:
211 bool - True iff it needs to be reexeced inside the chroot.
212
213 Raises:
214 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
215 """
216 chroot_assert = build_api_pb2.NO_ASSERTION
217 if method_options.HasField('method_chroot_assert'):
218 # Prefer the method option when set.
219 chroot_assert = method_options.method_chroot_assert
220 elif service_options.HasField('service_chroot_assert'):
221 # Fall back to the service option.
222 chroot_assert = service_options.service_chroot_assert
223
224 if chroot_assert == build_api_pb2.INSIDE:
225 return not cros_build_lib.IsInsideChroot()
226 elif chroot_assert == build_api_pb2.OUTSIDE:
227 # If it must be run outside we have to already be outside.
228 cros_build_lib.AssertOutsideChroot()
229
230 return False
231
Alex Kleinbd6edf82019-07-18 10:30:49 -0600232 def _ReexecuteInside(self, input_msg, output_msg, output_path, service_name,
233 method_name):
Alex Klein146d4772019-06-20 13:48:25 -0600234 """Re-execute the service inside the chroot.
235
236 Args:
237 input_msg (Message): The parsed input message.
Alex Kleinbd6edf82019-07-18 10:30:49 -0600238 output_msg (Message): The empty output message instance.
Alex Klein146d4772019-06-20 13:48:25 -0600239 output_path (str): The path for the serialized output.
240 service_name (str): The name of the service to run.
241 method_name (str): The name of the method to run.
242 """
243 # Parse the chroot and clear the chroot field in the input message.
244 chroot = field_handler.handle_chroot(input_msg)
245
246 base_dir = os.path.join(chroot.path, 'tmp')
247 with field_handler.handle_paths(input_msg, base_dir, prefix=chroot.path):
248 with osutils.TempDir(base_dir=base_dir) as tempdir:
Alex Kleinbd6edf82019-07-18 10:30:49 -0600249 new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
Alex Klein146d4772019-06-20 13:48:25 -0600250 chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
Alex Kleinbd6edf82019-07-18 10:30:49 -0600251 new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
Alex Klein146d4772019-06-20 13:48:25 -0600252 chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
253
254 logging.info('Writing input message to: %s', new_input)
255 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
256 osutils.Touch(new_output)
257
258 cmd = ['build_api', '%s/%s' % (service_name, method_name),
259 '--input-json', chroot_input, '--output-json', chroot_output]
260
261 try:
262 result = cros_build_lib.RunCommand(cmd, enter_chroot=True,
263 chroot_args=chroot.GetEnterArgs(),
264 error_code_ok=True,
265 extra_env=chroot.env)
266 except cros_build_lib.RunCommandError:
267 # A non-zero return code will not result in an error, but one is still
268 # thrown when the command cannot be run in the first place. This is
269 # known to happen at least when the PATH does not include the chromite
270 # bin dir.
271 raise CrosSdkNotRunError('Unable to enter the chroot.')
272
273 logging.info('Endpoint execution completed, return code: %d',
274 result.returncode)
275
Alex Kleinbd6edf82019-07-18 10:30:49 -0600276 # Transfer result files out of the chroot.
277 output_content = osutils.ReadFile(new_output)
278 if output_content:
279 json_format.Parse(output_content, output_msg)
280 field_handler.handle_result_paths(input_msg, output_msg, chroot)
281
282 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
Alex Klein146d4772019-06-20 13:48:25 -0600283
284 return result.returncode
285
286 def _GetChrootArgs(self, chroot):
287 """Translate a Chroot message to chroot enter args.
288
289 Args:
290 chroot (chromiumos.Chroot): A chroot message.
291
292 Returns:
293 list[str]: The cros_sdk args for the chroot.
294 """
295 args = []
296 if chroot.path:
297 args.extend(['--chroot', chroot.path])
298 if chroot.cache_dir:
299 args.extend(['--cache-dir', chroot.cache_dir])
300
301 return args
302
303 def _GetChrootEnv(self, chroot):
304 """Get chroot environment variables that need to be set."""
305 use_flags = [u.flag for u in chroot.env.use_flags]
306 features = [f.feature for f in chroot.env.features]
307
308 env = {}
309 if use_flags:
310 env['USE'] = ' '.join(use_flags)
311
312 # TODO(saklein) Remove the default when fully integrated in recipes.
313 env['FEATURES'] = 'separatedebug'
314 if features:
315 env['FEATURES'] = ' '.join(features)
316
317 return env
318
319 def _GetMethod(self, module_name, method_name):
320 """Get the implementation of the method for the service module.
321
322 Args:
323 module_name (str): The name of the service module.
324 method_name (str): The name of the method.
325
326 Returns:
327 callable - The method.
328
329 Raises:
330 MethodNotFoundError when the method cannot be found in the module.
331 ServiceModuleNotFoundError when the service module cannot be imported.
332 """
333 try:
334 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
335 except ImportError as e:
336 raise ServiceControllerNotFoundError(e.message)
337 try:
338 return getattr(module, method_name)
339 except AttributeError as e:
340 raise MethodNotFoundError(e.message)
341
342
343def RegisterServices(router):
344 """Register all the services.
345
346 Args:
347 router (Router): The router.
348 """
Alex Klein54e38e32019-06-21 14:54:17 -0600349 router.Register(api_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600350 router.Register(artifacts_pb2)
351 router.Register(binhost_pb2)
352 router.Register(depgraph_pb2)
353 router.Register(image_pb2)
Alex Kleineb77ffa2019-05-28 14:47:44 -0600354 router.Register(packages_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600355 router.Register(sdk_pb2)
356 router.Register(sysroot_pb2)
357 router.Register(test_pb2)
Tiancong Wangaf050172019-07-10 11:52:03 -0700358 router.Register(toolchain_pb2)
Alex Klein146d4772019-06-20 13:48:25 -0600359 logging.debug('Services registered successfully.')
360
361
362def GetRouter():
363 """Get a router that has had all of the services registered."""
364 router = Router()
365 RegisterServices(router)
366
367 return router