AETHER-3573 Simplify configuration files in enodebd

Change-Id: I048d743c0677c85244b87a6c6444f39c06f6bf4b
diff --git a/device_config/configuration_init.py b/device_config/configuration_init.py
index 8acb152..97ce399 100644
--- a/device_config/configuration_init.py
+++ b/device_config/configuration_init.py
@@ -3,6 +3,7 @@
 #
 # SPDX-License-Identifier: BSD-3-Clause
 
+from distutils.sysconfig import customize_compiler
 import json
 from collections import namedtuple
 from typing import Any, Optional, Union
@@ -53,8 +54,6 @@
 
 
 def build_desired_config(
-        mconfig: Any,
-        service_config: Any,
         device_config: EnodebConfiguration,
         data_model: DataModel,
         post_processor: EnodebConfigurationPostProcessor,
@@ -66,124 +65,32 @@
     current state of configuration for that device, as well as what
     configuration we want to set on the device.
     Args:
-        mconfig: Managed configuration, eNodeB protobuf message
-        service_config:
+        device_config: The current configuration of the device.
+        data_model: The data model for the device.
+        post_processor: The post-processor to use for the device.
     Returns:
         Desired data model configuration for the device
     """
 
-    print("DEVICE CFG: ", device_config)
+    # The configuration we want to push to the target device
+    desired_configuration = EnodebConfiguration(data_model)
+    # The configuration read from local configuration files by serial number specific
+    customized_configuration = _get_enb_config(device_config)
+    # device_config is the current configuration read from the target device
 
-    cfg_desired = EnodebConfiguration(data_model)
+    # Check if the customized configuration is valid
+    desired_configuration.check_desired_configuration(device_config, customized_configuration)
+    # Apply the customized configuration to the target device
+    desired_configuration.apply_desired_configuration(device_config, customized_configuration)
 
-    # Determine configuration parameters
-    _set_management_server(cfg_desired)
+    post_processor.postprocess(desired_configuration)
 
-    # Attempt to load device configuration from YANG before service mconfig
-    enb_config = _get_enb_yang_config(device_config) or \
-                 _get_enb_config(mconfig, device_config)
-
-    print(enb_config)
-
-    _set_earfcn_freq_band_mode(
-        device_config, cfg_desired, data_model,
-        enb_config.earfcndl,
-    )
-    if enb_config.subframe_assignment is not None:
-        _set_tdd_subframe_config(
-            device_config, cfg_desired,
-            enb_config.subframe_assignment,
-            enb_config.special_subframe_pattern,
-        )
-    _set_pci(cfg_desired, enb_config.pci)
-    _set_plmnids_tac(cfg_desired, enb_config.plmnid_list, enb_config.tac)
-    _set_bandwidth(cfg_desired, data_model, enb_config.bandwidth_mhz)
-    _set_cell_id(cfg_desired, enb_config.cell_id)
-    _set_perf_mgmt(
-        cfg_desired,
-        get_ip_from_if(service_config['tr069']['interface']),
-        service_config['tr069']['perf_mgmt_port'],
-    )
-    _set_misc_static_params(device_config, cfg_desired, data_model)
-    if enb_config.mme_address is not None and enb_config.mme_port is not None:
-        _set_s1_connection(
-            cfg_desired,
-            enb_config.mme_address,
-            enb_config.mme_port,
-        )
-    else:
-        _set_s1_connection(
-            cfg_desired, get_ip_from_if(service_config['s1_interface']),
-        )
-
-    # Enable LTE if we should
-    cfg_desired.set_parameter(
-        ParameterName.ADMIN_STATE,
-        enb_config.allow_enodeb_transmit,
-    )
-
-    post_processor.postprocess(mconfig, service_config, cfg_desired)
-    return cfg_desired
-
-
-def _get_enb_yang_config(
-        device_config: EnodebConfiguration,
-) -> Optional[SingleEnodebConfig]:
-    """"
-    Proof of concept configuration function to load eNB configs from YANG
-    data model. Attempts to load configuration from YANG for the eNodeB if
-    an entry exists with a matching serial number.
-    Args:
-        device_config: eNodeB device configuration
-    Returns:
-        None or a SingleEnodebConfig from YANG with matching serial number
-    """
-    enb = []
-    mme_list = []
-    mme_address = None
-    mme_port = None
-    try:
-        enb_serial = \
-            device_config.get_parameter(ParameterName.SERIAL_NUMBER)
-        config = json.loads(
-            load_service_mconfig_as_json('yang').get('value', '{}'),
-        )
-
-        enb.extend(
-            filter(
-                lambda entry: entry['serial'] == enb_serial,
-                config.get('cellular', {}).get('enodeb', []),
-            ),
-        )
-    except (ValueError, KeyError, LoadConfigError):
-        return None
-    if len(enb) == 0:
-        return None
-    enb_config = enb[0].get('config', {})
-    mme_list.extend(enb_config.get('mme', []))
-    if len(mme_list) > 0:
-        mme_address = mme_list[0].get('host')
-        mme_port = mme_list[0].get('port')
-    single_enodeb_config = SingleEnodebConfig(
-        earfcndl=enb_config.get('earfcndl'),
-        subframe_assignment=enb_config.get('subframe_assignment'),
-        special_subframe_pattern=enb_config.get('special_subframe_pattern'),
-        pci=enb_config.get('pci'),
-        plmnid_list=",".join(enb_config.get('plmnid', [])),
-        tac=enb_config.get('tac'),
-        bandwidth_mhz=enb_config.get('bandwidth_mhz'),
-        cell_id=enb_config.get('cell_id'),
-        allow_enodeb_transmit=enb_config.get('transmit_enabled'),
-        mme_address=mme_address,
-        mme_port=mme_port,
-    )
-    return single_enodeb_config
+    return desired_configuration
 
 
 def _get_enb_config(
-        mconfig: mconfigs_pb2.EnodebD,
-        device_config: EnodebConfiguration,
-) -> SingleEnodebConfig:
+    device_config: EnodebConfiguration,
+) -> dict:
     # The eNodeB parameters to be generated with default value,
     # It will load from eNB configs based on serial number or default value
     # The params is a nested list which contains 2 format of parameter names.
@@ -191,368 +98,24 @@
     #   magma_configs/serial_number/ and magma_configs/acs_common.yml
     # The second parameter is the name of gateway configuration in 
     #   override_configs/gateway.mconfig
-
-    # params.column1 = SingleEnodebConfig key
-    # params.column2 = sn config
-    # params.column3 = common_config
-    # params.column4 = mconfig
-
-    params = [
-        ["earfcndl", "earfcn_downlink1", "earfcndl" * 2],
-        ["subframe_assignment", "subframe_assignment", "subframeAssignment", "subframe_assignment"],
-        ["special_subframe_pattern", "subframe_configuration", "specialSubframePattern", "special_subframe_pattern"],
-        ["pci", "pci1", "pci", "pci"],
-        ["plmnid_list", "plmnid_list", "plmnidList", "plmnid_list"],
-        ["tac", "tac1", "tac", "tac"],
-        ["bandwidth_mhz", "downlink_bandwidth", "bandwidthMhz", "bandwidth_mhz"],
-        # Note: mconfig doesn't have allowEnodebTransmit
-        ["allow_enodeb_transmit", "allow_enodeb_transmit", "allowEnodebTransmit", "allowEnodebTransmit"]
-    ]
-    
-    extend_params = ["cell_id", "mme_address", "mme_port"]
+    def flatten_dictionary(dictionary: dict) -> dict:
+        return {k: v for item in dictionary.values() for k, v in item.items()}
 
     params_dict = dict()
 
-    common_config = load_common_config()
+    # The common configuration file is loaded from magma_configs/acs_common.yml
+    common_config = flatten_dictionary(load_common_config())
+    
+    # The serial number configuration file is loaded from magma_configs/serial_number/
+    # Get the specific configuration with the serial number as the key
     enb_configs = load_enb_config()
     enb_serial = device_config.get_parameter(ParameterName.SERIAL_NUMBER)
     enb_config = enb_configs.get(enb_serial, dict())
 
-    for param in params:
-        params_dict[param[0]] = enb_config.get(param[1], 
-            common_config.get(param[0], mconfig.__getattribute__(param[0]))
-        )
+    generated_config = dict()
+    for parameter_name in ParameterName.all_paramters():
+        parameter_value = enb_config.get(parameter_name, common_config.get(parameter_name))
+        if parameter_value is not None:
+            generated_config[parameter_name] = parameter_value
 
-    for param in extend_params:
-        params_dict[param] = enb_config.get(param, common_config.get(param, None))
-
-    return SingleEnodebConfig(**params_dict)
-
-
-def _set_pci(
-        cfg: EnodebConfiguration,
-        pci: Any,
-) -> None:
-    """
-    Set the following parameters:
-     - PCI
-    """
-
-    if pci is int and pci not in range(0, 504 + 1):
-        raise ConfigurationError('Invalid PCI (%d)' % pci)
-
-    if pci is str and any(map(lambda x: int(x) not in range(0, 504 + 1), pci.split(","))):
-        raise ConfigurationError('Invalid PCI (%s)' % pci)
-
-    cfg.set_parameter(ParameterName.PCI, pci)
-
-
-def _set_bandwidth(
-        cfg: EnodebConfiguration,
-        data_model: DataModel,
-        bandwidth_mhz: Any,
-) -> None:
-    """
-    Set the following parameters:
-     - DL bandwidth
-     - UL bandwidth
-    """
-    _set_param_if_present(
-        cfg, data_model, ParameterName.DL_BANDWIDTH,
-        bandwidth_mhz,
-    )
-    _set_param_if_present(
-        cfg, data_model, ParameterName.UL_BANDWIDTH,
-        bandwidth_mhz,
-    )
-
-
-def _set_cell_id(
-        cfg: EnodebConfiguration,
-        cell_id: int,
-) -> None:
-    config_assert(
-        cell_id in range(0, 268435456),
-        'Cell Identity should be from 0 - (2^28 - 1)',
-    )
-    cfg.set_parameter(ParameterName.CELL_ID, cell_id)
-
-
-def _set_tdd_subframe_config(
-        device_cfg: EnodebConfiguration,
-        cfg: EnodebConfiguration,
-        subframe_assignment: Any,
-        special_subframe_pattern: Any,
-) -> None:
-    """
-    Set the following parameters:
-     - Subframe assignment
-     - Special subframe pattern
-    """
-    # Don't try to set if this is not TDD mode
-    if (
-        device_cfg.has_parameter(ParameterName.DUPLEX_MODE_CAPABILITY)
-            and device_cfg.get_parameter(ParameterName.DUPLEX_MODE_CAPABILITY)
-            != 'TDDMode'
-    ):
-        return
-
-    config_assert(
-        subframe_assignment in range(0, 6 + 1),
-        'Invalid TDD subframe assignment (%d)' % subframe_assignment,
-    )
-    config_assert(
-        special_subframe_pattern in range(0, 9 + 1),
-        'Invalid TDD special subframe pattern (%d)'
-        % special_subframe_pattern,
-    )
-
-    cfg.set_parameter(
-        ParameterName.SUBFRAME_ASSIGNMENT,
-        subframe_assignment,
-    )
-    cfg.set_parameter(
-        ParameterName.SPECIAL_SUBFRAME_PATTERN,
-        special_subframe_pattern,
-    )
-
-
-def _set_management_server(cfg: EnodebConfiguration) -> None:
-    """
-    Set the following parameters:
-     - Periodic inform enable
-     - Periodic inform interval (hard-coded)
-    """
-    cfg.set_parameter(ParameterName.PERIODIC_INFORM_ENABLE, True)
-    # In seconds
-    cfg.set_parameter(ParameterName.PERIODIC_INFORM_INTERVAL, 5)
-
-
-def _set_s1_connection(
-        cfg: EnodebConfiguration,
-        mme_ip: Any,
-        mme_port: Any = DEFAULT_S1_PORT,
-) -> None:
-    """
-    Set the following parameters:
-     - MME IP
-     - MME port (defalts to 36412 as per TR-196 recommendation)
-    """
-    config_assert(type(mme_ip) == str, 'Invalid MME IP type')
-    config_assert(type(mme_port) == int, 'Invalid MME Port type')
-    cfg.set_parameter(ParameterName.MME_IP, mme_ip)
-    cfg.set_parameter(ParameterName.MME_PORT, mme_port)
-
-
-def _set_perf_mgmt(
-        cfg: EnodebConfiguration,
-        perf_mgmt_ip: str,
-        perf_mgmt_port: int,
-) -> None:
-    """
-    Set the following parameters:
-     - Perf mgmt enable
-     - Perf mgmt upload interval
-     - Perf mgmt upload URL
-    """
-    cfg.set_parameter(ParameterName.PERF_MGMT_ENABLE, True)
-    # Upload interval supported values (in secs):
-    # [60, 300, 900, 1800, 3600]
-    # Note: eNodeB crashes have been experienced with 60-sec interval.
-    # Hence using 300sec
-    cfg.set_parameter(
-        ParameterName.PERF_MGMT_UPLOAD_INTERVAL,
-        300,
-    )
-    cfg.set_parameter(
-        ParameterName.PERF_MGMT_UPLOAD_URL,
-        'http://%s:%d/' % (perf_mgmt_ip, perf_mgmt_port),
-    )
-
-
-def _set_misc_static_params(
-        device_cfg: EnodebConfiguration,
-        cfg: EnodebConfiguration,
-        data_model: DataModel,
-) -> None:
-    """
-    Set the following parameters:
-     - Local gateway enable
-     - GPS enable
-    """
-    _set_param_if_present(
-        cfg, data_model, ParameterName.LOCAL_GATEWAY_ENABLE,
-        0,
-    )
-    _set_param_if_present(cfg, data_model, ParameterName.GPS_ENABLE, True)
-    # For BaiCells eNodeBs, IPSec enable may be either integer or bool.
-    # Set to false/0 depending on the current type
-    if data_model.is_parameter_present(ParameterName.IP_SEC_ENABLE):
-        try:
-            int(device_cfg.get_parameter(ParameterName.IP_SEC_ENABLE))
-            cfg.set_parameter(ParameterName.IP_SEC_ENABLE, value=0)
-        except ValueError:
-            cfg.set_parameter(ParameterName.IP_SEC_ENABLE, value=False)
-
-    _set_param_if_present(cfg, data_model, ParameterName.CELL_RESERVED, False)
-    _set_param_if_present(
-        cfg, data_model, ParameterName.MME_POOL_ENABLE,
-        False,
-    )
-
-
-def _set_plmnids_tac(
-        cfg: EnodebConfiguration,
-        plmnids: Union[int, str],
-        tac: Any,
-) -> None:
-    """
-    Set the following parameters:
-     - PLMNID list (including all child parameters)
-
-    Input 'plmnids' is comma-separated list of PLMNIDs
-    """
-    # Convert int PLMNID to string
-    if type(plmnids) == int:
-        plmnid_str = str(plmnids)
-    else:
-        config_assert(type(plmnids) == str, 'PLMNID must be string')
-        plmnid_str = plmnids
-
-    # Multiple PLMNIDs will be supported using comma-separated list.
-    # Currently, just one supported
-    for char in plmnid_str:
-        config_assert(
-            char in '0123456789, ',
-            'Unhandled character (%s) in PLMNID' % char,
-        )
-    plmnid_list = plmnid_str.split(',')
-
-    # TODO - add support for multiple PLMNIDs
-    config_assert(
-        len(plmnid_list) == 1,
-        'Exactly one PLMNID must be configured',
-    )
-
-    # Validate PLMNIDs
-    plmnid_list[0] = plmnid_list[0].strip()
-    config_assert(
-        len(plmnid_list[0]) <= 6,
-        'PLMNID must be length <=6 (%s)' % plmnid_list[0],
-    )
-
-    # We just need one PLMN element in the config. Delete all others.
-    for i in range(1, 2):  # data_model.get_num_plmns() + 1):
-        object_name = ParameterName.PLMN_N % i
-        enable_plmn = i == 1
-        cfg.add_object(object_name)
-        cfg.set_parameter_for_object(
-            ParameterName.PLMN_N_ENABLE % i,
-            enable_plmn,
-            object_name,
-        )
-        if enable_plmn:
-            cfg.set_parameter_for_object(
-                ParameterName.PLMN_N_CELL_RESERVED % i,
-                False, object_name,
-            )
-            cfg.set_parameter_for_object(
-                ParameterName.PLMN_N_PRIMARY % i,
-                enable_plmn,
-                object_name,
-            )
-            cfg.set_parameter_for_object(
-                ParameterName.PLMN_N_PLMNID % i,
-                plmnid_list[i - 1],
-                object_name,
-            )
-    cfg.set_parameter(ParameterName.TAC, tac)
-
-
-def _set_earfcn_freq_band_mode(
-        device_cfg: EnodebConfiguration,
-        cfg: EnodebConfiguration,
-        data_model: DataModel,
-        earfcndl: int,
-) -> None:
-    """
-    Set the following parameters:
-     - EARFCNDL
-     - EARFCNUL
-     - Band
-    """
-    # Note: validation of EARFCNDL done by mapping function. If invalid
-    # EARFCN, raise ConfigurationError
-    try:
-        band, duplex_mode, earfcnul = map_earfcndl_to_band_earfcnul_mode(
-            earfcndl,
-        )
-    except ValueError as err:
-        raise ConfigurationError(err)
-
-    # Verify capabilities
-    if device_cfg.has_parameter(ParameterName.DUPLEX_MODE_CAPABILITY):
-        duplex_capability = \
-            device_cfg.get_parameter(ParameterName.DUPLEX_MODE_CAPABILITY)
-        if duplex_mode == DuplexMode.TDD and duplex_capability != 'TDDMode':
-            raise ConfigurationError((
-                'eNodeB duplex mode capability is <{0}>, '
-                'but earfcndl is <{1}>, giving duplex '
-                'mode <{2}> instead'
-            ).format(
-                duplex_capability, str(earfcndl), str(duplex_mode),
-            ))
-        elif duplex_mode == DuplexMode.FDD and duplex_capability != 'FDDMode':
-            raise ConfigurationError((
-                'eNodeB duplex mode capability is <{0}>, '
-                'but earfcndl is <{1}>, giving duplex '
-                'mode <{2}> instead'
-            ).format(
-                duplex_capability, str(earfcndl), str(duplex_mode),
-            ))
-        elif duplex_mode not in {DuplexMode.TDD, DuplexMode.FDD}:
-            raise ConfigurationError(
-                'Invalid duplex mode (%s)' % str(duplex_mode),
-            )
-
-    if device_cfg.has_parameter(ParameterName.BAND_CAPABILITY):
-        # Baicells indicated that they no longer use the band capability list,
-        # so it may not be populated correctly
-        band_capability_list = device_cfg.get_parameter(
-            ParameterName.BAND_CAPABILITY,
-        )
-        band_capabilities = band_capability_list.split(',')
-        if str(band) not in band_capabilities:
-            logger.warning(
-                'Band %d not in capabilities list (%s). Continuing'
-                ' with config because capabilities list may not be'
-                ' correct', band, band_capabilities,
-            )
-    cfg.set_parameter(ParameterName.EARFCNDL, earfcndl)
-    if duplex_mode == DuplexMode.FDD:
-        _set_param_if_present(
-            cfg, data_model, ParameterName.EARFCNUL,
-            earfcnul,
-        )
-    else:
-        logger.debug('Not setting EARFCNUL - duplex mode is not FDD')
-
-    _set_param_if_present(cfg, data_model, ParameterName.BAND, band)
-
-    if duplex_mode == DuplexMode.TDD:
-        logger.debug('Set EARFCNDL=%d, Band=%d', earfcndl, band)
-    elif duplex_mode == DuplexMode.FDD:
-        logger.debug(
-            'Set EARFCNDL=%d, EARFCNUL=%d, Band=%d',
-            earfcndl, earfcnul, band,
-        )
-
-
-def _set_param_if_present(
-        cfg: EnodebConfiguration,
-        data_model: DataModel,
-        param: ParameterName,
-        value: Any,
-) -> None:
-    if data_model.is_parameter_present(param):
-        cfg.set_parameter(param, value)
+    return generated_config
diff --git a/device_config/enodeb_config_postprocessor.py b/device_config/enodeb_config_postprocessor.py
index e8aebc6..39c5d31 100644
--- a/device_config/enodeb_config_postprocessor.py
+++ b/device_config/enodeb_config_postprocessor.py
@@ -16,7 +16,7 @@
     """
 
     @abstractmethod
-    def postprocess(self, mconfig: Any, service_cfg: Any, desired_cfg: EnodebConfiguration) -> None:
+    def postprocess(self, desired_cfg: EnodebConfiguration) -> None:
         """
         Implementation of function which overrides the desired configuration
         for the eNodeB
diff --git a/device_config/enodeb_configuration.py b/device_config/enodeb_configuration.py
index 2b35271..8a7050b 100644
--- a/device_config/enodeb_configuration.py
+++ b/device_config/enodeb_configuration.py
@@ -8,10 +8,27 @@
 
 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
@@ -81,6 +98,18 @@
         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]
 
@@ -151,3 +180,123 @@
         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
+        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 at 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)