diff --git a/main.py b/main.py index 47dfbaeb..fd552654 100755 --- a/main.py +++ b/main.py @@ -552,6 +552,13 @@ def delete_metadata(self, request: Request) -> JSONResponse: def redeploy(self, request: Request) -> JSONResponse: """Endpoint to force the redeployment of an EVC.""" circuit_id = request.path_params["circuit_id"] + try_avoid_same_s_vlan = request.query_params.get( + "try_avoid_same_s_vlan", "true" + ) + try_avoid_same_s_vlan = try_avoid_same_s_vlan.lower() + if try_avoid_same_s_vlan not in {"true", "false"}: + msg = "Parameter try_avoid_same_s_vlan has an invalid value." + raise HTTPException(400, detail=msg) log.debug("redeploy /v2/evc/%s/redeploy", circuit_id) try: evc = self.circuits[circuit_id] @@ -563,9 +570,12 @@ def redeploy(self, request: Request) -> JSONResponse: deployed = False if evc.is_enabled(): with evc.lock: - evc.remove_current_flows(sync=False) + path_dict = evc.remove_current_flows( + sync=False, + return_path=try_avoid_same_s_vlan == "true" + ) evc.remove_failover_flows(sync=True) - deployed = evc.deploy() + deployed = evc.deploy(path_dict) if deployed: result = {"response": f"Circuit {circuit_id} redeploy received."} status = 202 diff --git a/models/evc.py b/models/evc.py index 2f37c2a7..bf65ee54 100644 --- a/models/evc.py +++ b/models/evc.py @@ -580,7 +580,7 @@ def is_using_dynamic_path(self): return True return False - def deploy_to_backup_path(self): + def deploy_to_backup_path(self, old_path_dict: dict = None): """Deploy the backup path into the datapaths of this circuit. If the backup_path attribute is valid and up, this method will try to @@ -596,17 +596,17 @@ def deploy_to_backup_path(self): success = False if self.backup_path.status is EntityStatus.UP: - success = self.deploy_to_path(self.backup_path) + success = self.deploy_to_path(self.backup_path, old_path_dict) if success: return True if self.dynamic_backup_path or self.is_intra_switch(): - return self.deploy_to_path() + return self.deploy_to_path(old_path_dict=old_path_dict) return False - def deploy_to_primary_path(self): + def deploy_to_primary_path(self, old_path_dict: dict = None): """Deploy the primary path into the datapaths of this circuit. If the primary_path attribute is valid and up, this method will try to @@ -618,10 +618,10 @@ def deploy_to_primary_path(self): return True if self.primary_path.status is EntityStatus.UP: - return self.deploy_to_path(self.primary_path) + return self.deploy_to_path(self.primary_path, old_path_dict) return False - def deploy(self): + def deploy(self, old_path_dict: dict = None): """Deploy EVC to best path. Best path can be the primary path, if available. If not, the backup @@ -630,9 +630,9 @@ def deploy(self): if self.archived: return False self.enable() - success = self.deploy_to_primary_path() + success = self.deploy_to_primary_path(old_path_dict) if not success: - success = self.deploy_to_backup_path() + success = self.deploy_to_backup_path(old_path_dict) if success: emit_event(self._controller, "deployed", @@ -708,13 +708,25 @@ def remove_failover_flows(self, exclude_uni_switches=True, if sync: self.sync() - def remove_current_flows(self, force=True, sync=True): + def remove_current_flows( + self, + force=True, + sync=True, + return_path=False + ) -> dict[str, int]: """Remove all flows from current path or path intended for current path if exists.""" - switches = set() + switches, old_path_dict = set(), {} if not self.current_path and not self.is_intra_switch(): - return + return {} + + if return_path: + for link in self.current_path: + s_vlan = link.metadata.get("s_vlan") + if s_vlan: + old_path_dict[link.id] = s_vlan.value + current_path = self.current_path for link in current_path: switches.add(link.endpoint_a.switch.id) @@ -739,10 +751,12 @@ def remove_current_flows(self, force=True, sync=True): current_path.make_vlans_available(self._controller) except KytosTagError as err: log.error(f"Error removing {self} current_path: {err}") + return old_path_dict self.current_path = Path([]) self.deactivate() if sync: self.sync() + return old_path_dict def remove_path_flows( self, path=None, force=True @@ -887,7 +901,7 @@ def _try_to_activate_inter_evc(self) -> bool: return True # pylint: disable=too-many-branches, too-many-statements - def deploy_to_path(self, path=None): + def deploy_to_path(self, path=None, old_path_dict: dict = None): """Install the flows for this circuit. Procedures to deploy: @@ -904,10 +918,12 @@ def deploy_to_path(self, path=None): """ self.remove_current_flows(sync=False) use_path = path or Path([]) + if not old_path_dict: + old_path_dict = {} tag_errors = [] if self.should_deploy(use_path): try: - use_path.choose_vlans(self._controller) + use_path.choose_vlans(self._controller, old_path_dict) except KytosNoTagAvailableError as e: tag_errors.append(str(e)) use_path = None @@ -916,7 +932,7 @@ def deploy_to_path(self, path=None): if use_path is None: continue try: - use_path.choose_vlans(self._controller) + use_path.choose_vlans(self._controller, old_path_dict) break except KytosNoTagAvailableError as e: tag_errors.append(str(e)) diff --git a/models/path.py b/models/path.py index d7580fa4..f03770cf 100644 --- a/models/path.py +++ b/models/path.py @@ -36,10 +36,14 @@ def link_affected_by_interface(self, interface=None): return link return None - def choose_vlans(self, controller): + def choose_vlans(self, controller, old_path_dict: dict = None): """Choose the VLANs to be used for the circuit.""" + old_path_dict = old_path_dict if old_path_dict else {} for link in self: - tag_value = link.get_next_available_tag(controller, link.id) + tag_value = link.get_next_available_tag( + controller, link.id, + try_avoid_value=old_path_dict.get(link.id) + ) tag = TAG('vlan', tag_value) link.add_metadata("s_vlan", tag) diff --git a/openapi.yml b/openapi.yml index af7949fd..2d18c425 100644 --- a/openapi.yml +++ b/openapi.yml @@ -142,6 +142,12 @@ paths: required: true schema: type: string + - name: try_avoid_same_s_vlan + description: Avoid tags from currently deployed current_path. + in: query + schema: + type: boolean + required: false responses: '202': description: Accepted diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index 23760d84..cbbc6dd0 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -694,7 +694,7 @@ def test_deploy_to_backup_path1( deployed = evc.deploy_to_backup_path() - deploy_to_path_mocked.assert_called_once_with() + deploy_to_path_mocked.assert_called_once_with(old_path_dict=None) assert deployed is True @patch("httpx.post") @@ -838,6 +838,20 @@ def test_remove_current_flows(self, *args): switch_a = Switch("00:00:00:00:00:01") switch_b = Switch("00:00:00:00:00:02") switch_c = Switch("00:00:00:00:00:03") + link_a_b = get_link_mocked( + switch_a=switch_a, + switch_b=switch_b, + endpoint_a_port=9, + endpoint_b_port=10, + metadata={"s_vlan": Mock(value=5)}, + ) + link_b_c = get_link_mocked( + switch_a=switch_b, + switch_b=switch_c, + endpoint_a_port=11, + endpoint_b_port=12, + metadata={"s_vlan": Mock(value=6)}, + ) attributes = { "controller": get_controller_mock(), @@ -846,29 +860,20 @@ def test_remove_current_flows(self, *args): "uni_z": uni_z, "active": True, "enabled": True, - "primary_links": [ - get_link_mocked( - switch_a=switch_a, - switch_b=switch_b, - endpoint_a_port=9, - endpoint_b_port=10, - metadata={"s_vlan": 5}, - ), - get_link_mocked( - switch_a=switch_b, - switch_b=switch_c, - endpoint_a_port=11, - endpoint_b_port=12, - metadata={"s_vlan": 6}, - ), - ], + "primary_links": [link_a_b, link_b_c] + + } + + expected_old_path = { + link_a_b.id: 5, + link_b_c.id: 6 } evc = EVC(**attributes) evc.current_path = evc.primary_links - evc.remove_current_flows() - + old_path = evc.remove_current_flows(return_path=True) + assert old_path == expected_old_path assert send_flow_mods_mocked.call_count == 1 assert evc.is_active() is False flows = [ diff --git a/tests/unit/models/test_link_protection.py b/tests/unit/models/test_link_protection.py index 2d919bbe..b82b2382 100644 --- a/tests/unit/models/test_link_protection.py +++ b/tests/unit/models/test_link_protection.py @@ -449,7 +449,7 @@ async def test_handle_link_up_case_2( current_handle_link_up = evc.handle_link_up(primary_path[0]) assert deploy_mocked.call_count == 0 assert deploy_to_path_mocked.call_count == 1 - deploy_to_path_mocked.assert_called_once_with(evc.primary_path) + deploy_to_path_mocked.assert_called_once_with(evc.primary_path, None) assert current_handle_link_up @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.deploy") @@ -512,7 +512,7 @@ async def test_handle_link_up_case_3( assert deploy_mocked.call_count == 0 assert deploy_to_path_mocked.call_count == 1 - deploy_to_path_mocked.assert_called_once_with(evc.backup_path) + deploy_to_path_mocked.assert_called_once_with(evc.backup_path, None) assert current_handle_link_up @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.deploy_to_path") @@ -574,7 +574,7 @@ async def test_handle_link_up_case_4(self, *args): current_handle_link_up = evc.handle_link_up(backup_path[0]) assert deploy_to_path_mocked.call_count == 1 - deploy_to_path_mocked.assert_called_once_with() + deploy_to_path_mocked.assert_called_once_with(old_path_dict=None) assert current_handle_link_up async def test_handle_link_up_case_5(self): diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 4adbc10a..3e2150da 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -828,9 +828,25 @@ async def test_redeploy_evc(self): url = f"{self.base_endpoint}/v2/evc/1/redeploy" response = await self.api_client.patch(url) evc1.remove_failover_flows.assert_called() - evc1.remove_current_flows.assert_called() + evc1.remove_current_flows.assert_called_with( + sync=False, return_path=True + ) assert response.status_code == 202, response.data + url = f"{self.base_endpoint}/v2/evc/1/redeploy" + url = url + "?try_avoid_same_s_vlan=false" + response = await self.api_client.patch(url) + evc1.remove_current_flows.assert_called_with( + sync=False, return_path=False + ) + + url = f"{self.base_endpoint}/v2/evc/1/redeploy" + url = url + "?try_avoid_same_s_vlan=True" + response = await self.api_client.patch(url) + evc1.remove_current_flows.assert_called_with( + sync=False, return_path=True + ) + async def test_redeploy_evc_disabled(self): """Test endpoint to redeploy an EVC.""" evc1 = MagicMock()