blob: 40da132abecf8c3cd7eb0a7b6363efb4326a2ec3 [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
11import os
12
13from google.protobuf import empty_pb2
14from google.protobuf import json_format
15from google.protobuf import symbol_database
16
17from chromite.api import service
18from chromite.api.gen import build_api_pb2
Alex Klein249eda72019-01-18 15:40:54 -070019from chromite.api.gen import autotest_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
23
24
25class Error(Exception):
26 """Base error class for the module."""
27
28
29# API Service Errors.
30class UnknownServiceError(Error):
31 """Error raised when the requested service has not been registered."""
32
33
34class ServiceModuleNotDefinedError(Error):
35 """Error class for when no module is defined for a service."""
36
37
38class ServiceModuleNotFoundError(Error):
39 """Error raised when the service cannot be imported."""
40
41
42# API Method Errors.
43class UnknownMethodError(Error):
44 """The requested service exists but does not have the requested method."""
45
46
47class MethodNotFoundError(Error):
48 """Error raised when the method cannot be found in the service module."""
49
50
51def GetParser():
52 """Build the argument parser.
53
54 The API parser comprises a subparser hierarchy. The general form is:
55 `script service method`, e.g. `build_api image test`.
56 """
57 parser = commandline.ArgumentParser(description=__doc__)
58
59 parser.add_argument('service_method',
60 help='The "chromite.api.Service/Method" that is being '
61 'called.')
62
63 parser.add_argument(
64 '--input-json', type='path',
65 help='Path to the JSON serialized input argument protobuf message.')
66 parser.add_argument(
67 '--output-json', type='path',
68 help='The path to which the result protobuf message should be written.')
69
70 return parser
71
72
73def _ParseArgs(argv):
74 """Parse and validate arguments."""
75 parser = GetParser()
76 opts = parser.parse_args(argv)
77
78 parts = opts.service_method.split('/')
79
80 if len(parts) != 2:
81 parser.error('Must pass "Service/Method".')
82
83 opts.service = parts[0]
84 opts.method = parts[1]
85
86 opts.Freeze()
87 return opts
88
89
90class Router(object):
91 """Encapsulates the request dispatching logic."""
92
93 def __init__(self):
94 self._services = {}
95 self._aliases = {}
96 # All imported generated messages get added to this symbol db.
97 self._sym_db = symbol_database.Default()
98
99 extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
100 self._service_options = extensions['service_options']
101 self._method_options = extensions['method_options']
102
103 def Register(self, proto_module):
104 """Register the services from a generated proto module.
105
106 Args:
107 proto_module (module): The generated proto module whose service is being
108 registered.
109
110 Raises:
111 ServiceModuleNotDefinedError when the service cannot be found in the
112 provided module.
113 """
114 services = proto_module.DESCRIPTOR.services_by_name
115 for service_name, svc in services.items():
116 module_name = svc.GetOptions().Extensions[self._service_options].module
117
118 if not module_name:
119 raise ServiceModuleNotDefinedError(
120 'The module must be defined in the service definition: %s.%s' %
121 (proto_module, service_name))
122
123 self._services[svc.full_name] = (svc, module_name)
124
125 def Route(self, service_name, method_name, input_json):
126 """Dispatch the request.
127
128 Args:
129 service_name (str): The fully qualified service name.
130 method_name (str): The name of the method being called.
131 input_json (str): The JSON encoded input message data.
132
133 Returns:
134 google.protobuf.message.Message: An instance of the method's output
135 message class.
136
137 Raises:
138 ServiceModuleNotFoundError when the service module cannot be imported.
139 MethodNotFoundError when the method cannot be retrieved from the module.
140 """
141 try:
142 svc, module_name = self._services[service_name]
143 except KeyError:
144 raise UnknownServiceError('The %s service has not been registered.'
145 % service_name)
146
147 try:
148 method_desc = svc.methods_by_name[method_name]
149 except KeyError:
150 raise UnknownMethodError('The %s method has not been defined in the %s '
151 'service.' % (method_name, service_name))
152
153 # Service method argument magic: do not pass the arguments when the method
154 # is expecting the Empty message. Additions of optional arguments/return
155 # values are still backwards compatible, but the implementation signature
156 # is simplified and more explicit about what its expecting.
157 args = []
158 # Parse the input file to build an instance of the input message.
159 input_msg = self._sym_db.GetPrototype(method_desc.input_type)()
160 if not isinstance(input_msg, empty_pb2.Empty):
161 json_format.Parse(input_json, input_msg, ignore_unknown_fields=True)
162 args.append(input_msg)
163
164 # Get an empty output message instance.
165 output_msg = self._sym_db.GetPrototype(method_desc.output_type)()
166 if not isinstance(output_msg, empty_pb2.Empty):
167 args.append(output_msg)
168
169 # TODO(saklein) Do we need this? Are aliases useful? Maybe dump it.
170 # Allow proto-based method name override.
171 method_options = method_desc.GetOptions().Extensions[self._method_options]
172 if method_options.HasField('implementation_name'):
173 method_name = method_options.implementation_name
174
Alex Klein2bfacb22019-02-04 11:42:17 -0700175 # Check the chroot assertion settings before running.
176 service_options = svc.GetOptions().Extensions[self._service_options]
177 self._HandleChrootAssert(service_options, method_options)
178
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700179 # Import the module and get the method.
180 method_impl = self._GetMethod(module_name, method_name)
181
182 # Successfully located; call and return.
183 method_impl(*args)
184 return output_msg
185
Alex Klein2bfacb22019-02-04 11:42:17 -0700186 def _HandleChrootAssert(self, service_options, method_options):
187 """Check the chroot assert options and execute assertion as needed.
188
189 Args:
190 service_options (google.protobuf.Message): The service options.
191 method_options (google.protobuf.Message): The method options.
192 """
193 chroot_assert = build_api_pb2.NO_ASSERTION
194 if method_options.HasField('method_chroot_assert'):
195 # Prefer the method option when set.
196 chroot_assert = method_options.method_chroot_assert
197 elif service_options.HasField('service_chroot_assert'):
198 # Fall back to the service option.
199 chroot_assert = service_options.service_chroot_assert
200
201 # Execute appropriate assertion if set.
202 if chroot_assert == build_api_pb2.INSIDE:
203 cros_build_lib.AssertInsideChroot()
204 elif chroot_assert == build_api_pb2.OUTSIDE:
205 cros_build_lib.AssertOutsideChroot()
206
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700207 def _GetMethod(self, module_name, method_name):
208 """Get the implementation of the method for the service module.
209
210 Args:
211 module_name (str): The name of the service module.
212 method_name (str): The name of the method.
213
214 Returns:
215 callable - The method.
216
217 Raises:
218 MethodNotFoundError when the method cannot be found in the module.
219 ServiceModuleNotFoundError when the service module cannot be imported.
220 """
221 try:
222 module = importlib.import_module(service.IMPORT_PATTERN % module_name)
223 except ImportError as e:
224 raise ServiceModuleNotFoundError(e.message)
225 try:
226 return getattr(module, method_name)
227 except AttributeError as e:
228 raise MethodNotFoundError(e.message)
229
230
231def RegisterServices(router):
232 """Register all the services.
233
234 Args:
235 router (Router): The router.
236 """
Alex Klein249eda72019-01-18 15:40:54 -0700237 router.Register(autotest_pb2)
Alex Kleinf4dc4f52018-12-05 13:55:12 -0700238
239
240def main(argv):
241 opts = _ParseArgs(argv)
242
243 router = Router()
244 RegisterServices(router)
245
246 if os.path.exists(opts.input_json):
247 input_proto = osutils.ReadFile(opts.input_json)
248 else:
249 input_proto = None
250
251 output_msg = router.Route(opts.service, opts.method, input_proto)
252
253 if opts.output_json:
254 output_content = json_format.MessageToJson(output_msg)
255 osutils.WriteFile(opts.output_json, output_content)