Init commit for standalone enodebd

Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/device_config/__init__.py b/device_config/__init__.py
new file mode 100644
index 0000000..5c6cb64
--- /dev/null
+++ b/device_config/__init__.py
@@ -0,0 +1,12 @@
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
diff --git a/device_config/configuration_init.py b/device_config/configuration_init.py
new file mode 100644
index 0000000..cf9505b
--- /dev/null
+++ b/device_config/configuration_init.py
@@ -0,0 +1,573 @@
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+import json
+from collections import namedtuple
+from typing import Any, Optional, Union
+
+from lte.protos.mconfig import mconfigs_pb2
+from common.misc_utils import get_ip_from_if
+from configuration.exceptions import LoadConfigError
+from configuration.mconfig_managers import load_service_mconfig_as_json
+from data_models.data_model import DataModel
+from data_models.data_model_parameters import ParameterName
+from device_config.enodeb_config_postprocessor import (
+    EnodebConfigurationPostProcessor,
+)
+from device_config.enodeb_configuration import EnodebConfiguration
+from exceptions import ConfigurationError
+from logger import EnodebdLogger as logger
+from lte_utils import (
+    DuplexMode,
+    map_earfcndl_to_band_earfcnul_mode,
+    map_earfcndl_to_duplex_mode,
+)
+
+# LTE constants
+DEFAULT_S1_PORT = 36412
+# This is a known working value for supported eNB devices.
+# Cell Identity is a 28 bit number, but not all values are supported.
+DEFAULT_CELL_IDENTITY = 138777000
+
+SingleEnodebConfig = namedtuple(
+    'SingleEnodebConfig',
+    [
+        'earfcndl', 'subframe_assignment',
+        'special_subframe_pattern',
+        'pci', 'plmnid_list', 'tac',
+        'bandwidth_mhz', 'cell_id',
+        'allow_enodeb_transmit',
+        'mme_address', 'mme_port',
+    ],
+)
+
+
+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)
+
+
+def build_desired_config(
+        mconfig: Any,
+        service_config: Any,
+        device_config: EnodebConfiguration,
+        data_model: DataModel,
+        post_processor: EnodebConfigurationPostProcessor,
+) -> EnodebConfiguration:
+    """
+    Factory for initializing DESIRED data model configuration.
+
+    When working with the configuration of an eNodeB, we track the
+    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:
+    Returns:
+        Desired data model configuration for the device
+    """
+    cfg_desired = EnodebConfiguration(data_model)
+
+    # Determine configuration parameters
+    _set_management_server(cfg_desired)
+
+    # 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)
+
+    _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
+
+
+def _get_enb_config(
+        mconfig: mconfigs_pb2.EnodebD,
+        device_config: EnodebConfiguration,
+) -> SingleEnodebConfig:
+    # For fields that are specified per eNB
+    if mconfig.enb_configs_by_serial is not None and \
+            len(mconfig.enb_configs_by_serial) > 0:
+        enb_serial = \
+            device_config.get_parameter(ParameterName.SERIAL_NUMBER)
+        if enb_serial in mconfig.enb_configs_by_serial:
+            enb_config = mconfig.enb_configs_by_serial[enb_serial]
+            earfcndl = enb_config.earfcndl
+            pci = enb_config.pci
+            allow_enodeb_transmit = enb_config.transmit_enabled
+            tac = enb_config.tac
+            bandwidth_mhz = enb_config.bandwidth_mhz
+            cell_id = enb_config.cell_id
+            duplex_mode = map_earfcndl_to_duplex_mode(earfcndl)
+            subframe_assignment = None
+            special_subframe_pattern = None
+            if duplex_mode == DuplexMode.TDD:
+                subframe_assignment = enb_config.subframe_assignment
+                special_subframe_pattern = \
+                    enb_config.special_subframe_pattern
+        else:
+            raise ConfigurationError(
+                'Could not construct desired config '
+                'for eNB',
+            )
+    else:
+        pci = mconfig.pci
+        allow_enodeb_transmit = mconfig.allow_enodeb_transmit
+        tac = mconfig.tac
+        bandwidth_mhz = mconfig.bandwidth_mhz
+        cell_id = DEFAULT_CELL_IDENTITY
+        if mconfig.tdd_config is not None and str(mconfig.tdd_config) != '':
+            earfcndl = mconfig.tdd_config.earfcndl
+            subframe_assignment = mconfig.tdd_config.subframe_assignment
+            special_subframe_pattern = \
+                mconfig.tdd_config.special_subframe_pattern
+        elif mconfig.fdd_config is not None and str(mconfig.fdd_config) != '':
+            earfcndl = mconfig.fdd_config.earfcndl
+            subframe_assignment = None
+            special_subframe_pattern = None
+        else:
+            earfcndl = mconfig.earfcndl
+            subframe_assignment = mconfig.subframe_assignment
+            special_subframe_pattern = mconfig.special_subframe_pattern
+
+    # And now the rest of the fields
+    plmnid_list = mconfig.plmnid_list
+
+    single_enodeb_config = SingleEnodebConfig(
+        earfcndl=earfcndl,
+        subframe_assignment=subframe_assignment,
+        special_subframe_pattern=special_subframe_pattern,
+        pci=pci,
+        plmnid_list=plmnid_list,
+        tac=tac,
+        bandwidth_mhz=bandwidth_mhz,
+        cell_id=cell_id,
+        allow_enodeb_transmit=allow_enodeb_transmit,
+        mme_address=None,
+        mme_port=None,
+    )
+    return single_enodeb_config
+
+
+def _set_pci(
+        cfg: EnodebConfiguration,
+        pci: Any,
+) -> None:
+    """
+    Set the following parameters:
+     - PCI
+    """
+    if pci not in range(0, 504 + 1):
+        raise ConfigurationError('Invalid PCI (%d)' % 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)
diff --git a/device_config/configuration_util.py b/device_config/configuration_util.py
new file mode 100644
index 0000000..cf746c1
--- /dev/null
+++ b/device_config/configuration_util.py
@@ -0,0 +1,67 @@
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+from typing import NamedTuple, Optional
+
+from lte.protos.mconfig.mconfigs_pb2 import EnodebD
+
+EnodebConfig = NamedTuple(
+    'EnodebConfig', [
+        ('serial_num', str),
+        ('config', EnodebD.EnodebConfig),
+    ],
+)
+
+
+def get_enb_rf_tx_desired(mconfig: EnodebD, enb_serial: str) -> bool:
+    """ True if the mconfig specifies to enable transmit on the eNB """
+    if mconfig.enb_configs_by_serial is not None and \
+            len(mconfig.enb_configs_by_serial) > 0:
+        if enb_serial in mconfig.enb_configs_by_serial:
+            enb_config = mconfig.enb_configs_by_serial[enb_serial]
+            return enb_config.transmit_enabled
+        else:
+            raise KeyError('Missing eNB from mconfig: %s' % enb_serial)
+    return mconfig.allow_enodeb_transmit
+
+
+def is_enb_registered(mconfig: EnodebD, enb_serial: str) -> bool:
+    """
+    True if either:
+        - the eNodeB is registered by serial to the Access Gateway
+        or
+        - the Access Gateway accepts all eNodeB devices
+    """
+    if mconfig.enb_configs_by_serial is not None and \
+            len(mconfig.enb_configs_by_serial) > 0:
+        if enb_serial in mconfig.enb_configs_by_serial:
+            return True
+        else:
+            return False
+    return True
+
+
+def find_enb_by_cell_id(mconfig: EnodebD, cell_id: int) \
+        -> Optional[EnodebConfig]:
+    """
+    Returns eNB config if:
+        - the eNodeB is registered by serial to the Access Gateway
+        - cell ID is found in eNB status by serial
+    else: returns None
+    """
+    if mconfig.enb_configs_by_serial is not None and \
+            len(mconfig.enb_configs_by_serial) > 0:
+        for sn, enb in mconfig.enb_configs_by_serial.items():
+            if cell_id == enb.cell_id:
+                config = EnodebConfig(serial_num=sn, config=enb)
+                return config
+    return None
diff --git a/device_config/enodeb_config_postprocessor.py b/device_config/enodeb_config_postprocessor.py
new file mode 100644
index 0000000..5cb469a
--- /dev/null
+++ b/device_config/enodeb_config_postprocessor.py
@@ -0,0 +1,31 @@
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+from abc import ABC, abstractmethod
+from typing import Any
+
+from device_config.enodeb_configuration import EnodebConfiguration
+
+
+class EnodebConfigurationPostProcessor(ABC):
+    """
+    Overrides the desired configuration for the eNodeB, with subclass per
+    device/sw-version that requires non-standard configuration behavior.
+    """
+
+    @abstractmethod
+    def postprocess(self, mconfig: Any, service_cfg: Any, desired_cfg: EnodebConfiguration) -> None:
+        """
+        Implementation of function which overrides the desired configuration
+        for the eNodeB
+        """
+        pass
diff --git a/device_config/enodeb_configuration.py b/device_config/enodeb_configuration.py
new file mode 100644
index 0000000..5d28d50
--- /dev/null
+++ b/device_config/enodeb_configuration.py
@@ -0,0 +1,161 @@
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+import json
+from typing import Any, List
+
+from data_models.data_model import DataModel
+from data_models.data_model_parameters import ParameterName
+from exceptions import ConfigurationError
+from logger import EnodebdLogger as logger
+
+
+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 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 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 not defined in model.")