blob: 86dd3ce1688fd9d3f8e333687e5eac99bbc88ec2 [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 json
from typing import Any, List
from data_models.data_model import DataModel
from data_models.data_model_parameters import ParameterName
from collections import namedtuple
from lte_utils import DuplexMode, map_earfcndl_to_band_earfcnul_mode
from exceptions import ConfigurationError
from logger import EnodebdLogger as logger
SingleEnodebConfig = namedtuple(
'SingleEnodebConfig',
[
'earfcndl', 'subframe_assignment',
'special_subframe_pattern',
'pci', 'plmnid_list', 'tac',
'bandwidth_mhz', 'cell_id',
'allow_enodeb_transmit',
'mme_address', 'mme_port',
],
)
class EnodebConfiguration():
"""
This represents the data model configuration for a single
eNodeB device. This can correspond to either the current configuration
of the device, or what configuration we desire to have for the device.
"""
def __init__(self, data_model: DataModel) -> None:
"""
The fields initialized in the constructor here should be enough to
track state across any data model configuration.
Most objects for eNodeB data models cannot be added or deleted.
For those objects, we just track state with a simple mapping from
parameter name to value.
For objects which can be added/deleted, we track them separately.
"""
# DataModel
self._data_model = data_model
# Dict[ParameterName, Any]
self._param_to_value = {}
# Dict[ParameterName, Dict[ParameterName, Any]]
self._numbered_objects = {}
# If adding a PLMN object, then you would set something like
# self._numbered_objects['PLMN_1'] = {'PLMN_1_ENABLED': True}
@property
def data_model(self) -> DataModel:
"""
The data model configuration is tied to a single data model
"""
return self._data_model
def __str__(self) -> str:
return str(self._param_to_value)
def get_parameter_names(self) -> List[ParameterName]:
"""
Returns: list of ParameterName
"""
return list(self._param_to_value.keys())
def has_parameter(self, param_name: ParameterName) -> bool:
return param_name in self._param_to_value
def get_parameter(self, param_name: ParameterName) -> Any:
"""
Args:
param_name: ParameterName
Returns:
Any, value of the parameter, formatted to be understood by enodebd
"""
self._assert_param_in_model(param_name)
return self._param_to_value[param_name]
def set_parameter(
self,
param_name: ParameterName,
value: Any,
) -> None:
"""
Args:
param_name: the parameter name to configure
value: the value to set, formatted to be understood by enodebd
"""
self._assert_param_in_model(param_name)
self._param_to_value[param_name] = value
def set_parameter_if_present(self, parameter_name: ParameterName, value: Any) -> None:
"""
Args:
param_name: the parameter name to configure
value: the value to set, formatted to be understood by enodebd
"""
trparam_model = self.data_model
tr_param = trparam_model.get_parameter(parameter_name)
if tr_param is not None:
self._param_to_value[parameter_name] = value
def delete_parameter(self, param_name: ParameterName) -> None:
del self._param_to_value[param_name]
def get_object_names(self) -> List[ParameterName]:
return list(self._numbered_objects.keys())
def has_object(self, param_name: ParameterName) -> bool:
"""
Args:
param_name: The ParameterName of the object
Returns: True if set in configuration
"""
self._assert_param_in_model(param_name)
return param_name in self._numbered_objects
def add_object(self, param_name: ParameterName) -> None:
if param_name in self._numbered_objects:
raise ConfigurationError("Configuration already has object")
self._numbered_objects[param_name] = {}
def delete_object(self, param_name: ParameterName) -> None:
if param_name not in self._numbered_objects:
raise ConfigurationError("Configuration does not have object")
del self._numbered_objects[param_name]
def get_parameter_for_object(
self,
param_name: ParameterName,
object_name: ParameterName,
) -> Any:
return self._numbered_objects[object_name].get(param_name)
def set_parameter_for_object(
self,
param_name: ParameterName,
value: Any,
object_name: ParameterName,
) -> None:
"""
Args:
param_name: the parameter name to configure
value: the value to set, formatted to be understood by enodebd
object_name: ParameterName of object
"""
self._assert_param_in_model(object_name)
self._assert_param_in_model(param_name)
self._numbered_objects[object_name][param_name] = value
def get_parameter_names_for_object(
self,
object_name: ParameterName,
) -> List[ParameterName]:
return list(self._numbered_objects[object_name].keys())
def get_debug_info(self) -> str:
debug_info = 'Param values: {}, \n Object values: {}'
return debug_info.format(
json.dumps(self._param_to_value, indent=2),
json.dumps(
self._numbered_objects,
indent=2,
),
)
def _assert_param_in_model(self, param_name: ParameterName) -> None:
trparam_model = self.data_model
tr_param = trparam_model.get_parameter(param_name)
if tr_param is None:
logger.warning('Parameter <%s> not defined in model', param_name)
raise ConfigurationError("Parameter %s not defined in model." % param_name)
def check_desired_configuration(self, current_config, desired_config: dict) -> bool:
def config_assert(condition: bool, message: str = None) -> None:
""" To be used in place of 'assert' so that ConfigurationError is raised
for all config-related exceptions. """
if not condition:
raise ConfigurationError(message)
# _set_earfcn_freq_band_mode
# Originally:
# mconfig: loaded from proto definiation
# service_config: loaded from mconfig
# device_config: retrieved configuration from enodeb
# data_model: defined in device parameter
# device_config = config loaded from enodeb, desired_config = config loaded from file
# _check_earfcn_freqw_band_mode
try:
band, duplex_mode, _ = map_earfcndl_to_band_earfcnul_mode(desired_config["earfcn_downlink1"])
except ValueError as err:
raise ConfigurationError(err)
if current_config.has_parameter(ParameterName.DUPLEX_MODE_CAPABILITY):
duplex_capability = current_config.get_parameter(ParameterName.DUPLEX_MODE_CAPABILITY)
if duplex_mode == DuplexMode.TDD and duplex_capability != "TDDMode":
raise ConfigurationError("Duplex mode TDD is not supported by eNodeB")
elif duplex_mode == DuplexMode.FDD and duplex_capability != "FDDMode":
raise ConfigurationError("Duplex mode FDD is not supported by eNodeB")
elif duplex_mode not in [DuplexMode.TDD, DuplexMode.FDD]:
raise ConfigurationError("Invalid duplex mode")
if current_config.has_parameter(ParameterName.BAND_CAPABILITY):
band_capability = current_config.get_parameter(ParameterName.BAND_CAPABILITY).split(',')
if str(band) not in band_capability:
logger.warning("Band %d not in capability list %s", band, band_capability)
# _check_tdd_subframe_config
config_assert(
desired_config["subframe_assignment"] in range(0, 7),
"Invalid TDD special subframe assignment (%d)" % desired_config["subframe_assignment"],
)
config_assert(
desired_config["special_subframe_pattern"] in range(0, 10),
"Invalid TDD special subframe pattern (%d)" % desired_config["special_subframe_pattern"],
)
# _check_plmnids_tac
for char in str(desired_config["plmn_list"]):
config_assert(char in "0123456789, ", "Invalid PLMNID (%s)" % desired_config["plmn_list"])
# TODO - add support for multiple PLMNIDs
plmnid_list = str(desired_config["plmn_list"]).split(",")
config_assert(len(plmnid_list) == 1, "Only 1 PLMNID is supported")
config_assert(len(plmnid_list[0]) <= 6, "PLMNID must be length <= 6 (%s)" % plmnid_list[0])
# _check_s1_connection_configuration
config_assert(type(desired_config["mme_address"]) is str, "Invalid MME address")
config_assert(type(desired_config["mme_port"]) is int, "Invalid MME port")
def apply_desired_configuration(self, current_config, desired_config: SingleEnodebConfig) -> None:
# _set_earfcn_freq_band_mode
self.set_parameter(ParameterName.EARFCNDL, desired_config["earfcn_downlink1"])
band, duplex_mode, _ = map_earfcndl_to_band_earfcnul_mode(desired_config["earfcn_downlink1"])
if duplex_mode == DuplexMode.FDD:
self.set_parameter(ParameterName.EARFCNUL, desired_config["earfcn_uplink1"])
self.set_parameter_if_present(ParameterName.BAND, band)
# _set_tdd_subframe_config
if (current_config.has_parameter(ParameterName.DUPLEX_MODE_CAPABILITY)
and current_config.get_parameter(ParameterName.DUPLEX_MODE_CAPABILITY) == "TDDMode"):
self.set_parameter(ParameterName.SUBFRAME_ASSIGNMENT, desired_config["subframe_assignment"])
self.set_parameter(ParameterName.SPECIAL_SUBFRAME_PATTERN, desired_config["special_subframe_pattern"])
# _set_plmnids_tac
plmnid_list = str(desired_config["plmn_list"]).split(",")
for i in range(1, 2):
object_name = ParameterName.PLMN_N % i
enable_plmn = i == 1
self.add_object(object_name)
self.set_parameter_for_object(ParameterName.PLMN_N_ENABLE % i, enable_plmn, object_name)
if enable_plmn:
self.set_parameter_for_object(ParameterName.PLMN_N_CELL_RESERVED % i, False, object_name)
self.set_parameter_for_object(ParameterName.PLMN_N_PRIMARY % i, enable_plmn, object_name)
self.set_parameter_for_object(ParameterName.PLMN_N_PLMNID % i, plmnid_list[i - 1], object_name)
self.set_parameter(ParameterName.TAC1, desired_config["tac1"])
# _set_bandwidth
# FIXME: The DL_BANDWIDTH update request wasn't able to be accepted by enodeb
self.set_parameter(ParameterName.DL_BANDWIDTH, desired_config["downlink_bandwidth"])
self.set_parameter(ParameterName.UL_BANDWIDTH, desired_config["uplink_bandwidth"])
# _set_cell_id
self.set_parameter(ParameterName.CELL_ID, desired_config["cell_id"])
# _set_misc_static_params
self.set_parameter_if_present(ParameterName.LOCAL_GATEWAY_ENABLE, 0)
self.set_parameter_if_present(ParameterName.GPS_ENABLE, True)
self.set_parameter_if_present(ParameterName.IP_SEC_ENABLE, False)
self.set_parameter_if_present(ParameterName.CELL_RESERVED, False)
self.set_parameter_if_present(ParameterName.MME_POOL_ENABLE, False)
# _set_s1_connection_configuration
self.set_parameter(ParameterName.MME_ADDRESS, desired_config["mme_address"])
self.set_parameter(ParameterName.MME_PORT, desired_config["mme_port"])
# enable LTE if we should
self.set_parameter(ParameterName.ADMIN_STATE, desired_config["admin_state"])
# These parameters are already configured above
exclude_list = [
"earfcn_downlink1", "earfcn_uplink1", "subframe_assignment", "special_subframe_pattern",
"plmnid", "tac1", "downlink_bandwidth", "uplink_bandwidth", "cell_id",
"mme_address", "mme_port", "admin_state"
]
# Configure the additional parameters which are set in enodeb config files
for name, value in desired_config.items():
if name not in exclude_list:
self.set_parameter_if_present(name, value)