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