| # SPDX-FileCopyrightText: 2020 The Magma Authors. |
| # SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org> |
| # |
| # SPDX-License-Identifier: BSD-3-Clause |
| |
| import logging |
| import os |
| |
| import grpc |
| from configuration.exceptions import LoadConfigError |
| from configuration.service_configs import load_service_config |
| |
| GRPC_KEEPALIVE_MS = 30 * 1000 |
| |
| |
| class ServiceRegistry: |
| """ |
| ServiceRegistry provides the framework to discover services. |
| |
| ServiceRegistry takes care of service naming, and sets the connection |
| params like ip/port, TLS, certs, etc based on service level configuration. |
| """ |
| |
| _REGISTRY = {} |
| _PROXY_CONFIG = {} |
| _CHANNELS_CACHE = {} |
| |
| LOCAL = 'local' |
| CLOUD = 'cloud' |
| |
| @staticmethod |
| def get_service_address(service): |
| """ |
| Returns the (host, port) tuple for the service. |
| |
| Args: |
| service (string): Name of the service |
| Returns: |
| (host, port) tuple |
| Raises: |
| ValueError if the service is unknown |
| """ |
| registry = ServiceRegistry.get_registry() |
| if service not in registry["services"]: |
| raise ValueError("Invalid service name: %s" % service) |
| service_conf = registry["services"][service] |
| return service_conf["ip_address"], service_conf["port"] |
| |
| @staticmethod |
| def add_service(name, ip_address, port): |
| """ |
| Adds a service to the registry. |
| |
| Args: |
| name (string): Service name |
| ip_address (string): ip address string |
| port (int): service port |
| """ |
| registry = ServiceRegistry.get_registry() |
| service = {"ip_address": ip_address, "port": port} |
| registry["services"][name] = service |
| |
| @staticmethod |
| def list_services(): |
| """ |
| Returns the list of services in the registry. |
| |
| Returns: |
| list of services |
| """ |
| return ServiceRegistry.get_registry()["services"] |
| |
| @staticmethod |
| def reset(): |
| """ |
| Removes all the entries in the registry |
| """ |
| ServiceRegistry.get_registry()["services"] = {} |
| |
| @staticmethod |
| def get_bootstrap_rpc_channel(): |
| """ |
| Returns a RPC channel to the bootstrap service in CLOUD. |
| Returns: |
| grpc channel |
| """ |
| proxy_config = ServiceRegistry.get_proxy_config() |
| (ip, port) = ( |
| proxy_config['bootstrap_address'], |
| proxy_config['bootstrap_port'], |
| ) |
| authority = proxy_config['bootstrap_address'] |
| |
| try: |
| rootca = open(proxy_config['rootca_cert'], 'rb').read() |
| except FileNotFoundError as exp: |
| raise ValueError("SSL cert not found: %s" % exp) |
| |
| ssl_creds = grpc.ssl_channel_credentials(rootca) |
| return create_grpc_channel(ip, port, authority, ssl_creds) |
| |
| @staticmethod |
| def get_rpc_channel( |
| service, destination, proxy_cloud_connections=True, |
| grpc_options=None, |
| ): |
| """ |
| Returns a RPC channel to the service. The connection params |
| are obtained from the service registry and used. |
| TBD: pool connections to a service and reuse them. Right |
| now each call creates a new TCP/SSL/HTTP2 connection. |
| |
| Args: |
| service (string): Name of the service |
| destination (string): ServiceRegistry.LOCAL or ServiceRegistry.CLOUD |
| proxy_cloud_connections (bool): Override to connect direct to cloud |
| grpc_options (list): list of gRPC options params for the channel |
| Returns: |
| grpc channel |
| Raises: |
| ValueError if the service is unknown |
| """ |
| proxy_config = ServiceRegistry.get_proxy_config() |
| |
| # Control proxy uses the :authority: HTTP header to route to services. |
| if destination == ServiceRegistry.LOCAL: |
| authority = '%s.local' % (service) |
| else: |
| authority = '%s-%s' % (service, proxy_config['cloud_address']) |
| |
| should_use_proxy = proxy_config['proxy_cloud_connections'] and \ |
| proxy_cloud_connections |
| |
| # If speaking to a local service or to the proxy, the grpc channel |
| # can be reused. If speaking to the cloud directly, the client cert |
| # could become stale after the next bootstrapper run. |
| should_reuse_channel = should_use_proxy or \ |
| (destination == ServiceRegistry.LOCAL) |
| if should_reuse_channel: |
| channel = ServiceRegistry._CHANNELS_CACHE.get(authority, None) |
| if channel is not None: |
| return channel |
| |
| if grpc_options is None: |
| grpc_options = [ |
| ("grpc.keepalive_time_ms", GRPC_KEEPALIVE_MS), |
| ] |
| # We need to figure out the ip and port to connnect, if we need to use |
| # SSL and the authority to use. |
| if destination == ServiceRegistry.LOCAL: |
| # Connect to the local service directly |
| (ip, port) = ServiceRegistry.get_service_address(service) |
| channel = create_grpc_channel( |
| ip, port, authority, |
| options=grpc_options, |
| ) |
| elif should_use_proxy: |
| # Connect to the cloud via local control proxy |
| try: |
| (ip, unused_port) = ServiceRegistry.get_service_address( |
| "control_proxy", |
| ) |
| port = proxy_config['local_port'] |
| except ValueError as err: |
| logging.error(err) |
| (ip, port) = ('127.0.0.1', proxy_config['local_port']) |
| channel = create_grpc_channel( |
| ip, port, authority, |
| options=grpc_options, |
| ) |
| else: |
| # Connect to the cloud directly |
| ip = proxy_config['cloud_address'] |
| port = proxy_config['cloud_port'] |
| ssl_creds = get_ssl_creds() |
| channel = create_grpc_channel( |
| ip, port, authority, ssl_creds, |
| options=grpc_options, |
| ) |
| if should_reuse_channel: |
| ServiceRegistry._CHANNELS_CACHE[authority] = channel |
| return channel |
| |
| @staticmethod |
| def get_registry(): |
| """ |
| Returns _REGISTRY which holds the contents from the |
| config/service/service_registry.yml file. Its a static member and the |
| .yml file is loaded only once. |
| """ |
| if not ServiceRegistry._REGISTRY: |
| try: |
| ServiceRegistry._REGISTRY = load_service_config( |
| "service_registry", |
| ) |
| except LoadConfigError as err: |
| logging.error(err) |
| ServiceRegistry._REGISTRY = {"services": {}} |
| return ServiceRegistry._REGISTRY |
| |
| @staticmethod |
| def get_proxy_config(): |
| """ |
| Returns the control proxy config. The config file is loaded only |
| once and cached. |
| """ |
| if not ServiceRegistry._PROXY_CONFIG: |
| try: |
| ServiceRegistry._PROXY_CONFIG = load_service_config( |
| 'control_proxy', |
| ) |
| except LoadConfigError as err: |
| logging.error(err) |
| ServiceRegistry._PROXY_CONFIG = { |
| 'proxy_cloud_connections': True, |
| } |
| return ServiceRegistry._PROXY_CONFIG |
| |
| |
| def set_grpc_cipher_suites(): |
| """ |
| Set the cipher suites to be used for the gRPC TLS connection. |
| TODO (praveenr) t19265877: Update nghttpx in the cloud to recent version |
| and delete this. The current nghttpx version doesn't support the |
| ciphers needed by default for gRPC. |
| """ |
| os.environ["GRPC_SSL_CIPHER_SUITES"] = "ECDHE-ECDSA-AES256-GCM-SHA384:"\ |
| "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:"\ |
| "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:"\ |
| "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:"\ |
| "ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:"\ |
| "ECDHE-RSA-AES128-SHA256" |
| |
| |
| def get_ssl_creds(): |
| """ |
| Get the SSL credentials to use to communicate securely. |
| We use client side TLS auth, with the cert and keys |
| obtained during bootstrapping of the gateway. |
| |
| Returns: |
| gRPC ssl creds |
| Raises: |
| ValueError if the cert or key filename in the |
| control proxy config is incorrect. |
| """ |
| proxy_config = ServiceRegistry.get_proxy_config() |
| try: |
| with open(proxy_config['rootca_cert'], 'rb') as rootca_f: |
| with open(proxy_config['gateway_cert'], encoding="utf-8") as cert_f: |
| with open(proxy_config['gateway_key'], encoding="utf-8") as key_f: |
| rootca = rootca_f.read() |
| cert = cert_f.read().encode() |
| key = key_f.read().encode() |
| ssl_creds = grpc.ssl_channel_credentials( |
| root_certificates=rootca, |
| certificate_chain=cert, |
| private_key=key, |
| ) |
| except FileNotFoundError as exp: |
| raise ValueError("SSL cert not found: %s" % exp) |
| return ssl_creds |
| |
| |
| def create_grpc_channel(ip, port, authority, ssl_creds=None, options=None): |
| """ |
| Helper function to create a grpc channel. |
| |
| Args: |
| ip: IP address of the remote endpoint |
| port: port of the remote endpoint |
| authority: HTTP header that control proxy uses for routing |
| ssl_creds: Enables SSL |
| options: configuration options for gRPC channel |
| Returns: |
| grpc channel |
| """ |
| grpc_options = [('grpc.default_authority', authority)] |
| if options is not None: |
| grpc_options.extend(options) |
| if ssl_creds is not None: |
| set_grpc_cipher_suites() |
| channel = grpc.secure_channel( |
| target='%s:%s' % (ip, port), |
| credentials=ssl_creds, |
| options=grpc_options, |
| ) |
| else: |
| channel = grpc.insecure_channel( |
| target='%s:%s' % (ip, port), |
| options=grpc_options, |
| ) |
| return channel |