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