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