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