diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 69dc7972..8fecbd54 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -29,6 +29,7 @@ Changed - An inactive and enabled EVC will be redeploy if an attribute from ``attributes_requiring_redeploy`` is updated. - If a KytosEvent can't be put on ``buffers.app`` during ``setup()``, it'll make the NApp to fail to start - Disjointedness algorithm now takes into account switches, excepting the UNIs switches. Unwanted switches have the same value as the unwanted links. +- Archived EVCs are not longer kept in memory. They can only be found in the database. Deprecated ========== diff --git a/main.py b/main.py index 072c3d68..34b5ce27 100755 --- a/main.py +++ b/main.py @@ -73,11 +73,17 @@ def setup(self): self.load_all_evcs() self._topology_updated_at = None - def get_evcs_by_svc_level(self) -> list: + def get_evcs_by_svc_level(self, enable_filter: bool = True) -> list: """Get circuits sorted by desc service level and asc creation_time. In the future, as more ops are offloaded it should be get from the DB. """ + if enable_filter: + return sorted( + [circuit for circuit in self.circuits.values() + if circuit.is_enabled()], + key=lambda x: (-x.service_level, x.creation_time), + ) return sorted(self.circuits.values(), key=lambda x: (-x.service_level, x.creation_time)) @@ -116,7 +122,7 @@ def execute_consistency(self): """Execute consistency routine.""" circuits_to_check = [] stored_circuits = self.mongo_controller.get_circuits()['circuits'] - for circuit in self.get_evcs_by_svc_level(): + for circuit in self.get_evcs_by_svc_level(enable_filter=False): stored_circuits.pop(circuit.id, None) if self.should_be_checked(circuit): circuits_to_check.append(circuit) @@ -364,11 +370,6 @@ def update(self, request: Request) -> JSONResponse: log.debug("update result %s %s", result, 404) raise HTTPException(404, detail=result) from KeyError - if evc.archived: - result = "Can't update archived EVC" - log.debug("update result %s %s", result, 409) - raise HTTPException(409, detail=result) - try: updated_data = self._evc_dict_with_instances(data) self._check_no_tag_duplication( @@ -423,17 +424,12 @@ def delete_circuit(self, request: Request) -> JSONResponse: circuit_id = request.path_params["circuit_id"] log.debug("delete_circuit /v2/evc/%s", circuit_id) try: - evc = self.circuits[circuit_id] + evc = self.circuits.pop(circuit_id) except KeyError: result = f"circuit_id {circuit_id} not found" log.debug("delete_circuit result %s %s", result, 404) raise HTTPException(404, detail=result) from KeyError - if evc.archived: - result = f"Circuit {circuit_id} already removed" - log.debug("delete_circuit result %s %s", result, 404) - raise HTTPException(404, detail=result) - log.info("Removing %s", evc) with evc.lock: evc.remove_current_flows() @@ -604,11 +600,6 @@ def create_schedule(self, request: Request) -> JSONResponse: result = f"circuit_id {circuit_id} not found" log.debug("create_schedule result %s %s", result, 404) raise HTTPException(404, detail=result) - # Can not modify circuits deleted and archived - if evc.archived: - result = f"Circuit {circuit_id} is archived. Update is forbidden." - log.debug("create_schedule result %s %s", result, 409) - raise HTTPException(409, detail=result) # new schedule from dict new_schedule = CircuitSchedule.from_dict(schedule_data) @@ -659,10 +650,6 @@ def update_schedule(self, request: Request) -> JSONResponse: result = f"schedule_id {schedule_id} not found" log.debug("update_schedule result %s %s", result, 404) raise HTTPException(404, detail=result) - if evc.archived: - result = f"Circuit {evc.id} is archived. Update is forbidden." - log.debug("update_schedule result %s %s", result, 409) - raise HTTPException(409, detail=result) new_schedule = CircuitSchedule.from_dict(data) new_schedule.id = found_schedule.id @@ -702,11 +689,6 @@ def delete_schedule(self, request: Request) -> JSONResponse: log.debug("delete_schedule result %s %s", result, 404) raise HTTPException(404, detail=result) - if evc.archived: - result = f"Circuit {evc.id} is archived. Update is forbidden." - log.debug("delete_schedule result %s %s", result, 409) - raise HTTPException(409, detail=result) - # Remove the old schedule evc.circuit_scheduler.remove(found_schedule) @@ -808,20 +790,22 @@ def handle_interface_link_up(self, interface): Handler for interface link_up events """ for evc in self.get_evcs_by_svc_level(): - log.info("Event handle_interface_link_up %s", interface) - evc.handle_interface_link_up( - interface - ) + with evc.lock: + log.info("Event handle_interface_link_up %s", interface) + evc.handle_interface_link_up( + interface + ) def handle_interface_link_down(self, interface): """ Handler for interface link_down events """ for evc in self.get_evcs_by_svc_level(): - log.info("Event handle_interface_link_down %s", interface) - evc.handle_interface_link_down( - interface - ) + with evc.lock: + log.info("Event handle_interface_link_down %s", interface) + evc.handle_interface_link_down( + interface + ) @listen_to("kytos/topology.link_down") def on_link_down(self, event): diff --git a/models/evc.py b/models/evc.py index 303695a7..44da0f7c 100644 --- a/models/evc.py +++ b/models/evc.py @@ -1686,8 +1686,6 @@ def handle_interface_link_up(self, interface: Interface): """ Handler for interface link_up events """ - if self.archived: # TODO: Remove when addressing issue #369 - return if self.is_active(): return interfaces = (self.uni_a.interface, self.uni_z.interface) @@ -1718,8 +1716,6 @@ def handle_interface_link_down(self, interface): """ Handler for interface link_down events """ - if self.archived: - return if not self.is_active(): return interfaces = (self.uni_a.interface, self.uni_z.interface) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 0d37137a..c0b38b40 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1,5 +1,5 @@ """Module to test the main napp file.""" -from unittest.mock import (AsyncMock, MagicMock, Mock, PropertyMock, call, +from unittest.mock import (AsyncMock, MagicMock, Mock, call, create_autospec, patch) import pytest @@ -1085,23 +1085,12 @@ async def test_create_schedule_invalid_request(self, event_loop): response = await self.api_client.post(url, json={}) assert response.status_code == 400 - # case 2: content-type not specified - payload = { - "circuit_id": "bb:bb:bb", - "schedule": { - "frequency": "1 * * * *", - "action": "create" - } - } - response = await self.api_client.post(url, json=payload) - assert response.status_code == 409 - - # case 3: not a dictionary + # case 2: not a dictionary payload = [] response = await self.api_client.post(url, json=payload) assert response.status_code == 400 - # case 4: missing circuit id + # case 3: missing circuit id payload = { "schedule": { "frequency": "1 * * * *", @@ -1111,14 +1100,14 @@ async def test_create_schedule_invalid_request(self, event_loop): response = await self.api_client.post(url, json=payload) assert response.status_code == 400 - # case 5: missing schedule + # case 4: missing schedule payload = { "circuit_id": "bb:bb:bb" } response = await self.api_client.post(url, json=payload) assert response.status_code == 400 - # case 6: invalid circuit + # case 5: invalid circuit payload = { "circuit_id": "xx:xx:xx", "schedule": { @@ -1129,19 +1118,7 @@ async def test_create_schedule_invalid_request(self, event_loop): response = await self.api_client.post(url, json=payload) assert response.status_code == 404 - # case 7: archived or deleted evc - evc1.archived.return_value = True - payload = { - "circuit_id": "bb:bb:bb", - "schedule": { - "frequency": "1 * * * *", - "action": "create" - } - } - response = await self.api_client.post(url, json=payload) - assert response.status_code == 409 - - # case 8: invalid json + # case 6: invalid json response = await self.api_client.post(url, json="test") assert response.status_code == 400 @@ -1225,52 +1202,6 @@ async def test_update_no_schedule( response = await self.api_client.patch(url, json=payload) assert response.status_code == 404 - @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") - @patch("napps.kytos.mef_eline.main.Main._uni_from_dict") - @patch("napps.kytos.mef_eline.main.EVC.as_dict") - @patch("napps.kytos.mef_eline.models.evc.EVC._validate") - async def test_update_schedule_archived( - self, - validate_mock, - evc_as_dict_mock, - uni_from_dict_mock, - sched_add_mock, - event_loop - ): - """Test create a circuit schedule.""" - self.napp.controller.loop = event_loop - mongo_payload_1 = { - "circuits": { - "aa:aa:aa": { - "id": "aa:aa:aa", - "name": "my evc1", - "archived": True, - "circuit_scheduler": [ - { - "id": "1", - "frequency": "* * * * *", - "action": "create" - } - ], - } - } - } - - validate_mock.return_value = True - sched_add_mock.return_value = True - uni_from_dict_mock.side_effect = ["uni_a", "uni_z"] - evc_as_dict_mock.return_value = {} - self.napp.mongo_controller.get_circuits.return_value = mongo_payload_1 - - requested_schedule_id = "1" - url = f"{self.base_endpoint}/v2/evc/schedule/{requested_schedule_id}" - - payload = {"frequency": "*/1 * * * *", "action": "create"} - - # Call URL - response = await self.api_client.patch(url, json=payload) - assert response.status_code == 409 - @patch("apscheduler.schedulers.background.BackgroundScheduler.remove_job") @patch("napps.kytos.mef_eline.main.Main._uni_from_dict") @patch("napps.kytos.mef_eline.controllers.ELineController.upsert_evc") @@ -1327,46 +1258,6 @@ async def test_delete_schedule(self, *args): mongo_controller_upsert_mock.assert_called_once() assert "Schedule removed" in f"{response.json()}" - @patch("napps.kytos.mef_eline.main.Main._uni_from_dict") - @patch("napps.kytos.mef_eline.main.EVC.as_dict") - @patch("napps.kytos.mef_eline.models.evc.EVC._validate") - async def test_delete_schedule_archived(self, *args): - """Test create a circuit schedule.""" - ( - validate_mock, - evc_as_dict_mock, - uni_from_dict_mock, - ) = args - - mongo_payload_1 = { - "circuits": { - "2": { - "id": "2", - "name": "my evc1", - "archived": True, - "circuit_scheduler": [ - { - "id": "1", - "frequency": "* * * * *", - "action": "create" - } - ], - } - } - } - - validate_mock.return_value = True - uni_from_dict_mock.side_effect = ["uni_a", "uni_z"] - evc_as_dict_mock.return_value = {} - self.napp.mongo_controller.get_circuits.return_value = mongo_payload_1 - - requested_schedule_id = "1" - url = f"{self.base_endpoint}/v2/evc/schedule/{requested_schedule_id}" - - # Call URL - response = await self.api_client.delete(url) - assert response.status_code == 409 - @patch('napps.kytos.mef_eline.main.Main._find_evc_by_schedule_id') async def test_delete_schedule_not_found(self, mock_find_evc_by_sched): """Test delete a circuit schedule - unexisting.""" @@ -1395,6 +1286,14 @@ def test_get_evcs_by_svc_level(self) -> None: for i in range(2): assert evcs_by_level[i].creation_time == i + self.napp.circuits[1].is_enabled = lambda: False + evcs_by_level = self.napp.get_evcs_by_svc_level() + assert len(evcs_by_level) == 1 + + self.napp.circuits[1].is_enabled = lambda: False + evcs_by_level = self.napp.get_evcs_by_svc_level(enable_filter=False) + assert len(evcs_by_level) == 2 + async def test_get_circuit_not_found(self): """Test /v2/evc/ 404.""" self.napp.mongo_controller.get_circuit.return_value = None @@ -1538,18 +1437,6 @@ async def test_update_circuit( assert 200 == response.status_code evc_deploy.assert_called_once() - await self.api_client.delete( - f"{self.base_endpoint}/v2/evc/{circuit_id}" - ) - evc_deploy.reset_mock() - response = await self.api_client.patch( - f"{self.base_endpoint}/v2/evc/{circuit_id}", - json=payloads[1], - ) - evc_deploy.assert_not_called() - assert 409 == response.status_code - assert "Can't update archived EVC" in response.json()["description"] - @patch("napps.kytos.mef_eline.main.Main._check_no_tag_duplication") @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @@ -1941,6 +1828,7 @@ async def test_delete_archived_evc( json=payload1 ) assert 201 == response.status_code + assert len(self.napp.circuits) == 1 current_data = response.json() circuit_id = current_data["circuit_id"] @@ -1949,29 +1837,32 @@ async def test_delete_archived_evc( ) assert 200 == response.status_code assert mock_remove_tags.call_count == 1 + assert len(self.napp.circuits) == 0 response = await self.api_client.delete( f"{self.base_endpoint}/v2/evc/{circuit_id}" ) current_data = response.json() - expected_data = f"Circuit {circuit_id} already removed" + expected_data = f"circuit_id {circuit_id} not found" assert current_data["description"] == expected_data assert 404 == response.status_code + assert len(self.napp.circuits) == 0 def test_handle_link_up(self): """Test handle_link_up method.""" evc_mock = create_autospec(EVC) evc_mock.service_level, evc_mock.creation_time = 0, 1 - evc_mock.is_enabled = MagicMock(side_effect=[True, False, True]) + evc_mock.is_enabled = MagicMock(side_effect=[ + True, False, True, True, True + ]) evc_mock.lock = MagicMock() - type(evc_mock).archived = PropertyMock( - side_effect=[True, False, False] - ) + evc_mock.archived = False evcs = [evc_mock, evc_mock, evc_mock] event = KytosEvent(name="test", content={"link": "abc"}) self.napp.circuits = dict(zip(["1", "2", "3"], evcs)) self.napp.handle_link_up(event) - evc_mock.handle_link_up.assert_called_once_with("abc") + assert evc_mock.handle_link_up.call_count == 2 + evc_mock.handle_link_up.assert_called_with("abc") @patch("time.sleep", return_value=None) @patch("napps.kytos.mef_eline.main.settings")