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