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