blob: 9119133d6893eb2103a6377d5e6bb03279bb50ca [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 Kleinf4dc4f52018-12-05 13:55:12 -070011
Alex Kleinf4dc4f52018-12-05 13:55:12 -070012from google.protobuf import json_format
13from google.protobuf import symbol_database
14
Alex Kleinb7cdbe62019-02-22 11:41:32 -070015from chromite.api import controller
Alex Klein7107bdd2019-03-14 17:14:31 -060016from chromite.api.gen.chromite.api import build_api_pb2
17from chromite.api.gen.chromite.api import depgraph_pb2
18from chromite.api.gen.chromite.api import image_pb2
19from chromite.api.gen.chromite.api import sdk_pb2
20from chromite.api.gen.chromite.api import test_archive_pb2
Alex Kleinf4dc4f52018-12-05 13:55:12 -070021from chromite.lib import commandline
Alex Klein2bfacb22019-02-04 11:42:17 -070022from chromite.lib import cros_build_lib
Alex Kleinf4dc4f52018-12-05 13:55:12 -070023from chromite.lib import osutils
Alex Klein00b1f1e2019-02-08 13:53:42 -070024from chromite.utils import matching
Alex Kleinf4dc4f52018-12-05 13:55:12 -070025
26
27class Error(Exception):
28 """Base error class for the module."""
29
30
Alex Klein7a115172019-02-08 14:14:20 -070031class InvalidInputFormatError(Error):
32 """Raised when the passed input protobuf can't be parsed."""
33
34
Alex Kleinf4dc4f52018-12-05 13:55:12 -070035# API Service Errors.
36class UnknownServiceError(Error):
37 """Error raised when the requested service has not been registered."""
38
39
Alex Kleinb7cdbe62019-02-22 11:41:32 -070040class ControllerModuleNotDefinedError(Error):
41 """Error class for when no controller is defined for a service."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070042
43
Alex Kleinb7cdbe62019-02-22 11:41:32 -070044class ServiceControllerNotFoundError(Error):
45 """Error raised when the service's controller cannot be imported."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070046
47
48# API Method Errors.
49class UnknownMethodError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070050 """The service is defined in the proto but the method is not."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070051
52
53class MethodNotFoundError(Error):
Alex Kleinb7cdbe62019-02-22 11:41:32 -070054 """The method's implementation cannot be found in the service's controller."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070055
56
57def GetParser():
Alex Klein00b1f1e2019-02-08 13:53:42 -070058 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070059 parser = commandline.ArgumentParser(description=__doc__)
60
61 parser.add_argument('service_method',
62 help='The "chromite.api.Service/Method" that is being '
63 'called.')
64
65 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070066 '--input-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070067 help='Path to the JSON serialized input argument protobuf message.')
68 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070069 '--output-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070070 help='The path to which the result protobuf message should be written.')
71
72 return parser
73
74
Alex Klein00b1f1e2019-02-08 13:53:42 -070075def _ParseArgs(argv, router):
Alex Kleinf4dc4f52018-12-05 13:55:12 -070076 """Parse and validate arguments."""
77 parser = GetParser()
78 opts = parser.parse_args(argv)
79
Alex Klein00b1f1e2019-02-08 13:53:42 -070080 methods = router.ListMethods()
81 if opts.service_method not in methods:
82 matched = matching.GetMostLikelyMatchedObject(methods, opts.service_method,
83 matched_score_threshold=0.6)
84 error = 'Unrecognized service name.'
85 if matched:
86 error += '\nDid you mean: \n%s' % '\n'.join(matched)
87 parser.error(error)
88
Alex Kleinf4dc4f52018-12-05 13:55:12 -070089 parts = opts.service_method.split('/')
90
91 if len(parts) != 2:
92 parser.error('Must pass "Service/Method".')
93
94 opts.service = parts[0]
95 opts.method = parts[1]
96
97 opts.Freeze()
98 return opts
99
100
101class Router(object):
102 """Encapsulates the request dispatching logic."""
103
104 def __init__(self):
105 self._services = {}
106 self._aliases = {}
107 # All imported generated messages get added to this symbol db.
108 self._sym_db = symbol_database.Default()
109
110 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
111 self._service_options = extensions['service_options']
112 self._method_options = extensions['method_options']
113
114 def Register(self, proto_module):
115 """Register the services from a generated proto module.
116
117 Args:
118 proto_module (module): The generated proto module whose service is being
119 registered.
120
121 Raises:
122 ServiceModuleNotDefinedError when the service cannot be found in the
123 provided module.
124 """
125 services = proto_module.DESCRIPTOR.services_by_name
126 for service_name, svc in services.items():
127 module_name = svc.GetOptions().Extensions[self._service_options].module
128
129 if not module_name:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700130 raise ControllerModuleNotDefinedError(
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700131 'The module must be defined in the service definition: %s.%s' %
132 (proto_module, service_name))
133
134 self._services[svc.full_name] = (svc, module_name)
135
Alex Klein00b1f1e2019-02-08 13:53:42 -0700136 def ListMethods(self):
137 """List all methods registered with the router."""
138 services = []
139 for service_name, (svc, _module) in self._services.items():
140 for method_name in svc.methods_by_name.keys():
141 services.append('%s/%s' % (service_name, method_name))
142
143 return sorted(services)
144
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700145 def Route(self, service_name, method_name, input_json):
146 """Dispatch the request.
147
148 Args:
149 service_name (str): The fully qualified service name.
150 method_name (str): The name of the method being called.
151 input_json (str): The JSON encoded input message data.
152
153 Returns:
154 google.protobuf.message.Message: An instance of the method's output
155 message class.
156
157 Raises:
158 ServiceModuleNotFoundError when the service module cannot be imported.
159 MethodNotFoundError when the method cannot be retrieved from the module.
160 """
161 try:
162 svc, module_name = self._services[service_name]
163 except KeyError:
164 raise UnknownServiceError('The %s service has not been registered.'
165 % service_name)
166
167 try:
168 method_desc = svc.methods_by_name[method_name]
169 except KeyError:
170 raise UnknownMethodError('The %s method has not been defined in the %s '
171 'service.' % (method_name, service_name))
172
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700173 # Parse the input file to build an instance of the input message.
174 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
Alex Klein7a115172019-02-08 14:14:20 -0700175 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700176 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
Alex Klein7a115172019-02-08 14:14:20 -0700177 except json_format.ParseError as e:
178 raise InvalidInputFormatError(
179 'Unable to parse the input json: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700180
181 # Get an empty output message instance.
182 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700183
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700184 # Allow proto-based method name override.
185 method_options = method_desc.GetOptions().Extensions[self._method_options]
186 if method_options.HasField('implementation_name'):
187 method_name = method_options.implementation_name
188
Alex Klein2bfacb22019-02-04 11:42:17 -0700189 # Check the chroot assertion settings before running.
190 service_options = svc.GetOptions().Extensions[self._service_options]
191 self._HandleChrootAssert(service_options, method_options)
192
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700193 # Import the module and get the method.
194 method_impl = self._GetMethod(module_name, method_name)
195
196 # Successfully located; call and return.
Alex Klein7a115172019-02-08 14:14:20 -0700197 method_impl(input_msg, output_msg)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700198 return output_msg
199
Alex Klein2bfacb22019-02-04 11:42:17 -0700200 def _HandleChrootAssert(self, service_options, method_options):
201 """Check the chroot assert options and execute assertion as needed.
202
203 Args:
204 service_options (google.protobuf.Message): The service options.
205 method_options (google.protobuf.Message): The method options.
206 """
207 chroot_assert = build_api_pb2.NO_ASSERTION
208 if method_options.HasField('method_chroot_assert'):
209 # Prefer the method option when set.
210 chroot_assert = method_options.method_chroot_assert
211 elif service_options.HasField('service_chroot_assert'):
212 # Fall back to the service option.
213 chroot_assert = service_options.service_chroot_assert
214
215 # Execute appropriate assertion if set.
216 if chroot_assert == build_api_pb2.INSIDE:
217 cros_build_lib.AssertInsideChroot()
218 elif chroot_assert == build_api_pb2.OUTSIDE:
219 cros_build_lib.AssertOutsideChroot()
220
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700221 def _GetMethod(self, module_name, method_name):
222 """Get the implementation of the method for the service module.
223
224 Args:
225 module_name (str): The name of the service module.
226 method_name (str): The name of the method.
227
228 Returns:
229 callable - The method.
230
231 Raises:
232 MethodNotFoundError when the method cannot be found in the module.
233 ServiceModuleNotFoundError when the service module cannot be imported.
234 """
235 try:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700236 module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700237 except ImportError as e:
Alex Kleinb7cdbe62019-02-22 11:41:32 -0700238 raise ServiceControllerNotFoundError(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700239 try:
240 return getattr(module, method_name)
241 except AttributeError as e:
242 raise MethodNotFoundError(e.message)
243
244
245def RegisterServices(router):
246 """Register all the services.
247
248 Args:
249 router (Router): The router.
250 """
Ned Nguyen9a7a9052019-02-05 11:04:03 -0700251 router.Register(depgraph_pb2)
Alex Klein2966e302019-01-17 13:29:38 -0700252 router.Register(image_pb2)
Alex Klein19c4cc42019-02-27 14:47:57 -0700253 router.Register(sdk_pb2)
Alex Kleinb400da62019-02-20 16:42:50 -0700254 router.Register(test_archive_pb2)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700255
256
257def main(argv):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700258 router = Router()
259 RegisterServices(router)
260
Alex Klein00b1f1e2019-02-08 13:53:42 -0700261 opts = _ParseArgs(argv, router)
262
Alex Klein7a115172019-02-08 14:14:20 -0700263 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700264 input_proto = osutils.ReadFile(opts.input_json)
Alex Klein7a115172019-02-08 14:14:20 -0700265 except IOError as e:
266 cros_build_lib.Die('Unable to read input file: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700267
Alex Klein7a115172019-02-08 14:14:20 -0700268 try:
269 output_msg = router.Route(opts.service, opts.method, input_proto)
270 except Error as e:
271 # Error derivatives are handled nicely, but let anything else bubble up.
272 cros_build_lib.Die(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700273
Alex Klein7a115172019-02-08 14:14:20 -0700274 output_content = json_format.MessageToJson(output_msg)
275 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700276 osutils.WriteFile(opts.output_json, output_content)
Alex Klein7a115172019-02-08 14:14:20 -0700277 except IOError as e:
278 cros_build_lib.Die('Unable to write output file: %s' % e.message)