blob: 785a25c603bb4a7c94e23a0d2989ef4561ee682d [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 Kleinf4dc4f52018-12-05 13:55:12 -070012
Alex Kleinf4dc4f52018-12-05 13:55:12 -070013from google.protobuf import json_format
14from google.protobuf import symbol_database
15
Alex Kleinb7cdbe62019-02-22 11:41:32 -070016from chromite.api import controller
Evan Hernandezaeb556a2019-04-03 11:28:49 -060017from chromite.api.gen.chromite.api import artifacts_pb2
18from chromite.api.gen.chromite.api import binhost_pb2
Alex Klein7107bdd2019-03-14 17:14:31 -060019from chromite.api.gen.chromite.api import build_api_pb2
20from chromite.api.gen.chromite.api import depgraph_pb2
21from chromite.api.gen.chromite.api import image_pb2
22from chromite.api.gen.chromite.api import sdk_pb2
Alex Kleind4e1e422019-03-18 16:00:41 -060023from chromite.api.gen.chromite.api import sysroot_pb2
Alex Kleinc5403d62019-04-03 09:34:59 -060024from chromite.api.gen.chromite.api import test_pb2
Alex Kleinf4dc4f52018-12-05 13:55:12 -070025from chromite.lib import commandline
Alex Klein2bfacb22019-02-04 11:42:17 -070026from chromite.lib import cros_build_lib
Alex Kleinf4dc4f52018-12-05 13:55:12 -070027from chromite.lib import osutils
Alex Klein00b1f1e2019-02-08 13:53:42 -070028from chromite.utils import matching
Alex Kleinf4dc4f52018-12-05 13:55:12 -070029
30
31class Error(Exception):
32 """Base error class for the module."""
33
34
Alex Klein5bcb4d22019-03-21 13:51:54 -060035class InvalidInputFileError(Error):
36 """Raised when the input file cannot be read."""
37
38
Alex Klein7a115172019-02-08 14:14:20 -070039class InvalidInputFormatError(Error):
40 """Raised when the passed input protobuf can't be parsed."""
41
42
Alex Klein5bcb4d22019-03-21 13:51:54 -060043class InvalidOutputFileError(Error):
44 """Raised when the output file cannot be written."""
45
46
Alex Kleinf4dc4f52018-12-05 13:55:12 -070047# API Service Errors.
48class UnknownServiceError(Error):
49 """Error raised when the requested service has not been registered."""
50
51
Alex Kleinb7cdbe62019-02-22 11:41:32 -070052class ControllerModuleNotDefinedError(Error):
53 """Error class for when no controller is defined for a service."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070054
55
Alex Kleinb7cdbe62019-02-22 11:41:32 -070056class ServiceControllerNotFoundError(Error):
57 """Error raised when the service's controller cannot be imported."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070058
59
60# API Method Errors.
61class UnknownMethodError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070062 """The service is defined in the proto but the method is not."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070063
64
65class MethodNotFoundError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070066 """The method's implementation cannot be found in the service's controller."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070067
68
69def GetParser():
Alex Klein00b1f1e2019-02-08 13:53:42 -070070 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070071 parser = commandline.ArgumentParser(description=__doc__)
72
73 parser.add_argument('service_method',
74 help='The "chromite.api.Service/Method" that is being '
75 'called.')
76
77 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070078 '--input-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070079 help='Path to the JSON serialized input argument protobuf message.')
80 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070081 '--output-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070082 help='The path to which the result protobuf message should be written.')
83
84 return parser
85
86
Alex Klein00b1f1e2019-02-08 13:53:42 -070087def _ParseArgs(argv, router):
Alex Kleinf4dc4f52018-12-05 13:55:12 -070088 """Parse and validate arguments."""
89 parser = GetParser()
90 opts = parser.parse_args(argv)
91
Alex Klein00b1f1e2019-02-08 13:53:42 -070092 methods = router.ListMethods()
93 if opts.service_method not in methods:
94 matched = matching.GetMostLikelyMatchedObject(methods, opts.service_method,
95 matched_score_threshold=0.6)
96 error = 'Unrecognized service name.'
97 if matched:
98 error += '\nDid you mean: \n%s' % '\n'.join(matched)
99 parser.error(error)
100
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700101 parts = opts.service_method.split('/')
102
103 if len(parts) != 2:
104 parser.error('Must pass "Service/Method".')
105
106 opts.service = parts[0]
107 opts.method = parts[1]
108
Alex Klein5bcb4d22019-03-21 13:51:54 -0600109 if not os.path.exists(opts.input_json):
110 parser.error('Input file does not exist.')
111
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700112 opts.Freeze()
113 return opts
114
115
116class Router(object):
117 """Encapsulates the request dispatching logic."""
118
119 def __init__(self):
120 self._services = {}
121 self._aliases = {}
122 # All imported generated messages get added to this symbol db.
123 self._sym_db = symbol_database.Default()
124
125 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
126 self._service_options = extensions['service_options']
127 self._method_options = extensions['method_options']
128
129 def Register(self, proto_module):
130 """Register the services from a generated proto module.
131
132 Args:
133 proto_module (module): The generated proto module whose service is being
134 registered.
135
136 Raises:
137 ServiceModuleNotDefinedError when the service cannot be found in the
138 provided module.
139 """
140 services = proto_module.DESCRIPTOR.services_by_name
141 for service_name, svc in services.items():
142 module_name = svc.GetOptions().Extensions[self._service_options].module
143
144 if not module_name:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700145 raise ControllerModuleNotDefinedError(
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700146 'The module must be defined in the service definition: %s.%s' %
147 (proto_module, service_name))
148
149 self._services[svc.full_name] = (svc, module_name)
150
Alex Klein00b1f1e2019-02-08 13:53:42 -0700151 def ListMethods(self):
152 """List all methods registered with the router."""
153 services = []
154 for service_name, (svc, _module) in self._services.items():
155 for method_name in svc.methods_by_name.keys():
156 services.append('%s/%s' % (service_name, method_name))
157
158 return sorted(services)
159
Alex Klein5bcb4d22019-03-21 13:51:54 -0600160 def Route(self, service_name, method_name, input_path, output_path):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700161 """Dispatch the request.
162
163 Args:
164 service_name (str): The fully qualified service name.
165 method_name (str): The name of the method being called.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600166 input_path (str): The path to the input message file.
167 output_path (str): The path where the output message should be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700168
169 Returns:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600170 int: The return code.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700171
172 Raises:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600173 InvalidInputFileError when the input file cannot be read.
174 InvalidOutputFileError when the output file cannot be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700175 ServiceModuleNotFoundError when the service module cannot be imported.
176 MethodNotFoundError when the method cannot be retrieved from the module.
177 """
178 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600179 input_json = osutils.ReadFile(input_path)
180 except IOError as e:
181 raise InvalidInputFileError('Unable to read input file: %s' % e.message)
182
183 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700184 svc, module_name = self._services[service_name]
185 except KeyError:
186 raise UnknownServiceError('The %s service has not been registered.'
187 % service_name)
188
189 try:
190 method_desc = svc.methods_by_name[method_name]
191 except KeyError:
192 raise UnknownMethodError('The %s method has not been defined in the %s '
193 'service.' % (method_name, service_name))
194
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700195 # Parse the input file to build an instance of the input message.
196 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
Alex Klein7a115172019-02-08 14:14:20 -0700197 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700198 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
Alex Klein7a115172019-02-08 14:14:20 -0700199 except json_format.ParseError as e:
200 raise InvalidInputFormatError(
201 'Unable to parse the input json: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700202
203 # Get an empty output message instance.
204 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700205
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700206 # Allow proto-based method name override.
207 method_options = method_desc.GetOptions().Extensions[self._method_options]
208 if method_options.HasField('implementation_name'):
209 method_name = method_options.implementation_name
210
Alex Klein2bfacb22019-02-04 11:42:17 -0700211 # Check the chroot assertion settings before running.
212 service_options = svc.GetOptions().Extensions[self._service_options]
213 self._HandleChrootAssert(service_options, method_options)
214
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700215 # Import the module and get the method.
216 method_impl = self._GetMethod(module_name, method_name)
217
218 # Successfully located; call and return.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600219 return_code = method_impl(input_msg, output_msg)
220 if return_code is None:
221 return_code = 0
222
223 try:
224 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
225 except IOError as e:
226 raise InvalidOutputFileError('Cannot write output file: %s' % e.message)
227
228 return return_code
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700229
Alex Klein2bfacb22019-02-04 11:42:17 -0700230 def _HandleChrootAssert(self, service_options, method_options):
231 """Check the chroot assert options and execute assertion as needed.
232
233 Args:
234 service_options (google.protobuf.Message): The service options.
235 method_options (google.protobuf.Message): The method options.
236 """
237 chroot_assert = build_api_pb2.NO_ASSERTION
238 if method_options.HasField('method_chroot_assert'):
239 # Prefer the method option when set.
240 chroot_assert = method_options.method_chroot_assert
241 elif service_options.HasField('service_chroot_assert'):
242 # Fall back to the service option.
243 chroot_assert = service_options.service_chroot_assert
244
245 # Execute appropriate assertion if set.
246 if chroot_assert == build_api_pb2.INSIDE:
247 cros_build_lib.AssertInsideChroot()
248 elif chroot_assert == build_api_pb2.OUTSIDE:
249 cros_build_lib.AssertOutsideChroot()
250
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700251 def _GetMethod(self, module_name, method_name):
252 """Get the implementation of the method for the service module.
253
254 Args:
255 module_name (str): The name of the service module.
256 method_name (str): The name of the method.
257
258 Returns:
259 callable - The method.
260
261 Raises:
262 MethodNotFoundError when the method cannot be found in the module.
263 ServiceModuleNotFoundError when the service module cannot be imported.
264 """
265 try:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700266 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700267 except ImportError as e:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700268 raise ServiceControllerNotFoundError(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700269 try:
270 return getattr(module, method_name)
271 except AttributeError as e:
272 raise MethodNotFoundError(e.message)
273
274
275def RegisterServices(router):
276 """Register all the services.
277
278 Args:
279 router (Router): The router.
280 """
Evan Hernandezaeb556a2019-04-03 11:28:49 -0600281 router.Register(artifacts_pb2)
282 router.Register(binhost_pb2)
Ned Nguyen9a7a9052019-02-05 11:04:03 -0700283 router.Register(depgraph_pb2)
Alex Klein2966e302019-01-17 13:29:38 -0700284 router.Register(image_pb2)
Alex Klein19c4cc42019-02-27 14:47:57 -0700285 router.Register(sdk_pb2)
Alex Kleind4e1e422019-03-18 16:00:41 -0600286 router.Register(sysroot_pb2)
Alex Kleinc5403d62019-04-03 09:34:59 -0600287 router.Register(test_pb2)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700288
289
290def main(argv):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700291 router = Router()
292 RegisterServices(router)
293
Alex Klein00b1f1e2019-02-08 13:53:42 -0700294 opts = _ParseArgs(argv, router)
295
Alex Klein7a115172019-02-08 14:14:20 -0700296 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600297 return router.Route(opts.service, opts.method, opts.input_json,
298 opts.output_json)
Alex Klein7a115172019-02-08 14:14:20 -0700299 except Error as e:
300 # Error derivatives are handled nicely, but let anything else bubble up.
301 cros_build_lib.Die(e.message)