Init commit for standalone enodebd

Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/configuration/mconfig_managers.py b/configuration/mconfig_managers.py
new file mode 100644
index 0000000..bd39c4c
--- /dev/null
+++ b/configuration/mconfig_managers.py
@@ -0,0 +1,235 @@
+"""
+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 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') 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)