blob: 6c08d9f6d653d39491d57a02cbfa20f2c7debc1d [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
23from chromite.api.gen.chromite.api import test_archive_pb2
Alex Kleinf4dc4f52018-12-05 13:55:12 -070024from chromite.lib import commandline
Alex Klein2bfacb22019-02-04 11:42:17 -070025from chromite.lib import cros_build_lib
Alex Kleinf4dc4f52018-12-05 13:55:12 -070026from chromite.lib import osutils
Alex Klein00b1f1e2019-02-08 13:53:42 -070027from chromite.utils import matching
Alex Kleinf4dc4f52018-12-05 13:55:12 -070028
29
30class Error(Exception):
31 """Base error class for the module."""
32
33
Alex Klein5bcb4d22019-03-21 13:51:54 -060034class InvalidInputFileError(Error):
35 """Raised when the input file cannot be read."""
36
37
Alex Klein7a115172019-02-08 14:14:20 -070038class InvalidInputFormatError(Error):
39 """Raised when the passed input protobuf can't be parsed."""
40
41
Alex Klein5bcb4d22019-03-21 13:51:54 -060042class InvalidOutputFileError(Error):
43 """Raised when the output file cannot be written."""
44
45
Alex Kleinf4dc4f52018-12-05 13:55:12 -070046# API Service Errors.
47class UnknownServiceError(Error):
48 """Error raised when the requested service has not been registered."""
49
50
Alex Kleinb7cdbe62019-02-22 11:41:32 -070051class ControllerModuleNotDefinedError(Error):
52 """Error class for when no controller is defined for a service."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070053
54
Alex Kleinb7cdbe62019-02-22 11:41:32 -070055class ServiceControllerNotFoundError(Error):
56 """Error raised when the service's controller cannot be imported."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070057
58
59# API Method Errors.
60class UnknownMethodError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070061 """The service is defined in the proto but the method is not."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070062
63
64class MethodNotFoundError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070065 """The method's implementation cannot be found in the service's controller."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070066
67
68def GetParser():
Alex Klein00b1f1e2019-02-08 13:53:42 -070069 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070070 parser = commandline.ArgumentParser(description=__doc__)
71
72 parser.add_argument('service_method',
73 help='The "chromite.api.Service/Method" that is being '
74 'called.')
75
76 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070077 '--input-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070078 help='Path to the JSON serialized input argument protobuf message.')
79 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070080 '--output-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070081 help='The path to which the result protobuf message should be written.')
82
83 return parser
84
85
Alex Klein00b1f1e2019-02-08 13:53:42 -070086def _ParseArgs(argv, router):
Alex Kleinf4dc4f52018-12-05 13:55:12 -070087 """Parse and validate arguments."""
88 parser = GetParser()
89 opts = parser.parse_args(argv)
90
Alex Klein00b1f1e2019-02-08 13:53:42 -070091 methods = router.ListMethods()
92 if opts.service_method not in methods:
93 matched = matching.GetMostLikelyMatchedObject(methods, opts.service_method,
94 matched_score_threshold=0.6)
95 error = 'Unrecognized service name.'
96 if matched:
97 error += '\nDid you mean: \n%s' % '\n'.join(matched)
98 parser.error(error)
99
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700100 parts = opts.service_method.split('/')
101
102 if len(parts) != 2:
103 parser.error('Must pass "Service/Method".')
104
105 opts.service = parts[0]
106 opts.method = parts[1]
107
Alex Klein5bcb4d22019-03-21 13:51:54 -0600108 if not os.path.exists(opts.input_json):
109 parser.error('Input file does not exist.')
110
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700111 opts.Freeze()
112 return opts
113
114
115class Router(object):
116 """Encapsulates the request dispatching logic."""
117
118 def __init__(self):
119 self._services = {}
120 self._aliases = {}
121 # All imported generated messages get added to this symbol db.
122 self._sym_db = symbol_database.Default()
123
124 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
125 self._service_options = extensions['service_options']
126 self._method_options = extensions['method_options']
127
128 def Register(self, proto_module):
129 """Register the services from a generated proto module.
130
131 Args:
132 proto_module (module): The generated proto module whose service is being
133 registered.
134
135 Raises:
136 ServiceModuleNotDefinedError when the service cannot be found in the
137 provided module.
138 """
139 services = proto_module.DESCRIPTOR.services_by_name
140 for service_name, svc in services.items():
141 module_name = svc.GetOptions().Extensions[self._service_options].module
142
143 if not module_name:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700144 raise ControllerModuleNotDefinedError(
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700145 'The module must be defined in the service definition: %s.%s' %
146 (proto_module, service_name))
147
148 self._services[svc.full_name] = (svc, module_name)
149
Alex Klein00b1f1e2019-02-08 13:53:42 -0700150 def ListMethods(self):
151 """List all methods registered with the router."""
152 services = []
153 for service_name, (svc, _module) in self._services.items():
154 for method_name in svc.methods_by_name.keys():
155 services.append('%s/%s' % (service_name, method_name))
156
157 return sorted(services)
158
Alex Klein5bcb4d22019-03-21 13:51:54 -0600159 def Route(self, service_name, method_name, input_path, output_path):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700160 """Dispatch the request.
161
162 Args:
163 service_name (str): The fully qualified service name.
164 method_name (str): The name of the method being called.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600165 input_path (str): The path to the input message file.
166 output_path (str): The path where the output message should be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700167
168 Returns:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600169 int: The return code.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700170
171 Raises:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600172 InvalidInputFileError when the input file cannot be read.
173 InvalidOutputFileError when the output file cannot be written.
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700174 ServiceModuleNotFoundError when the service module cannot be imported.
175 MethodNotFoundError when the method cannot be retrieved from the module.
176 """
177 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600178 input_json = osutils.ReadFile(input_path)
179 except IOError as e:
180 raise InvalidInputFileError('Unable to read input file: %s' % e.message)
181
182 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700183 svc, module_name = self._services[service_name]
184 except KeyError:
185 raise UnknownServiceError('The %s service has not been registered.'
186 % service_name)
187
188 try:
189 method_desc = svc.methods_by_name[method_name]
190 except KeyError:
191 raise UnknownMethodError('The %s method has not been defined in the %s '
192 'service.' % (method_name, service_name))
193
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700194 # Parse the input file to build an instance of the input message.
195 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
Alex Klein7a115172019-02-08 14:14:20 -0700196 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700197 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
Alex Klein7a115172019-02-08 14:14:20 -0700198 except json_format.ParseError as e:
199 raise InvalidInputFormatError(
200 'Unable to parse the input json: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700201
202 # Get an empty output message instance.
203 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700204
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700205 # Allow proto-based method name override.
206 method_options = method_desc.GetOptions().Extensions[self._method_options]
207 if method_options.HasField('implementation_name'):
208 method_name = method_options.implementation_name
209
Alex Klein2bfacb22019-02-04 11:42:17 -0700210 # Check the chroot assertion settings before running.
211 service_options = svc.GetOptions().Extensions[self._service_options]
212 self._HandleChrootAssert(service_options, method_options)
213
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700214 # Import the module and get the method.
215 method_impl = self._GetMethod(module_name, method_name)
216
217 # Successfully located; call and return.
Alex Klein5bcb4d22019-03-21 13:51:54 -0600218 return_code = method_impl(input_msg, output_msg)
219 if return_code is None:
220 return_code = 0
221
222 try:
223 osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
224 except IOError as e:
225 raise InvalidOutputFileError('Cannot write output file: %s' % e.message)
226
227 return return_code
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700228
Alex Klein2bfacb22019-02-04 11:42:17 -0700229 def _HandleChrootAssert(self, service_options, method_options):
230 """Check the chroot assert options and execute assertion as needed.
231
232 Args:
233 service_options (google.protobuf.Message): The service options.
234 method_options (google.protobuf.Message): The method options.
235 """
236 chroot_assert = build_api_pb2.NO_ASSERTION
237 if method_options.HasField('method_chroot_assert'):
238 # Prefer the method option when set.
239 chroot_assert = method_options.method_chroot_assert
240 elif service_options.HasField('service_chroot_assert'):
241 # Fall back to the service option.
242 chroot_assert = service_options.service_chroot_assert
243
244 # Execute appropriate assertion if set.
245 if chroot_assert == build_api_pb2.INSIDE:
246 cros_build_lib.AssertInsideChroot()
247 elif chroot_assert == build_api_pb2.OUTSIDE:
248 cros_build_lib.AssertOutsideChroot()
249
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700250 def _GetMethod(self, module_name, method_name):
251 """Get the implementation of the method for the service module.
252
253 Args:
254 module_name (str): The name of the service module.
255 method_name (str): The name of the method.
256
257 Returns:
258 callable - The method.
259
260 Raises:
261 MethodNotFoundError when the method cannot be found in the module.
262 ServiceModuleNotFoundError when the service module cannot be imported.
263 """
264 try:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700265 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700266 except ImportError as e:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700267 raise ServiceControllerNotFoundError(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700268 try:
269 return getattr(module, method_name)
270 except AttributeError as e:
271 raise MethodNotFoundError(e.message)
272
273
274def RegisterServices(router):
275 """Register all the services.
276
277 Args:
278 router (Router): The router.
279 """
Evan Hernandezaeb556a2019-04-03 11:28:49 -0600280 router.Register(artifacts_pb2)
281 router.Register(binhost_pb2)
Ned Nguyen9a7a9052019-02-05 11:04:03 -0700282 router.Register(depgraph_pb2)
Alex Klein2966e302019-01-17 13:29:38 -0700283 router.Register(image_pb2)
Alex Klein19c4cc42019-02-27 14:47:57 -0700284 router.Register(sdk_pb2)
Alex Kleinb400da62019-02-20 16:42:50 -0700285 router.Register(test_archive_pb2)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700286
287
288def main(argv):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700289 router = Router()
290 RegisterServices(router)
291
Alex Klein00b1f1e2019-02-08 13:53:42 -0700292 opts = _ParseArgs(argv, router)
293
Alex Klein7a115172019-02-08 14:14:20 -0700294 try:
Alex Klein5bcb4d22019-03-21 13:51:54 -0600295 return router.Route(opts.service, opts.method, opts.input_json,
296 opts.output_json)
Alex Klein7a115172019-02-08 14:14:20 -0700297 except Error as e:
298 # Error derivatives are handled nicely, but let anything else bubble up.
299 cros_build_lib.Die(e.message)