d737680ef226f264d9c76c7893ec3624e210ce33
[ric-plt/a1.git] / a1 / data.py
1 """
2 Represents A1s database and database access functions.
3 In the future, this may change to use a different backend, possibly dramatically.
4 Hopefully, the access functions are a good api so nothing else has to change when this happens
5
6 For now, the database is in memory.
7 We use dict data structures (KV) with the expectation of having to move this into Redis
8 """
9 # ==================================================================================
10 #       Copyright (c) 2019-2020 Nokia
11 #       Copyright (c) 2018-2020 AT&T Intellectual Property.
12 #
13 #   Licensed under the Apache License, Version 2.0 (the "License");
14 #   you may not use this file except in compliance with the License.
15 #   You may obtain a copy of the License at
16 #
17 #          http://www.apache.org/licenses/LICENSE-2.0
18 #
19 #   Unless required by applicable law or agreed to in writing, software
20 #   distributed under the License is distributed on an "AS IS" BASIS,
21 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 #   See the License for the specific language governing permissions and
23 #   limitations under the License.
24 # ==================================================================================
25 import os
26 import time
27 from threading import Thread
28 import msgpack
29 from mdclogpy import Logger
30
31 from ricsdl.syncstorage import SyncStorage
32
33 from a1.exceptions import PolicyTypeNotFound, PolicyInstanceNotFound, PolicyTypeAlreadyExists, CantDeleteNonEmptyType
34
35 mdc_logger = Logger(name=__name__)
36
37
38 INSTANCE_DELETE_NO_RESP_TTL = int(os.environ.get("INSTANCE_DELETE_NO_RESP_TTL", 5))
39 INSTANCE_DELETE_RESP_TTL = int(os.environ.get("INSTANCE_DELETE_RESP_TTL", 5))
40
41 A1NS = "A1m_ns"
42
43
44 class SDLWrapper:
45     """
46     This is a wrapper around the expected SDL Python interface.
47     The usage of POLICY_DATA will be replaced with  SDL when SDL for python is available.
48     The eventual SDL API is expected to be very close to what is here.
49
50     We use msgpack for binary (de)serialization: https://msgpack.org/index.html
51     """
52
53     def __init__(self):
54         self.sdl = SyncStorage()
55
56     def set(self, key, value):
57         """set a key"""
58         self.sdl.set(A1NS, {key: msgpack.packb(value, use_bin_type=True)})
59
60     def get(self, key):
61         """get a key"""
62         ret_dict = self.sdl.get(A1NS, {key})
63         if key in ret_dict:
64             return msgpack.unpackb(ret_dict[key], raw=False)
65
66         return None
67
68     def find_and_get(self, prefix):
69         """get all k v pairs that start with prefix"""
70         # note: SDL "*" usage is inconsistent with real python regex, where it would be ".*"
71         ret_dict = self.sdl.find_and_get(A1NS, "{0}*".format(prefix))
72         return {k: msgpack.unpackb(v, raw=False) for k, v in ret_dict.items()}
73
74     def delete(self, key):
75         """ delete a key"""
76         self.sdl.remove(A1NS, {key})
77
78
79 SDL = SDLWrapper()
80
81 TYPE_PREFIX = "a1.policy_type."
82 INSTANCE_PREFIX = "a1.policy_instance."
83 METADATA_PREFIX = "a1.policy_inst_metadata."
84 HANDLER_PREFIX = "a1.policy_handler."
85
86
87 # Internal helpers
88
89
90 def _generate_type_key(policy_type_id):
91     """
92     generate a key for a policy type
93     """
94     return "{0}{1}".format(TYPE_PREFIX, policy_type_id)
95
96
97 def _generate_instance_key(policy_type_id, policy_instance_id):
98     """
99     generate a key for a policy instance
100     """
101     return "{0}{1}.{2}".format(INSTANCE_PREFIX, policy_type_id, policy_instance_id)
102
103
104 def _generate_instance_metadata_key(policy_type_id, policy_instance_id):
105     """
106     generate a key for a policy instance metadata
107     """
108     return "{0}{1}.{2}".format(METADATA_PREFIX, policy_type_id, policy_instance_id)
109
110
111 def _generate_handler_prefix(policy_type_id, policy_instance_id):
112     """
113     generate the prefix to a handler key
114     """
115     return "{0}{1}.{2}.".format(HANDLER_PREFIX, policy_type_id, policy_instance_id)
116
117
118 def _generate_handler_key(policy_type_id, policy_instance_id, handler_id):
119     """
120     generate a key for a policy handler
121     """
122     return "{0}{1}".format(_generate_handler_prefix(policy_type_id, policy_instance_id), handler_id)
123
124
125 def _type_is_valid(policy_type_id):
126     """
127     check that a type is valid
128     """
129     if SDL.get(_generate_type_key(policy_type_id)) is None:
130         raise PolicyTypeNotFound()
131
132
133 def _instance_is_valid(policy_type_id, policy_instance_id):
134     """
135     check that an instance is valid
136     """
137     _type_is_valid(policy_type_id)
138     if SDL.get(_generate_instance_key(policy_type_id, policy_instance_id)) is None:
139         raise PolicyInstanceNotFound
140
141
142 def _get_statuses(policy_type_id, policy_instance_id):
143     """
144     shared helper to get statuses for an instance
145     """
146     _instance_is_valid(policy_type_id, policy_instance_id)
147     prefixes_for_handler = "{0}{1}.{2}.".format(HANDLER_PREFIX, policy_type_id, policy_instance_id)
148     return list(SDL.find_and_get(prefixes_for_handler).values())
149
150
151 def _get_instance_list(policy_type_id):
152     """
153     shared helper to get instance list for a type
154     """
155     _type_is_valid(policy_type_id)
156     prefixes_for_type = "{0}{1}.".format(INSTANCE_PREFIX, policy_type_id)
157     instancekeys = SDL.find_and_get(prefixes_for_type).keys()
158     return [k.split(prefixes_for_type)[1] for k in instancekeys]
159
160
161 def _clear_handlers(policy_type_id, policy_instance_id):
162     """
163     delete all the handlers for a policy instance
164     """
165     all_handlers_pref = _generate_handler_prefix(policy_type_id, policy_instance_id)
166     keys = SDL.find_and_get(all_handlers_pref)
167     for k in keys:
168         SDL.delete(k)
169
170
171 def _get_metadata(policy_type_id, policy_instance_id):
172     """
173     get instance metadata
174     """
175     _instance_is_valid(policy_type_id, policy_instance_id)
176     metadata_key = _generate_instance_metadata_key(policy_type_id, policy_instance_id)
177     return SDL.get(metadata_key)
178
179
180 def _delete_after(policy_type_id, policy_instance_id, ttl):
181     """
182     this is a blocking function, must call this in a thread to not block!
183     waits ttl seconds, then deletes the instance
184     """
185     _instance_is_valid(policy_type_id, policy_instance_id)
186
187     time.sleep(ttl)
188
189     # ready to delete
190     _clear_handlers(policy_type_id, policy_instance_id)  # delete all the handlers
191     SDL.delete(_generate_instance_key(policy_type_id, policy_instance_id))  # delete instance
192     SDL.delete(_generate_instance_metadata_key(policy_type_id, policy_instance_id))  # delete instance metadata
193     mdc_logger.debug("type {0} instance {1} deleted".format(policy_type_id, policy_instance_id))
194
195
196 # Types
197
198
199 def get_type_list():
200     """
201     retrieve all type ids
202     """
203     typekeys = SDL.find_and_get(TYPE_PREFIX).keys()
204     # policy types are ints but they get butchered to strings in the KV
205     return [int(k.split(TYPE_PREFIX)[1]) for k in typekeys]
206
207
208 def store_policy_type(policy_type_id, body):
209     """
210     store a policy type if it doesn't already exist
211     """
212     key = _generate_type_key(policy_type_id)
213     if SDL.get(key) is not None:
214         raise PolicyTypeAlreadyExists()
215     SDL.set(key, body)
216
217
218 def delete_policy_type(policy_type_id):
219     """
220     delete a policy type; can only be done if there are no instances (business logic)
221     """
222     pil = get_instance_list(policy_type_id)
223     if pil == []:  # empty, can delete
224         SDL.delete(_generate_type_key(policy_type_id))
225     else:
226         raise CantDeleteNonEmptyType()
227
228
229 def get_policy_type(policy_type_id):
230     """
231     retrieve a type
232     """
233     _type_is_valid(policy_type_id)
234     return SDL.get(_generate_type_key(policy_type_id))
235
236
237 # Instances
238
239
240 def store_policy_instance(policy_type_id, policy_instance_id, instance):
241     """
242     Store a policy instance
243     """
244     _type_is_valid(policy_type_id)
245     creation_timestamp = time.time()
246
247     # store the instance
248     key = _generate_instance_key(policy_type_id, policy_instance_id)
249     if SDL.get(key) is not None:
250         # Reset the statuses because this is a new policy instance, even if it was overwritten
251         _clear_handlers(policy_type_id, policy_instance_id)  # delete all the handlers
252     SDL.set(key, instance)
253
254     metadata_key = _generate_instance_metadata_key(policy_type_id, policy_instance_id)
255     SDL.set(metadata_key, {"created_at": creation_timestamp, "has_been_deleted": False})
256
257
258 def get_policy_instance(policy_type_id, policy_instance_id):
259     """
260     Retrieve a policy instance
261     """
262     _instance_is_valid(policy_type_id, policy_instance_id)
263     return SDL.get(_generate_instance_key(policy_type_id, policy_instance_id))
264
265
266 def get_instance_list(policy_type_id):
267     """
268     retrieve all instance ids for a type
269     """
270     return _get_instance_list(policy_type_id)
271
272
273 def delete_policy_instance(policy_type_id, policy_instance_id):
274     """
275     initially sets has_been_deleted
276     then launches a thread that waits until the relevent timer expires, and finally deletes the instance
277     """
278     _instance_is_valid(policy_type_id, policy_instance_id)
279
280     # set the metadata first
281     deleted_timestamp = time.time()
282     metadata_key = _generate_instance_metadata_key(policy_type_id, policy_instance_id)
283     existing_metadata = _get_metadata(policy_type_id, policy_instance_id)
284     SDL.set(
285         metadata_key,
286         {"created_at": existing_metadata["created_at"], "has_been_deleted": True, "deleted_at": deleted_timestamp},
287     )
288
289     # wait, then delete
290     vector = _get_statuses(policy_type_id, policy_instance_id)
291     if vector == []:
292         # handler is empty; we wait for t1 to expire then goodnight
293         clos = lambda: _delete_after(policy_type_id, policy_instance_id, INSTANCE_DELETE_NO_RESP_TTL)
294     else:
295         # handler is not empty, we wait max t1,t2 to expire then goodnight
296         clos = lambda: _delete_after(
297             policy_type_id, policy_instance_id, max(INSTANCE_DELETE_RESP_TTL, INSTANCE_DELETE_NO_RESP_TTL)
298         )
299     Thread(target=clos).start()
300
301
302 # Statuses
303
304
305 def set_policy_instance_status(policy_type_id, policy_instance_id, handler_id, status):
306     """
307     update the database status for a handler
308     called from a1's rmr thread
309     """
310     _type_is_valid(policy_type_id)
311     _instance_is_valid(policy_type_id, policy_instance_id)
312     SDL.set(_generate_handler_key(policy_type_id, policy_instance_id, handler_id), status)
313
314
315 def get_policy_instance_status(policy_type_id, policy_instance_id):
316     """
317     Gets the status of an instance
318     """
319     _instance_is_valid(policy_type_id, policy_instance_id)
320     metadata = _get_metadata(policy_type_id, policy_instance_id)
321     metadata["instance_status"] = "NOT IN EFFECT"
322     for i in _get_statuses(policy_type_id, policy_instance_id):
323         if i == "OK":
324             metadata["instance_status"] = "IN EFFECT"
325             break
326     return metadata