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