blob: d55909e52b812f0b4782efffeafcb00dd97939e9 [file] [log] [blame]
Wei-Yu Chenad55cb82022-02-15 20:07:01 +08001# SPDX-FileCopyrightText: 2020 The Magma Authors.
2# SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org>
3#
4# SPDX-License-Identifier: BSD-3-Clause
Wei-Yu Chen49950b92021-11-08 19:19:18 +08005
Wei-Yu Chen49950b92021-11-08 19:19:18 +08006import abc
7import contextlib
8import json
9import os
10from typing import Any, Generic, TypeVar
11
12import configuration.events as magma_configuration_events
13from google.protobuf import json_format
14from common import serialization_utils
15from configuration.exceptions import LoadConfigError
16from configuration.mconfigs import (
17 filter_configs_by_key,
18 unpack_mconfig_any,
19)
20from orc8r.protos.mconfig_pb2 import GatewayConfigs, GatewayConfigsMetadata
21
22T = TypeVar('T')
23
24MCONFIG_DIR = './magma_configs'
25MCONFIG_OVERRIDE_DIR = './override_configs'
26DEFAULT_MCONFIG_DIR = os.environ.get('MAGMA_CONFIG_LOCATION', MCONFIG_DIR)
27
28
29def get_mconfig_manager():
30 """
31 Get the mconfig manager implementation that the system is configured to
32 use.
33
34 Returns: MconfigManager implementation
35 """
36 # This is stubbed out after deleting the streamed mconfig manager
37 return MconfigManagerImpl()
38
39
40def load_service_mconfig(service: str, mconfig_struct: Any) -> Any:
41 """
42 Utility function to load the mconfig for a specific service using the
43 configured mconfig manager.
44 """
45 return get_mconfig_manager().load_service_mconfig(service, mconfig_struct)
46
47
48def load_service_mconfig_as_json(service_name: str) -> Any:
49 """
50 Loads the managed configuration from its json file stored on disk.
51
52 Args:
53 service_name (str): name of the service to load the config for
54
55 Returns: Loaded config value for the service as parsed json struct, not
56 protobuf message struct
57 """
58 return get_mconfig_manager().load_service_mconfig_as_json(service_name)
59
60
61class MconfigManager(Generic[T]):
62 """
63 Interface for a class which handles loading and updating some cloud-
64 managed configuration (mconfig).
65 """
66
67 @abc.abstractmethod
68 def load_mconfig(self) -> T:
69 """
70 Load the managed configuration from its stored location.
71
72 Returns: Loaded mconfig
73 """
74 pass
75
76 @abc.abstractmethod
77 def load_service_mconfig(
78 self, service_name: str,
79 mconfig_struct: Any,
80 ) -> Any:
81 """
82 Load a specific service's managed configuration.
83
84 Args:
85 service_name (str): name of the service to load a config for
86 mconfig_struct (Any): protobuf message struct of the managed config
87 for the service
88
89 Returns: Loaded config value for the service
90 """
91 pass
92
93 @abc.abstractmethod
94 def load_mconfig_metadata(self) -> GatewayConfigsMetadata:
95 """
96 Load the metadata of the managed configuration.
97
98 Returns: Loaded mconfig metadata
99 """
100 pass
101
102 @abc.abstractmethod
103 def update_stored_mconfig(self, updated_value: str):
104 """
105 Update the stored mconfig to the provided serialized value
106
107 Args:
108 updated_value: Serialized value of new mconfig value to store
109 """
110 pass
111
112 @abc.abstractmethod
113 def deserialize_mconfig(
114 self, serialized_value: str,
115 allow_unknown_fields: bool = True,
116 ) -> T:
117 """
118 Deserialize the given string to the managed mconfig.
119
120 Args:
121 serialized_value:
122 Serialized value of a managed mconfig
123 allow_unknown_fields:
124 Set to true to suppress errors from parsing unknown fields
125
126 Returns: deserialized mconfig value
127 """
128 pass
129
130 @abc.abstractmethod
131 def delete_stored_mconfig(self):
132 """
133 Delete the stored mconfig file.
134 """
135 pass
136
137
138class MconfigManagerImpl(MconfigManager[GatewayConfigs]):
139 """
140 Legacy mconfig manager for non-offset mconfigs
141 """
142
143 MCONFIG_FILE_NAME = 'gateway.mconfig'
144 MCONFIG_PATH = os.path.join(MCONFIG_OVERRIDE_DIR, MCONFIG_FILE_NAME)
145
146 def load_mconfig(self) -> GatewayConfigs:
147 cfg_file_name = self._get_mconfig_file_path()
148 try:
149 with open(cfg_file_name, 'r', encoding='utf-8') as cfg_file:
150 mconfig_str = cfg_file.read()
151 return self.deserialize_mconfig(mconfig_str)
152 except (OSError, json.JSONDecodeError, json_format.ParseError) as e:
Wei-Yu Chen5cbdfbb2021-12-02 01:10:21 +0800153 raise LoadConfigError('Error loading mconfig, mconfig may have format issue') from e
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800154
155 def load_service_mconfig(
156 self, service_name: str,
157 mconfig_struct: Any,
158 ) -> Any:
159 mconfig = self.load_mconfig()
160 if service_name not in mconfig.configs_by_key:
161 raise LoadConfigError(
162 "Service ({}) missing in mconfig".format(service_name),
163 )
164
165 service_mconfig = mconfig.configs_by_key[service_name]
166 return unpack_mconfig_any(service_mconfig, mconfig_struct)
167
168 def load_service_mconfig_as_json(self, service_name) -> Any:
169 cfg_file_name = self._get_mconfig_file_path()
170 with open(cfg_file_name, 'r', encoding='utf-8') as f:
171 json_mconfig = json.load(f)
172 service_configs = json_mconfig.get('configsByKey', {})
173 service_configs.update(json_mconfig.get('configs_by_key', {}))
174 if service_name not in service_configs:
175 raise LoadConfigError(
176 "Service ({}) missing in mconfig".format(service_name),
177 )
178
179 return service_configs[service_name]
180
181 def load_mconfig_metadata(self) -> GatewayConfigsMetadata:
182 mconfig = self.load_mconfig()
183 return mconfig.metadata
184
185 def deserialize_mconfig(
186 self, serialized_value: str,
187 allow_unknown_fields: bool = True,
188 ) -> GatewayConfigs:
189 # First parse as JSON in case there are types unrecognized by
190 # protobuf symbol database
191 json_mconfig = json.loads(serialized_value)
192 cfgs_by_key_json = json_mconfig.get('configs_by_key', {})
193 cfgs_by_key_json.update(json_mconfig.get('configsByKey', {}))
194 filtered_cfgs_by_key = filter_configs_by_key(cfgs_by_key_json)
195
196 # Set configs to filtered map, re-dump and parse
197 if 'configs_by_key' in json_mconfig:
198 json_mconfig.pop('configs_by_key')
199 json_mconfig['configsByKey'] = filtered_cfgs_by_key
200 json_mconfig_dumped = json.dumps(json_mconfig)
201
202 # Workaround for outdated protobuf library on sandcastle
203 if allow_unknown_fields:
204 return json_format.Parse(
205 json_mconfig_dumped,
206 GatewayConfigs(),
207 ignore_unknown_fields=True,
208 )
209 else:
210 return json_format.Parse(json_mconfig_dumped, GatewayConfigs())
211
212 def delete_stored_mconfig(self):
213 with contextlib.suppress(FileNotFoundError):
214 os.remove(self.MCONFIG_PATH)
215 magma_configuration_events.deleted_stored_mconfig()
216
217 def update_stored_mconfig(self, updated_value: str) -> GatewayConfigs:
218 parsed = json.loads(updated_value)
219 serialization_utils.write_to_file_atomically(
220 self.MCONFIG_PATH, json.dumps(parsed, indent=4, sort_keys=True),
221 )
222 magma_configuration_events.updated_stored_mconfig()
223
224 def _get_mconfig_file_path(self):
225 if os.path.isfile(self.MCONFIG_PATH):
226 return self.MCONFIG_PATH
227 else:
228 return os.path.join(DEFAULT_MCONFIG_DIR, self.MCONFIG_FILE_NAME)