From cdf63d280c2877e4289882c1fe7ebde14fece6a2 Mon Sep 17 00:00:00 2001 From: Antonio Francisco Date: Tue, 13 Jul 2021 12:27:58 -0300 Subject: [PATCH] Roolback deployment when flow_manager fails. If flow_manager returns an error code, stop the deployment and uninstall already installed flows, keeping the EVC inactive. Fixes #26 --- exceptions.py | 4 ++ models.py | 35 ++++++---- tests/unit/models/test_evc_deploy.py | 83 +++++++++++++++++++++-- tests/unit/models/test_link_protection.py | 7 +- tests/unit/test_main.py | 6 +- 5 files changed, 116 insertions(+), 19 deletions(-) diff --git a/exceptions.py b/exceptions.py index cceaa651..85d292f3 100644 --- a/exceptions.py +++ b/exceptions.py @@ -11,3 +11,7 @@ class EVCException(MEFELineException): class ValidationException(EVCException): """Exception for validation errors.""" + + +class FlowModException(MEFELineException): + """Exception for FlowMod errors.""" diff --git a/models.py b/models.py index 9b746ab0..1f5f2343 100644 --- a/models.py +++ b/models.py @@ -12,6 +12,7 @@ from kytos.core.interface import UNI from kytos.core.link import Link from napps.kytos.mef_eline import settings +from napps.kytos.mef_eline.exceptions import FlowModException from napps.kytos.mef_eline.storehouse import StoreHouse from napps.kytos.mef_eline.utils import emit_event @@ -530,13 +531,15 @@ def remove(self): self.sync() emit_event(self._controller, 'undeployed', evc_id=self.id) - def remove_current_flows(self): + def remove_current_flows(self, current_path=None): """Remove all flows from current path.""" switches = set() switches.add(self.uni_a.interface.switch) switches.add(self.uni_z.interface.switch) - for link in self.current_path: + if not current_path: + current_path = self.current_path + for link in current_path: switches.add(link.endpoint_a.switch) switches.add(link.endpoint_b.switch) @@ -546,7 +549,7 @@ def remove_current_flows(self): for switch in switches: self._send_flow_mods(switch, [match], 'delete') - self.current_path.make_vlans_available() + current_path.make_vlans_available() self.current_path = Path([]) self.deactivate() self.sync() @@ -608,14 +611,20 @@ def deploy_to_path(self, path=None): else: use_path = None - if use_path: - self._install_nni_flows(use_path) - self._install_uni_flows(use_path) - elif self.uni_a.interface.switch == self.uni_z.interface.switch: - self._install_direct_uni_flows() - use_path = Path() - else: - log.warn(f"{self} was not deployed. No available path was found.") + try: + if use_path: + self._install_nni_flows(use_path) + self._install_uni_flows(use_path) + elif self.uni_a.interface.switch == self.uni_z.interface.switch: + use_path = Path() + self._install_direct_uni_flows() + else: + log.warn(f"{self} was not deployed. " + "No available path was found.") + return False + except FlowModException: + log.error(f'Error deploying EVC {self} when calling flow_manager.') + self.remove_current_flows(use_path) return False self.activate() self.current_path = use_path @@ -743,7 +752,9 @@ def _send_flow_mods(switch, flow_mods, command='flows'): endpoint = f'{settings.MANAGER_URL}/{command}/{switch.id}' data = {"flows": flow_mods} - requests.post(endpoint, json=data) + response = requests.post(endpoint, json=data) + if response.status_code >= 400: + raise FlowModException def get_cookie(self): """Return the cookie integer from evc id.""" diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index 9317f852..23c9f65e 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -1,7 +1,7 @@ """Method to thest EVCDeploy class.""" import sys from unittest import TestCase -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch from kytos.core.interface import Interface from kytos.core.switch import Switch @@ -12,6 +12,7 @@ from napps.kytos.mef_eline.models import EVC, Path # NOQA from napps.kytos.mef_eline.settings import MANAGER_URL # NOQA +from napps.kytos.mef_eline.exceptions import FlowModException # NOQA from tests.helpers import get_link_mocked,\ get_uni_mocked, get_controller_mock # NOQA @@ -92,6 +93,10 @@ def test_send_flow_mods_case1(self, requests_mock): flow_mods = {"id": 20} switch = Mock(spec=Switch, id=1) + response = MagicMock() + response.status_code = 201 + requests_mock.post.return_value = response + # pylint: disable=protected-access EVC._send_flow_mods(switch, flow_mods) @@ -106,6 +111,9 @@ def test_send_flow_mods_case2(self, requests_mock): """Test if you are sending flow_mods.""" flow_mods = {"id": 20} switch = Mock(spec=Switch, id=1) + response = MagicMock() + response.status_code = 201 + requests_mock.post.return_value = response # pylint: disable=protected-access EVC._send_flow_mods(switch, flow_mods, command='delete') @@ -369,7 +377,11 @@ def test_deploy_successfully(self, *args): # pylint: disable=too-many-locals (should_deploy_mock, activate_mock, install_uni_flows_mock, install_nni_flows, chose_vlans_mock, - log_mock, _, _) = args + log_mock, _, requests_mock) = args + + response = MagicMock() + response.status_code = 201 + requests_mock.return_value = response should_deploy_mock.return_value = True uni_a = get_uni_mocked(interface_port=2, tag_value=82, @@ -425,7 +437,11 @@ def test_deploy_fail(self, *args): # pylint: disable=too-many-locals (sync_mock, should_deploy_mock, activate_mock, install_uni_flows_mock, install_nni_flows, choose_vlans_mock, - discover_new_paths, log_mock, _) = args + discover_new_paths, log_mock, requests_mock) = args + + response = MagicMock() + response.status_code = 201 + requests_mock.return_value = response uni_a = get_uni_mocked(interface_port=2, tag_value=82, switch_id="switch_uni_a", @@ -462,6 +478,61 @@ def test_deploy_fail(self, *args): self.assertEqual(sync_mock.call_count, 1) self.assertFalse(deployed) + @patch('napps.kytos.mef_eline.models.log') + @patch('napps.kytos.mef_eline.models.EVC.discover_new_paths', + return_value=[]) + @patch('napps.kytos.mef_eline.models.Path.choose_vlans') + @patch('napps.kytos.mef_eline.models.EVC._install_nni_flows') + @patch('napps.kytos.mef_eline.models.EVC.should_deploy') + @patch('napps.kytos.mef_eline.models.EVC.remove_current_flows') + @patch('napps.kytos.mef_eline.models.EVC.sync') + def test_deploy_error(self, *args): + """Test if all methods is ignored when the should_deploy is false.""" + # pylint: disable=too-many-locals + (sync_mock, remove_current_flows, should_deploy_mock, + install_nni_flows, choose_vlans_mock, + discover_new_paths, log_mock) = args + + install_nni_flows.side_effect = FlowModException + should_deploy_mock.return_value = True + uni_a = get_uni_mocked(interface_port=2, tag_value=82, + switch_id="switch_uni_a", is_valid=True) + uni_z = get_uni_mocked(interface_port=3, tag_value=83, + switch_id="switch_uni_z", is_valid=True) + + primary_links = [ + get_link_mocked(endpoint_a_port=9, endpoint_b_port=10, + metadata={"s_vlan": 5}), + get_link_mocked(endpoint_a_port=11, endpoint_b_port=12, + metadata={"s_vlan": 6}) + ] + + attributes = { + "controller": get_controller_mock(), + "name": "custom_name", + "uni_a": uni_a, + "uni_z": uni_z, + "primary_links": primary_links, + "queue_id": 5 + } + # Setup path to deploy + path = Path() + path.append(primary_links[0]) + path.append(primary_links[1]) + + evc = EVC(**attributes) + + deployed = evc.deploy_to_path(path) + + self.assertEqual(discover_new_paths.call_count, 0) + self.assertEqual(should_deploy_mock.call_count, 1) + self.assertEqual(install_nni_flows.call_count, 1) + self.assertEqual(choose_vlans_mock.call_count, 1) + self.assertEqual(log_mock.error.call_count, 1) + self.assertEqual(sync_mock.call_count, 0) + self.assertEqual(remove_current_flows.call_count, 2) + self.assertFalse(deployed) + @patch('napps.kytos.mef_eline.models.EVC.deploy_to_path') @patch('napps.kytos.mef_eline.models.EVC.discover_new_paths') def test_deploy_to_backup_path1(self, discover_new_paths_mocked, @@ -512,7 +583,11 @@ def test_deploy_without_path_case1(self, *args): # pylint: disable=too-many-locals (discover_new_paths_mocked, should_deploy_mock, activate_mock, install_uni_flows_mock, install_nni_flows, chose_vlans_mock, - log_mock, _, _) = args + log_mock, _, requests_mock) = args + + response = MagicMock() + response.status_code = 201 + requests_mock.return_value = response should_deploy_mock.return_value = False uni_a = get_uni_mocked(interface_port=2, tag_value=82, diff --git a/tests/unit/models/test_link_protection.py b/tests/unit/models/test_link_protection.py index 2414886d..078ce87c 100644 --- a/tests/unit/models/test_link_protection.py +++ b/tests/unit/models/test_link_protection.py @@ -1,7 +1,7 @@ """Module to test the LinkProtection class.""" import sys from unittest import TestCase -from unittest.mock import patch +from unittest.mock import MagicMock, patch from unittest.mock import Mock from kytos.core.common import EntityStatus @@ -96,9 +96,12 @@ def test_deploy_to_case_1(self, log_mocked): @patch('napps.kytos.mef_eline.models.Path.status', EntityStatus.UP) def test_deploy_to_case_2(self, install_uni_flows_mocked, install_nni_flows_mocked, - deploy_mocked, *_): + deploy_mocked, _, requests_mock): """Test deploy with all links up.""" deploy_mocked.return_value = True + response = MagicMock() + response.status_code = 201 + requests_mock.return_value = response primary_path = [ get_link_mocked(status=EntityStatus.UP), diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 3be74555..7ff71373 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1161,7 +1161,11 @@ def test_delete_schedule_archived(self, *args): @patch('napps.kytos.mef_eline.main.EVC.as_dict') def test_update_circuit(self, *args): """Test update a circuit circuit.""" - (evc_as_dict_mock, uni_from_dict_mock, evc_deploy, *mocks) = args + (evc_as_dict_mock, uni_from_dict_mock, evc_deploy, + *mocks, requests_mock) = args + response = MagicMock() + response.status_code = 201 + requests_mock.return_value = response for mock in mocks: mock.return_value = True