blob: 99cf69ba0b7770301ef485bfca7da982170a60bc [file] [log] [blame]
Alex Kleinf4dc4f52018-12-05 13:55:12 -07001# -*- coding: utf-8 -*-
2# Copyright 2018 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"""The build API entry point."""
7
8from __future__ import print_function
9
10import importlib
Alex Klein5bcb4d22019-03-21 13:51:54 -060011import os
Alex Klein00aa8072019-04-15 16:36:00 -060012import shutil
Alex Kleinf4dc4f52018-12-05 13:55:12 -070013
Alex Kleinf4dc4f52018-12-05 13:55:12 -070014from google.protobuf import json_format
15from google.protobuf import symbol_database
16
Alex Kleinb7cdbe62019-02-22 11:41:32 -070017from chromite.api import controller
Evan Hernandezaeb556a2019-04-03 11:28:49 -060018from chromite.api.gen.chromite.api import artifacts_pb2
19from chromite.api.gen.chromite.api import binhost_pb2
Alex Klein7107bdd2019-03-14 17:14:31 -060020from chromite.api.gen.chromite.api import build_api_pb2
21from chromite.api.gen.chromite.api import depgraph_pb2
22from chromite.api.gen.chromite.api import image_pb2
23from chromite.api.gen.chromite.api import sdk_pb2
Alex Kleind4e1e422019-03-18 16:00:41 -060024from chromite.api.gen.chromite.api import sysroot_pb2
Alex Kleinc5403d62019-04-03 09:34:59 -060025from chromite.api.gen.chromite.api import test_pb2
Alex Klein00aa8072019-04-15 16:36:00 -060026from chromite.api.gen.chromiumos import common_pb2
27from chromite.lib import constants
Alex Kleinf4dc4f52018-12-05 13:55:12 -070028from chromite.lib import commandline
Alex Klein2bfacb22019-02-04 11:42:17 -070029from chromite.lib import cros_build_lib
Alex Kleinf4dc4f52018-12-05 13:55:12 -070030from chromite.lib import osutils
Alex Klein00b1f1e2019-02-08 13:53:42 -070031from chromite.utils import matching
Alex Kleinf4dc4f52018-12-05 13:55:12 -070032
33
34class Error(Exception):
35 """Base error class for the module."""
36
37
Alex Klein5bcb4d22019-03-21 13:51:54 -060038class InvalidInputFileError(Error):
39 """Raised when the input file cannot be read."""
40
41
Alex Klein7a115172019-02-08 14:14:20 -070042class InvalidInputFormatError(Error):
43 """Raised when the passed input protobuf can't be parsed."""
44
45
Alex Klein5bcb4d22019-03-21 13:51:54 -060046class InvalidOutputFileError(Error):
47 """Raised when the output file cannot be written."""
48
49
Alex Kleinf4dc4f52018-12-05 13:55:12 -070050# API Service Errors.
51class UnknownServiceError(Error):
52 """Error raised when the requested service has not been registered."""
53
54
Alex Kleinb7cdbe62019-02-22 11:41:32 -070055class ControllerModuleNotDefinedError(Error):
56 """Error class for when no controller is defined for a service."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070057
58
Alex Kleinb7cdbe62019-02-22 11:41:32 -070059class ServiceControllerNotFoundError(Error):
60 """Error raised when the service's controller cannot be imported."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070061
62
63# API Method Errors.
64class UnknownMethodError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070065 """The service is defined in the proto but the method is not."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070066
67
68class MethodNotFoundError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070069 """The method's implementation cannot be found in the service's controller."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070070
71
72def GetParser():
Alex Klein00b1f1e2019-02-08 13:53:42 -070073 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070074 parser = commandline.ArgumentParser(description=__doc__)
75
76 parser.add_argument('service_method',
77 help='The "chromite.api.Service/Method" that is being '
78 'called.')
79
80 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070081 '--input-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070082 help='Path to the JSON serialized input argument protobuf message.')
83 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070084 '--output-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070085 help='The path to which the result protobuf message should be written.')
86
87 return parser
88
89
Alex Klein00b1f1e2019-02-08 13:53:42 -070090def _ParseArgs(argv, router):
Alex Kleinf4dc4f52018-12-05 13:55:12 -070091 """Parse and validate arguments."""
92 parser = GetParser()
93 opts = parser.parse_args(argv)
94
Alex Klein00b1f1e2019-02-08 13:53:42 -070095 methods = router.ListMethods()
96 if opts.service_method not in methods:
Alex Klein00aa8072019-04-15 16:36:00 -060097 # Unknown method, try to match against known methods and make a suggestion.
98 # This is just for developer sanity, e.g. misspellings when testing.
Alex Klein00b1f1e2019-02-08 13:53:42 -070099 matched = matching.GetMostLikelyMatchedObject(methods, opts.service_method,
100 matched_score_threshold=0.6)
101 error = 'Unrecognized service name.'
102 if matched:
103 error += '\nDid you mean: \n%s' % '\n'.join(matched)
104 parser.error(error)
105
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700106 parts = opts.service_method.split('/')
107
108 if len(parts) != 2:
109 parser.error('Must pass "Service/Method".')
110
111 opts.service = parts[0]
112 opts.method = parts[1]
113
Alex Klein5bcb4d22019-03-21 13:51:54 -0600114 if not os.path.exists(opts.input_json):
115 parser.error('Input file does not exist.')
116
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700117 opts.Freeze()
118 return opts
119
120
121class Router(object):
122 """Encapsulates the request dispatching logic."""
123
124 def __init__(self):
125 self._services = {}
126 self._aliases = {}
127 # All imported generated messages get added to this symbol db.
128 self._sym_db = symbol_database.Default()
129
130 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
131 self._service_options = extensions['service_options']
132 self._method_options = extensions['method_options']
133
134 def Register(self, proto_module):
135 """Register the services from a generated proto module.
136
137 Args:
138 proto_module (module): The generated proto module whose service is being
139 registered.
140
141 Raises:
142 ServiceModuleNotDefinedError when the service cannot be found in the
143 provided module.
144 """
145 services = proto_module.DESCRIPTOR.services_by_name
146 for service_name, svc in services.items():
147 module_name = svc.GetOptions().Extensions[self._service_options].module
148
149 if not module_name:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700150 raise ControllerModuleNotDefinedError(
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700151 'The module must be defined in the service definition: %s.%s' %
152 (proto_module, service_name))
153
154 self._services[svc.full_name] = (svc, module_name)
155
Alex Klein00b1f1e2019-02-08 13:53:42 -0700156 def ListMethods(self):
157 """List all methods registered with the router."""
158 services = []
159 for service_name, (svc, _module) in self._services.items():
160 for method_name in svc.methods_by_name.keys():
161 services.append('%s/%s' % (service_name, method_name))
162
163 return sorted(services)
164
Alex Klein5bcb4d22019-03-21 13:51:54 -0600165 def Route(self, service_name, method_name, input_path, output_path):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700166 """Dispatch the request.
167
168 Args:
169 service_name (str): The fully qualified service name.
170 method_name (str): The name of the method being called.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600171 input_path (str): The path to the input message file.
172 output_path (str): The path where the output message should be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700173
174 Returns:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600175 int: The return code.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700176
177 Raises:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600178 InvalidInputFileError when the input file cannot be read.
179 InvalidOutputFileError when the output file cannot be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700180 ServiceModuleNotFoundError when the service module cannot be imported.
181 MethodNotFoundError when the method cannot be retrieved from the module.
182 """
183 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600184 input_json = osutils.ReadFile(input_path)
185 except IOError as e:
186 raise InvalidInputFileError('Unable to read input file: %s' % e.message)
187
188 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700189 svc, module_name = self._services[service_name]
190 except KeyError:
191 raise UnknownServiceError('The %s service has not been registered.'
192 % service_name)
193
194 try:
195 method_desc = svc.methods_by_name[method_name]
196 except KeyError:
197 raise UnknownMethodError('The %s method has not been defined in the %s '
198 'service.' % (method_name, service_name))
199
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700200 # Parse the input file to build an instance of the input message.
201 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
Alex Klein7a115172019-02-08 14:14:20 -0700202 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700203 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
Alex Klein7a115172019-02-08 14:14:20 -0700204 except json_format.ParseError as e:
205 raise InvalidInputFormatError(
206 'Unable to parse the input json: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700207
208 # Get an empty output message instance.
209 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700210
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700211 # Allow proto-based method name override.
212 method_options = method_desc.GetOptions().Extensions[self._method_options]
213 if method_options.HasField('implementation_name'):
214 method_name = method_options.implementation_name
215
Alex Klein00aa8072019-04-15 16:36:00 -0600216 # Check the chroot settings before running.
Alex Klein2bfacb22019-02-04 11:42:17 -0700217 service_options = svc.GetOptions().Extensions[self._service_options]
Alex Klein00aa8072019-04-15 16:36:00 -0600218 if self._ChrootCheck(service_options, method_options):
219 # Run inside the chroot instead.
220 return self._ReexecuteInside(input_msg, output_path, service_name,
221 method_name)
Alex Klein2bfacb22019-02-04 11:42:17 -0700222
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700223 # Import the module and get the method.
224 method_impl = self._GetMethod(module_name, method_name)
225
226 # Successfully located; call and return.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600227 return_code = method_impl(input_msg, output_msg)
228 if return_code is None:
229 return_code = 0
230
231 try:
232 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
233 except IOError as e:
234 raise InvalidOutputFileError('Cannot write output file: %s' % e.message)
235
236 return return_code
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700237
Alex Klein00aa8072019-04-15 16:36:00 -0600238 def _ChrootCheck(self, service_options, method_options):
239 """Check the chroot options, and execute assertion or note reexec as needed.
Alex Klein2bfacb22019-02-04 11:42:17 -0700240
241 Args:
242 service_options (google.protobuf.Message): The service options.
243 method_options (google.protobuf.Message): The method options.
Alex Klein00aa8072019-04-15 16:36:00 -0600244
245 Returns:
246 bool - True iff it needs to be reexeced inside the chroot.
247
248 Raises:
249 cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
Alex Klein2bfacb22019-02-04 11:42:17 -0700250 """
251 chroot_assert = build_api_pb2.NO_ASSERTION
252 if method_options.HasField('method_chroot_assert'):
253 # Prefer the method option when set.
254 chroot_assert = method_options.method_chroot_assert
255 elif service_options.HasField('service_chroot_assert'):
256 # Fall back to the service option.
257 chroot_assert = service_options.service_chroot_assert
258
Alex Klein2bfacb22019-02-04 11:42:17 -0700259 if chroot_assert == build_api_pb2.INSIDE:
Alex Klein00aa8072019-04-15 16:36:00 -0600260 return not cros_build_lib.IsInsideChroot()
Alex Klein2bfacb22019-02-04 11:42:17 -0700261 elif chroot_assert == build_api_pb2.OUTSIDE:
Alex Klein00aa8072019-04-15 16:36:00 -0600262 # If it must be run outside we have to already be outside.
Alex Klein2bfacb22019-02-04 11:42:17 -0700263 cros_build_lib.AssertOutsideChroot()
264
Alex Klein00aa8072019-04-15 16:36:00 -0600265 return False
266
267 def _ReexecuteInside(self, input_msg, output_path, service_name,
268 method_name):
269 """Re-execute the service inside the chroot.
270
271 Args:
272 input_msg (Message): The parsed input message.
273 output_path (str): The path for the serialized output.
274 service_name (str): The name of the service to run.
275 method_name (str): The name of the method to run.
276 """
277 chroot_args = []
278 chroot_path = constants.DEFAULT_CHROOT_PATH
279 chroot_field_name = None
280 # Find the Chroot field. Search for the field by type to prevent it being
281 # tied to a naming convention.
282 for descriptor in input_msg.DESCRIPTOR.fields:
283 field = getattr(input_msg, descriptor.name)
284 if isinstance(field, common_pb2.Chroot):
285 chroot_field_name = descriptor.name
286 chroot = field
287 chroot_path = chroot.path
288 chroot_args.extend(self._GetChrootArgs(chroot))
289 break
290
291 base_dir = os.path.join(chroot_path, 'tmp')
292 with osutils.TempDir(base_dir=base_dir) as tempdir:
293 new_input = os.path.join(tempdir, 'input.json')
294 chroot_input = '/%s' % os.path.relpath(new_input, chroot_path)
295 new_output = os.path.join(tempdir, 'output.json')
296 chroot_output = '/%s' % os.path.relpath(new_output, chroot_path)
297
298 if chroot_field_name:
299 input_msg.ClearField(chroot_field_name)
300 osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
301 osutils.Touch(new_output)
302
303 cmd = ['build_api', '%s/%s' % (service_name, method_name),
304 '--input-json', chroot_input, '--output-json', chroot_output]
305 result = cros_build_lib.RunCommand(cmd, enter_chroot=True,
306 chroot_args=chroot_args,
307 error_code_ok=True)
308 shutil.move(new_output, output_path)
309
310 return result.returncode
311
312 def _GetChrootArgs(self, chroot):
313 """Translate a Chroot message to chroot enter args.
314
315 Args:
316 chroot (chromiumos.Chroot): A chroot message.
317
318 Returns:
319 list[str]: The cros_sdk args for the chroot.
320 """
321 args = []
322 if chroot.path:
323 args.extend(['--chroot', chroot.path])
324 if chroot.cache_dir:
325 args.extend(['--cache-dir', chroot.cache_dir])
326
327 return args
328
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700329 def _GetMethod(self, module_name, method_name):
330 """Get the implementation of the method for the service module.
331
332 Args:
333 module_name (str): The name of the service module.
334 method_name (str): The name of the method.
335
336 Returns:
337 callable - The method.
338
339 Raises:
340 MethodNotFoundError when the method cannot be found in the module.
341 ServiceModuleNotFoundError when the service module cannot be imported.
342 """
343 try:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700344 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700345 except ImportError as e:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700346 raise ServiceControllerNotFoundError(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700347 try:
348 return getattr(module, method_name)
349 except AttributeError as e:
350 raise MethodNotFoundError(e.message)
351
352
353def RegisterServices(router):
354 """Register all the services.
355
356 Args:
357 router (Router): The router.
358 """
Evan Hernandezaeb556a2019-04-03 11:28:49 -0600359 router.Register(artifacts_pb2)
360 router.Register(binhost_pb2)
Ned Nguyen9a7a9052019-02-05 11:04:03 -0700361 router.Register(depgraph_pb2)
Alex Klein2966e302019-01-17 13:29:38 -0700362 router.Register(image_pb2)
Alex Klein19c4cc42019-02-27 14:47:57 -0700363 router.Register(sdk_pb2)
Alex Kleind4e1e422019-03-18 16:00:41 -0600364 router.Register(sysroot_pb2)
Alex Kleinc5403d62019-04-03 09:34:59 -0600365 router.Register(test_pb2)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700366
367
368def main(argv):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700369 router = Router()
370 RegisterServices(router)
371
Alex Klein00b1f1e2019-02-08 13:53:42 -0700372 opts = _ParseArgs(argv, router)
373
Alex Klein7a115172019-02-08 14:14:20 -0700374 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600375 return router.Route(opts.service, opts.method, opts.input_json,
376 opts.output_json)
Alex Klein7a115172019-02-08 14:14:20 -0700377 except Error as e:
378 # Error derivatives are handled nicely, but let anything else bubble up.
379 cros_build_lib.Die(e.message)