blob: 70055146fe7e6e1ce324bc514bd737b46d1a4524 [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
15from chromite.api import service
Ned Nguyen9a7a9052019-02-05 11:04:03 -070016from chromite.api.gen import build_api_pb2
17from chromite.api.gen import depgraph_pb2
Alex Klein2966e302019-01-17 13:29:38 -070018from chromite.api.gen import image_pb2
Alex Kleinb400da62019-02-20 16:42:50 -070019from chromite.api.gen import test_archive_pb2
Alex Kleinf4dc4f52018-12-05 13:55:12 -070020from chromite.lib import commandline
Alex Klein2bfacb22019-02-04 11:42:17 -070021from chromite.lib import cros_build_lib
Alex Kleinf4dc4f52018-12-05 13:55:12 -070022from chromite.lib import osutils
Alex Klein00b1f1e2019-02-08 13:53:42 -070023from chromite.utils import matching
Alex Kleinf4dc4f52018-12-05 13:55:12 -070024
25
26class Error(Exception):
27 """Base error class for the module."""
28
29
Alex Klein7a115172019-02-08 14:14:20 -070030class InvalidInputFormatError(Error):
31 """Raised when the passed input protobuf can't be parsed."""
32
33
Alex Kleinf4dc4f52018-12-05 13:55:12 -070034# API Service Errors.
35class UnknownServiceError(Error):
36 """Error raised when the requested service has not been registered."""
37
38
39class ServiceModuleNotDefinedError(Error):
40 """Error class for when no module is defined for a service."""
41
42
43class ServiceModuleNotFoundError(Error):
44 """Error raised when the service cannot be imported."""
45
46
47# API Method Errors.
48class UnknownMethodError(Error):
49 """The requested service exists but does not have the requested method."""
50
51
52class MethodNotFoundError(Error):
53 """Error raised when the method cannot be found in the service module."""
54
55
56def GetParser():
Alex Klein00b1f1e2019-02-08 13:53:42 -070057 """Build the argument parser."""
Alex Kleinf4dc4f52018-12-05 13:55:12 -070058 parser = commandline.ArgumentParser(description=__doc__)
59
60 parser.add_argument('service_method',
61 help='The "chromite.api.Service/Method" that is being '
62 'called.')
63
64 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070065 '--input-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070066 help='Path to the JSON serialized input argument protobuf message.')
67 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070068 '--output-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070069 help='The path to which the result protobuf message should be written.')
70
71 return parser
72
73
Alex Klein00b1f1e2019-02-08 13:53:42 -070074def _ParseArgs(argv, router):
Alex Kleinf4dc4f52018-12-05 13:55:12 -070075 """Parse and validate arguments."""
76 parser = GetParser()
77 opts = parser.parse_args(argv)
78
Alex Klein00b1f1e2019-02-08 13:53:42 -070079 methods = router.ListMethods()
80 if opts.service_method not in methods:
81 matched = matching.GetMostLikelyMatchedObject(methods, opts.service_method,
82 matched_score_threshold=0.6)
83 error = 'Unrecognized service name.'
84 if matched:
85 error += '\nDid you mean: \n%s' % '\n'.join(matched)
86 parser.error(error)
87
Alex Kleinf4dc4f52018-12-05 13:55:12 -070088 parts = opts.service_method.split('/')
89
90 if len(parts) != 2:
91 parser.error('Must pass "Service/Method".')
92
93 opts.service = parts[0]
94 opts.method = parts[1]
95
96 opts.Freeze()
97 return opts
98
99
100class Router(object):
101 """Encapsulates the request dispatching logic."""
102
103 def __init__(self):
104 self._services = {}
105 self._aliases = {}
106 # All imported generated messages get added to this symbol db.
107 self._sym_db = symbol_database.Default()
108
109 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
110 self._service_options = extensions['service_options']
111 self._method_options = extensions['method_options']
112
113 def Register(self, proto_module):
114 """Register the services from a generated proto module.
115
116 Args:
117 proto_module (module): The generated proto module whose service is being
118 registered.
119
120 Raises:
121 ServiceModuleNotDefinedError when the service cannot be found in the
122 provided module.
123 """
124 services = proto_module.DESCRIPTOR.services_by_name
125 for service_name, svc in services.items():
126 module_name = svc.GetOptions().Extensions[self._service_options].module
127
128 if not module_name:
129 raise ServiceModuleNotDefinedError(
130 'The module must be defined in the service definition: %s.%s' %
131 (proto_module, service_name))
132
133 self._services[svc.full_name] = (svc, module_name)
134
Alex Klein00b1f1e2019-02-08 13:53:42 -0700135 def ListMethods(self):
136 """List all methods registered with the router."""
137 services = []
138 for service_name, (svc, _module) in self._services.items():
139 for method_name in svc.methods_by_name.keys():
140 services.append('%s/%s' % (service_name, method_name))
141
142 return sorted(services)
143
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700144 def Route(self, service_name, method_name, input_json):
145 """Dispatch the request.
146
147 Args:
148 service_name (str): The fully qualified service name.
149 method_name (str): The name of the method being called.
150 input_json (str): The JSON encoded input message data.
151
152 Returns:
153 google.protobuf.message.Message: An instance of the method's output
154 message class.
155
156 Raises:
157 ServiceModuleNotFoundError when the service module cannot be imported.
158 MethodNotFoundError when the method cannot be retrieved from the module.
159 """
160 try:
161 svc, module_name = self._services[service_name]
162 except KeyError:
163 raise UnknownServiceError('The %s service has not been registered.'
164 % service_name)
165
166 try:
167 method_desc = svc.methods_by_name[method_name]
168 except KeyError:
169 raise UnknownMethodError('The %s method has not been defined in the %s '
170 'service.' % (method_name, service_name))
171
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700172 # Parse the input file to build an instance of the input message.
173 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
Alex Klein7a115172019-02-08 14:14:20 -0700174 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700175 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
Alex Klein7a115172019-02-08 14:14:20 -0700176 except json_format.ParseError as e:
177 raise InvalidInputFormatError(
178 'Unable to parse the input json: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700179
180 # Get an empty output message instance.
181 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700182
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700183 # Allow proto-based method name override.
184 method_options = method_desc.GetOptions().Extensions[self._method_options]
185 if method_options.HasField('implementation_name'):
186 method_name = method_options.implementation_name
187
Alex Klein2bfacb22019-02-04 11:42:17 -0700188 # Check the chroot assertion settings before running.
189 service_options = svc.GetOptions().Extensions[self._service_options]
190 self._HandleChrootAssert(service_options, method_options)
191
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700192 # Import the module and get the method.
193 method_impl = self._GetMethod(module_name, method_name)
194
195 # Successfully located; call and return.
Alex Klein7a115172019-02-08 14:14:20 -0700196 method_impl(input_msg, output_msg)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700197 return output_msg
198
Alex Klein2bfacb22019-02-04 11:42:17 -0700199 def _HandleChrootAssert(self, service_options, method_options):
200 """Check the chroot assert options and execute assertion as needed.
201
202 Args:
203 service_options (google.protobuf.Message): The service options.
204 method_options (google.protobuf.Message): The method options.
205 """
206 chroot_assert = build_api_pb2.NO_ASSERTION
207 if method_options.HasField('method_chroot_assert'):
208 # Prefer the method option when set.
209 chroot_assert = method_options.method_chroot_assert
210 elif service_options.HasField('service_chroot_assert'):
211 # Fall back to the service option.
212 chroot_assert = service_options.service_chroot_assert
213
214 # Execute appropriate assertion if set.
215 if chroot_assert == build_api_pb2.INSIDE:
216 cros_build_lib.AssertInsideChroot()
217 elif chroot_assert == build_api_pb2.OUTSIDE:
218 cros_build_lib.AssertOutsideChroot()
219
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700220 def _GetMethod(self, module_name, method_name):
221 """Get the implementation of the method for the service module.
222
223 Args:
224 module_name (str): The name of the service module.
225 method_name (str): The name of the method.
226
227 Returns:
228 callable - The method.
229
230 Raises:
231 MethodNotFoundError when the method cannot be found in the module.
232 ServiceModuleNotFoundError when the service module cannot be imported.
233 """
234 try:
235 module = importlib.import_module(service.IMPORT_PATTERN % module_name)
236 except ImportError as e:
237 raise ServiceModuleNotFoundError(e.message)
238 try:
239 return getattr(module, method_name)
240 except AttributeError as e:
241 raise MethodNotFoundError(e.message)
242
243
244def RegisterServices(router):
245 """Register all the services.
246
247 Args:
248 router (Router): The router.
249 """
Ned Nguyen9a7a9052019-02-05 11:04:03 -0700250 router.Register(depgraph_pb2)
Alex Klein2966e302019-01-17 13:29:38 -0700251 router.Register(image_pb2)
Alex Kleinb400da62019-02-20 16:42:50 -0700252 router.Register(test_archive_pb2)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700253
254
255def main(argv):
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700256 router = Router()
257 RegisterServices(router)
258
Alex Klein00b1f1e2019-02-08 13:53:42 -0700259 opts = _ParseArgs(argv, router)
260
Alex Klein7a115172019-02-08 14:14:20 -0700261 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700262 input_proto = osutils.ReadFile(opts.input_json)
Alex Klein7a115172019-02-08 14:14:20 -0700263 except IOError as e:
264 cros_build_lib.Die('Unable to read input file: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700265
Alex Klein7a115172019-02-08 14:14:20 -0700266 try:
267 output_msg = router.Route(opts.service, opts.method, input_proto)
268 except Error as e:
269 # Error derivatives are handled nicely, but let anything else bubble up.
270 cros_build_lib.Die(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700271
Alex Klein7a115172019-02-08 14:14:20 -0700272 output_content = json_format.MessageToJson(output_msg)
273 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700274 osutils.WriteFile(opts.output_json, output_content)
Alex Klein7a115172019-02-08 14:14:20 -0700275 except IOError as e:
276 cros_build_lib.Die('Unable to write output file: %s' % e.message)