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