Add sdl healthcheck to a1's healthcheck
[ric-plt/a1.git] / tests / test_controller.py
1 """
2 tests for controller
3 """
4 # ==================================================================================
5 #       Copyright (c) 2019-2020 Nokia
6 #       Copyright (c) 2018-2020 AT&T Intellectual Property.
7 #
8 #   Licensed under the Apache License, Version 2.0 (the "License");
9 #   you may not use this file except in compliance with the License.
10 #   You may obtain a copy of the License at
11 #
12 #          http://www.apache.org/licenses/LICENSE-2.0
13 #
14 #   Unless required by applicable law or agreed to in writing, software
15 #   distributed under the License is distributed on an "AS IS" BASIS,
16 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 #   See the License for the specific language governing permissions and
18 #   limitations under the License.
19 # ==================================================================================
20 import time
21 import json
22 from rmr.rmr_mocks import rmr_mocks
23 from ricsdl.syncstorage import SyncStorage
24 from ricsdl.exceptions import RejectedByBackend, NotConnected, BackendError
25 from a1 import a1rmr, data
26
27 RCV_ID = "test_receiver"
28 ADM_CRTL_TID = 6660666
29 ADM_CTRL_IID = "admission_control_policy"
30 ADM_CTRL_POLICIES = "/a1-p/policytypes/{0}/policies".format(ADM_CRTL_TID)
31 ADM_CTRL_INSTANCE = ADM_CTRL_POLICIES + "/" + ADM_CTRL_IID
32 ADM_CTRL_INSTANCE_STATUS = ADM_CTRL_INSTANCE + "/status"
33 ADM_CTRL_TYPE = "/a1-p/policytypes/{0}".format(ADM_CRTL_TID)
34 ACK_MT = 20011
35
36
37 def _fake_dequeue():
38     """for monkeypatching with a good status"""
39     pay = json.dumps(
40         {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "OK"}
41     ).encode()
42     fake_msg = {"payload": pay, "message type": ACK_MT}
43     return [(fake_msg, None)]
44
45
46 def _fake_dequeue_none():
47     """for monkeypatching with no waiting messages"""
48     return []
49
50
51 def _fake_dequeue_deleted():
52     """for monkeypatching  with a DELETED status"""
53     new_msgs = []
54     good_pay = json.dumps(
55         {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "DELETED"}
56     ).encode()
57
58     # non existent type id
59     pay = json.dumps(
60         {"policy_type_id": 911, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "DELETED"}
61     ).encode()
62     fake_msg = {"payload": pay, "message type": ACK_MT}
63     new_msgs.append((fake_msg, None))
64
65     # bad instance id
66     pay = json.dumps(
67         {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": "darkness", "handler_id": RCV_ID, "status": "DELETED"}
68     ).encode()
69     fake_msg = {"payload": pay, "message type": ACK_MT}
70     new_msgs.append((fake_msg, None))
71
72     # good body but bad message type
73     fake_msg = {"payload": good_pay, "message type": ACK_MT * 3}
74     new_msgs.append((fake_msg, None))
75
76     # insert a bad one with a malformed body to make sure we keep going
77     new_msgs.append(({"payload": "asdf", "message type": ACK_MT}, None))
78
79     # not even a json
80     new_msgs.append(("asdf", None))
81
82     # good
83     fake_msg = {"payload": good_pay, "message type": ACK_MT}
84     new_msgs.append((fake_msg, None))
85
86     return new_msgs
87
88
89 def _test_put_patch(monkeypatch):
90     rmr_mocks.patch_rmr(monkeypatch)
91     # assert that rmr bad states don't cause problems
92     monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(10))
93
94
95 def _no_ac(client):
96     # no type there yet
97     res = client.get(ADM_CTRL_TYPE)
98     assert res.status_code == 404
99
100     # no types at all
101     res = client.get("/a1-p/policytypes")
102     assert res.status_code == 200
103     assert res.json == []
104
105     # instance 404 because type not there yet
106     res = client.get(ADM_CTRL_POLICIES)
107     assert res.status_code == 404
108
109
110 def _put_ac_type(client, typedef):
111     _no_ac(client)
112
113     # put the type
114     res = client.put(ADM_CTRL_TYPE, json=typedef)
115     assert res.status_code == 201
116
117     # cant replace types
118     res = client.put(ADM_CTRL_TYPE, json=typedef)
119     assert res.status_code == 400
120
121     # type there now
122     res = client.get(ADM_CTRL_TYPE)
123     assert res.status_code == 200
124     assert res.json == typedef
125
126     # type in type list
127     res = client.get("/a1-p/policytypes")
128     assert res.status_code == 200
129     assert res.json == [ADM_CRTL_TID]
130
131     # instance 200 but empty list
132     res = client.get(ADM_CTRL_POLICIES)
133     assert res.status_code == 200
134     assert res.json == []
135
136
137 def _delete_ac_type(client):
138     res = client.delete(ADM_CTRL_TYPE)
139     assert res.status_code == 204
140
141     # cant get
142     res = client.get(ADM_CTRL_TYPE)
143     assert res.status_code == 404
144
145     # cant invoke delete on it again
146     res = client.delete(ADM_CTRL_TYPE)
147     assert res.status_code == 404
148
149     _no_ac(client)
150
151
152 def _put_ac_instance(client, monkeypatch, instancedef):
153     # no instance there yet
154     res = client.get(ADM_CTRL_INSTANCE)
155     assert res.status_code == 404
156     res = client.get(ADM_CTRL_INSTANCE_STATUS)
157     assert res.status_code == 404
158
159     # create a good instance
160     _test_put_patch(monkeypatch)
161     res = client.put(ADM_CTRL_INSTANCE, json=instancedef)
162     assert res.status_code == 202
163
164     # replace is allowed on instances
165     res = client.put(ADM_CTRL_INSTANCE, json=instancedef)
166     assert res.status_code == 202
167
168     # instance 200 and in list
169     res = client.get(ADM_CTRL_POLICIES)
170     assert res.status_code == 200
171     assert res.json == [ADM_CTRL_IID]
172
173
174 def _delete_instance(client):
175     # cant delete type until there are no instances
176     res = client.delete(ADM_CTRL_TYPE)
177     assert res.status_code == 400
178
179     # delete it
180     res = client.delete(ADM_CTRL_INSTANCE)
181     assert res.status_code == 202
182
183     # should be able to do multiple deletes until it's actually gone
184     res = client.delete(ADM_CTRL_INSTANCE)
185     assert res.status_code == 202
186
187
188 def _instance_is_gone(client, seconds_to_try=10):
189     for _ in range(seconds_to_try):
190         # idea here is that we have to wait for the seperate thread to process the event
191         try:
192             res = client.get(ADM_CTRL_INSTANCE_STATUS)
193             assert res.status_code == 404
194         except AssertionError:
195             time.sleep(1)
196
197     res = client.get(ADM_CTRL_INSTANCE_STATUS)
198     assert res.status_code == 404
199
200     # list still 200 but no instance
201     res = client.get(ADM_CTRL_POLICIES)
202     assert res.status_code == 200
203     assert res.json == []
204
205     # cant get instance
206     res = client.get(ADM_CTRL_INSTANCE)
207     assert res.status_code == 404
208
209
210 def _verify_instance_and_status(client, expected_instance, expected_status, expected_deleted, seconds_to_try=5):
211     # get the instance
212     res = client.get(ADM_CTRL_INSTANCE)
213     assert res.status_code == 200
214     assert res.json == expected_instance
215
216     for _ in range(seconds_to_try):
217         # idea here is that we have to wait for the seperate thread to process the event
218         res = client.get(ADM_CTRL_INSTANCE_STATUS)
219         assert res.status_code == 200
220         assert res.json["has_been_deleted"] == expected_deleted
221         try:
222             assert res.json["instance_status"] == expected_status
223             return
224         except AssertionError:
225             time.sleep(1)
226     assert res.json["instance_status"] == expected_status
227
228
229 # Module level Hack
230
231
232 def setup_module():
233     """module level setup"""
234
235     data.SDL.sdl = SyncStorage(fake_db_backend="dict")
236
237     def noop():
238         pass
239
240     # launch the thread with a fake init func and a patched rcv func; we will "repatch" later
241     a1rmr.start_rmr_thread(init_func_override=noop, rcv_func_override=_fake_dequeue_none)
242
243
244 # Actual Tests
245
246
247 def test_workflow(client, monkeypatch, adm_type_good, adm_instance_good):
248     """
249     test a full A1 workflow
250     """
251
252     # put type and instance
253     _put_ac_type(client, adm_type_good)
254     _put_ac_instance(client, monkeypatch, adm_instance_good)
255
256     """
257     we test the state transition diagram of all 5 states here;
258     1. not in effect, not deleted
259     2. in effect, not deleted
260     3. in effect, deleted
261     4. not in effect, deleted
262     5. gone (timeout expires)
263     """
264
265     # try a status get but we didn't get any ACKs yet to test NOT IN EFFECT
266     _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", False)
267
268     # now pretend we did get a good ACK
269     a1rmr.replace_rcv_func(_fake_dequeue)
270     _verify_instance_and_status(client, adm_instance_good, "IN EFFECT", False)
271
272     # delete the instance
273     _delete_instance(client)
274
275     # status after a delete, but there are no messages yet, should still return
276     _verify_instance_and_status(client, adm_instance_good, "IN EFFECT", True)
277
278     # now pretend we deleted successfully
279     a1rmr.replace_rcv_func(_fake_dequeue_deleted)
280
281     # status should be reflected first (before delete triggers)
282     _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", True)
283
284     # instance should be totally gone after a few seconds
285     _instance_is_gone(client)
286
287     # delete the type
288     _delete_ac_type(client)
289
290
291 def test_cleanup_via_t1(client, monkeypatch, adm_type_good, adm_instance_good):
292     """
293     create a type, create an instance, but no acks ever come in, delete instance
294     """
295     _put_ac_type(client, adm_type_good)
296
297     a1rmr.replace_rcv_func(_fake_dequeue_none)
298
299     _put_ac_instance(client, monkeypatch, adm_instance_good)
300
301     """
302     here we test the state transition diagram when it never goes into effect:
303     1. not in effect, not deleted
304     2. not in effect, deleted
305     3. gone (timeout expires)
306     """
307
308     _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", False)
309
310     # delete the instance
311     _delete_instance(client)
312
313     _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", True)
314
315     # instance should be totally gone after a few seconds
316     _instance_is_gone(client)
317
318     # delete the type
319     _delete_ac_type(client)
320
321
322 def test_bad_instances(client, monkeypatch, adm_type_good):
323     """
324     test various failure modes
325     """
326     # put the type (needed for some of the tests below)
327     rmr_mocks.patch_rmr(monkeypatch)
328     res = client.put(ADM_CTRL_TYPE, json=adm_type_good)
329     assert res.status_code == 201
330
331     # bad body
332     res = client.put(ADM_CTRL_INSTANCE, json={"not": "expected"})
333     assert res.status_code == 400
334
335     # bad media type
336     res = client.put(ADM_CTRL_INSTANCE, data="notajson")
337     assert res.status_code == 415
338
339     # delete a non existent instance
340     res = client.delete(ADM_CTRL_INSTANCE + "DARKNESS")
341     assert res.status_code == 404
342
343     # get a non existent instance
344     a1rmr.replace_rcv_func(_fake_dequeue)
345     res = client.get(ADM_CTRL_INSTANCE + "DARKNESS")
346     assert res.status_code == 404
347
348     # delete the type (as cleanup)
349     res = client.delete(ADM_CTRL_TYPE)
350     assert res.status_code == 204
351
352     # test 503 handlers
353
354     def monkey_set(key, value):
355         # set a key override function that throws sdl errors on certain keys
356         if key == "a1.policy_type.111":
357             raise RejectedByBackend()
358         if key == "a1.policy_type.112":
359             raise NotConnected()
360         if key == "a1.policy_type.113":
361             raise BackendError()
362
363     monkeypatch.setattr("a1.data.SDL.set", monkey_set)
364
365     res = client.put("/a1-p/policytypes/111", json=adm_type_good)
366     assert res.status_code == 503
367     res = client.put("/a1-p/policytypes/112", json=adm_type_good)
368     assert res.status_code == 503
369     res = client.put("/a1-p/policytypes/113", json=adm_type_good)
370     assert res.status_code == 503
371
372
373 def test_illegal_types(client, adm_type_good):
374     """
375     Test illegal types
376     """
377     res = client.put("/a1-p/policytypes/0", json=adm_type_good)
378     assert res.status_code == 400
379     res = client.put("/a1-p/policytypes/2147483648", json=adm_type_good)
380     assert res.status_code == 400
381
382
383 def test_healthcheck(client):
384     """
385     test healthcheck
386     """
387     res = client.get("/a1-p/healthcheck")
388     assert res.status_code == 200
389
390
391 def teardown_module():
392     """module teardown"""
393     a1rmr.stop_rmr_thread()