blob: f5565522416965131cc1120778f2dd22155b341e [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Router class for the Build API.
Handles routing requests to the appropriate controller and handles service
registration.
"""
from __future__ import print_function
import collections
import importlib
import os
import sys
from google.protobuf import symbol_database
from chromite.api import controller
from chromite.api import field_handler
from chromite.api.gen.chromite.api import android_pb2
from chromite.api.gen.chromite.api import api_pb2
from chromite.api.gen.chromite.api import artifacts_pb2
from chromite.api.gen.chromite.api import binhost_pb2
from chromite.api.gen.chromite.api import build_api_pb2
from chromite.api.gen.chromite.api import depgraph_pb2
from chromite.api.gen.chromite.api import firmware_pb2
from chromite.api.gen.chromite.api import image_pb2
from chromite.api.gen.chromite.api import packages_pb2
from chromite.api.gen.chromite.api import payload_pb2
from chromite.api.gen.chromite.api import sdk_pb2
from chromite.api.gen.chromite.api import sysroot_pb2
from chromite.api.gen.chromite.api import test_pb2
from chromite.api.gen.chromite.api import toolchain_pb2
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import osutils
from chromite.utils import memoize
MethodData = collections.namedtuple(
'MethodData', ('service_descriptor', 'module_name', 'method_descriptor'))
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
class Error(Exception):
"""Base error class for the module."""
class InvalidSdkError(Error):
"""Raised when the SDK is invalid or does not exist."""
class CrosSdkNotRunError(Error):
"""Raised when the cros_sdk command could not be run to enter the chroot."""
# API Service Errors.
class UnknownServiceError(Error):
"""Error raised when the requested service has not been registered."""
class ControllerModuleNotDefinedError(Error):
"""Error class for when no controller has been defined for a service."""
class ServiceControllerNotFoundError(Error):
"""Error raised when the service's controller cannot be imported."""
# API Method Errors.
class UnknownMethodError(Error):
"""The service has been defined in the proto, but the method has not."""
class MethodNotFoundError(Error):
"""The method's implementation cannot be found in the service's controller."""
class Router(object):
"""Encapsulates the request dispatching logic."""
REEXEC_INPUT_FILE = 'input_proto'
REEXEC_OUTPUT_FILE = 'output_proto'
REEXEC_CONFIG_FILE = 'config_proto'
def __init__(self):
self._services = {}
self._aliases = {}
# All imported generated messages get added to this symbol db.
self._sym_db = symbol_database.Default()
# Save the service and method extension info for looking up
# configured extension data.
extensions = build_api_pb2.DESCRIPTOR.extensions_by_name
self._svc_options_ext = extensions['service_options']
self._method_options_ext = extensions['method_options']
@memoize.Memoize
def _get_method_data(self, service_name, method_name):
"""Get the descriptors and module name for the given Service/Method."""
try:
svc, module_name = self._services[service_name]
except KeyError:
raise UnknownServiceError(
'The %s service has not been registered.' % service_name)
try:
method_desc = svc.methods_by_name[method_name]
except KeyError:
raise UnknownMethodError('The %s method has not been defined in the %s '
'service.' % (method_name, service_name))
return MethodData(
service_descriptor=svc,
module_name=module_name,
method_descriptor=method_desc)
def _get_input_message_instance(self, service_name, method_name):
"""Get an empty input message instance for the specified method."""
method_data = self._get_method_data(service_name, method_name)
return self._sym_db.GetPrototype(method_data.method_descriptor.input_type)()
def _get_output_message_instance(self, service_name, method_name):
"""Get an empty output message instance for the specified method."""
method_data = self._get_method_data(service_name, method_name)
return self._sym_db.GetPrototype(
method_data.method_descriptor.output_type)()
def _get_module_name(self, service_name, method_name):
"""Get the name of the module containing the endpoint implementation."""
return self._get_method_data(service_name, method_name).module_name
def _get_service_options(self, service_name, method_name):
"""Get the configured service options for the endpoint."""
method_data = self._get_method_data(service_name, method_name)
svc_extensions = method_data.service_descriptor.GetOptions().Extensions
return svc_extensions[self._svc_options_ext]
def _get_method_options(self, service_name, method_name):
"""Get the configured method options for the endpoint."""
method_data = self._get_method_data(service_name, method_name)
method_extensions = method_data.method_descriptor.GetOptions().Extensions
return method_extensions[self._method_options_ext]
def Register(self, proto_module):
"""Register the services from a generated proto module.
Args:
proto_module (module): The generated proto module to register.
Raises:
ServiceModuleNotDefinedError when the service cannot be found in the
provided module.
"""
services = proto_module.DESCRIPTOR.services_by_name
for service_name, svc in services.items():
module_name = svc.GetOptions().Extensions[self._svc_options_ext].module
if not module_name:
raise ControllerModuleNotDefinedError(
'The module must be defined in the service definition: %s.%s' %
(proto_module, service_name))
self._services[svc.full_name] = (svc, module_name)
def ListMethods(self):
"""List all methods registered with the router."""
services = []
for service_name, (svc, _module) in self._services.items():
for method_name in svc.methods_by_name.keys():
services.append('%s/%s' % (service_name, method_name))
return sorted(services)
def Route(self, service_name, method_name, config, input_handler,
output_handlers, config_handler):
"""Dispatch the request.
Args:
service_name (str): The fully qualified service name.
method_name (str): The name of the method being called.
config (api_config.ApiConfig): The call configs.
input_handler (message_util.MessageHandler): The request message handler.
output_handlers (list[message_util.MessageHandler]): The response message
handlers.
config_handler (message_util.MessageHandler): The config message handler.
Returns:
int: The return code.
Raises:
InvalidInputFileError when the input file cannot be read.
InvalidOutputFileError when the output file cannot be written.
ServiceModuleNotFoundError when the service module cannot be imported.
MethodNotFoundError when the method cannot be retrieved from the module.
"""
input_msg = self._get_input_message_instance(service_name, method_name)
input_handler.read_into(input_msg)
# Get an empty output message instance.
output_msg = self._get_output_message_instance(service_name, method_name)
# Fetch the method options for chroot and method name overrides.
method_options = self._get_method_options(service_name, method_name)
# Check the chroot settings before running.
service_options = self._get_service_options(service_name, method_name)
if self._ChrootCheck(service_options, method_options, config):
# Run inside the chroot instead.
logging.info('Re-executing the endpoint inside the chroot.')
return self._ReexecuteInside(input_msg, output_msg, config, input_handler,
output_handlers, config_handler,
service_name, method_name)
# Allow proto-based method name override.
if method_options.HasField('implementation_name'):
implementation_name = method_options.implementation_name
else:
implementation_name = method_name
# Import the module and get the method.
module_name = self._get_module_name(service_name, method_name)
method_impl = self._GetMethod(module_name, implementation_name)
# Successfully located; call and return.
return_code = method_impl(input_msg, output_msg, config)
if return_code is None:
return_code = controller.RETURN_CODE_SUCCESS
for h in output_handlers:
h.write_from(output_msg)
return return_code
def _ChrootCheck(self, service_options, method_options,
config: 'api_config.ApiConfig'):
"""Check the chroot options, and execute assertion or note reexec as needed.
Args:
service_options (google.protobuf.Message): The service options.
method_options (google.protobuf.Message): The method options.
config: The Build API call config instance.
Returns:
bool - True iff it needs to be reexeced inside the chroot.
Raises:
cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
"""
if not config.run_endpoint:
# Do not enter the chroot for validate only and mock calls.
return False
chroot_assert = build_api_pb2.NO_ASSERTION
if method_options.HasField('method_chroot_assert'):
# Prefer the method option when set.
chroot_assert = method_options.method_chroot_assert
elif service_options.HasField('service_chroot_assert'):
# Fall back to the service option.
chroot_assert = service_options.service_chroot_assert
if chroot_assert == build_api_pb2.INSIDE:
return not cros_build_lib.IsInsideChroot()
elif chroot_assert == build_api_pb2.OUTSIDE:
# If it must be run outside we have to already be outside.
cros_build_lib.AssertOutsideChroot()
return False
def _ReexecuteInside(self, input_msg, output_msg, config, input_handler,
output_handlers, config_handler, service_name,
method_name):
"""Re-execute the service inside the chroot.
Args:
input_msg (Message): The parsed input message.
output_msg (Message): The empty output message instance.
config (api_config.ApiConfig): The call configs.
input_handler (MessageHandler): Input message handler.
output_handlers (list[MessageHandler]): Output message handlers.
config_handler (MessageHandler): Config message handler.
service_name (str): The name of the service to run.
method_name (str): The name of the method to run.
"""
# Parse the chroot and clear the chroot field in the input message.
chroot = field_handler.handle_chroot(input_msg)
if not chroot.exists():
raise InvalidSdkError('Chroot does not exist.')
# Use a ContextManagerStack to avoid the deep nesting this many
# context managers introduces.
with cros_build_lib.ContextManagerStack() as stack:
# TempDirs setup.
tempdir = stack.Add(chroot.tempdir).tempdir
sync_tempdir = stack.Add(chroot.tempdir).tempdir
# The copy-paths-in context manager to handle Path messages.
stack.Add(
field_handler.copy_paths_in,
input_msg,
chroot.tmp,
prefix=chroot.path)
# The sync-directories context manager to handle SyncedDir messages.
stack.Add(
field_handler.sync_dirs, input_msg, sync_tempdir, prefix=chroot.path)
# Parse goma.
chroot.goma = field_handler.handle_goma(input_msg, chroot.path)
# Build inside-chroot paths for the input, output, and config messages.
new_input = os.path.join(tempdir, self.REEXEC_INPUT_FILE)
chroot_input = '/%s' % os.path.relpath(new_input, chroot.path)
new_output = os.path.join(tempdir, self.REEXEC_OUTPUT_FILE)
chroot_output = '/%s' % os.path.relpath(new_output, chroot.path)
new_config = os.path.join(tempdir, self.REEXEC_CONFIG_FILE)
chroot_config = '/%s' % os.path.relpath(new_config, chroot.path)
# Setup the inside-chroot message files.
logging.info('Writing input message to: %s', new_input)
input_handler.write_from(input_msg, path=new_input)
osutils.Touch(new_output)
logging.info('Writing config message to: %s', new_config)
config_handler.write_from(config.get_proto(), path=new_config)
# We can use a single output to write the rest of them. Use the
# first one as the reexec output and just translate its output in
# the rest of the handlers after.
output_handler = output_handlers[0]
cmd = [
'build_api',
'%s/%s' % (service_name, method_name),
input_handler.input_arg,
chroot_input,
output_handler.output_arg,
chroot_output,
config_handler.config_arg,
chroot_config,
'--debug',
]
try:
result = cros_build_lib.run(
cmd,
enter_chroot=True,
chroot_args=chroot.get_enter_args(),
check=False,
extra_env=chroot.env)
except cros_build_lib.RunCommandError:
# A non-zero return code will not result in an error, but one
# is still thrown when the command cannot be run in the first
# place. This is known to happen at least when the PATH does
# not include the chromite bin dir.
raise CrosSdkNotRunError('Unable to enter the chroot.')
logging.info('Endpoint execution completed, return code: %d',
result.returncode)
# Transfer result files out of the chroot.
output_handler.read_into(output_msg, path=new_output)
field_handler.extract_results(input_msg, output_msg, chroot)
# Write out all of the response formats.
for handler in output_handlers:
handler.write_from(output_msg)
return result.returncode
def _GetMethod(self, module_name, method_name):
"""Get the implementation of the method for the service module.
Args:
module_name (str): The name of the service module.
method_name (str): The name of the method.
Returns:
callable - The method.
Raises:
MethodNotFoundError when the method cannot be found in the module.
ServiceModuleNotFoundError when the service module cannot be imported.
"""
try:
module = importlib.import_module(controller.IMPORT_PATTERN % module_name)
except ImportError as e:
raise ServiceControllerNotFoundError(str(e))
try:
return getattr(module, method_name)
except AttributeError as e:
raise MethodNotFoundError(str(e))
def RegisterServices(router):
"""Register all the services.
Args:
router (Router): The router.
"""
router.Register(android_pb2)
router.Register(api_pb2)
router.Register(artifacts_pb2)
router.Register(binhost_pb2)
router.Register(depgraph_pb2)
router.Register(firmware_pb2)
router.Register(image_pb2)
router.Register(packages_pb2)
router.Register(payload_pb2)
router.Register(sdk_pb2)
router.Register(sysroot_pb2)
router.Register(test_pb2)
router.Register(toolchain_pb2)
logging.debug('Services registered successfully.')
def GetRouter():
"""Get a router that has had all of the services registered."""
router = Router()
RegisterServices(router)
return router