blob: d55909e52b812f0b4782efffeafcb00dd97939e9 [file] [log] [blame]
# SPDX-FileCopyrightText: 2020 The Magma Authors.
# SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org>
#
# SPDX-License-Identifier: BSD-3-Clause
import abc
import contextlib
import json
import os
from typing import Any, Generic, TypeVar
import configuration.events as magma_configuration_events
from google.protobuf import json_format
from common import serialization_utils
from configuration.exceptions import LoadConfigError
from configuration.mconfigs import (
filter_configs_by_key,
unpack_mconfig_any,
)
from orc8r.protos.mconfig_pb2 import GatewayConfigs, GatewayConfigsMetadata
T = TypeVar('T')
MCONFIG_DIR = './magma_configs'
MCONFIG_OVERRIDE_DIR = './override_configs'
DEFAULT_MCONFIG_DIR = os.environ.get('MAGMA_CONFIG_LOCATION', MCONFIG_DIR)
def get_mconfig_manager():
"""
Get the mconfig manager implementation that the system is configured to
use.
Returns: MconfigManager implementation
"""
# This is stubbed out after deleting the streamed mconfig manager
return MconfigManagerImpl()
def load_service_mconfig(service: str, mconfig_struct: Any) -> Any:
"""
Utility function to load the mconfig for a specific service using the
configured mconfig manager.
"""
return get_mconfig_manager().load_service_mconfig(service, mconfig_struct)
def load_service_mconfig_as_json(service_name: str) -> Any:
"""
Loads the managed configuration from its json file stored on disk.
Args:
service_name (str): name of the service to load the config for
Returns: Loaded config value for the service as parsed json struct, not
protobuf message struct
"""
return get_mconfig_manager().load_service_mconfig_as_json(service_name)
class MconfigManager(Generic[T]):
"""
Interface for a class which handles loading and updating some cloud-
managed configuration (mconfig).
"""
@abc.abstractmethod
def load_mconfig(self) -> T:
"""
Load the managed configuration from its stored location.
Returns: Loaded mconfig
"""
pass
@abc.abstractmethod
def load_service_mconfig(
self, service_name: str,
mconfig_struct: Any,
) -> Any:
"""
Load a specific service's managed configuration.
Args:
service_name (str): name of the service to load a config for
mconfig_struct (Any): protobuf message struct of the managed config
for the service
Returns: Loaded config value for the service
"""
pass
@abc.abstractmethod
def load_mconfig_metadata(self) -> GatewayConfigsMetadata:
"""
Load the metadata of the managed configuration.
Returns: Loaded mconfig metadata
"""
pass
@abc.abstractmethod
def update_stored_mconfig(self, updated_value: str):
"""
Update the stored mconfig to the provided serialized value
Args:
updated_value: Serialized value of new mconfig value to store
"""
pass
@abc.abstractmethod
def deserialize_mconfig(
self, serialized_value: str,
allow_unknown_fields: bool = True,
) -> T:
"""
Deserialize the given string to the managed mconfig.
Args:
serialized_value:
Serialized value of a managed mconfig
allow_unknown_fields:
Set to true to suppress errors from parsing unknown fields
Returns: deserialized mconfig value
"""
pass
@abc.abstractmethod
def delete_stored_mconfig(self):
"""
Delete the stored mconfig file.
"""
pass
class MconfigManagerImpl(MconfigManager[GatewayConfigs]):
"""
Legacy mconfig manager for non-offset mconfigs
"""
MCONFIG_FILE_NAME = 'gateway.mconfig'
MCONFIG_PATH = os.path.join(MCONFIG_OVERRIDE_DIR, MCONFIG_FILE_NAME)
def load_mconfig(self) -> GatewayConfigs:
cfg_file_name = self._get_mconfig_file_path()
try:
with open(cfg_file_name, 'r', encoding='utf-8') as cfg_file:
mconfig_str = cfg_file.read()
return self.deserialize_mconfig(mconfig_str)
except (OSError, json.JSONDecodeError, json_format.ParseError) as e:
raise LoadConfigError('Error loading mconfig, mconfig may have format issue') from e
def load_service_mconfig(
self, service_name: str,
mconfig_struct: Any,
) -> Any:
mconfig = self.load_mconfig()
if service_name not in mconfig.configs_by_key:
raise LoadConfigError(
"Service ({}) missing in mconfig".format(service_name),
)
service_mconfig = mconfig.configs_by_key[service_name]
return unpack_mconfig_any(service_mconfig, mconfig_struct)
def load_service_mconfig_as_json(self, service_name) -> Any:
cfg_file_name = self._get_mconfig_file_path()
with open(cfg_file_name, 'r', encoding='utf-8') as f:
json_mconfig = json.load(f)
service_configs = json_mconfig.get('configsByKey', {})
service_configs.update(json_mconfig.get('configs_by_key', {}))
if service_name not in service_configs:
raise LoadConfigError(
"Service ({}) missing in mconfig".format(service_name),
)
return service_configs[service_name]
def load_mconfig_metadata(self) -> GatewayConfigsMetadata:
mconfig = self.load_mconfig()
return mconfig.metadata
def deserialize_mconfig(
self, serialized_value: str,
allow_unknown_fields: bool = True,
) -> GatewayConfigs:
# First parse as JSON in case there are types unrecognized by
# protobuf symbol database
json_mconfig = json.loads(serialized_value)
cfgs_by_key_json = json_mconfig.get('configs_by_key', {})
cfgs_by_key_json.update(json_mconfig.get('configsByKey', {}))
filtered_cfgs_by_key = filter_configs_by_key(cfgs_by_key_json)
# Set configs to filtered map, re-dump and parse
if 'configs_by_key' in json_mconfig:
json_mconfig.pop('configs_by_key')
json_mconfig['configsByKey'] = filtered_cfgs_by_key
json_mconfig_dumped = json.dumps(json_mconfig)
# Workaround for outdated protobuf library on sandcastle
if allow_unknown_fields:
return json_format.Parse(
json_mconfig_dumped,
GatewayConfigs(),
ignore_unknown_fields=True,
)
else:
return json_format.Parse(json_mconfig_dumped, GatewayConfigs())
def delete_stored_mconfig(self):
with contextlib.suppress(FileNotFoundError):
os.remove(self.MCONFIG_PATH)
magma_configuration_events.deleted_stored_mconfig()
def update_stored_mconfig(self, updated_value: str) -> GatewayConfigs:
parsed = json.loads(updated_value)
serialization_utils.write_to_file_atomically(
self.MCONFIG_PATH, json.dumps(parsed, indent=4, sort_keys=True),
)
magma_configuration_events.updated_stored_mconfig()
def _get_mconfig_file_path(self):
if os.path.isfile(self.MCONFIG_PATH):
return self.MCONFIG_PATH
else:
return os.path.join(DEFAULT_MCONFIG_DIR, self.MCONFIG_FILE_NAME)