blob: a970522db4054cba07c0745fec82d6a3af836927 [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
16from chromite.api.gen import build_api_pb2
Alex Klein249eda72019-01-18 15:40:54 -070017from chromite.api.gen import autotest_pb2
Alex Klein2966e302019-01-17 13:29:38 -070018from chromite.api.gen import image_pb2
Alex Kleinf4dc4f52018-12-05 13:55:12 -070019from chromite.lib import commandline
Alex Klein2bfacb22019-02-04 11:42:17 -070020from chromite.lib import cros_build_lib
Alex Kleinf4dc4f52018-12-05 13:55:12 -070021from chromite.lib import osutils
22
23
24class Error(Exception):
25 """Base error class for the module."""
26
27
Alex Klein7a115172019-02-08 14:14:20 -070028class InvalidInputFormatError(Error):
29 """Raised when the passed input protobuf can't be parsed."""
30
31
Alex Kleinf4dc4f52018-12-05 13:55:12 -070032# API Service Errors.
33class UnknownServiceError(Error):
34 """Error raised when the requested service has not been registered."""
35
36
37class ServiceModuleNotDefinedError(Error):
38 """Error class for when no module is defined for a service."""
39
40
41class ServiceModuleNotFoundError(Error):
42 """Error raised when the service cannot be imported."""
43
44
45# API Method Errors.
46class UnknownMethodError(Error):
47 """The requested service exists but does not have the requested method."""
48
49
50class MethodNotFoundError(Error):
51 """Error raised when the method cannot be found in the service module."""
52
53
54def GetParser():
55 """Build the argument parser.
56
57 The API parser comprises a subparser hierarchy. The general form is:
58 `script service method`, e.g. `build_api image test`.
59 """
60 parser = commandline.ArgumentParser(description=__doc__)
61
62 parser.add_argument('service_method',
63 help='The "chromite.api.Service/Method" that is being '
64 'called.')
65
66 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070067 '--input-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070068 help='Path to the JSON serialized input argument protobuf message.')
69 parser.add_argument(
Alex Klein7a115172019-02-08 14:14:20 -070070 '--output-json', type='path', required=True,
Alex Kleinf4dc4f52018-12-05 13:55:12 -070071 help='The path to which the result protobuf message should be written.')
72
73 return parser
74
75
76def _ParseArgs(argv):
77 """Parse and validate arguments."""
78 parser = GetParser()
79 opts = parser.parse_args(argv)
80
81 parts = opts.service_method.split('/')
82
83 if len(parts) != 2:
84 parser.error('Must pass "Service/Method".')
85
86 opts.service = parts[0]
87 opts.method = parts[1]
88
89 opts.Freeze()
90 return opts
91
92
93class Router(object):
94 """Encapsulates the request dispatching logic."""
95
96 def __init__(self):
97 self._services = {}
98 self._aliases = {}
99 # All imported generated messages get added to this symbol db.
100 self._sym_db = symbol_database.Default()
101
102 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
103 self._service_options = extensions['service_options']
104 self._method_options = extensions['method_options']
105
106 def Register(self, proto_module):
107 """Register the services from a generated proto module.
108
109 Args:
110 proto_module (module): The generated proto module whose service is being
111 registered.
112
113 Raises:
114 ServiceModuleNotDefinedError when the service cannot be found in the
115 provided module.
116 """
117 services = proto_module.DESCRIPTOR.services_by_name
118 for service_name, svc in services.items():
119 module_name = svc.GetOptions().Extensions[self._service_options].module
120
121 if not module_name:
122 raise ServiceModuleNotDefinedError(
123 'The module must be defined in the service definition: %s.%s' %
124 (proto_module, service_name))
125
126 self._services[svc.full_name] = (svc, module_name)
127
128 def Route(self, service_name, method_name, input_json):
129 """Dispatch the request.
130
131 Args:
132 service_name (str): The fully qualified service name.
133 method_name (str): The name of the method being called.
134 input_json (str): The JSON encoded input message data.
135
136 Returns:
137 google.protobuf.message.Message: An instance of the method's output
138 message class.
139
140 Raises:
141 ServiceModuleNotFoundError when the service module cannot be imported.
142 MethodNotFoundError when the method cannot be retrieved from the module.
143 """
144 try:
145 svc, module_name = self._services[service_name]
146 except KeyError:
147 raise UnknownServiceError('The %s service has not been registered.'
148 % service_name)
149
150 try:
151 method_desc = svc.methods_by_name[method_name]
152 except KeyError:
153 raise UnknownMethodError('The %s method has not been defined in the %s '
154 'service.' % (method_name, service_name))
155
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700156 # Parse the input file to build an instance of the input message.
157 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
Alex Klein7a115172019-02-08 14:14:20 -0700158 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700159 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
Alex Klein7a115172019-02-08 14:14:20 -0700160 except json_format.ParseError as e:
161 raise InvalidInputFormatError(
162 'Unable to parse the input json: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700163
164 # Get an empty output message instance.
165 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700166
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700167 # Allow proto-based method name override.
168 method_options = method_desc.GetOptions().Extensions[self._method_options]
169 if method_options.HasField('implementation_name'):
170 method_name = method_options.implementation_name
171
Alex Klein2bfacb22019-02-04 11:42:17 -0700172 # Check the chroot assertion settings before running.
173 service_options = svc.GetOptions().Extensions[self._service_options]
174 self._HandleChrootAssert(service_options, method_options)
175
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700176 # Import the module and get the method.
177 method_impl = self._GetMethod(module_name, method_name)
178
179 # Successfully located; call and return.
Alex Klein7a115172019-02-08 14:14:20 -0700180 method_impl(input_msg, output_msg)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700181 return output_msg
182
Alex Klein2bfacb22019-02-04 11:42:17 -0700183 def _HandleChrootAssert(self, service_options, method_options):
184 """Check the chroot assert options and execute assertion as needed.
185
186 Args:
187 service_options (google.protobuf.Message): The service options.
188 method_options (google.protobuf.Message): The method options.
189 """
190 chroot_assert = build_api_pb2.NO_ASSERTION
191 if method_options.HasField('method_chroot_assert'):
192 # Prefer the method option when set.
193 chroot_assert = method_options.method_chroot_assert
194 elif service_options.HasField('service_chroot_assert'):
195 # Fall back to the service option.
196 chroot_assert = service_options.service_chroot_assert
197
198 # Execute appropriate assertion if set.
199 if chroot_assert == build_api_pb2.INSIDE:
200 cros_build_lib.AssertInsideChroot()
201 elif chroot_assert == build_api_pb2.OUTSIDE:
202 cros_build_lib.AssertOutsideChroot()
203
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700204 def _GetMethod(self, module_name, method_name):
205 """Get the implementation of the method for the service module.
206
207 Args:
208 module_name (str): The name of the service module.
209 method_name (str): The name of the method.
210
211 Returns:
212 callable - The method.
213
214 Raises:
215 MethodNotFoundError when the method cannot be found in the module.
216 ServiceModuleNotFoundError when the service module cannot be imported.
217 """
218 try:
219 module = importlib.import_module(service.IMPORT_PATTERN % module_name)
220 except ImportError as e:
221 raise ServiceModuleNotFoundError(e.message)
222 try:
223 return getattr(module, method_name)
224 except AttributeError as e:
225 raise MethodNotFoundError(e.message)
226
227
228def RegisterServices(router):
229 """Register all the services.
230
231 Args:
232 router (Router): The router.
233 """
Alex Klein249eda72019-01-18 15:40:54 -0700234 router.Register(autotest_pb2)
Alex Klein2966e302019-01-17 13:29:38 -0700235 router.Register(image_pb2)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700236
237
238def main(argv):
239 opts = _ParseArgs(argv)
240
241 router = Router()
242 RegisterServices(router)
243
Alex Klein7a115172019-02-08 14:14:20 -0700244 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700245 input_proto = osutils.ReadFile(opts.input_json)
Alex Klein7a115172019-02-08 14:14:20 -0700246 except IOError as e:
247 cros_build_lib.Die('Unable to read input file: %s' % e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700248
Alex Klein7a115172019-02-08 14:14:20 -0700249 try:
250 output_msg = router.Route(opts.service, opts.method, input_proto)
251 except Error as e:
252 # Error derivatives are handled nicely, but let anything else bubble up.
253 cros_build_lib.Die(e.message)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700254
Alex Klein7a115172019-02-08 14:14:20 -0700255 output_content = json_format.MessageToJson(output_msg)
256 try:
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700257 osutils.WriteFile(opts.output_json, output_content)
Alex Klein7a115172019-02-08 14:14:20 -0700258 except IOError as e:
259 cros_build_lib.Die('Unable to write output file: %s' % e.message)