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