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