Init commit for standalone enodebd

Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/data_models/__init__.py b/data_models/__init__.py
new file mode 100644
index 0000000..5c6cb64
--- /dev/null
+++ b/data_models/__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/data_models/data_model.py b/data_models/data_model.py
new file mode 100644
index 0000000..0c8ba27
--- /dev/null
+++ b/data_models/data_model.py
@@ -0,0 +1,271 @@
+"""
+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 collections import namedtuple
+from typing import Any, Callable, Dict, List, Optional
+
+from data_models.data_model_parameters import ParameterName
+
+TrParam = namedtuple('TrParam', ['path', 'is_invasive', 'type', 'is_optional'])
+
+# We may want to model nodes in the datamodel that are derived from other fields
+# in the datamodel and thus maynot have a representation in tr69.
+# e.g PTP_STATUS in FreedomFiOne is True iff GPS is in sync and SyncStatus is
+# True.
+# Explicitly map these params to invalid paths so setters and getters know they
+# should not try to read or write these nodes on the eNB side.
+InvalidTrParamPath = "INVALID_TR_PATH"
+
+
+class DataModel(ABC):
+    """
+    Class to represent relevant data model parameters.
+
+    Also should contain transform functions for certain parameters that are
+    represented differently in the eNodeB device than it is in Magma.
+
+    Subclass this for each data model implementation.
+
+    This class is effectively read-only.
+    """
+
+    def __init__(self):
+        self._presence_by_param = {}
+
+    def are_param_presences_known(self) -> bool:
+        """
+        True if all optional parameters' presence are known in data model
+        """
+        optional_params = self.get_names_of_optional_params()
+        for param in optional_params:
+            if param not in self._presence_by_param:
+                return False
+        return True
+
+    def is_parameter_present(self, param_name: ParameterName) -> bool:
+        """ Is the parameter missing from the device's data model """
+        param_info = self.get_parameter(param_name)
+        if param_info is None:
+            return False
+        if not param_info.is_optional:
+            return True
+        if param_name not in self._presence_by_param:
+            raise KeyError(
+                'Parameter presence not yet marked in data '
+                'model: %s' % param_name,
+            )
+        return self._presence_by_param[param_name]
+
+    def set_parameter_presence(
+        self,
+        param_name: ParameterName,
+        is_present: bool,
+    ) -> None:
+        """ Mark optional parameter as either missing or not """
+        self._presence_by_param[param_name] = is_present
+
+    def get_missing_params(self) -> List[ParameterName]:
+        """
+        Return optional params confirmed to be missing from data model.
+        NOTE: Make sure we already know which parameters are present or not
+        """
+        all_missing = []
+        for param in self.get_names_of_optional_params():
+            if self.is_parameter_present(param):
+                all_missing.append(param)
+        return all_missing
+
+    def get_present_params(self) -> List[ParameterName]:
+        """
+        Return optional params confirmed to be present in data model.
+        NOTE: Make sure we already know which parameters are present or not
+        """
+        all_optional = self.get_names_of_optional_params()
+        all_present = self.get_parameter_names()
+        for param in all_optional:
+            if not self.is_parameter_present(param):
+                all_present.remove(param)
+        return all_present
+
+    @classmethod
+    def get_names_of_optional_params(cls) -> List[ParameterName]:
+        all_optional_params = []
+        for name in cls.get_parameter_names():
+            if cls.get_parameter(name).is_optional:
+                all_optional_params.append(name)
+        return all_optional_params
+
+    @classmethod
+    def transform_for_magma(
+        cls,
+        param_name: ParameterName,
+        enb_value: Any,
+    ) -> Any:
+        """
+        Convert a parameter from its device specific formatting to the
+        consistent format that magma understands.
+        For the same parameter, different data models have their own
+        idiosyncrasies. For this reason, it's important to nominalize these
+        values before processing them in Magma code.
+
+        Args:
+            param_name: The parameter name
+            enb_value: Native value of the parameter
+
+        Returns:
+            Returns the nominal value of the parameter that is understood
+            by Magma code.
+        """
+        transforms = cls._get_magma_transforms()
+        if param_name in transforms:
+            transform_function = transforms[param_name]
+            return transform_function(enb_value)
+        return enb_value
+
+    @classmethod
+    def transform_for_enb(
+        cls,
+        param_name: ParameterName,
+        magma_value: Any,
+    ) -> Any:
+        """
+        Convert a parameter from the format that Magma understands to
+        the device specific formatting.
+        For the same parameter, different data models have their own
+        idiosyncrasies. For this reason, it's important to nominalize these
+        values before processing them in Magma code.
+
+        Args:
+            param_name: The parameter name. The transform is dependent on the
+                        exact parameter.
+            magma_value: Nominal value of the parameter.
+
+        Returns:
+            Returns the native value of the parameter that will be set in the
+            CPE data model configuration.
+        """
+        transforms = cls._get_enb_transforms()
+        if param_name in transforms:
+            transform_function = transforms[param_name]
+            return transform_function(magma_value)
+        return magma_value
+
+    @classmethod
+    def get_parameter_name_from_path(
+        cls,
+        param_path: str,
+    ) -> Optional[ParameterName]:
+        """
+        Args:
+            param_path: Parameter path,
+                eg. "Device.DeviceInfo.X_BAICELLS_COM_GPS_Status"
+        Returns:
+            ParameterName or None if there is no ParameterName matching
+        """
+        all_param_names = cls.get_parameter_names()
+        numbered_param_names = cls.get_numbered_param_names()
+        for _obj_name, param_name_list in numbered_param_names.items():
+            all_param_names = all_param_names + param_name_list
+
+        for param_name in all_param_names:
+            param_info = cls.get_parameter(param_name)
+            if param_info is not None and param_path == param_info.path:
+                return param_name
+        return None
+
+    @classmethod
+    @abstractmethod
+    def get_parameter(cls, param_name: ParameterName) -> Optional[TrParam]:
+        """
+        Args:
+            param_name: String of the parameter name
+
+        Returns:
+            TrParam or None if it doesn't exist
+        """
+        pass
+
+    @classmethod
+    @abstractmethod
+    def _get_magma_transforms(
+        cls,
+    ) -> Dict[ParameterName, Callable[[Any], Any]]:
+        """
+        For the same parameter, different data models have their own
+        idiosyncrasies. For this reason, it's important to nominalize these
+        values before processing them in Magma code.
+
+        Returns:
+            Dictionary with key of parameter name, and value of a transform
+            function taking the device-specific value of the parameter and
+            returning the value in format understood by Magma.
+        """
+        pass
+
+    @classmethod
+    @abstractmethod
+    def _get_enb_transforms(
+        cls,
+    ) -> Dict[ParameterName, Callable[[Any], Any]]:
+        """
+        For the same parameter, different data models have their own
+        idiosyncrasies. For this reason, it's important to nominalize these
+        values before processing them in Magma code.
+
+        Returns:
+            Dictionary with key of parameter name, and value of a transform
+            function taking the nominal value of the parameter and returning
+            the device-understood value.
+        """
+        pass
+
+    @classmethod
+    @abstractmethod
+    def get_load_parameters(cls) -> List[ParameterName]:
+        """
+        Returns:
+            List of all parameters to query when reading eNodeB state
+        """
+        pass
+
+    @classmethod
+    @abstractmethod
+    def get_num_plmns(cls) -> int:
+        """
+        Returns:
+            The number of PLMNs in the configuration.
+        """
+        pass
+
+    @classmethod
+    @abstractmethod
+    def get_parameter_names(cls) -> List[ParameterName]:
+        """
+        Returns:
+            A list of all parameter names that are neither numbered objects,
+            or belonging to numbered objects
+        """
+        pass
+
+    @classmethod
+    @abstractmethod
+    def get_numbered_param_names(
+        cls,
+    ) -> Dict[ParameterName, List[ParameterName]]:
+        """
+        Returns:
+            A key for all parameters that are numbered objects, and the value
+            is the list of parameters that belong to that numbered object
+        """
+        pass
diff --git a/data_models/data_model_parameters.py b/data_models/data_model_parameters.py
new file mode 100644
index 0000000..27df4d9
--- /dev/null
+++ b/data_models/data_model_parameters.py
@@ -0,0 +1,94 @@
+"""
+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.
+"""
+
+
+class ParameterName():
+    # Top-level objects
+    DEVICE = 'Device'
+    FAP_SERVICE = 'FAPService'
+
+    # Device info parameters
+    GPS_STATUS = 'GPS status'
+    PTP_STATUS = 'PTP status'
+    MME_STATUS = 'MME status'
+    REM_STATUS = 'REM status'
+
+    LOCAL_GATEWAY_ENABLE = 'Local gateway enable'
+    GPS_ENABLE = 'GPS enable'
+    GPS_LAT = 'GPS lat'
+    GPS_LONG = 'GPS long'
+    SW_VERSION = 'SW version'
+
+    SERIAL_NUMBER = 'Serial number'
+    CELL_ID = 'Cell ID'
+
+    # Capabilities
+    DUPLEX_MODE_CAPABILITY = 'Duplex mode capability'
+    BAND_CAPABILITY = 'Band capability'
+
+    # RF-related parameters
+    EARFCNDL = 'EARFCNDL'
+    EARFCNUL = 'EARFCNUL'
+    BAND = 'Band'
+    PCI = 'PCI'
+    DL_BANDWIDTH = 'DL bandwidth'
+    UL_BANDWIDTH = 'UL bandwidth'
+    SUBFRAME_ASSIGNMENT = 'Subframe assignment'
+    SPECIAL_SUBFRAME_PATTERN = 'Special subframe pattern'
+
+    # Other LTE parameters
+    ADMIN_STATE = 'Admin state'
+    OP_STATE = 'Opstate'
+    RF_TX_STATUS = 'RF TX status'
+
+    # RAN parameters
+    CELL_RESERVED = 'Cell reserved'
+    CELL_BARRED = 'Cell barred'
+
+    # Core network parameters
+    MME_IP = 'MME IP'
+    MME_PORT = 'MME port'
+    NUM_PLMNS = 'Num PLMNs'
+    PLMN = 'PLMN'
+    PLMN_LIST = 'PLMN List'
+
+    # PLMN parameters
+    PLMN_N = 'PLMN %d'
+    PLMN_N_CELL_RESERVED = 'PLMN %d cell reserved'
+    PLMN_N_ENABLE = 'PLMN %d enable'
+    PLMN_N_PRIMARY = 'PLMN %d primary'
+    PLMN_N_PLMNID = 'PLMN %d PLMNID'
+
+    # PLMN arrays are added below
+    TAC = 'TAC'
+    IP_SEC_ENABLE = 'IPSec enable'
+    MME_POOL_ENABLE = 'MME pool enable'
+
+    # Management server parameters
+    PERIODIC_INFORM_ENABLE = 'Periodic inform enable'
+    PERIODIC_INFORM_INTERVAL = 'Periodic inform interval'
+
+    # Performance management parameters
+    PERF_MGMT_ENABLE = 'Perf mgmt enable'
+    PERF_MGMT_UPLOAD_INTERVAL = 'Perf mgmt upload interval'
+    PERF_MGMT_UPLOAD_URL = 'Perf mgmt upload URL'
+    PERF_MGMT_USER = 'Perf mgmt username'
+    PERF_MGMT_PASSWORD = 'Perf mgmt password'
+
+
+class TrParameterType():
+    BOOLEAN = 'boolean'
+    STRING = 'string'
+    INT = 'int'
+    UNSIGNED_INT = 'unsignedInt'
+    OBJECT = 'object'
diff --git a/data_models/transform_for_enb.py b/data_models/transform_for_enb.py
new file mode 100644
index 0000000..7f3aaf8
--- /dev/null
+++ b/data_models/transform_for_enb.py
@@ -0,0 +1,78 @@
+"""
+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 exceptions import ConfigurationError
+
+CELL_RESERVED_MAP = {
+    True: 'reserved',
+    False: 'notReserved',
+}
+
+
+INVERT_CELL_RESERVED_MAP = {
+    True: 'notReserved',
+    False: 'reserved',
+}
+
+
+def admin_state(flag):
+    return 'UP' if flag else 'DOWN'
+
+
+def cell_reserved(value):
+    return CELL_RESERVED_MAP.get(value)
+
+
+def invert_cell_reserved(value):
+    """
+    We need to handle Baicells bug which inverts the meaning of 'cell reserved'
+    """
+    return INVERT_CELL_RESERVED_MAP.get(value)
+
+
+def invert_cell_barred(value: bool):
+    """
+    We need to handle Baicells bug which inverts the meaning of 'cell barred'
+    """
+    return not value
+
+
+def bandwidth(bandwidth_mhz):
+    """
+    Map bandwidth in MHz to number of RBs
+    TODO: TR-196 spec says this should be '6' rather than 'n6', but
+    BaiCells eNodeB uses 'n6'. Need to resolve this.
+
+    Args:
+        bandwidth_mhz (int): Bandwidth in MHz
+    Returns:
+        str: Bandwidth in RBS
+    """
+    if bandwidth_mhz == 1.4:
+        bandwidth_rbs = 'n6'
+    elif bandwidth_mhz == 3:
+        bandwidth_rbs = 'n15'
+    elif bandwidth_mhz == 5:
+        bandwidth_rbs = 'n25'
+    elif bandwidth_mhz == 10:
+        bandwidth_rbs = 'n50'
+    elif bandwidth_mhz == 15:
+        bandwidth_rbs = 'n75'
+    elif bandwidth_mhz == 20:
+        bandwidth_rbs = 'n100'
+    else:
+        raise ConfigurationError(
+            'Unknown bandwidth_mhz (%s)' %
+            str(bandwidth_mhz),
+        )
+    return bandwidth_rbs
diff --git a/data_models/transform_for_magma.py b/data_models/transform_for_magma.py
new file mode 100644
index 0000000..715cfba
--- /dev/null
+++ b/data_models/transform_for_magma.py
@@ -0,0 +1,89 @@
+"""
+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 textwrap
+from typing import Optional, Union
+
+from exceptions import ConfigurationError
+from logger import EnodebdLogger as logger
+
+DUPLEX_MAP = {
+    '01': 'TDDMode',
+    '02': 'FDDMode',
+}
+
+BANDWIDTH_RBS_TO_MHZ_MAP = {
+    'n6': 1.4,
+    'n15': 3,
+    'n25': 5,
+    'n50': 10,
+    'n75': 15,
+    'n100': 20,
+}
+
+BANDWIDTH_MHZ_LIST = {1.4, 3, 5, 10, 15, 20}
+
+
+def duplex_mode(value: str) -> Optional[str]:
+    return DUPLEX_MAP.get(value)
+
+
+def band_capability(value: str) -> str:
+    return ','.join([str(int(b, 16)) for b in textwrap.wrap(value, 2)])
+
+
+def gps_tr181(value: str) -> str:
+    """Convert GPS value (lat or lng) to float
+
+    Per TR-181 specification, coordinates are returned in degrees,
+    multiplied by 1,000,000.
+
+    Args:
+        value (string): GPS value (latitude or longitude)
+    Returns:
+        str: GPS value (latitude/longitude) in degrees
+    """
+    try:
+        return str(float(value) / 1e6)
+    except Exception:  # pylint: disable=broad-except
+        return value
+
+
+def bandwidth(bandwidth_rbs: Union[str, int, float]) -> float:
+    """
+    Map bandwidth in number of RBs to MHz
+    TODO: TR-196 spec says this should be '6' rather than 'n6', but
+    BaiCells eNodeB uses 'n6'. Need to resolve this.
+
+    Args:
+        bandwidth_rbs (str): Bandwidth in number of RBs
+    Returns:
+        str: Bandwidth in MHz
+    """
+    if bandwidth_rbs in BANDWIDTH_RBS_TO_MHZ_MAP:
+        return BANDWIDTH_RBS_TO_MHZ_MAP[bandwidth_rbs]
+
+    logger.warning('Unknown bandwidth_rbs (%s)', str(bandwidth_rbs))
+    if bandwidth_rbs in BANDWIDTH_MHZ_LIST:
+        return bandwidth_rbs
+    elif isinstance(bandwidth_rbs, str):
+        mhz = None
+        if bandwidth_rbs.isdigit():
+            mhz = int(bandwidth_rbs)
+        elif bandwidth_rbs.replace('.', '', 1).isdigit():
+            mhz = float(bandwidth_rbs)
+        if mhz in BANDWIDTH_MHZ_LIST:
+            return mhz
+    raise ConfigurationError(
+        'Unknown bandwidth specification (%s)' %
+        str(bandwidth_rbs),
+    )