From 000ddb7a32ceaf9635534480c758a7357ffe68a2 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Fri, 20 Oct 2023 04:22:35 -0400 Subject: [PATCH 01/28] Modified get_vlan_tags_and_masks to support list of ranges --- tests/unit/test_utils.py | 24 ++++++++++++------------ utils.py | 21 +++++++++++---------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index b1b778e5..f23a13f2 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -74,30 +74,30 @@ def test_map_dl_vlan(self): "vlan_range,expected", [ ( - [101, 200], + [[101, 200]], [ - (101, 4095), - (102, 4094), - (104, 4088), - (112, 4080), - (128, 4032), - (192, 4088), - (200, 4095), + "101/4095", + "102/4094", + "104/4088", + "112/4080", + "128/4032", + "192/4088", + "200/4095", ] ), ( - [101, 90], + [[101, 90]], [] ), ( - [34, 34], - [(34, 4095)] + [[34, 34]], + ["34/4095"] ) ] ) def test_get_vlan_tags_and_masks(self, vlan_range, expected): """Test get_vlan_tags_and_masks""" - assert get_vlan_tags_and_masks(*vlan_range) == expected + assert get_vlan_tags_and_masks(vlan_range) == expected def test_check_disabled_component(self): """Test check disabled component""" diff --git a/utils.py b/utils.py index fd721243..23bf08e1 100644 --- a/utils.py +++ b/utils.py @@ -80,17 +80,18 @@ def max_power2_divisor(number: int, limit: int = 4096) -> int: return limit -def get_vlan_tags_and_masks(start: int, end: int) -> list[tuple[int, int]]: +def get_vlan_tags_and_masks(tag_ranges: list[list[int]]) -> list[str]: """Get the minimum number of vlan/mask pairs for a given range.""" - limit = end + 1 - tags_and_masks = [] - while start < limit: - divisor = max_power2_divisor(start) - while divisor > limit - start: - divisor //= 2 - tags_and_masks.append((start, 4096-divisor)) - start += divisor - return tags_and_masks + masks_list = [] + for start, end in tag_ranges: + limit = end + 1 + while start < limit: + divisor = max_power2_divisor(start) + while divisor > limit - start: + divisor //= 2 + masks_list.append(f"{start}/{4096-divisor}") + start += divisor + return masks_list def check_disabled_component(uni_a: UNI, uni_z: UNI): From 9132610fd517a7525caa3b9cf6d7388b155cdaa8 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 23 Oct 2023 10:38:16 -0400 Subject: [PATCH 02/28] Updated get_vlan_tags_and_masks and test --- tests/unit/test_utils.py | 13 +++++++++++++ utils.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index f23a13f2..1d76cb6b 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -92,6 +92,19 @@ def test_map_dl_vlan(self): ( [[34, 34]], ["34/4095"] + ), + ( + [ + [34, 34], + [128,128], + [130, 135] + ], + [ + "34/4095", + "128/4095", + "130/4094", + "132/4092" + ] ) ] ) diff --git a/utils.py b/utils.py index 23bf08e1..efe258e2 100644 --- a/utils.py +++ b/utils.py @@ -81,7 +81,7 @@ def max_power2_divisor(number: int, limit: int = 4096) -> int: def get_vlan_tags_and_masks(tag_ranges: list[list[int]]) -> list[str]: - """Get the minimum number of vlan/mask pairs for a given range.""" + """Get a list of vlan/mask pairs for a given list of ranges.""" masks_list = [] for start, end in tag_ranges: limit = end + 1 From b1f3afbff2337646b1b49ca91871df33d4147f2e Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 23 Oct 2023 12:50:16 -0400 Subject: [PATCH 03/28] Fixed lint --- tests/unit/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 1d76cb6b..d965bf15 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -96,7 +96,7 @@ def test_map_dl_vlan(self): ( [ [34, 34], - [128,128], + [128, 128], [130, 135] ], [ From 812177af6aec5362078e0f8c88534da18499d076 Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Tue, 31 Oct 2023 17:50:48 -0300 Subject: [PATCH 04/28] feat: add set_vlan only if in_vlan_a != in_vlan_z when pushing UNI flow related to issue 389 --- models/evc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/evc.py b/models/evc.py index fa2845e5..662ba649 100644 --- a/models/evc.py +++ b/models/evc.py @@ -1209,7 +1209,7 @@ def _prepare_push_flow(self, *args, queue_id=None): # if in_vlan is set, it must be included in the match flow_mod["match"]["dl_vlan"] = in_vlan - if new_c_vlan not in self.special_cases: + if new_c_vlan not in self.special_cases and in_vlan != new_c_vlan: # new_in_vlan is an integer but zero, action to set is required new_action = {"action_type": "set_vlan", "vlan_id": new_c_vlan} flow_mod["actions"].insert(0, new_action) From 110d91ea960c91a9f0a04e4380da6d1c00efa09b Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Tue, 31 Oct 2023 17:54:40 -0300 Subject: [PATCH 05/28] tests: covered with unit test accordingly --- tests/unit/models/test_evc_deploy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index f9eba74d..e61c7df7 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -232,6 +232,7 @@ def test_prepare_pop_flow(self): "in_vlan_a,in_vlan_z", [ (100, 50), + (100, 100), (100, "4096/4096"), (100, 0), (100, None), @@ -286,7 +287,7 @@ def test_prepare_push_flow(self, in_vlan_a, in_vlan_z): expected_flow_mod["priority"] = evc.get_priority(in_vlan_a) if in_vlan_a is not None: expected_flow_mod['match']['dl_vlan'] = in_vlan_a - if in_vlan_z not in evc.special_cases: + if in_vlan_z not in evc.special_cases and in_vlan_a != in_vlan_z: new_action = {"action_type": "set_vlan", "vlan_id": in_vlan_z} expected_flow_mod["actions"].insert(0, new_action) From 8b0d694aebf4d58c6f727a522076a2301e30c9f3 Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Tue, 31 Oct 2023 17:58:19 -0300 Subject: [PATCH 06/28] docs: updated CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 447b00b7..7c661dfa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,7 @@ Changed - EVCs will try to maintain their current_path on link status changes - UNIs now will use and free tags from ``Interface.available_tags``. - UNI tag_type is changed to string from 1, 2 and 3 values to ``"vlan"``, ``"vlan_qinq"`` and ``"mpls"`` respectively. +- Add ``set_vlan`` only if UNI A vlan and UNI z vlan are different. Deprecated ========== From 4325eb02a59931aee24d70b1473c0272be852b00 Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Wed, 1 Nov 2023 16:30:56 -0300 Subject: [PATCH 07/28] feat: added scripts/redeploy_evpls_same_vlans.py --- scripts/redeploy_evpls_same_vlans.py | 146 +++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 scripts/redeploy_evpls_same_vlans.py diff --git a/scripts/redeploy_evpls_same_vlans.py b/scripts/redeploy_evpls_same_vlans.py new file mode 100644 index 00000000..87e58029 --- /dev/null +++ b/scripts/redeploy_evpls_same_vlans.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import json +import sys +import logging + +import argparse +import asyncio +import httpx + +logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO +) +logger = logging.getLogger(__name__) + + +def is_symmetric_evpl(evc: dict) -> bool: + """Check whether it's a symmetric (same UNIs vlans) evpl.""" + uni_a, uni_z = evc["uni_a"], evc["uni_z"] + return ( + "tag" in uni_a + and "tag" in uni_z + and uni_a["tag"]["tag_type"] == "vlan" + and uni_z["tag"]["tag_type"] == "vlan" + and isinstance(uni_a["tag"]["value"], int) + and isinstance(uni_z["tag"]["value"], int) + and uni_a["tag"]["value"] == uni_z["tag"]["value"] + ) + + +async def redeploy(evc_id: str, base_url: str): + """Redeploy.""" + endpoint = "/mef_eline/v2/evc" + async with httpx.AsyncClient(base_url=base_url) as client: + res = await client.patch(f"{endpoint}/{evc_id}/redeploy") + logger.info(f"Redeployed evc_id {evc_id}") + assert ( + res.status_code == 202 + ), f"failed to redeploy evc_id: {evc_id} {res.status_code} {res.text}" + + +async def list_symmetric_evpls(base_url: str, included_evcs_filter: str = "") -> dict: + """List symmetric (same UNI vlan) evpls.""" + endpoint = "/mef_eline/v2/evc" + async with httpx.AsyncClient(base_url=base_url) as client: + resp = await client.get(endpoint, timeout=20) + evcs = { + evc_id: evc for evc_id, evc in resp.json().items() if is_symmetric_evpl(evc) + } + if included_evcs_filter: + included = set(included_evcs_filter.split(",")) + evcs = {evc_id: evc for evc_id, evc in evcs.items() if evc_id in included} + return evcs + + +async def update_command(args: argparse.Namespace) -> None: + """update command. + + It'll list all symmetric EVPLs (same UNIs vlans) and redeploy them + concurrently. The concurrency slot and wait time can be controlled with + batch_size and batch_sleep_secs + + If any coroutine fails its exception will be bubbled up. + """ + evcs = await list_symmetric_evpls(args.base_url, args.included_evcs_filter) + coros = [redeploy(evc_id, args.base_url) for evc_id, evc in evcs.items()] + batch_size = args.batch_size if args.batch_size > 0 else len(coros) + batch_sleep = args.batch_sleep_secs if args.batch_sleep_secs >= 0 else 0 + + logger.info( + f"It'll redeploy {len(coros)} EVPL(s) using batch_size {batch_size} " + f"and batch_sleep {batch_sleep}" + ) + + for i in range(0, len(coros), batch_size): + sliced = coros[i : i + batch_size] + if i > 0 and batch_sleep: + logger.info(f"Sleeping for {batch_sleep}...") + await asyncio.sleep(batch_sleep) + await asyncio.gather(*sliced) + + +async def list_command(args: argparse.Namespace) -> None: + """list command.""" + evcs = await list_symmetric_evpls(args.base_url, args.included_evcs_filter) + evcs = { + evc_id: { + "name": evc["name"], + "uni_a": evc["uni_a"], + "uni_z": evc["uni_z"], + } + for evc_id, evc in evcs.items() + } + print(json.dumps(evcs)) + + +async def main() -> None: + """Main function.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(title="commands", dest="command") + + update_parser = subparsers.add_parser("update", help="Update command") + update_parser.add_argument( + "--batch_sleep_secs", type=int, help="Batch sleep in seconds", default=5 + ) + update_parser.add_argument("--batch_size", type=int, help="Batch size", default=10) + update_parser.add_argument( + "--included_evcs_filter", + type=str, + help="Included filtered EVC ids separated by comma", + default="", + ) + update_parser.add_argument( + "--base_url", + type=str, + default="http://localhost:8181/api/kytos", + help="Kytos-ng API base url", + ) + + list_parser = subparsers.add_parser("list", help="List command") + list_parser.add_argument( + "--base_url", + type=str, + default="http://localhost:8181/api/kytos", + help="Kytos-ng API base url", + ) + list_parser.add_argument( + "--included_evcs_filter", + type=str, + help="Included filtered EVC ids separated by comma", + default="", + ) + args = parser.parse_args() + + try: + if args.command == "update": + await update_command(args) + elif args.command == "list": + await list_command(args) + except (httpx.HTTPError, AssertionError) as exc: + logger.exception(f"Error when running '{args.command}': {exc}") + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) From 5c417284f5aa981732a6df8ef4fa9ce427111f0f Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Wed, 1 Nov 2023 16:55:59 -0300 Subject: [PATCH 08/28] docs: updated scripts/README.md with redeploy_evpls_same_vlans.py --- scripts/README.md | 103 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/scripts/README.md b/scripts/README.md index 5dff3c91..06ba14fe 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -108,4 +108,105 @@ CMD=update_database python3 scripts/003_vlan_type_string.py ``` `update_database` changes the value of every outdated TAG from integer to their respective string value. - \ No newline at end of file +

Redeploy symmetric UNI vlans EVPLs

+ +[`redeploy_evpls_same_vlans.py`](./redeploy_evpls_same_vlans.py) is a CLI script to list and redeploy symmetric (same vlan on both UNIs) EVPLs. + +You should use this script if you want to avoid a redudant `set_vlan` instruction that used to be present in the instruction set. This script by triggering an EVC redeploy will force that all flows get pushed and overwritten again, it'll temporarily create traffic disruption. The redeploy in this case is just to force that the flows are pushed right away instead of waiting for a network convergence that might result in the flows getting pushed again. + +#### Pre-requisites + +- There's no additional dependency other than the existing core ones + +#### How to use + +This script exposes two commmands: `list` and `update`. + +- First you want to `list` to double check which symmetric EVPLs have been found. If you need to just include a subset you can use the ``--included_evcs_filter`` string passing a string of evc ids separated by comma value. + +```shell +python scripts/redeploy_evpls_same_vlans.py list --included_evcs_filter 'dc533ac942a541,eab1fedf3d654f' | jq + +{ + "dc533ac942a541": { + "name": "1046-1046", + "uni_a": { + "tag": { + "tag_type": "vlan", + "value": 1046 + }, + "interface_id": "00:00:00:00:00:00:00:01:1" + }, + "uni_z": { + "tag": { + "tag_type": "vlan", + "value": 1046 + }, + "interface_id": "00:00:00:00:00:00:00:03:1" + } + }, + "eab1fedf3d654f": { + "name": "1070-1070", + "uni_a": { + "tag": { + "tag_type": "vlan", + "value": 1070 + }, + "interface_id": "00:00:00:00:00:00:00:01:1" + }, + "uni_z": { + "tag": { + "tag_type": "vlan", + "value": 1070 + }, + "interface_id": "00:00:00:00:00:00:00:03:1" + } + } +} +``` + +- If you're OK with the EVPLs listed on `list`, then you can proceed to `update` to trigger a redeploy. You can also set ``--batch_size`` and ``--batch_sleep_secs`` to control respectively how many EVPLs will be redeployed concurrently and how long to wait after each batch is sent: + +``` +python scripts/redeploy_evpls_same_vlans.py update --batch_size 10 --batch_sleep_secs 5 --included_evcs_filter 'dc533ac942a541,eab1fedf3d654f' + +2023-11-01 16:29:45,980 - INFO - It'll redeploy 2 EVPL(s) using batch_size 10 and batch_sleep 5 +2023-11-01 16:29:46,123 - INFO - Redeployed evc_id dc533ac942a541 +2023-11-01 16:29:46,143 - INFO - Redeployed evc_id eab1fedf3d654f +``` + +- If you want to redeploy all symmetric EVPLs by redeploying 10 EVCs concurrently and waiting for 5 seconds: + +``` +python scripts/redeploy_evpls_same_vlans.py update --batch_size 10 --batch_sleep_secs 5 + + +2023-11-01 16:23:11,081 - INFO - It'll redeploy 100 EVPL(s) using batch_size 10 and batch_sleep 5 +2023-11-01 16:23:11,724 - INFO - Redeployed evc_id 0ca460bafb7442 +2023-11-01 16:23:11,725 - INFO - Redeployed evc_id 0645d179d9174f +2023-11-01 16:23:11,752 - INFO - Redeployed evc_id 0b45959b6a484b +2023-11-01 16:23:11,763 - INFO - Redeployed evc_id 0a270fd5a2ce47 +2023-11-01 16:23:11,779 - INFO - Redeployed evc_id 08a72e3c1ecb40 +2023-11-01 16:23:11,780 - INFO - Redeployed evc_id 09a5a3b14f9048 +2023-11-01 16:23:11,780 - INFO - Redeployed evc_id 0e658df33a9d46 +2023-11-01 16:23:11,783 - INFO - Redeployed evc_id 1096fff414c649 +2023-11-01 16:23:11,789 - INFO - Redeployed evc_id 0a5702d65da64c +2023-11-01 16:23:11,802 - INFO - Redeployed evc_id 07e3c962346947 +2023-11-01 16:23:11,802 - INFO - Sleeping for 5... +2023-11-01 16:23:17,498 - INFO - Redeployed evc_id 1b884a1dd8f147 +2023-11-01 16:23:17,538 - INFO - Redeployed evc_id 23270946ce1044 +2023-11-01 16:23:17,541 - INFO - Redeployed evc_id 18610fbbcfe54e +2023-11-01 16:23:17,543 - INFO - Redeployed evc_id 1a10cb2638d746 +2023-11-01 16:23:17,543 - INFO - Redeployed evc_id 25f2269466cc42 +2023-11-01 16:23:17,544 - INFO - Redeployed evc_id 2c332447842b42 +2023-11-01 16:23:17,546 - INFO - Redeployed evc_id 2ddf3e33b5fd4b +2023-11-01 16:23:17,547 - INFO - Redeployed evc_id 168346ab0be845 +2023-11-01 16:23:17,554 - INFO - Redeployed evc_id 21aff155f11e49 +2023-11-01 16:23:17,555 - INFO - Redeployed evc_id 215aeb07f34543 +2023-11-01 16:23:17,555 - INFO - Sleeping for 5... + +``` +
+ + + From 35bc81d258af5cd63b4101b792dc58506d269b3d Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Wed, 1 Nov 2023 17:03:47 -0300 Subject: [PATCH 09/28] docs: updated CHANGELOG.rst --- CHANGELOG.rst | 1 + scripts/README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7c661dfa..679cfde8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,7 @@ Deprecated General Information =================== - ``scripts/vlan_type_string.py`` can be used to update the collection ``evcs`` by changing ``tag_type`` from integer to string. +- ``scripts/redeploy_evpls_same_vlans.py`` can be used to redeploy symmetric (same UNI vlans) EVPLs in batch. Fixed ===== diff --git a/scripts/README.md b/scripts/README.md index 06ba14fe..46a6a856 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -175,7 +175,7 @@ python scripts/redeploy_evpls_same_vlans.py update --batch_size 10 --batch_sleep 2023-11-01 16:29:46,143 - INFO - Redeployed evc_id eab1fedf3d654f ``` -- If you want to redeploy all symmetric EVPLs by redeploying 10 EVCs concurrently and waiting for 5 seconds: +- If you want to redeploy all symmetric EVPLs batching 10 EVCs concurrently and waiting for 5 seconds per batch: ``` python scripts/redeploy_evpls_same_vlans.py update --batch_size 10 --batch_sleep_secs 5 From f150a7914bb22cbb50da15954cacb599f049aa11 Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Wed, 1 Nov 2023 17:09:42 -0300 Subject: [PATCH 10/28] docs: fixed typos --- scripts/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index 46a6a856..89e3a0e1 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -112,7 +112,7 @@ CMD=update_database python3 scripts/003_vlan_type_string.py [`redeploy_evpls_same_vlans.py`](./redeploy_evpls_same_vlans.py) is a CLI script to list and redeploy symmetric (same vlan on both UNIs) EVPLs. -You should use this script if you want to avoid a redudant `set_vlan` instruction that used to be present in the instruction set. This script by triggering an EVC redeploy will force that all flows get pushed and overwritten again, it'll temporarily create traffic disruption. The redeploy in this case is just to force that the flows are pushed right away instead of waiting for a network convergence that might result in the flows getting pushed again. +You should use this script if you want to avoid a redundant `set_vlan` instruction that used to be present in the instruction set. This script by triggering an EVC redeploy will force that all flows get pushed and overwritten again, it'll temporarily create traffic disruption. The redeploy in this case is just to force that the flows are pushed right away instead of waiting for a network convergence that might result in the flows getting pushed again. #### Pre-requisites @@ -120,7 +120,7 @@ You should use this script if you want to avoid a redudant `set_vlan` instructio #### How to use -This script exposes two commmands: `list` and `update`. +This script exposes two commands: `list` and `update`. - First you want to `list` to double check which symmetric EVPLs have been found. If you need to just include a subset you can use the ``--included_evcs_filter`` string passing a string of evc ids separated by comma value. From 4f04069b4fa181c0bd230f59eb33532f084dfbea Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Fri, 3 Nov 2023 14:15:45 -0300 Subject: [PATCH 11/28] docs: augmented scripts/README.md --- scripts/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/README.md b/scripts/README.md index 89e3a0e1..78a8fd5e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -116,6 +116,7 @@ You should use this script if you want to avoid a redundant `set_vlan` instructi #### Pre-requisites +- `kytosd` must be running - There's no additional dependency other than the existing core ones #### How to use From 6d53522397cbb154533ed6fb36b8d361517293c4 Mon Sep 17 00:00:00 2001 From: Vinicius Arcanjo Date: Fri, 3 Nov 2023 14:37:18 -0300 Subject: [PATCH 12/28] docs: added extra phrase on scripts/README.md --- scripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/README.md b/scripts/README.md index 78a8fd5e..98406ce9 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -112,7 +112,7 @@ CMD=update_database python3 scripts/003_vlan_type_string.py [`redeploy_evpls_same_vlans.py`](./redeploy_evpls_same_vlans.py) is a CLI script to list and redeploy symmetric (same vlan on both UNIs) EVPLs. -You should use this script if you want to avoid a redundant `set_vlan` instruction that used to be present in the instruction set. This script by triggering an EVC redeploy will force that all flows get pushed and overwritten again, it'll temporarily create traffic disruption. The redeploy in this case is just to force that the flows are pushed right away instead of waiting for a network convergence that might result in the flows getting pushed again. +You should use this script if you want to avoid a redundant `set_vlan` instruction that used to be present in the instruction set and if you are upgrading from `2023.1.0`. This script by triggering an EVC redeploy will force that all flows get pushed and overwritten again, it'll temporarily create traffic disruption. The redeploy in this case is just to force that the flows are pushed right away instead of waiting for a network convergence that might result in the flows getting pushed again. #### Pre-requisites From f88fb447787fd59d14b79224e5450f8390c900b5 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 6 Nov 2023 18:03:31 -0500 Subject: [PATCH 13/28] Added vlan_range support --- db/models.py | 4 +- main.py | 50 +++--- models/evc.py | 329 ++++++++++++++++++++++++++++----------- models/path.py | 18 ++- openapi.yml | 10 +- requirements/dev.in | 2 +- tests/unit/test_main.py | 27 +++- tests/unit/test_utils.py | 27 ++-- utils.py | 49 +++++- 9 files changed, 363 insertions(+), 153 deletions(-) diff --git a/db/models.py b/db/models.py index a104b712..346e36fe 100644 --- a/db/models.py +++ b/db/models.py @@ -38,11 +38,13 @@ class CircuitScheduleDoc(BaseModel): class TAGDoc(BaseModel): """TAG model""" tag_type: str - value: Union[int, str] + value: Union[int, str, list] @validator('value') def validate_value(cls, value): """Validate value when is a string""" + if isinstance(value, list): + return value if isinstance(value, int): return value if isinstance(value, str) and value in ("any", "untagged"): diff --git a/main.py b/main.py index 4f6dbb8f..1e46086b 100644 --- a/main.py +++ b/main.py @@ -12,19 +12,22 @@ from kytos.core import KytosNApp, log, rest from kytos.core.events import KytosEvent +from kytos.core.exceptions import KytosInvalidRanges, KytosTagsAreNotAvailable from kytos.core.helpers import (alisten_to, listen_to, load_spec, validate_openapi) -from kytos.core.interface import TAG, UNI +from kytos.core.interface import TAG, UNI, TAGRange from kytos.core.link import Link from kytos.core.rest_api import (HTTPException, JSONResponse, Request, get_json_or_400) +from kytos.core.tag_ranges import get_tag_ranges from napps.kytos.mef_eline import controllers, settings from napps.kytos.mef_eline.exceptions import DisabledSwitch, InvalidPath from napps.kytos.mef_eline.models import (EVC, DynamicPathManager, EVCDeploy, Path) from napps.kytos.mef_eline.scheduler import CircuitSchedule, Scheduler from napps.kytos.mef_eline.utils import (aemit_event, check_disabled_component, - emit_event, map_evc_event_content) + emit_event, get_vlan_tags_and_masks, + map_evc_event_content) # pylint: disable=too-many-public-methods @@ -269,11 +272,9 @@ def create_circuit(self, request: Request) -> JSONResponse: detail=f"backup_path is not valid: {exception}" ) from exception - # verify duplicated evc - if self._is_duplicated_evc(evc): - result = "The EVC already exists." - log.debug("create_circuit result %s %s", result, 409) - raise HTTPException(409, detail=result) + if not evc._tag_lists_equal(): + detail = "UNI_A and UNI_Z tag lists should be the same." + raise HTTPException(400, detail=detail) try: evc._validate_has_primary_or_dynamic() @@ -282,7 +283,7 @@ def create_circuit(self, request: Request) -> JSONResponse: try: self._use_uni_tags(evc) - except ValueError as exception: + except KytosTagsAreNotAvailable as exception: raise HTTPException(400, detail=str(exception)) from exception # save circuit @@ -315,12 +316,12 @@ def _use_uni_tags(evc): uni_a = evc.uni_a try: evc._use_uni_vlan(uni_a) - except ValueError as err: + except KytosTagsAreNotAvailable as err: raise err try: uni_z = evc.uni_z evc._use_uni_vlan(uni_z) - except ValueError as err: + except KytosTagsAreNotAvailable as err: evc.make_uni_vlan_available(uni_a) raise err @@ -370,6 +371,10 @@ def update(self, request: Request) -> JSONResponse: log.error(exception) log.debug("update result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception + except KytosTagsAreNotAvailable as exception: + log.error(exception) + log.debug("update result %s %s", exception, 400) + raise HTTPException(400, detail=str(exception)) from exception except DisabledSwitch as exception: log.debug("update result %s %s", exception, 409) raise HTTPException( @@ -705,21 +710,6 @@ def delete_schedule(self, request: Request) -> JSONResponse: log.debug("delete_schedule result %s %s", result, status) return JSONResponse(result, status_code=status) - def _is_duplicated_evc(self, evc): - """Verify if the circuit given is duplicated with the stored evcs. - - Args: - evc (EVC): circuit to be analysed. - - Returns: - boolean: True if the circuit is duplicated, otherwise False. - - """ - for circuit in tuple(self.circuits.values()): - if not circuit.archived and circuit.shares_uni(evc): - return True - return False - @listen_to("kytos/topology.link_up") def on_link_up(self, event): """Change circuit when link is up or end_maintenance.""" @@ -1001,7 +991,15 @@ def _uni_from_dict(self, uni_dict): tag_type = tag_dict.get("tag_type") tag_type = tag_convert.get(tag_type, tag_type) tag_value = tag_dict.get("value") - tag = TAG(tag_type, tag_value) + if isinstance(tag_value, list): + try: + tag_value = get_tag_ranges(tag_value) + except KytosInvalidRanges as err: + raise err + mask_list = get_vlan_tags_and_masks(tag_value) + tag = TAGRange(tag_type, tag_value, mask_list) + else: + tag = TAG(tag_type, tag_value) else: tag = None uni = UNI(interface, tag) diff --git a/models/evc.py b/models/evc.py index 662ba649..efa22a87 100644 --- a/models/evc.py +++ b/models/evc.py @@ -1,6 +1,7 @@ """Classes used in the main application.""" # pylint: disable=too-many-lines import traceback from collections import OrderedDict +from copy import deepcopy from datetime import datetime from operator import eq, ne from threading import Lock @@ -13,16 +14,20 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity -from kytos.core.exceptions import KytosNoTagAvailableError +from kytos.core.exceptions import (KytosNoTagAvailableError, + KytosTagsAreNotAvailable, + KytosTagsNotInTagRanges) from kytos.core.helpers import get_time, now -from kytos.core.interface import UNI, Interface +from kytos.core.interface import UNI, Interface, TAGRange from kytos.core.link import Link +from kytos.core.tag_ranges import range_difference from napps.kytos.mef_eline import controllers, settings from napps.kytos.mef_eline.exceptions import FlowModException, InvalidPath from napps.kytos.mef_eline.utils import (check_disabled_component, compare_endpoint_trace, compare_uni_out_trace, emit_event, - map_dl_vlan, map_evc_event_content) + make_uni_list, map_dl_vlan, + map_evc_event_content) from .path import DynamicPathManager, Path @@ -171,7 +176,7 @@ def sync(self, keys: set = None): return self._mongo_controller.upsert_evc(self.as_dict()) - def _get_unis_use_tags(self, **kwargs) -> (UNI, UNI): + def _get_unis_use_tags(self, **kwargs) -> tuple[UNI, UNI]: """Obtain both UNIs (uni_a, uni_z). If a UNI is changing, verify tags""" uni_a = kwargs.get("uni_a", None) @@ -179,24 +184,27 @@ def _get_unis_use_tags(self, **kwargs) -> (UNI, UNI): if uni_a and uni_a != self.uni_a: uni_a_flag = True try: - self._use_uni_vlan(uni_a) - except ValueError as err: + # uni_a - self.uni_a + self._use_uni_vlan(uni_a, uni_dif=self.uni_a) + except KytosTagsAreNotAvailable as err: raise err uni_z = kwargs.get("uni_z", None) if uni_z and uni_z != self.uni_z: try: - self._use_uni_vlan(uni_z) - self.make_uni_vlan_available(self.uni_z) - except ValueError as err: + self._use_uni_vlan(uni_z, uni_dif=self.uni_z) + self.make_uni_vlan_available(self.uni_z, uni_dif=uni_z) + except KytosTagsAreNotAvailable as err: if uni_a_flag: - self.make_uni_vlan_available(uni_a) + # self.uni_a - uni_a + self.make_uni_vlan_available(uni_a, uni_dif=self.uni_a) raise err else: uni_z = self.uni_z if uni_a_flag: - self.make_uni_vlan_available(self.uni_a) + # self.uni_a - uni_a + self.make_uni_vlan_available(self.uni_a, uni_dif=uni_a) else: uni_a = self.uni_a return uni_a, uni_z @@ -217,6 +225,10 @@ def update(self, **kwargs): """ enable, redeploy = (None, None) + if not self._tag_lists_equal(**kwargs): + raise ValueError( + "UNI_A and UNI_Z tag lists should be the same." + ) uni_a, uni_z = self._get_unis_use_tags(**kwargs) check_disabled_component(uni_a, uni_z) self._validate_has_primary_or_dynamic( @@ -277,7 +289,6 @@ def _validate(self, **kwargs): """Do Basic validations. Verify required attributes: name, uni_a, uni_z - Verify if the attributes uni_a and uni_z are valid. Raises: ValueError: message with error detail. @@ -293,6 +304,23 @@ def _validate(self, **kwargs): if not isinstance(uni, UNI): raise ValueError(f"{attribute} is an invalid UNI.") + def _tag_lists_equal(self, **kwargs): + """Verify that tag lists are the same.""" + uni_a = kwargs.get("uni_a") or self.uni_a + uni_z = kwargs.get("uni_z") or self.uni_z + uni_a_list = uni_z_list = False + if (uni_a.user_tag and isinstance(uni_a.user_tag, TAGRange)): + uni_a_list = True + if (uni_z.user_tag and isinstance(uni_z.user_tag, TAGRange)): + uni_z_list = True + if uni_a_list and uni_z_list: + if uni_a.user_tag.value == uni_z.user_tag.value: + return True + return False + if uni_a_list == uni_z_list: + return True + return False + def _validate_has_primary_or_dynamic( self, primary_path=None, @@ -420,33 +448,52 @@ def archive(self): """Archive this EVC on deletion.""" self.archived = True - def _use_uni_vlan(self, uni: UNI): + def _use_uni_vlan( + self, + uni: UNI, + uni_dif: Union[None, UNI] = None + ): """Use tags from UNI""" if uni.user_tag is None: return tag = uni.user_tag.value + if not tag or isinstance(tag, str): + return tag_type = uni.user_tag.tag_type - if isinstance(tag, int): - result = uni.interface.use_tags( + if isinstance(tag, list) and uni_dif: + if isinstance(uni_dif.user_tag, list): + tag = range_difference(tag, uni_dif.user_tag.value) + try: + uni.interface.use_tags( self._controller, tag, tag_type ) - if not result: - intf = uni.interface.id - raise ValueError(f"Tag {tag} is not available in {intf}") + except KytosTagsAreNotAvailable as err: + raise err - def make_uni_vlan_available(self, uni: UNI): + def make_uni_vlan_available( + self, + uni: UNI, + uni_dif: Union[None, UNI] = None, + ): """Make available tag from UNI""" if uni.user_tag is None: return tag = uni.user_tag.value + if not tag or isinstance(tag, str): + return tag_type = uni.user_tag.tag_type - if isinstance(tag, int): - result = uni.interface.make_tags_available( + if isinstance(tag, list) and uni_dif: + if isinstance(uni_dif.user_tag, list): + tag = range_difference(tag, uni_dif.user_tag.value) + try: + conflict = uni.interface.make_tags_available( self._controller, tag, tag_type ) - if not result: - intf = uni.interface.id - log.warning(f"Tag {tag} was already available in {intf}") + except KytosTagsNotInTagRanges as err: + log.error(f"Error in circuit {self._id}: {err}") + if conflict: + intf = uni.interface.id + log.warning(f"Tags {conflict} was already available in {intf}") def remove_uni_tags(self): """Remove both UNI usage of a tag""" @@ -657,7 +704,10 @@ def remove_failover_flows(self, exclude_uni_switches=True, f"Error removing flows from switch {switch.id} for" f"EVC {self}: {err}" ) - self.failover_path.make_vlans_available(self._controller) + try: + self.failover_path.make_vlans_available(self._controller) + except KytosTagsNotInTagRanges as err: + log.error(f"Error when removing failover flows: {err}") self.failover_path = Path([]) if sync: self.sync() @@ -687,8 +737,10 @@ def remove_current_flows(self, current_path=None, force=True): f"Error removing flows from switch {switch.id} for" f"EVC {self}: {err}" ) - - current_path.make_vlans_available(self._controller) + try: + current_path.make_vlans_available(self._controller) + except KytosTagsNotInTagRanges as err: + log.error(f"Error when removing current path flows: {err}") self.current_path = Path([]) self.deactivate() self.sync() @@ -742,8 +794,10 @@ def remove_path_flows(self, path=None, force=True): "Error removing failover flows: " f"dpid={dpid} evc={self} error={err}" ) - - path.make_vlans_available(self._controller) + try: + path.make_vlans_available(self._controller) + except KytosTagsNotInTagRanges as err: + log.error(f"Error when removing path flows: {err}") @staticmethod def links_zipped(path=None): @@ -896,6 +950,7 @@ def get_failover_flows(self): return {} return self._prepare_uni_flows(self.failover_path, skip_out=True) + # pylint: disable=too-many-branches def _prepare_direct_uni_flows(self): """Prepare flows connecting two UNIs for intra-switch EVC.""" vlan_a = self._get_value_from_uni_tag(self.uni_a) @@ -910,13 +965,7 @@ def _prepare_direct_uni_flows(self): self.queue_id, vlan_z ) - if vlan_a is not None: - flow_mod_az["match"]["dl_vlan"] = vlan_a - - if vlan_z is not None: - flow_mod_za["match"]["dl_vlan"] = vlan_z - - if vlan_z not in self.special_cases: + if not isinstance(vlan_z, list) and vlan_z not in self.special_cases: flow_mod_az["actions"].insert( 0, {"action_type": "set_vlan", "vlan_id": vlan_z} ) @@ -924,8 +973,12 @@ def _prepare_direct_uni_flows(self): flow_mod_az["actions"].insert( 0, {"action_type": "push_vlan", "tag_type": "c"} ) + if vlan_a == 0: + flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"}) + elif vlan_a == 0 and vlan_z == "4096/4096": + flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"}) - if vlan_a not in self.special_cases: + if not isinstance(vlan_a, list) and vlan_a not in self.special_cases: flow_mod_za["actions"].insert( 0, {"action_type": "set_vlan", "vlan_id": vlan_a} ) @@ -935,15 +988,31 @@ def _prepare_direct_uni_flows(self): ) if vlan_z == 0: flow_mod_az["actions"].insert(0, {"action_type": "pop_vlan"}) - elif vlan_a == "4096/4096" and vlan_z == 0: flow_mod_az["actions"].insert(0, {"action_type": "pop_vlan"}) - elif vlan_a == 0 and vlan_z: - flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"}) - + flows = [] + if isinstance(vlan_a, list): + for mask_a in vlan_a: + flow_aux = deepcopy(flow_mod_az) + flow_aux["match"]["dl_vlan"] = mask_a + flows.append(flow_aux) + else: + if vlan_a is not None: + flow_mod_az["match"]["dl_vlan"] = vlan_a + flows.append(flow_mod_az) + + if isinstance(vlan_z, list): + for mask_z in vlan_z: + flow_aux = deepcopy(flow_mod_za) + flow_aux["match"]["dl_vlan"] = mask_z + flows.append(flow_aux) + else: + if vlan_z is not None: + flow_mod_za["match"]["dl_vlan"] = vlan_z + flows.append(flow_mod_za) return ( - self.uni_a.interface.switch.id, [flow_mod_az, flow_mod_za] + self.uni_a.interface.switch.id, flows ) def _install_direct_uni_flows(self): @@ -999,16 +1068,18 @@ def _install_nni_flows(self, path=None): self._send_flow_mods(dpid, flows) @staticmethod - def _get_value_from_uni_tag(uni): + def _get_value_from_uni_tag(uni: UNI): """Returns the value from tag. In case of any and untagged it should return 4096/4096 and 0 respectively""" special = {"any": "4096/4096", "untagged": 0} - if uni.user_tag: value = uni.user_tag.value + if isinstance(value, list): + return uni.user_tag.mask_list return special.get(value, value) return None + # pylint: disable=too-many-locals def _prepare_uni_flows(self, path=None, skip_in=False, skip_out=False): """Prepare flows to install UNIs.""" uni_flows = {} @@ -1036,15 +1107,27 @@ def _prepare_uni_flows(self, path=None, skip_in=False, skip_out=False): # Flow for one direction, pushing the service tag if not skip_in: - push_flow = self._prepare_push_flow( - self.uni_a.interface, - endpoint_a, - in_vlan_a, - out_vlan_a, - in_vlan_z, - queue_id=self.queue_id, - ) - flows_a.append(push_flow) + if isinstance(in_vlan_a, list): + for in_mask_a in in_vlan_a: + push_flow = self._prepare_push_flow( + self.uni_a.interface, + endpoint_a, + in_mask_a, + out_vlan_a, + in_vlan_z, + queue_id=self.queue_id, + ) + flows_a.append(push_flow) + else: + push_flow = self._prepare_push_flow( + self.uni_a.interface, + endpoint_a, + in_vlan_a, + out_vlan_a, + in_vlan_z, + queue_id=self.queue_id, + ) + flows_a.append(push_flow) # Flow for the other direction, popping the service tag if not skip_out: @@ -1063,15 +1146,27 @@ def _prepare_uni_flows(self, path=None, skip_in=False, skip_out=False): # Flow for one direction, pushing the service tag if not skip_in: - push_flow = self._prepare_push_flow( - self.uni_z.interface, - endpoint_z, - in_vlan_z, - out_vlan_z, - in_vlan_a, - queue_id=self.queue_id, - ) - flows_z.append(push_flow) + if isinstance(in_vlan_z, list): + for in_mask_z in in_vlan_z: + push_flow = self._prepare_push_flow( + self.uni_z.interface, + endpoint_z, + in_mask_z, + out_vlan_z, + in_vlan_a, + queue_id=self.queue_id, + ) + flows_z.append(push_flow) + else: + push_flow = self._prepare_push_flow( + self.uni_z.interface, + endpoint_z, + in_vlan_z, + out_vlan_z, + in_vlan_a, + queue_id=self.queue_id, + ) + flows_z.append(push_flow) # Flow for the other direction, popping the service tag if not skip_out: @@ -1133,6 +1228,8 @@ def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict: @staticmethod def get_priority(vlan): """Return priority value depending on vlan value""" + if isinstance(vlan, list): + return settings.EPL_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: @@ -1185,7 +1282,7 @@ def _prepare_push_flow(self, *args, queue_id=None): Arguments: in_interface(str): Interface input. out_interface(str): Interface output. - in_vlan(str): Vlan input. + in_vlan(int,str,list,None): Vlan input. out_vlan(str): Vlan output. new_c_vlan(str): New client vlan. @@ -1209,7 +1306,8 @@ def _prepare_push_flow(self, *args, queue_id=None): # if in_vlan is set, it must be included in the match flow_mod["match"]["dl_vlan"] = in_vlan - if new_c_vlan not in self.special_cases and in_vlan != new_c_vlan: + if (not isinstance(new_c_vlan, list) and in_vlan != new_c_vlan and + new_c_vlan not in self.special_cases): # new_in_vlan is an integer but zero, action to set is required new_action = {"action_type": "set_vlan", "vlan_id": new_c_vlan} flow_mod["actions"].insert(0, new_action) @@ -1226,7 +1324,9 @@ def _prepare_push_flow(self, *args, queue_id=None): new_action = {"action_type": "pop_vlan"} flow_mod["actions"].insert(0, new_action) - elif not in_vlan and new_c_vlan not in self.special_cases: + elif (not in_vlan and + (not isinstance(new_c_vlan, list) and + new_c_vlan not in self.special_cases)): # new_in_vlan is an integer but zero and in_vlan is not set # then it is set now new_action = {"action_type": "push_vlan", "tag_type": "c"} @@ -1248,21 +1348,23 @@ def _prepare_pop_flow( return flow_mod @staticmethod - def run_bulk_sdntraces(uni_list): + def run_bulk_sdntraces( + uni_list: list[tuple[Interface, Union[str, int, None]]] + ) -> dict: """Run SDN traces on control plane starting from EVC UNIs.""" endpoint = f"{settings.SDN_TRACE_CP_URL}/traces" data = [] - for uni in uni_list: + for interface, tag_value in uni_list: data_uni = { "trace": { "switch": { - "dpid": uni.interface.switch.dpid, - "in_port": uni.interface.port_number, + "dpid": interface.switch.dpid, + "in_port": interface.port_number, } } } - if uni.user_tag: - uni_dl_vlan = map_dl_vlan(uni.user_tag.value) + if tag_value: + uni_dl_vlan = map_dl_vlan(tag_value) if uni_dl_vlan: data_uni["trace"]["eth"] = { "dl_type": 0x8100, @@ -1279,24 +1381,32 @@ def run_bulk_sdntraces(uni_list): return {"result": []} return response.json() - # pylint: disable=too-many-return-statements + # pylint: disable=too-many-return-statements, too-many-arguments @staticmethod - def check_trace(circuit, trace_a, trace_z): + def check_trace( + tag_a: Union[None, int, str], + tag_z: Union[None, int, str], + interface_a: Interface, + interface_z: Interface, + current_path: list, + trace_a: list, + trace_z: list + ) -> bool: """Auxiliar function to check an individual trace""" if ( - len(trace_a) != len(circuit.current_path) + 1 - or not compare_uni_out_trace(circuit.uni_z, trace_a[-1]) + len(trace_a) != len(current_path) + 1 + or not compare_uni_out_trace(tag_z, interface_z, trace_a[-1]) ): log.warning(f"Invalid trace from uni_a: {trace_a}") return False if ( - len(trace_z) != len(circuit.current_path) + 1 - or not compare_uni_out_trace(circuit.uni_a, trace_z[-1]) + len(trace_z) != len(current_path) + 1 + or not compare_uni_out_trace(tag_a, interface_a, trace_z[-1]) ): log.warning(f"Invalid trace from uni_z: {trace_z}") return False - for link, trace1, trace2 in zip(circuit.current_path, + for link, trace1, trace2 in zip(current_path, trace_a[1:], trace_z[:0:-1]): metadata_vlan = None @@ -1320,33 +1430,66 @@ def check_trace(circuit, trace_a, trace_z): return True @staticmethod - def check_list_traces(list_circuits): + def check_range(circuit, traces: list) -> bool: + """Check traces when for UNI with TAGRange""" + check = True + for i, mask in enumerate(circuit.uni_a.user_tag.mask_list): + trace_a = traces[i*2] + trace_z = traces[i*2+1] + check &= EVCDeploy.check_trace( + mask, mask, + circuit.uni_a.interface, + circuit.uni_z.interface, + circuit.current_path, + trace_a, trace_z, + ) + return check + + @staticmethod + def check_list_traces(list_circuits: list) -> dict: """Check if current_path is deployed comparing with SDN traces.""" if not list_circuits: return {} - uni_list = [] - for circuit in list_circuits: - uni_list.append(circuit.uni_a) - uni_list.append(circuit.uni_z) - - traces = EVCDeploy.run_bulk_sdntraces(uni_list) - traces = traces["result"] - circuits_checked = {} + uni_list = make_uni_list(list_circuits) + traces = EVCDeploy.run_bulk_sdntraces(uni_list)["result"] + if not traces: - return circuits_checked + return {} try: - for i, circuit in enumerate(list_circuits): - trace_a = traces[2*i] - trace_z = traces[2*i+1] - circuits_checked[circuit.id] = EVCDeploy.check_trace( - circuit, trace_a, trace_z + circuits_checked = {} + i = 0 + for circuit in list_circuits: + if isinstance(circuit.uni_a.user_tag, TAGRange): + length = len(circuit.uni_a.user_tag.mask_list) + circuits_checked[circuit.id] = EVCDeploy.check_range( + circuit, traces[0:length*2] ) + i += length*2 + else: + trace_a = traces[i] + trace_z = traces[i+1] + tag_a = None + if circuit.uni_a.user_tag: + tag_a = circuit.uni_a.user_tag.value + tag_z = None + if circuit.uni_z.user_tag: + tag_z = circuit.uni_z.user_tag.value + circuits_checked[circuit.id] = EVCDeploy.check_trace( + tag_a, + tag_z, + circuit.uni_a.interface, + circuit.uni_z.interface, + circuit.current_path, + trace_a, trace_z + ) + i += 2 except IndexError as err: log.error( f"Bulk sdntraces returned fewer items than expected." f"Error = {err}" ) + return {} return circuits_checked diff --git a/models/path.py b/models/path.py index 6a4b2203..ed917e20 100644 --- a/models/path.py +++ b/models/path.py @@ -3,6 +3,7 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity +from kytos.core.exceptions import KytosTagsNotInTagRanges from kytos.core.interface import TAG from kytos.core.link import Link from napps.kytos.mef_eline import settings @@ -44,14 +45,17 @@ def make_vlans_available(self, controller): """Make the VLANs used in a path available when undeployed.""" for link in self: tag = link.get_metadata("s_vlan") - result_a, result_b = link.make_tags_available( - controller, tag.value, link.id, tag.tag_type - ) - if result_a is False: - log.error(f"Tag {tag} was already available in" + try: + conflict_a, conflict_b = link.make_tags_available( + controller, tag.value, link.id, tag.tag_type + ) + except KytosTagsNotInTagRanges as err: + raise err + if conflict_a: + log.error(f"Tags {conflict_a} was already available in" f"{link.endpoint_a.id}") - if result_b is False: - log.error(f"Tag {tag} was already available in" + if conflict_b: + log.error(f"Tags {conflict_b} was already available in" f"{link.endpoint_b.id}") link.remove_metadata("s_vlan") diff --git a/openapi.yml b/openapi.yml index 863fa216..d796279a 100644 --- a/openapi.yml +++ b/openapi.yml @@ -695,9 +695,13 @@ components: - type: integer format: int32 - type: string - enum: - - any - - untagged + - type: array + minItems: 1 + items: + anyOf: + - type: array + - type: integer + example: [[1, 500], 2096, [3001]] CircuitSchedule: # Can be referenced via '#/components/schemas/CircuitSchedule' type: object diff --git a/requirements/dev.in b/requirements/dev.in index 0683acaa..59cfbe24 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -5,5 +5,5 @@ # pip-compile --output-file requirements/dev.txt requirements/dev.in # -e git+https://github.com/kytos-ng/python-openflow.git#egg=python-openflow --e git+https://github.com/kytos-ng/kytos.git#egg=kytos[dev] +-e git+https://github.com/kytos-ng/kytos.git@epic/vlan_range#egg=kytos[dev] -e . diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 9384ae0b..096c6637 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -397,6 +397,7 @@ async def test_circuit_with_invalid_id(self): expected_result = "circuit_id 3 not found" assert response.json()["description"] == expected_result + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -413,6 +414,7 @@ async def test_create_a_circuit_case_1( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test create a new circuit.""" @@ -422,6 +424,7 @@ async def test_create_a_circuit_case_1( mongo_controller_upsert_mock.return_value = True evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -613,6 +616,7 @@ async def test_create_a_circuit_invalid_queue_id(self, event_loop): assert response.status_code == 400 assert expected_data in current_data["description"] + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -629,6 +633,7 @@ async def test_create_circuit_already_enabled( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test create an already created circuit.""" @@ -639,6 +644,7 @@ async def test_create_circuit_already_enabled( sched_add_mock.return_value = True evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -676,10 +682,17 @@ async def test_create_circuit_already_enabled( assert current_data["description"] == expected_data assert 409 == response.status_code + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._uni_from_dict") - async def test_create_circuit_case_5(self, uni_from_dict_mock, event_loop): + async def test_create_circuit_case_5( + self, + uni_from_dict_mock, + mock_tags_equal, + event_loop + ): """Test when neither primary path nor dynamic_backup_path is set.""" self.napp.controller.loop = event_loop + mock_tags_equal.return_value = True url = f"{self.base_endpoint}/v2/evc/" uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) @@ -1441,6 +1454,7 @@ async def test_update_circuit( assert 409 == response.status_code assert "Can't update archived EVC" in response.json()["description"] + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1457,6 +1471,7 @@ async def test_update_circuit_invalid_json( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test update a circuit circuit.""" @@ -1466,6 +1481,7 @@ async def test_update_circuit_invalid_json( sched_add_mock.return_value = True evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -1509,6 +1525,7 @@ async def test_update_circuit_invalid_json( assert 400 == response.status_code assert "must have a primary path or" in current_data["description"] + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1529,6 +1546,7 @@ async def test_update_circuit_invalid_path( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test update a circuit circuit.""" @@ -1540,6 +1558,7 @@ async def test_update_circuit_invalid_path( evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True link_from_dict_mock.return_value = 1 + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -1681,6 +1700,7 @@ def test_uni_from_dict_non_existent_intf(self): with pytest.raises(ValueError): self.napp._uni_from_dict(uni_dict) + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1695,6 +1715,7 @@ async def test_update_evc_no_json_mime( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test update a circuit with wrong mimetype.""" @@ -1703,6 +1724,7 @@ async def test_update_evc_no_json_mime( sched_add_mock.return_value = True evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -1751,6 +1773,7 @@ async def test_delete_no_evc(self): assert current_data["description"] == expected_data assert 404 == response.status_code + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.models.evc.EVC.remove_uni_tags") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.remove_current_flows") @@ -1771,6 +1794,7 @@ async def test_delete_archived_evc( remove_current_flows_mock, mock_remove_tags, mock_use_uni, + mock_tags_equal, event_loop ): """Try to delete an archived EVC""" @@ -1781,6 +1805,7 @@ async def test_delete_archived_evc( evc_deploy_mock.return_value = True remove_current_flows_mock.return_value = True mock_use_uni.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index d965bf15..64267151 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -42,26 +42,25 @@ def test_compare_endpoint_trace(self, switch, expected): def test_compare_uni_out_trace(self): """Test compare_uni_out_trace method.""" # case1: trace without 'out' info, should return True - uni = MagicMock() - assert compare_uni_out_trace(uni, {}) + interface = MagicMock() + assert compare_uni_out_trace(None, interface, {}) # case2: trace with valid port and VLAN, should return True - uni.interface.port_number = 1 - uni.user_tag.value = 123 + interface.port_number = 1 + tag_value = 123 trace = {"out": {"port": 1, "vlan": 123}} - assert compare_uni_out_trace(uni, trace) + assert compare_uni_out_trace(tag_value, interface, trace) # case3: UNI has VLAN but trace dont have, should return False trace = {"out": {"port": 1}} - assert compare_uni_out_trace(uni, trace) is False + assert compare_uni_out_trace(tag_value, interface, trace) is False # case4: UNI and trace dont have VLAN should return True - uni.user_tag = None - assert compare_uni_out_trace(uni, trace) + assert compare_uni_out_trace(None, interface, trace) # case5: UNI dont have VLAN but trace has, should return False trace = {"out": {"port": 1, "vlan": 123}} - assert compare_uni_out_trace(uni, trace) is False + assert compare_uni_out_trace(None, interface, trace) is False def test_map_dl_vlan(self): """Test map_dl_vlan""" @@ -76,13 +75,13 @@ def test_map_dl_vlan(self): ( [[101, 200]], [ - "101/4095", + 101, "102/4094", "104/4088", "112/4080", "128/4032", "192/4088", - "200/4095", + 200, ] ), ( @@ -91,7 +90,7 @@ def test_map_dl_vlan(self): ), ( [[34, 34]], - ["34/4095"] + [34] ), ( [ @@ -100,8 +99,8 @@ def test_map_dl_vlan(self): [130, 135] ], [ - "34/4095", - "128/4095", + 34, + 128, "130/4094", "132/4092" ] diff --git a/utils.py b/utils.py index efe258e2..ff59ea8c 100644 --- a/utils.py +++ b/utils.py @@ -1,7 +1,9 @@ """Utility functions.""" +from typing import Union + from kytos.core.common import EntityStatus from kytos.core.events import KytosEvent -from kytos.core.interface import UNI +from kytos.core.interface import UNI, Interface, TAGRange from napps.kytos.mef_eline.exceptions import DisabledSwitch @@ -43,7 +45,7 @@ def compare_endpoint_trace(endpoint, vlan, trace): ) -def map_dl_vlan(value): +def map_dl_vlan(value: Union[str, int]) -> bool: """Map dl_vlan value with the following criteria: dl_vlan = untagged or 0 -> None dl_vlan = any or "4096/4096" -> 1 @@ -59,16 +61,20 @@ def map_dl_vlan(value): return value & (mask & 4095) -def compare_uni_out_trace(uni, trace): +def compare_uni_out_trace( + tag_value: Union[None, int, str], + interface: Interface, + trace: dict +) -> bool: """Check if the trace last step (output) matches the UNI attributes.""" # keep compatibility for old versions of sdntrace-cp if "out" not in trace: return True if not isinstance(trace["out"], dict): return False - uni_vlan = map_dl_vlan(uni.user_tag.value) if uni.user_tag else None + uni_vlan = map_dl_vlan(tag_value) if tag_value else None return ( - uni.interface.port_number == trace["out"].get("port") + interface.port_number == trace["out"].get("port") and uni_vlan == trace["out"].get("vlan") ) @@ -80,7 +86,7 @@ def max_power2_divisor(number: int, limit: int = 4096) -> int: return limit -def get_vlan_tags_and_masks(tag_ranges: list[list[int]]) -> list[str]: +def get_vlan_tags_and_masks(tag_ranges: list[list[int]]) -> list[int, str]: """Get a list of vlan/mask pairs for a given list of ranges.""" masks_list = [] for start, end in tag_ranges: @@ -89,7 +95,11 @@ def get_vlan_tags_and_masks(tag_ranges: list[list[int]]) -> list[str]: divisor = max_power2_divisor(start) while divisor > limit - start: divisor //= 2 - masks_list.append(f"{start}/{4096-divisor}") + mask = 4096 - divisor + if mask == 4095: + masks_list.append(start) + else: + masks_list.append(f"{start}/{mask}") start += divisor return masks_list @@ -107,3 +117,28 @@ def check_disabled_component(uni_a: UNI, uni_z: UNI): if uni_z.interface.status == EntityStatus.DISABLED: id_ = uni_z.interface.id raise DisabledSwitch(f"Interface {id_} is disabled") + + +def make_uni_list(list_circuits: list) -> list: + """Make uni list to be sent to sdntrace""" + uni_list = [] + for circuit in list_circuits: + if isinstance(circuit.uni_a.user_tag, TAGRange): + # TAGRange value from uni_a and uni_z are currently mirrored + for mask in circuit.uni_a.user_tag.mask_list: + uni_list.append((circuit.uni_a.interface, mask)) + uni_list.append((circuit.uni_z.interface, mask)) + else: + tag_a = None + if circuit.uni_a.user_tag: + tag_a = circuit.uni_a.user_tag.value + uni_list.append( + (circuit.uni_a.interface, tag_a) + ) + tag_z = None + if circuit.uni_z.user_tag: + tag_z = circuit.uni_z.user_tag.value + uni_list.append( + (circuit.uni_z.interface, tag_z) + ) + return uni_list From 4cd70e2b4d96cbf8ab4edad0fb4623e3c84286a0 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 9 Nov 2023 03:05:49 -0500 Subject: [PATCH 14/28] Fixed trace - Deleted unnecessary raises --- db/models.py | 2 +- main.py | 24 ++++++++++++------------ models/evc.py | 41 +++++++++++++++++------------------------ models/path.py | 9 +++------ 4 files changed, 33 insertions(+), 43 deletions(-) diff --git a/db/models.py b/db/models.py index 346e36fe..18b2d17d 100644 --- a/db/models.py +++ b/db/models.py @@ -38,7 +38,7 @@ class CircuitScheduleDoc(BaseModel): class TAGDoc(BaseModel): """TAG model""" tag_type: str - value: Union[int, str, list] + value: Union[int, str, list[list[int]]] @validator('value') def validate_value(cls, value): diff --git a/main.py b/main.py index 1e46086b..d3bbc95f 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from kytos.core import KytosNApp, log, rest from kytos.core.events import KytosEvent -from kytos.core.exceptions import KytosInvalidRanges, KytosTagsAreNotAvailable +from kytos.core.exceptions import KytosTagsAreNotAvailable, KytosTagError from kytos.core.helpers import (alisten_to, listen_to, load_spec, validate_openapi) from kytos.core.interface import TAG, UNI, TAGRange @@ -237,7 +237,9 @@ def create_circuit(self, request: Request) -> JSONResponse: except ValueError as exception: log.debug("create_circuit result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception - + except KytosTagError as exception: + log.debug("create_circuit result %s %s", exception, 400) + raise HTTPException(400, detail=str(exception)) from exception try: check_disabled_component(evc.uni_a, evc.uni_z) except DisabledSwitch as exception: @@ -314,10 +316,7 @@ def create_circuit(self, request: Request) -> JSONResponse: @staticmethod def _use_uni_tags(evc): uni_a = evc.uni_a - try: - evc._use_uni_vlan(uni_a) - except KytosTagsAreNotAvailable as err: - raise err + evc._use_uni_vlan(uni_a) try: uni_z = evc.uni_z evc._use_uni_vlan(uni_z) @@ -365,14 +364,14 @@ def update(self, request: Request) -> JSONResponse: enable, redeploy = evc.update( **self._evc_dict_with_instances(data) ) + except KytosTagError as exception: + raise HTTPException(400, detail=str(exception)) from exception except ValidationError as exception: raise HTTPException(400, detail=str(exception)) from exception except ValueError as exception: - log.error(exception) log.debug("update result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception except KytosTagsAreNotAvailable as exception: - log.error(exception) log.debug("update result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception except DisabledSwitch as exception: @@ -901,6 +900,10 @@ def _load_evc(self, circuit_dict): f"Could not load EVC: dict={circuit_dict} error={exception}" ) return None + except KytosTagError as exception: + log.error( + f"Could not load EVC: dict={circuit_dict} error={exception}" + ) if evc.archived: return None @@ -992,10 +995,7 @@ def _uni_from_dict(self, uni_dict): tag_type = tag_convert.get(tag_type, tag_type) tag_value = tag_dict.get("value") if isinstance(tag_value, list): - try: - tag_value = get_tag_ranges(tag_value) - except KytosInvalidRanges as err: - raise err + tag_value = get_tag_ranges(tag_value) mask_list = get_vlan_tags_and_masks(tag_value) tag = TAGRange(tag_type, tag_value, mask_list) else: diff --git a/models/evc.py b/models/evc.py index efa22a87..bd2916a1 100644 --- a/models/evc.py +++ b/models/evc.py @@ -183,11 +183,7 @@ def _get_unis_use_tags(self, **kwargs) -> tuple[UNI, UNI]: uni_a_flag = False if uni_a and uni_a != self.uni_a: uni_a_flag = True - try: - # uni_a - self.uni_a - self._use_uni_vlan(uni_a, uni_dif=self.uni_a) - except KytosTagsAreNotAvailable as err: - raise err + self._use_uni_vlan(uni_a, uni_dif=self.uni_a) uni_z = kwargs.get("uni_z", None) if uni_z and uni_z != self.uni_z: @@ -314,12 +310,8 @@ def _tag_lists_equal(self, **kwargs): if (uni_z.user_tag and isinstance(uni_z.user_tag, TAGRange)): uni_z_list = True if uni_a_list and uni_z_list: - if uni_a.user_tag.value == uni_z.user_tag.value: - return True - return False - if uni_a_list == uni_z_list: - return True - return False + return uni_a.user_tag.value == uni_z.user_tag.value + return uni_a_list == uni_z_list def _validate_has_primary_or_dynamic( self, @@ -460,15 +452,14 @@ def _use_uni_vlan( if not tag or isinstance(tag, str): return tag_type = uni.user_tag.tag_type - if isinstance(tag, list) and uni_dif: - if isinstance(uni_dif.user_tag, list): - tag = range_difference(tag, uni_dif.user_tag.value) - try: - uni.interface.use_tags( - self._controller, tag, tag_type - ) - except KytosTagsAreNotAvailable as err: - raise err + if (uni_dif and isinstance(tag, list) and + isinstance(uni_dif.user_tag, list)): + tag = range_difference(tag, uni_dif.user_tag.value) + if not tag: + return + uni.interface.use_tags( + self._controller, tag, tag_type + ) def make_uni_vlan_available( self, @@ -482,9 +473,11 @@ def make_uni_vlan_available( if not tag or isinstance(tag, str): return tag_type = uni.user_tag.tag_type - if isinstance(tag, list) and uni_dif: - if isinstance(uni_dif.user_tag, list): - tag = range_difference(tag, uni_dif.user_tag.value) + if (uni_dif and isinstance(tag, list) and + isinstance(uni_dif.user_tag, list)): + tag = range_difference(tag, uni_dif.user_tag.value) + if not tag: + return try: conflict = uni.interface.make_tags_available( self._controller, tag, tag_type @@ -1463,7 +1456,7 @@ def check_list_traces(list_circuits: list) -> dict: if isinstance(circuit.uni_a.user_tag, TAGRange): length = len(circuit.uni_a.user_tag.mask_list) circuits_checked[circuit.id] = EVCDeploy.check_range( - circuit, traces[0:length*2] + circuit, traces[i:i+length*2] ) i += length*2 else: diff --git a/models/path.py b/models/path.py index ed917e20..e5a596ba 100644 --- a/models/path.py +++ b/models/path.py @@ -45,12 +45,9 @@ def make_vlans_available(self, controller): """Make the VLANs used in a path available when undeployed.""" for link in self: tag = link.get_metadata("s_vlan") - try: - conflict_a, conflict_b = link.make_tags_available( - controller, tag.value, link.id, tag.tag_type - ) - except KytosTagsNotInTagRanges as err: - raise err + conflict_a, conflict_b = link.make_tags_available( + controller, tag.value, link.id, tag.tag_type + ) if conflict_a: log.error(f"Tags {conflict_a} was already available in" f"{link.endpoint_a.id}") From 8f8bae4b88f05c07a93440015b60769cd73295b1 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 9 Nov 2023 16:19:30 -0500 Subject: [PATCH 15/28] Updated exceptions --- db/models.py | 1 + main.py | 30 +++++++++++++++++++++++------- models/evc.py | 15 ++++++--------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/db/models.py b/db/models.py index 18b2d17d..64695eff 100644 --- a/db/models.py +++ b/db/models.py @@ -39,6 +39,7 @@ class TAGDoc(BaseModel): """TAG model""" tag_type: str value: Union[int, str, list[list[int]]] + mask_list: Optional[list[str, int]] @validator('value') def validate_value(cls, value): diff --git a/main.py b/main.py index d3bbc95f..931901d2 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from kytos.core import KytosNApp, log, rest from kytos.core.events import KytosEvent -from kytos.core.exceptions import KytosTagsAreNotAvailable, KytosTagError +from kytos.core.exceptions import KytosTagError from kytos.core.helpers import (alisten_to, listen_to, load_spec, validate_openapi) from kytos.core.interface import TAG, UNI, TAGRange @@ -274,6 +274,11 @@ def create_circuit(self, request: Request) -> JSONResponse: detail=f"backup_path is not valid: {exception}" ) from exception + if self._is_duplicated_evc(evc): + result = "The EVC already exists." + log.debug("create_circuit result %s %s", result, 409) + raise HTTPException(409, detail=result) + if not evc._tag_lists_equal(): detail = "UNI_A and UNI_Z tag lists should be the same." raise HTTPException(400, detail=detail) @@ -285,7 +290,7 @@ def create_circuit(self, request: Request) -> JSONResponse: try: self._use_uni_tags(evc) - except KytosTagsAreNotAvailable as exception: + except KytosTagError as exception: raise HTTPException(400, detail=str(exception)) from exception # save circuit @@ -320,7 +325,7 @@ def _use_uni_tags(evc): try: uni_z = evc.uni_z evc._use_uni_vlan(uni_z) - except KytosTagsAreNotAvailable as err: + except KytosTagError as err: evc.make_uni_vlan_available(uni_a) raise err @@ -371,9 +376,6 @@ def update(self, request: Request) -> JSONResponse: except ValueError as exception: log.debug("update result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception - except KytosTagsAreNotAvailable as exception: - log.debug("update result %s %s", exception, 400) - raise HTTPException(400, detail=str(exception)) from exception except DisabledSwitch as exception: log.debug("update result %s %s", exception, 409) raise HTTPException( @@ -709,6 +711,21 @@ def delete_schedule(self, request: Request) -> JSONResponse: log.debug("delete_schedule result %s %s", result, status) return JSONResponse(result, status_code=status) + def _is_duplicated_evc(self, evc): + """Verify if the circuit given is duplicated with the stored evcs. + + Args: + evc (EVC): circuit to be analysed. + + Returns: + boolean: True if the circuit is duplicated, otherwise False. + + """ + for circuit in tuple(self.circuits.values()): + if not circuit.archived and circuit.shares_uni(evc): + return True + return False + @listen_to("kytos/topology.link_up") def on_link_up(self, event): """Change circuit when link is up or end_maintenance.""" @@ -1003,7 +1020,6 @@ def _uni_from_dict(self, uni_dict): else: tag = None uni = UNI(interface, tag) - return uni def _link_from_dict(self, link_dict): diff --git a/models/evc.py b/models/evc.py index bd2916a1..afacc44d 100644 --- a/models/evc.py +++ b/models/evc.py @@ -15,8 +15,7 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity from kytos.core.exceptions import (KytosNoTagAvailableError, - KytosTagsAreNotAvailable, - KytosTagsNotInTagRanges) + KytosTagError) from kytos.core.helpers import get_time, now from kytos.core.interface import UNI, Interface, TAGRange from kytos.core.link import Link @@ -190,16 +189,14 @@ def _get_unis_use_tags(self, **kwargs) -> tuple[UNI, UNI]: try: self._use_uni_vlan(uni_z, uni_dif=self.uni_z) self.make_uni_vlan_available(self.uni_z, uni_dif=uni_z) - except KytosTagsAreNotAvailable as err: + except KytosTagError as err: if uni_a_flag: - # self.uni_a - uni_a self.make_uni_vlan_available(uni_a, uni_dif=self.uni_a) raise err else: uni_z = self.uni_z if uni_a_flag: - # self.uni_a - uni_a self.make_uni_vlan_available(self.uni_a, uni_dif=uni_a) else: uni_a = self.uni_a @@ -482,7 +479,7 @@ def make_uni_vlan_available( conflict = uni.interface.make_tags_available( self._controller, tag, tag_type ) - except KytosTagsNotInTagRanges as err: + except KytosTagError as err: log.error(f"Error in circuit {self._id}: {err}") if conflict: intf = uni.interface.id @@ -699,7 +696,7 @@ def remove_failover_flows(self, exclude_uni_switches=True, ) try: self.failover_path.make_vlans_available(self._controller) - except KytosTagsNotInTagRanges as err: + except KytosTagError as err: log.error(f"Error when removing failover flows: {err}") self.failover_path = Path([]) if sync: @@ -732,7 +729,7 @@ def remove_current_flows(self, current_path=None, force=True): ) try: current_path.make_vlans_available(self._controller) - except KytosTagsNotInTagRanges as err: + except KytosTagError as err: log.error(f"Error when removing current path flows: {err}") self.current_path = Path([]) self.deactivate() @@ -789,7 +786,7 @@ def remove_path_flows(self, path=None, force=True): ) try: path.make_vlans_available(self._controller) - except KytosTagsNotInTagRanges as err: + except KytosTagError as err: log.error(f"Error when removing path flows: {err}") @staticmethod From 84ca0c0fa71d1a850798f532a972ebc865dce4a6 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 13 Nov 2023 02:35:13 -0500 Subject: [PATCH 16/28] Added unit tests - Fixed partial vlan list update --- main.py | 3 +- models/evc.py | 12 +- models/path.py | 1 - tests/helpers.py | 7 +- tests/unit/models/test_evc_base.py | 105 ++++++++++--- tests/unit/models/test_evc_deploy.py | 214 ++++++++++++++++++++++++--- tests/unit/test_db_models.py | 5 + tests/unit/test_main.py | 154 +++++++++++++++++-- 8 files changed, 440 insertions(+), 61 deletions(-) diff --git a/main.py b/main.py index 931901d2..99af8e4b 100644 --- a/main.py +++ b/main.py @@ -202,6 +202,7 @@ def get_circuit(self, request: Request) -> JSONResponse: log.debug("get_circuit result %s %s", circuit, status) return JSONResponse(circuit, status_code=status) + # pylint: disable=too-many-branches, too-many-statements @rest("/v2/evc/", methods=["POST"]) @validate_openapi(spec) def create_circuit(self, request: Request) -> JSONResponse: @@ -921,7 +922,7 @@ def _load_evc(self, circuit_dict): log.error( f"Could not load EVC: dict={circuit_dict} error={exception}" ) - + return None if evc.archived: return None diff --git a/models/evc.py b/models/evc.py index afacc44d..b71d952f 100644 --- a/models/evc.py +++ b/models/evc.py @@ -14,8 +14,7 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity -from kytos.core.exceptions import (KytosNoTagAvailableError, - KytosTagError) +from kytos.core.exceptions import KytosNoTagAvailableError, KytosTagError from kytos.core.helpers import get_time, now from kytos.core.interface import UNI, Interface, TAGRange from kytos.core.link import Link @@ -449,8 +448,8 @@ def _use_uni_vlan( if not tag or isinstance(tag, str): return tag_type = uni.user_tag.tag_type - if (uni_dif and isinstance(tag, list) and - isinstance(uni_dif.user_tag, list)): + if (uni_dif and isinstance(tag, list) and + isinstance(uni_dif.user_tag.value, list)): tag = range_difference(tag, uni_dif.user_tag.value) if not tag: return @@ -471,7 +470,7 @@ def make_uni_vlan_available( return tag_type = uni.user_tag.tag_type if (uni_dif and isinstance(tag, list) and - isinstance(uni_dif.user_tag, list)): + isinstance(uni_dif.user_tag.value, list)): tag = range_difference(tag, uni_dif.user_tag.value) if not tag: return @@ -481,6 +480,7 @@ def make_uni_vlan_available( ) except KytosTagError as err: log.error(f"Error in circuit {self._id}: {err}") + return if conflict: intf = uni.interface.id log.warning(f"Tags {conflict} was already available in {intf}") @@ -1219,7 +1219,7 @@ def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict: def get_priority(vlan): """Return priority value depending on vlan value""" if isinstance(vlan, list): - return settings.EPL_SB_PRIORITY + return settings.EVPL_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: diff --git a/models/path.py b/models/path.py index e5a596ba..936ba85e 100644 --- a/models/path.py +++ b/models/path.py @@ -3,7 +3,6 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity -from kytos.core.exceptions import KytosTagsNotInTagRanges from kytos.core.interface import TAG from kytos.core.link import Link from napps.kytos.mef_eline import settings diff --git a/tests/helpers.py b/tests/helpers.py index 388330be..341a257c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -4,7 +4,7 @@ from kytos.core import Controller from kytos.core.common import EntityStatus from kytos.core.config import KytosConfig -from kytos.core.interface import TAG, UNI, Interface +from kytos.core.interface import TAG, TAGRange, UNI, Interface from kytos.core.link import Link from kytos.core.switch import Switch from kytos.lib.helpers import get_interface_mock, get_switch_mock @@ -101,7 +101,10 @@ def get_uni_mocked(**kwargs): switch.id = kwargs.get("switch_id", "custom_switch_id") switch.dpid = kwargs.get("switch_dpid", "custom_switch_dpid") interface = Interface(interface_name, interface_port, switch) - tag = TAG(tag_type, tag_value) + if isinstance(tag_value, list): + tag = TAGRange(tag_type, tag_value) + else: + tag = TAG(tag_type, tag_value) uni = Mock(spec=UNI, interface=interface, user_tag=tag) uni.is_valid.return_value = is_valid uni.as_dict.return_value = { diff --git a/tests/unit/models/test_evc_base.py b/tests/unit/models/test_evc_base.py index bb7475ed..cb427962 100644 --- a/tests/unit/models/test_evc_base.py +++ b/tests/unit/models/test_evc_base.py @@ -1,6 +1,8 @@ """Module to test the EVCBase class.""" import sys from unittest.mock import MagicMock, patch, call +from kytos.core.exceptions import KytosTagError +from kytos.core.interface import TAGRange from napps.kytos.mef_eline.models import Path import pytest # pylint: disable=wrong-import-position @@ -253,6 +255,22 @@ def test_update_queue_null(self, _sync_mock): _, redeploy = evc.update(**update_dict) assert redeploy + def test_update_different_tag_lists(self): + """Test update when tag lists are different.""" + attributes = { + "controller": get_controller_mock(), + "name": "circuit_name", + "enable": True, + "dynamic_backup_path": True, + "uni_a": get_uni_mocked(is_valid=True), + "uni_z": get_uni_mocked(is_valid=True), + } + uni = MagicMock(user_tag=TAGRange("vlan", [[1, 10]])) + update_dict = {"uni_a": uni} + evc = EVC(**attributes) + with pytest.raises(ValueError): + evc.update(**update_dict) + def test_circuit_representation(self): """Test the method __repr__.""" attributes = { @@ -488,13 +506,19 @@ def test_get_unis_use_tags(self): unis = {"uni_a": new_uni_a, "uni_z": new_uni_z} evc._get_unis_use_tags(**unis) - expected = [call(new_uni_a), call(new_uni_z)] + expected = [ + call(new_uni_a, uni_dif=old_uni_a), + call(new_uni_z, uni_dif=old_uni_z) + ] evc._use_uni_vlan.assert_has_calls(expected) - expected = [call(old_uni_z), call(old_uni_a)] + expected = [ + call(old_uni_z, uni_dif=new_uni_z), + call(old_uni_a, uni_dif=new_uni_a) + ] evc.make_uni_vlan_available.assert_has_calls(expected) def test_get_unis_use_tags_error(self): - """Test _get_unis_use_tags with ValueError""" + """Test _get_unis_use_tags with KytosTagError""" old_uni_a = get_uni_mocked( interface_port=2, is_valid=True @@ -513,34 +537,38 @@ def test_get_unis_use_tags_error(self): evc = EVC(**attributes) evc._use_uni_vlan = MagicMock() - # UNI Z ValueError - evc._use_uni_vlan.side_effect = [None, ValueError()] + # UNI Z KytosTagError + evc._use_uni_vlan.side_effect = [None, KytosTagError("")] evc.make_uni_vlan_available = MagicMock() new_uni_a = get_uni_mocked(tag_value=200, is_valid=True) new_uni_z = get_uni_mocked(tag_value=200, is_valid=True) unis = {"uni_a": new_uni_a, "uni_z": new_uni_z} - with pytest.raises(ValueError): + with pytest.raises(KytosTagError): evc._get_unis_use_tags(**unis) - expected = [call(new_uni_a), call(new_uni_z)] + expected = [ + call(new_uni_a, uni_dif=old_uni_a), + call(new_uni_z, uni_dif=old_uni_z) + ] evc._use_uni_vlan.assert_has_calls(expected) assert evc.make_uni_vlan_available.call_count == 1 assert evc.make_uni_vlan_available.call_args[0][0] == new_uni_a - # UNI A ValueError + # UNI A KytosTagError evc = EVC(**attributes) evc._use_uni_vlan = MagicMock() - evc._use_uni_vlan.side_effect = [ValueError(), None] + evc._use_uni_vlan.side_effect = [KytosTagError(""), None] evc.make_uni_vlan_available = MagicMock() new_uni_a = get_uni_mocked(tag_value=200, is_valid=True) new_uni_z = get_uni_mocked(tag_value=200, is_valid=True) unis = {"uni_a": new_uni_a, "uni_z": new_uni_z} - with pytest.raises(ValueError): + with pytest.raises(KytosTagError): evc._get_unis_use_tags(**unis) assert evc._use_uni_vlan.call_count == 1 assert evc._use_uni_vlan.call_args[0][0] == new_uni_a assert evc.make_uni_vlan_available.call_count == 0 - def test_use_uni_vlan(self): + @patch("napps.kytos.mef_eline.models.evc.range_difference") + def test_use_uni_vlan(self, mock_difference): """Test _use_uni_vlan""" attributes = { "controller": get_controller_mock(), @@ -558,16 +586,31 @@ def test_use_uni_vlan(self): assert args[2] == uni.user_tag.tag_type assert uni.interface.use_tags.call_count == 1 - uni.interface.use_tags.return_value = False - with pytest.raises(ValueError): - evc._use_uni_vlan(uni) + uni.user_tag.value = "any" + evc._use_uni_vlan(uni) + assert uni.interface.use_tags.call_count == 1 + + uni.user_tag.value = [[1, 10]] + uni_dif = get_uni_mocked(tag_value=[[1, 2]]) + mock_difference.return_value = [[3, 10]] + evc._use_uni_vlan(uni, uni_dif) + assert uni.interface.use_tags.call_count == 2 + + mock_difference.return_value = [] + evc._use_uni_vlan(uni, uni_dif) assert uni.interface.use_tags.call_count == 2 + uni.interface.use_tags.side_effect = KytosTagError("") + with pytest.raises(KytosTagError): + evc._use_uni_vlan(uni) + assert uni.interface.use_tags.call_count == 3 + uni.user_tag = None evc._use_uni_vlan(uni) - assert uni.interface.use_tags.call_count == 2 + assert uni.interface.use_tags.call_count == 3 - def test_make_uni_vlan_available(self): + @patch("napps.kytos.mef_eline.models.evc.log") + def test_make_uni_vlan_available(self, mock_log): """Test make_uni_vlan_available""" attributes = { "controller": get_controller_mock(), @@ -586,13 +629,22 @@ def test_make_uni_vlan_available(self): assert args[2] == uni.user_tag.tag_type assert uni.interface.make_tags_available.call_count == 1 - uni.interface.make_tags_available.return_value = False + uni.user_tag.value = None evc.make_uni_vlan_available(uni) + assert uni.interface.make_tags_available.call_count == 1 + + uni.user_tag.value = [[1, 10]] + uni_dif = get_uni_mocked(tag_value=[[1, 2]]) + evc.make_uni_vlan_available(uni, uni_dif) assert uni.interface.make_tags_available.call_count == 2 + uni.interface.make_tags_available.side_effect = KytosTagError("") + evc.make_uni_vlan_available(uni) + assert mock_log.error.call_count == 1 + uni.user_tag = None evc.make_uni_vlan_available(uni) - assert uni.interface.make_tags_available.call_count == 2 + assert uni.interface.make_tags_available.call_count == 3 def test_remove_uni_tags(self): """Test remove_uni_tags""" @@ -607,3 +659,20 @@ def test_remove_uni_tags(self): evc.make_uni_vlan_available = MagicMock() evc.remove_uni_tags() assert evc.make_uni_vlan_available.call_count == 2 + + def test_tag_lists_equal(self): + """Test _tag_lists_equal""" + attributes = { + "controller": get_controller_mock(), + "name": "circuit_name", + "enable": True, + "uni_a": get_uni_mocked(is_valid=True), + "uni_z": get_uni_mocked(is_valid=True) + } + evc = EVC(**attributes) + uni = MagicMock(user_tag=TAGRange("vlan", [[1, 10]])) + update_dict = {"uni_z": uni} + assert evc._tag_lists_equal(**update_dict) is False + + update_dict = {"uni_a": uni, "uni_z": uni} + assert evc._tag_lists_equal(**update_dict) diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index e61c7df7..f920d98a 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -9,7 +9,7 @@ from kytos.core.exceptions import KytosNoTagAvailableError from kytos.core.interface import Interface from kytos.core.switch import Switch - +from requests.exceptions import Timeout # pylint: disable=wrong-import-position sys.path.insert(0, "/var/lib/kytos/napps/..") # pylint: enable=wrong-import-position @@ -193,6 +193,12 @@ def test_prepare_flow_mod(self): } assert expected_flow_mod == flow_mod + evc.sb_priority = 1234 + flow_mod = evc._prepare_flow_mod(interface_a, interface_z, 3) + assert flow_mod["priority"] == 1234 + assert flow_mod["actions"][1]["action_type"] == "set_queue" + assert flow_mod["actions"][1]["queue_id"] == 3 + def test_prepare_pop_flow(self): """Test prepare pop flow method.""" attributes = { @@ -1385,7 +1391,8 @@ def test_run_bulk_sdntraces(self, put_mock): } } ] - result = EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + arg_tuple = [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + result = EVCDeploy.run_bulk_sdntraces(arg_tuple) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1394,7 +1401,12 @@ def test_run_bulk_sdntraces(self, put_mock): assert result['result'] == "ok" response.status_code = 400 - result = EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + result = EVCDeploy.run_bulk_sdntraces(arg_tuple) + assert result == {"result": []} + + put_mock.side_effect = Timeout + response.status_code = 200 + result = EVCDeploy.run_bulk_sdntraces(arg_tuple) assert result == {"result": []} @patch("requests.put") @@ -1413,9 +1425,10 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): } } ] - evc.uni_a.user_tag.value = 'untagged' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1425,7 +1438,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): assert 'eth' not in args evc.uni_a.user_tag.value = 0 - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1435,7 +1450,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): assert 'eth' not in args evc.uni_a.user_tag.value = '5/2' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1446,7 +1463,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): expected_payload[0]['trace']['eth'] = {'dl_type': 0x8100, 'dl_vlan': 1} evc.uni_a.user_tag.value = 'any' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1456,7 +1475,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): assert args['eth'] == {'dl_type': 33024, 'dl_vlan': 1} evc.uni_a.user_tag.value = '4096/4096' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1470,7 +1491,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): 'dl_vlan': 10 } evc.uni_a.user_tag.value = '10/10' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1484,7 +1507,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): 'dl_vlan': 1 } evc.uni_a.user_tag.value = '5/3' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1498,7 +1523,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): 'dl_vlan': 10 } evc.uni_a.user_tag.value = 10 - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1836,6 +1863,45 @@ def test_check_list_traces_invalid_types(self, run_bulk_sdntraces_mock, _): # type loop assert result[evc.id] is False + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_range") + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.run_bulk_sdntraces") + def test_check_list_traces_vlan_list(self, *args): + """Test check_list_traces with vlan list""" + mock_bulk, mock_range, mock_trace = args + mask_list = [1, '2/4094', '4/4094'] + evc = self.create_evc_inter_switch([[1, 5]], [[1, 5]]) + evc.uni_a.user_tag.mask_list = mask_list + evc.uni_z.user_tag.mask_list = mask_list + mock_bulk.return_value = {"result": ["mock"] * 6} + mock_range.return_value = True + actual_return = EVC.check_list_traces([evc]) + assert actual_return == {evc._id: True} + assert mock_trace.call_count == 0 + assert mock_range.call_count == 1 + args = mock_range.call_args[0] + assert args[0] == evc + assert args[1] == ["mock"] * 6 + + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") + @patch("napps.kytos.mef_eline.models.evc.log") + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.run_bulk_sdntraces") + def test_check_list_traces_empty(self, mock_bulk, mock_log, mock_trace): + """Test check_list_traces with empty return""" + evc = self.create_evc_inter_switch(1, 1) + actual_return = EVC.check_list_traces([]) + assert not actual_return + + mock_bulk.return_value = {"result": []} + actual_return = EVC.check_list_traces([evc]) + assert not actual_return + + mock_bulk.return_value = {"result": ["mock"]} + mock_trace.return_value = True + actual_return = EVC.check_list_traces([evc]) + assert mock_log.error.call_count == 1 + assert not actual_return + @patch( "napps.kytos.mef_eline.models.path.DynamicPathManager" ".get_disjoint_paths" @@ -1865,22 +1931,28 @@ def test_is_eligible_for_failover_path(self): def test_get_value_from_uni_tag(self): """Test _get_value_from_uni_tag""" - uni = get_uni_mocked(tag_value=None) - value = EVC._get_value_from_uni_tag(uni) - assert value is None - uni = get_uni_mocked(tag_value="any") value = EVC._get_value_from_uni_tag(uni) assert value == "4096/4096" - uni = get_uni_mocked(tag_value="untagged") + uni.user_tag.value = "untagged" value = EVC._get_value_from_uni_tag(uni) assert value == 0 - uni = get_uni_mocked(tag_value=100) + uni.user_tag.value = 100 value = EVC._get_value_from_uni_tag(uni) assert value == 100 + uni.user_tag = None + value = EVC._get_value_from_uni_tag(uni) + assert value is None + + uni = get_uni_mocked(tag_value=[[12, 20]]) + uni.user_tag.mask_list = ['12/4092', '16/4092', '20/4094'] + + value = EVC._get_value_from_uni_tag(uni) + assert value == ['12/4092', '16/4092', '20/4094'] + def test_get_priority(self): """Test get_priority_from_vlan""" evpl_value = EVC.get_priority(100) @@ -1895,6 +1967,9 @@ def test_get_priority(self): epl_value = EVC.get_priority(None) assert epl_value == EPL_SB_PRIORITY + epl_value = EVC.get_priority([[1, 5]]) + assert epl_value == EVPL_SB_PRIORITY + def test_set_flow_table_group_id(self): """Test set_flow_table_group_id""" self.evc_deploy.table_group = {"epl": 3, "evpl": 4} @@ -1915,3 +1990,106 @@ def test_get_endpoint_by_id(self): assert result == link.endpoint_a result = self.evc_deploy.get_endpoint_by_id(link, "01", operator.ne) assert result == link.endpoint_b + + @patch("napps.kytos.mef_eline.models.evc.EVC._prepare_pop_flow") + @patch("napps.kytos.mef_eline.models.evc.EVC.get_endpoint_by_id") + @patch("napps.kytos.mef_eline.models.evc.EVC._prepare_push_flow") + def test_prepare_uni_flows(self, mock_push, mock_endpoint, _): + """Test _prepare_uni_flows""" + mask_list = [1, '2/4094', '4/4094'] + uni_a = get_uni_mocked(interface_port=1, tag_value=[[1, 5]]) + uni_a.user_tag.mask_list = mask_list + uni_z = get_uni_mocked(interface_port=2, tag_value=[[1, 5]]) + uni_z.user_tag.mask_list = mask_list + mock_endpoint.return_value = "mock_endpoint" + attributes = { + "table_group": {"evpl": 3, "epl": 4}, + "controller": get_controller_mock(), + "name": "custom_name", + "uni_a": uni_a, + "uni_z": uni_z, + } + evc = EVC(**attributes) + link = get_link_mocked() + evc._prepare_uni_flows(Path([link])) + call_list = [] + for i in range(0, 3): + call_list.append(call( + uni_a.interface, + "mock_endpoint", + mask_list[i], + None, + mask_list, + queue_id=-1 + )) + for i in range(0, 3): + call_list.append(call( + uni_z.interface, + "mock_endpoint", + mask_list[i], + None, + mask_list, + queue_id=-1 + )) + mock_push.assert_has_calls(call_list) + + def test_prepare_direct_uni_flows(self): + """Test _prepare_direct_uni_flows""" + mask_list = [1, '2/4094', '4/4094'] + uni_a = get_uni_mocked(interface_port=1, tag_value=[[1, 5]]) + uni_a.user_tag.mask_list = mask_list + uni_z = get_uni_mocked(interface_port=2, tag_value=[[1, 5]]) + uni_z.user_tag.mask_list = mask_list + attributes = { + "table_group": {"evpl": 3, "epl": 4}, + "controller": get_controller_mock(), + "name": "custom_name", + "uni_a": uni_a, + "uni_z": uni_z, + } + evc = EVC(**attributes) + flows = evc._prepare_direct_uni_flows()[1] + assert len(flows) == 6 + for i in range(0, 3): + assert flows[i]["match"]["in_port"] == 1 + assert flows[i]["match"]["dl_vlan"] == mask_list[i] + assert flows[i]["priority"] == EVPL_SB_PRIORITY + for i in range(3, 6): + assert flows[i]["match"]["in_port"] == 2 + assert flows[i]["match"]["dl_vlan"] == mask_list[i-3] + assert flows[i]["priority"] == EVPL_SB_PRIORITY + + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") + def test_check_range(self, mock_check_range): + """Test check_range""" + mask_list = [1, '2/4094', '4/4094'] + uni_a = get_uni_mocked(interface_port=1, tag_value=[[1, 5]]) + uni_a.user_tag.mask_list = mask_list + uni_z = get_uni_mocked(interface_port=2, tag_value=[[1, 5]]) + uni_z.user_tag.mask_list = mask_list + attributes = { + "table_group": {"evpl": 3, "epl": 4}, + "controller": get_controller_mock(), + "name": "custom_name", + "uni_a": uni_a, + "uni_z": uni_z, + } + circuit = EVC(**attributes) + traces = list(range(0, 6)) + mock_check_range.return_value = True + check = EVC.check_range(circuit, traces) + call_list = [] + for i in range(0, 3): + call_list.append(call( + mask_list[i], mask_list[i], + uni_a.interface, + uni_z.interface, + circuit.current_path, + i*2, i*2+1 + )) + mock_check_range.assert_has_calls(call_list) + assert check + + mock_check_range.side_effect = [True, False, True] + check = EVC.check_range(circuit, traces) + assert check is False diff --git a/tests/unit/test_db_models.py b/tests/unit/test_db_models.py index d0a2848b..185a5167 100644 --- a/tests/unit/test_db_models.py +++ b/tests/unit/test_db_models.py @@ -108,6 +108,11 @@ def test_tagdoc_value(self): assert tag.tag_type == 'vlan' assert tag.value == "any" + tag_list = {"tag_type": 'vlan', "value": [[1, 10]]} + tag = TAGDoc(**tag_list) + assert tag.tag_type == 'vlan' + assert tag.value == [[1, 10]] + def test_tagdoc_fail(self): """Test TAGDoc value fail case""" tag_fail = {"tag_type": 'vlan', "value": "test_fail"} diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 096c6637..f7eaff13 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -6,7 +6,8 @@ from kytos.lib.helpers import get_controller_mock, get_test_client from kytos.core.common import EntityStatus from kytos.core.events import KytosEvent -from kytos.core.interface import UNI, Interface +from kytos.core.exceptions import KytosTagError +from kytos.core.interface import TAGRange, UNI, Interface from napps.kytos.mef_eline.exceptions import InvalidPath from napps.kytos.mef_eline.models import EVC from napps.kytos.mef_eline.tests.helpers import get_uni_mocked @@ -37,6 +38,13 @@ async def test_on_table_enabled(): await napp.on_table_enabled(event) assert controller.buffers.app.aput.call_count == 1 + # Failure with early return + content = {} + event = KytosEvent(name="kytos/of_multi_table.enable_table", + content=content) + await napp.on_table_enabled(event) + assert controller.buffers.app.aput.call_count == 1 + # pylint: disable=too-many-public-methods, too-many-lines # pylint: disable=too-many-arguments,too-many-locals @@ -397,7 +405,7 @@ async def test_circuit_with_invalid_id(self): expected_result = "circuit_id 3 not found" assert response.json()["description"] == expected_result - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -616,7 +624,7 @@ async def test_create_a_circuit_invalid_queue_id(self, event_loop): assert response.status_code == 400 assert expected_data in current_data["description"] - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -682,7 +690,7 @@ async def test_create_circuit_already_enabled( assert current_data["description"] == expected_data assert 409 == response.status_code - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._uni_from_dict") async def test_create_circuit_case_5( self, @@ -722,6 +730,98 @@ async def test_create_circuit_case_5( assert 400 == response.status_code, response.data assert current_data["description"] == expected_data + @patch("napps.kytos.mef_eline.main.Main._evc_from_dict") + async def test_create_circuit_case_6(self, mock_evc, event_loop): + """Test create_circuit with KytosTagError""" + self.napp.controller.loop = event_loop + url = f"{self.base_endpoint}/v2/evc/" + mock_evc.side_effect = KytosTagError("") + payload = { + "name": "my evc1", + "uni_a": { + "interface_id": "00:00:00:00:00:00:00:01:1", + }, + "uni_z": { + "interface_id": "00:00:00:00:00:00:00:02:2", + }, + } + response = await self.api_client.post(url, json=payload) + assert response.status_code == 400, response.data + + @patch("napps.kytos.mef_eline.main.check_disabled_component") + @patch("napps.kytos.mef_eline.main.Main._evc_from_dict") + async def test_create_circuit_case_7( + self, + mock_evc, + mock_check_disabled_component, + event_loop + ): + """Test create_circuit with InvalidPath""" + self.napp.controller.loop = event_loop + mock_check_disabled_component.return_value = True + url = f"{self.base_endpoint}/v2/evc/" + uni1 = get_uni_mocked() + uni2 = get_uni_mocked() + evc = MagicMock(uni_a=uni1, uni_z=uni2) + evc.primary_path = MagicMock() + evc.backup_path = MagicMock() + + # Backup_path invalid + evc.backup_path.is_valid = MagicMock(side_effect=InvalidPath) + mock_evc.return_value = evc + payload = { + "name": "my evc1", + "uni_a": { + "interface_id": "00:00:00:00:00:00:00:01:1", + }, + "uni_z": { + "interface_id": "00:00:00:00:00:00:00:02:2", + }, + } + response = await self.api_client.post(url, json=payload) + assert response.status_code == 400, response.data + + # Backup_path invalid + evc.primary_path.is_valid = MagicMock(side_effect=InvalidPath) + mock_evc.return_value = evc + + response = await self.api_client.post(url, json=payload) + assert response.status_code == 400, response.data + + @patch("napps.kytos.mef_eline.main.Main._is_duplicated_evc") + @patch("napps.kytos.mef_eline.main.check_disabled_component") + @patch("napps.kytos.mef_eline.main.Main._evc_from_dict") + async def test_create_circuit_case_8( + self, + mock_evc, + mock_check_disabled_component, + mock_duplicated, + event_loop + ): + """Test create_circuit wit no equal tag lists""" + self.napp.controller.loop = event_loop + mock_check_disabled_component.return_value = True + mock_duplicated.return_value = False + url = f"{self.base_endpoint}/v2/evc/" + uni1 = get_uni_mocked() + uni2 = get_uni_mocked() + evc = MagicMock(uni_a=uni1, uni_z=uni2) + evc._tag_lists_equal = MagicMock(return_value=False) + mock_evc.return_value = evc + payload = { + "name": "my evc1", + "uni_a": { + "interface_id": "00:00:00:00:00:00:00:01:1", + "tag": {"tag_type": 'vlan', "value": [[50, 100]]}, + }, + "uni_z": { + "interface_id": "00:00:00:00:00:00:00:02:2", + "tag": {"tag_type": 'vlan', "value": [[1, 10]]}, + }, + } + response = await self.api_client.post(url, json=payload) + assert response.status_code == 400, response.data + async def test_redeploy_evc(self): """Test endpoint to redeploy an EVC.""" evc1 = MagicMock() @@ -1454,7 +1554,7 @@ async def test_update_circuit( assert 409 == response.status_code assert "Can't update archived EVC" in response.json()["description"] - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1525,7 +1625,7 @@ async def test_update_circuit_invalid_json( assert 400 == response.status_code assert "must have a primary path or" in current_data["description"] - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1700,7 +1800,7 @@ def test_uni_from_dict_non_existent_intf(self): with pytest.raises(ValueError): self.napp._uni_from_dict(uni_dict) - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1773,7 +1873,7 @@ async def test_delete_no_evc(self): assert current_data["description"] == expected_data assert 404 == response.status_code - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.models.evc.EVC.remove_uni_tags") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.remove_current_flows") @@ -2216,14 +2316,18 @@ def test_load_evc(self, evc_from_dict_mock): evc_dict = MagicMock() assert not self.napp._load_evc(evc_dict) - # case2: archived evc + # case 2: early return with KytosTagError exception + evc_from_dict_mock.side_effect = KytosTagError("") + assert not self.napp._load_evc(evc_dict) + + # case 3: archived evc evc = MagicMock() evc.archived = True evc_from_dict_mock.side_effect = None evc_from_dict_mock.return_value = evc assert not self.napp._load_evc(evc_dict) - # case3: success creating + # case 4: success creating evc.archived = False evc.id = 1 self.napp.sched = MagicMock() @@ -2268,7 +2372,12 @@ def test_uni_from_dict(self, _get_interface_by_id_mock): uni = self.napp._uni_from_dict(uni_dict) assert uni == uni_mock - # case4: success creation without tag + # case4: success creation of tag list + uni_dict["tag"]["value"] = [[1, 10]] + uni = self.napp._uni_from_dict(uni_dict) + assert isinstance(uni.user_tag, TAGRange) + + # case5: success creation without tag uni_mock.user_tag = None del uni_dict["tag"] uni = self.napp._uni_from_dict(uni_dict) @@ -2364,6 +2473,21 @@ async def test_delete_bulk_metadata(self, event_loop): assert calls == 1 assert evc_mock.remove_metadata.call_count == 1 + async def test_delete_bulk_metadata_error(self, event_loop): + """Test bulk_delete_metadata with ciruit erroring""" + self.napp.controller.loop = event_loop + evc_mock = create_autospec(EVC) + evcs = [evc_mock, evc_mock] + self.napp.circuits = dict(zip(["1", "2"], evcs)) + payload = {"circuit_ids": ["1", "2", "3"]} + response = await self.api_client.request( + "DELETE", + f"{self.base_endpoint}/v2/evc/metadata/metadata1", + json=payload + ) + assert response.status_code == 404, response.data + assert response.json()["description"] == ["3"] + async def test_use_uni_tags(self, event_loop): """Test _use_uni_tags""" self.napp.controller.loop = event_loop @@ -2375,14 +2499,14 @@ async def test_use_uni_tags(self, event_loop): assert evc_mock._use_uni_vlan.call_args[0][0] == evc_mock.uni_z # One UNI tag is not available - evc_mock._use_uni_vlan.side_effect = [ValueError(), None] - with pytest.raises(ValueError): + evc_mock._use_uni_vlan.side_effect = [KytosTagError(""), None] + with pytest.raises(KytosTagError): self.napp._use_uni_tags(evc_mock) assert evc_mock._use_uni_vlan.call_count == 3 assert evc_mock.make_uni_vlan_available.call_count == 0 - evc_mock._use_uni_vlan.side_effect = [None, ValueError()] - with pytest.raises(ValueError): + evc_mock._use_uni_vlan.side_effect = [None, KytosTagError("")] + with pytest.raises(KytosTagError): self.napp._use_uni_tags(evc_mock) assert evc_mock._use_uni_vlan.call_count == 5 assert evc_mock.make_uni_vlan_available.call_count == 1 From 2c7b49c203e4cafbc9cf1abebe1532eaccc7ce7d Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Tue, 14 Nov 2023 14:18:41 -0500 Subject: [PATCH 17/28] Added `RANGE_SB_PRIORITY` - Updated changelog --- CHANGELOG.rst | 2 ++ models/evc.py | 9 +++++---- settings.py | 1 + tests/unit/models/test_evc_deploy.py | 9 +++++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 679cfde8..c7a96b54 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,8 @@ Added - Added a UI button for redeploying an EVC. - UNI tag_type are now accepted as string. - EVCs now listen to ``switch.interface.(link_up|link_down|created|deleted)`` events for activation/deactivation +- Circuits with a vlan range are supported now. The ranges follows ``list[list[int]]`` format and both UNIs vlan should have the same ranges. +- Added default priority ``RANGE_SB_PRIORITY`` with value as ``18000`` for circuits with vlan ranges. Changed ======= diff --git a/models/evc.py b/models/evc.py index b71d952f..b9a1eaf6 100644 --- a/models/evc.py +++ b/models/evc.py @@ -1219,7 +1219,7 @@ def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict: def get_priority(vlan): """Return priority value depending on vlan value""" if isinstance(vlan, list): - return settings.EVPL_SB_PRIORITY + return settings.RANGE_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: @@ -1272,9 +1272,9 @@ def _prepare_push_flow(self, *args, queue_id=None): Arguments: in_interface(str): Interface input. out_interface(str): Interface output. - in_vlan(int,str,list,None): Vlan input. + in_vlan(int,str,None): Vlan input. out_vlan(str): Vlan output. - new_c_vlan(str): New client vlan. + new_c_vlan(int,str,list,None): New client vlan. Return: dict: An python dictionary representing a FlowMod @@ -1282,8 +1282,9 @@ def _prepare_push_flow(self, *args, queue_id=None): """ # assign all arguments in_interface, out_interface, in_vlan, out_vlan, new_c_vlan = args + vlan_pri = in_vlan if not isinstance(new_c_vlan, list) else new_c_vlan flow_mod = self._prepare_flow_mod( - in_interface, out_interface, queue_id, in_vlan + in_interface, out_interface, queue_id, vlan_pri ) # the service tag must be always pushed new_action = {"action_type": "set_vlan", "vlan_id": out_vlan} diff --git a/settings.py b/settings.py index 68b81452..dcdc9a65 100644 --- a/settings.py +++ b/settings.py @@ -43,6 +43,7 @@ EPL_SB_PRIORITY = 10000 ANY_SB_PRIORITY = 15000 UNTAGGED_SB_PRIORITY = 20000 +RANGE_SB_PRIORITY = 18000 # Time (seconds) to check if an evc has been updated # or flows have been deleted. diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index f920d98a..4cb8388c 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -18,7 +18,8 @@ from napps.kytos.mef_eline.models import EVC, EVCDeploy, Path # NOQA from napps.kytos.mef_eline.settings import (ANY_SB_PRIORITY, # NOQA EPL_SB_PRIORITY, EVPL_SB_PRIORITY, - MANAGER_URL, SDN_TRACE_CP_URL, + MANAGER_URL, RANGE_SB_PRIORITY, + SDN_TRACE_CP_URL, UNTAGGED_SB_PRIORITY) from napps.kytos.mef_eline.tests.helpers import (get_link_mocked, # NOQA get_uni_mocked) @@ -1968,7 +1969,7 @@ def test_get_priority(self): assert epl_value == EPL_SB_PRIORITY epl_value = EVC.get_priority([[1, 5]]) - assert epl_value == EVPL_SB_PRIORITY + assert epl_value == RANGE_SB_PRIORITY def test_set_flow_table_group_id(self): """Test set_flow_table_group_id""" @@ -2053,11 +2054,11 @@ def test_prepare_direct_uni_flows(self): for i in range(0, 3): assert flows[i]["match"]["in_port"] == 1 assert flows[i]["match"]["dl_vlan"] == mask_list[i] - assert flows[i]["priority"] == EVPL_SB_PRIORITY + assert flows[i]["priority"] == RANGE_SB_PRIORITY for i in range(3, 6): assert flows[i]["match"]["in_port"] == 2 assert flows[i]["match"]["dl_vlan"] == mask_list[i-3] - assert flows[i]["priority"] == EVPL_SB_PRIORITY + assert flows[i]["priority"] == RANGE_SB_PRIORITY @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") def test_check_range(self, mock_check_range): From 1086884a3906b023f81aa932ec2cde66f5652920 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 16 Nov 2023 19:45:36 -0500 Subject: [PATCH 18/28] Using `_is_duplicated_evc()` when updating - Reverted RANGE_SB_PRIORITY adittion --- CHANGELOG.rst | 1 - main.py | 8 +++++++- models/evc.py | 2 +- settings.py | 1 - tests/unit/models/test_evc_deploy.py | 8 ++++---- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c7a96b54..698597ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,6 @@ Added - UNI tag_type are now accepted as string. - EVCs now listen to ``switch.interface.(link_up|link_down|created|deleted)`` events for activation/deactivation - Circuits with a vlan range are supported now. The ranges follows ``list[list[int]]`` format and both UNIs vlan should have the same ranges. -- Added default priority ``RANGE_SB_PRIORITY`` with value as ``18000`` for circuits with vlan ranges. Changed ======= diff --git a/main.py b/main.py index 99af8e4b..ddfb3433 100644 --- a/main.py +++ b/main.py @@ -384,6 +384,11 @@ def update(self, request: Request) -> JSONResponse: detail=f"Path is not valid: {exception}" ) from exception + if self._is_duplicated_evc(evc): + result = "The EVC already exists." + log.debug("create_circuit result %s %s", result, 409) + raise HTTPException(409, detail=result) + if evc.is_active(): if enable is False: # disable if active with evc.lock: @@ -723,7 +728,8 @@ def _is_duplicated_evc(self, evc): """ for circuit in tuple(self.circuits.values()): - if not circuit.archived and circuit.shares_uni(evc): + if (not circuit.archived and circuit._id != evc._id + and circuit.shares_uni(evc)): return True return False diff --git a/models/evc.py b/models/evc.py index b9a1eaf6..5f46ec75 100644 --- a/models/evc.py +++ b/models/evc.py @@ -1219,7 +1219,7 @@ def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict: def get_priority(vlan): """Return priority value depending on vlan value""" if isinstance(vlan, list): - return settings.RANGE_SB_PRIORITY + return settings.EVPL_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: diff --git a/settings.py b/settings.py index dcdc9a65..68b81452 100644 --- a/settings.py +++ b/settings.py @@ -43,7 +43,6 @@ EPL_SB_PRIORITY = 10000 ANY_SB_PRIORITY = 15000 UNTAGGED_SB_PRIORITY = 20000 -RANGE_SB_PRIORITY = 18000 # Time (seconds) to check if an evc has been updated # or flows have been deleted. diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index 4cb8388c..5e7ee526 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -18,7 +18,7 @@ from napps.kytos.mef_eline.models import EVC, EVCDeploy, Path # NOQA from napps.kytos.mef_eline.settings import (ANY_SB_PRIORITY, # NOQA EPL_SB_PRIORITY, EVPL_SB_PRIORITY, - MANAGER_URL, RANGE_SB_PRIORITY, + MANAGER_URL, SDN_TRACE_CP_URL, UNTAGGED_SB_PRIORITY) from napps.kytos.mef_eline.tests.helpers import (get_link_mocked, # NOQA @@ -1969,7 +1969,7 @@ def test_get_priority(self): assert epl_value == EPL_SB_PRIORITY epl_value = EVC.get_priority([[1, 5]]) - assert epl_value == RANGE_SB_PRIORITY + assert epl_value == EVPL_SB_PRIORITY def test_set_flow_table_group_id(self): """Test set_flow_table_group_id""" @@ -2054,11 +2054,11 @@ def test_prepare_direct_uni_flows(self): for i in range(0, 3): assert flows[i]["match"]["in_port"] == 1 assert flows[i]["match"]["dl_vlan"] == mask_list[i] - assert flows[i]["priority"] == RANGE_SB_PRIORITY + assert flows[i]["priority"] == EVPL_SB_PRIORITY for i in range(3, 6): assert flows[i]["match"]["in_port"] == 2 assert flows[i]["match"]["dl_vlan"] == mask_list[i-3] - assert flows[i]["priority"] == RANGE_SB_PRIORITY + assert flows[i]["priority"] == EVPL_SB_PRIORITY @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") def test_check_range(self, mock_check_range): From a29489619e26a6ebea63ad69ce6c386992644dfc Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 16 Nov 2023 20:02:56 -0500 Subject: [PATCH 19/28] Bypassed tag checking --- models/evc.py | 4 ++-- models/path.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/models/evc.py b/models/evc.py index 5f46ec75..be75104b 100644 --- a/models/evc.py +++ b/models/evc.py @@ -454,7 +454,7 @@ def _use_uni_vlan( if not tag: return uni.interface.use_tags( - self._controller, tag, tag_type + self._controller, tag, tag_type, False ) def make_uni_vlan_available( @@ -476,7 +476,7 @@ def make_uni_vlan_available( return try: conflict = uni.interface.make_tags_available( - self._controller, tag, tag_type + self._controller, tag, tag_type, False ) except KytosTagError as err: log.error(f"Error in circuit {self._id}: {err}") diff --git a/models/path.py b/models/path.py index 936ba85e..e94f1bfc 100644 --- a/models/path.py +++ b/models/path.py @@ -45,7 +45,7 @@ def make_vlans_available(self, controller): for link in self: tag = link.get_metadata("s_vlan") conflict_a, conflict_b = link.make_tags_available( - controller, tag.value, link.id, tag.tag_type + controller, tag.value, link.id, tag.tag_type, False ) if conflict_a: log.error(f"Tags {conflict_a} was already available in" From 7052f3bbe38179e70b79d60e23a3f30f137394f3 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 20 Nov 2023 18:47:40 -0500 Subject: [PATCH 20/28] Updated arguments Interface tag functions --- models/evc.py | 14 +++++++++----- models/path.py | 3 ++- utils.py | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/models/evc.py b/models/evc.py index 62419000..1cd42b52 100644 --- a/models/evc.py +++ b/models/evc.py @@ -454,7 +454,7 @@ def _use_uni_vlan( if not tag: return uni.interface.use_tags( - self._controller, tag, tag_type, False + self._controller, tag, tag_type, use_lock=True, check_order=False ) def make_uni_vlan_available( @@ -476,7 +476,8 @@ def make_uni_vlan_available( return try: conflict = uni.interface.make_tags_available( - self._controller, tag, tag_type, False + self._controller, tag, tag_type, use_lock=True, + check_order=False ) except KytosTagError as err: log.error(f"Error in circuit {self._id}: {err}") @@ -1424,7 +1425,9 @@ def check_trace( def check_range(circuit, traces: list) -> bool: """Check traces when for UNI with TAGRange""" check = True - for i, mask in enumerate(circuit.uni_a.user_tag.mask_list): + mask_list = (circuit.uni_a.user_tag.mask_list or + circuit.uni_z.user_tag.mask_list) + for i, mask in enumerate(mask_list): trace_a = traces[i*2] trace_z = traces[i*2+1] check &= EVCDeploy.check_trace( @@ -1452,7 +1455,8 @@ def check_list_traces(list_circuits: list) -> dict: i = 0 for circuit in list_circuits: if isinstance(circuit.uni_a.user_tag, TAGRange): - length = len(circuit.uni_a.user_tag.mask_list) + length = (len(circuit.uni_a.user_tag.mask_list) or + len(circuit.uni_z.user_tag.mask_list)) circuits_checked[circuit.id] = EVCDeploy.check_range( circuit, traces[i:i+length*2] ) @@ -1657,7 +1661,7 @@ def handle_interface_link_up(self, interface: Interface): """ Handler for interface link_up events """ - if self.archived: # TODO: Remove when addressing issue #369 + if self.archived: return if self.is_active(): return diff --git a/models/path.py b/models/path.py index e94f1bfc..53e25ee0 100644 --- a/models/path.py +++ b/models/path.py @@ -45,7 +45,8 @@ def make_vlans_available(self, controller): for link in self: tag = link.get_metadata("s_vlan") conflict_a, conflict_b = link.make_tags_available( - controller, tag.value, link.id, tag.tag_type, False + controller, tag.value, link.id, tag.tag_type, + check_order=False ) if conflict_a: log.error(f"Tags {conflict_a} was already available in" diff --git a/utils.py b/utils.py index ff59ea8c..a7d5a41d 100644 --- a/utils.py +++ b/utils.py @@ -125,7 +125,9 @@ def make_uni_list(list_circuits: list) -> list: for circuit in list_circuits: if isinstance(circuit.uni_a.user_tag, TAGRange): # TAGRange value from uni_a and uni_z are currently mirrored - for mask in circuit.uni_a.user_tag.mask_list: + mask_list = (circuit.uni_a.user_tag.mask_list or + circuit.uni_z.user_tag.mask_list) + for mask in mask_list: uni_list.append((circuit.uni_a.interface, mask)) uni_list.append((circuit.uni_z.interface, mask)) else: From 7dde1560abceb7845107f25a23dcddfb7ee6074f Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 6 Nov 2023 18:03:31 -0500 Subject: [PATCH 21/28] Added vlan_range support --- db/models.py | 4 +- main.py | 50 +++--- models/evc.py | 329 ++++++++++++++++++++++++++++----------- models/path.py | 18 ++- openapi.yml | 10 +- requirements/dev.in | 2 +- tests/unit/test_main.py | 27 +++- tests/unit/test_utils.py | 27 ++-- utils.py | 49 +++++- 9 files changed, 363 insertions(+), 153 deletions(-) diff --git a/db/models.py b/db/models.py index a104b712..346e36fe 100644 --- a/db/models.py +++ b/db/models.py @@ -38,11 +38,13 @@ class CircuitScheduleDoc(BaseModel): class TAGDoc(BaseModel): """TAG model""" tag_type: str - value: Union[int, str] + value: Union[int, str, list] @validator('value') def validate_value(cls, value): """Validate value when is a string""" + if isinstance(value, list): + return value if isinstance(value, int): return value if isinstance(value, str) and value in ("any", "untagged"): diff --git a/main.py b/main.py index 4f6dbb8f..1e46086b 100644 --- a/main.py +++ b/main.py @@ -12,19 +12,22 @@ from kytos.core import KytosNApp, log, rest from kytos.core.events import KytosEvent +from kytos.core.exceptions import KytosInvalidRanges, KytosTagsAreNotAvailable from kytos.core.helpers import (alisten_to, listen_to, load_spec, validate_openapi) -from kytos.core.interface import TAG, UNI +from kytos.core.interface import TAG, UNI, TAGRange from kytos.core.link import Link from kytos.core.rest_api import (HTTPException, JSONResponse, Request, get_json_or_400) +from kytos.core.tag_ranges import get_tag_ranges from napps.kytos.mef_eline import controllers, settings from napps.kytos.mef_eline.exceptions import DisabledSwitch, InvalidPath from napps.kytos.mef_eline.models import (EVC, DynamicPathManager, EVCDeploy, Path) from napps.kytos.mef_eline.scheduler import CircuitSchedule, Scheduler from napps.kytos.mef_eline.utils import (aemit_event, check_disabled_component, - emit_event, map_evc_event_content) + emit_event, get_vlan_tags_and_masks, + map_evc_event_content) # pylint: disable=too-many-public-methods @@ -269,11 +272,9 @@ def create_circuit(self, request: Request) -> JSONResponse: detail=f"backup_path is not valid: {exception}" ) from exception - # verify duplicated evc - if self._is_duplicated_evc(evc): - result = "The EVC already exists." - log.debug("create_circuit result %s %s", result, 409) - raise HTTPException(409, detail=result) + if not evc._tag_lists_equal(): + detail = "UNI_A and UNI_Z tag lists should be the same." + raise HTTPException(400, detail=detail) try: evc._validate_has_primary_or_dynamic() @@ -282,7 +283,7 @@ def create_circuit(self, request: Request) -> JSONResponse: try: self._use_uni_tags(evc) - except ValueError as exception: + except KytosTagsAreNotAvailable as exception: raise HTTPException(400, detail=str(exception)) from exception # save circuit @@ -315,12 +316,12 @@ def _use_uni_tags(evc): uni_a = evc.uni_a try: evc._use_uni_vlan(uni_a) - except ValueError as err: + except KytosTagsAreNotAvailable as err: raise err try: uni_z = evc.uni_z evc._use_uni_vlan(uni_z) - except ValueError as err: + except KytosTagsAreNotAvailable as err: evc.make_uni_vlan_available(uni_a) raise err @@ -370,6 +371,10 @@ def update(self, request: Request) -> JSONResponse: log.error(exception) log.debug("update result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception + except KytosTagsAreNotAvailable as exception: + log.error(exception) + log.debug("update result %s %s", exception, 400) + raise HTTPException(400, detail=str(exception)) from exception except DisabledSwitch as exception: log.debug("update result %s %s", exception, 409) raise HTTPException( @@ -705,21 +710,6 @@ def delete_schedule(self, request: Request) -> JSONResponse: log.debug("delete_schedule result %s %s", result, status) return JSONResponse(result, status_code=status) - def _is_duplicated_evc(self, evc): - """Verify if the circuit given is duplicated with the stored evcs. - - Args: - evc (EVC): circuit to be analysed. - - Returns: - boolean: True if the circuit is duplicated, otherwise False. - - """ - for circuit in tuple(self.circuits.values()): - if not circuit.archived and circuit.shares_uni(evc): - return True - return False - @listen_to("kytos/topology.link_up") def on_link_up(self, event): """Change circuit when link is up or end_maintenance.""" @@ -1001,7 +991,15 @@ def _uni_from_dict(self, uni_dict): tag_type = tag_dict.get("tag_type") tag_type = tag_convert.get(tag_type, tag_type) tag_value = tag_dict.get("value") - tag = TAG(tag_type, tag_value) + if isinstance(tag_value, list): + try: + tag_value = get_tag_ranges(tag_value) + except KytosInvalidRanges as err: + raise err + mask_list = get_vlan_tags_and_masks(tag_value) + tag = TAGRange(tag_type, tag_value, mask_list) + else: + tag = TAG(tag_type, tag_value) else: tag = None uni = UNI(interface, tag) diff --git a/models/evc.py b/models/evc.py index 3d1056de..f901dd27 100644 --- a/models/evc.py +++ b/models/evc.py @@ -1,6 +1,7 @@ """Classes used in the main application.""" # pylint: disable=too-many-lines import traceback from collections import OrderedDict +from copy import deepcopy from datetime import datetime from operator import eq, ne from threading import Lock @@ -13,16 +14,20 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity -from kytos.core.exceptions import KytosNoTagAvailableError +from kytos.core.exceptions import (KytosNoTagAvailableError, + KytosTagsAreNotAvailable, + KytosTagsNotInTagRanges) from kytos.core.helpers import get_time, now -from kytos.core.interface import UNI, Interface +from kytos.core.interface import UNI, Interface, TAGRange from kytos.core.link import Link +from kytos.core.tag_ranges import range_difference from napps.kytos.mef_eline import controllers, settings from napps.kytos.mef_eline.exceptions import FlowModException, InvalidPath from napps.kytos.mef_eline.utils import (check_disabled_component, compare_endpoint_trace, compare_uni_out_trace, emit_event, - map_dl_vlan, map_evc_event_content) + make_uni_list, map_dl_vlan, + map_evc_event_content) from .path import DynamicPathManager, Path @@ -171,7 +176,7 @@ def sync(self, keys: set = None): return self._mongo_controller.upsert_evc(self.as_dict()) - def _get_unis_use_tags(self, **kwargs) -> (UNI, UNI): + def _get_unis_use_tags(self, **kwargs) -> tuple[UNI, UNI]: """Obtain both UNIs (uni_a, uni_z). If a UNI is changing, verify tags""" uni_a = kwargs.get("uni_a", None) @@ -179,24 +184,27 @@ def _get_unis_use_tags(self, **kwargs) -> (UNI, UNI): if uni_a and uni_a != self.uni_a: uni_a_flag = True try: - self._use_uni_vlan(uni_a) - except ValueError as err: + # uni_a - self.uni_a + self._use_uni_vlan(uni_a, uni_dif=self.uni_a) + except KytosTagsAreNotAvailable as err: raise err uni_z = kwargs.get("uni_z", None) if uni_z and uni_z != self.uni_z: try: - self._use_uni_vlan(uni_z) - self.make_uni_vlan_available(self.uni_z) - except ValueError as err: + self._use_uni_vlan(uni_z, uni_dif=self.uni_z) + self.make_uni_vlan_available(self.uni_z, uni_dif=uni_z) + except KytosTagsAreNotAvailable as err: if uni_a_flag: - self.make_uni_vlan_available(uni_a) + # self.uni_a - uni_a + self.make_uni_vlan_available(uni_a, uni_dif=self.uni_a) raise err else: uni_z = self.uni_z if uni_a_flag: - self.make_uni_vlan_available(self.uni_a) + # self.uni_a - uni_a + self.make_uni_vlan_available(self.uni_a, uni_dif=uni_a) else: uni_a = self.uni_a return uni_a, uni_z @@ -217,6 +225,10 @@ def update(self, **kwargs): """ enable, redeploy = (None, None) + if not self._tag_lists_equal(**kwargs): + raise ValueError( + "UNI_A and UNI_Z tag lists should be the same." + ) uni_a, uni_z = self._get_unis_use_tags(**kwargs) check_disabled_component(uni_a, uni_z) self._validate_has_primary_or_dynamic( @@ -277,7 +289,6 @@ def _validate(self, **kwargs): """Do Basic validations. Verify required attributes: name, uni_a, uni_z - Verify if the attributes uni_a and uni_z are valid. Raises: ValueError: message with error detail. @@ -293,6 +304,23 @@ def _validate(self, **kwargs): if not isinstance(uni, UNI): raise ValueError(f"{attribute} is an invalid UNI.") + def _tag_lists_equal(self, **kwargs): + """Verify that tag lists are the same.""" + uni_a = kwargs.get("uni_a") or self.uni_a + uni_z = kwargs.get("uni_z") or self.uni_z + uni_a_list = uni_z_list = False + if (uni_a.user_tag and isinstance(uni_a.user_tag, TAGRange)): + uni_a_list = True + if (uni_z.user_tag and isinstance(uni_z.user_tag, TAGRange)): + uni_z_list = True + if uni_a_list and uni_z_list: + if uni_a.user_tag.value == uni_z.user_tag.value: + return True + return False + if uni_a_list == uni_z_list: + return True + return False + def _validate_has_primary_or_dynamic( self, primary_path=None, @@ -420,33 +448,52 @@ def archive(self): """Archive this EVC on deletion.""" self.archived = True - def _use_uni_vlan(self, uni: UNI): + def _use_uni_vlan( + self, + uni: UNI, + uni_dif: Union[None, UNI] = None + ): """Use tags from UNI""" if uni.user_tag is None: return tag = uni.user_tag.value + if not tag or isinstance(tag, str): + return tag_type = uni.user_tag.tag_type - if isinstance(tag, int): - result = uni.interface.use_tags( + if isinstance(tag, list) and uni_dif: + if isinstance(uni_dif.user_tag, list): + tag = range_difference(tag, uni_dif.user_tag.value) + try: + uni.interface.use_tags( self._controller, tag, tag_type ) - if not result: - intf = uni.interface.id - raise ValueError(f"Tag {tag} is not available in {intf}") + except KytosTagsAreNotAvailable as err: + raise err - def make_uni_vlan_available(self, uni: UNI): + def make_uni_vlan_available( + self, + uni: UNI, + uni_dif: Union[None, UNI] = None, + ): """Make available tag from UNI""" if uni.user_tag is None: return tag = uni.user_tag.value + if not tag or isinstance(tag, str): + return tag_type = uni.user_tag.tag_type - if isinstance(tag, int): - result = uni.interface.make_tags_available( + if isinstance(tag, list) and uni_dif: + if isinstance(uni_dif.user_tag, list): + tag = range_difference(tag, uni_dif.user_tag.value) + try: + conflict = uni.interface.make_tags_available( self._controller, tag, tag_type ) - if not result: - intf = uni.interface.id - log.warning(f"Tag {tag} was already available in {intf}") + except KytosTagsNotInTagRanges as err: + log.error(f"Error in circuit {self._id}: {err}") + if conflict: + intf = uni.interface.id + log.warning(f"Tags {conflict} was already available in {intf}") def remove_uni_tags(self): """Remove both UNI usage of a tag""" @@ -657,7 +704,10 @@ def remove_failover_flows(self, exclude_uni_switches=True, f"Error removing flows from switch {switch.id} for" f"EVC {self}: {err}" ) - self.failover_path.make_vlans_available(self._controller) + try: + self.failover_path.make_vlans_available(self._controller) + except KytosTagsNotInTagRanges as err: + log.error(f"Error when removing failover flows: {err}") self.failover_path = Path([]) if sync: self.sync() @@ -687,8 +737,10 @@ def remove_current_flows(self, current_path=None, force=True): f"Error removing flows from switch {switch.id} for" f"EVC {self}: {err}" ) - - current_path.make_vlans_available(self._controller) + try: + current_path.make_vlans_available(self._controller) + except KytosTagsNotInTagRanges as err: + log.error(f"Error when removing current path flows: {err}") self.current_path = Path([]) self.deactivate() self.sync() @@ -742,8 +794,10 @@ def remove_path_flows(self, path=None, force=True): "Error removing failover flows: " f"dpid={dpid} evc={self} error={err}" ) - - path.make_vlans_available(self._controller) + try: + path.make_vlans_available(self._controller) + except KytosTagsNotInTagRanges as err: + log.error(f"Error when removing path flows: {err}") @staticmethod def links_zipped(path=None): @@ -896,6 +950,7 @@ def get_failover_flows(self): return {} return self._prepare_uni_flows(self.failover_path, skip_out=True) + # pylint: disable=too-many-branches def _prepare_direct_uni_flows(self): """Prepare flows connecting two UNIs for intra-switch EVC.""" vlan_a = self._get_value_from_uni_tag(self.uni_a) @@ -910,13 +965,7 @@ def _prepare_direct_uni_flows(self): self.queue_id, vlan_z ) - if vlan_a is not None: - flow_mod_az["match"]["dl_vlan"] = vlan_a - - if vlan_z is not None: - flow_mod_za["match"]["dl_vlan"] = vlan_z - - if vlan_z not in self.special_cases: + if not isinstance(vlan_z, list) and vlan_z not in self.special_cases: flow_mod_az["actions"].insert( 0, {"action_type": "set_vlan", "vlan_id": vlan_z} ) @@ -924,8 +973,12 @@ def _prepare_direct_uni_flows(self): flow_mod_az["actions"].insert( 0, {"action_type": "push_vlan", "tag_type": "c"} ) + if vlan_a == 0: + flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"}) + elif vlan_a == 0 and vlan_z == "4096/4096": + flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"}) - if vlan_a not in self.special_cases: + if not isinstance(vlan_a, list) and vlan_a not in self.special_cases: flow_mod_za["actions"].insert( 0, {"action_type": "set_vlan", "vlan_id": vlan_a} ) @@ -935,15 +988,31 @@ def _prepare_direct_uni_flows(self): ) if vlan_z == 0: flow_mod_az["actions"].insert(0, {"action_type": "pop_vlan"}) - elif vlan_a == "4096/4096" and vlan_z == 0: flow_mod_az["actions"].insert(0, {"action_type": "pop_vlan"}) - elif vlan_a == 0 and vlan_z: - flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"}) - + flows = [] + if isinstance(vlan_a, list): + for mask_a in vlan_a: + flow_aux = deepcopy(flow_mod_az) + flow_aux["match"]["dl_vlan"] = mask_a + flows.append(flow_aux) + else: + if vlan_a is not None: + flow_mod_az["match"]["dl_vlan"] = vlan_a + flows.append(flow_mod_az) + + if isinstance(vlan_z, list): + for mask_z in vlan_z: + flow_aux = deepcopy(flow_mod_za) + flow_aux["match"]["dl_vlan"] = mask_z + flows.append(flow_aux) + else: + if vlan_z is not None: + flow_mod_za["match"]["dl_vlan"] = vlan_z + flows.append(flow_mod_za) return ( - self.uni_a.interface.switch.id, [flow_mod_az, flow_mod_za] + self.uni_a.interface.switch.id, flows ) def _install_direct_uni_flows(self): @@ -999,16 +1068,18 @@ def _install_nni_flows(self, path=None): self._send_flow_mods(dpid, flows) @staticmethod - def _get_value_from_uni_tag(uni): + def _get_value_from_uni_tag(uni: UNI): """Returns the value from tag. In case of any and untagged it should return 4096/4096 and 0 respectively""" special = {"any": "4096/4096", "untagged": 0} - if uni.user_tag: value = uni.user_tag.value + if isinstance(value, list): + return uni.user_tag.mask_list return special.get(value, value) return None + # pylint: disable=too-many-locals def _prepare_uni_flows(self, path=None, skip_in=False, skip_out=False): """Prepare flows to install UNIs.""" uni_flows = {} @@ -1036,15 +1107,27 @@ def _prepare_uni_flows(self, path=None, skip_in=False, skip_out=False): # Flow for one direction, pushing the service tag if not skip_in: - push_flow = self._prepare_push_flow( - self.uni_a.interface, - endpoint_a, - in_vlan_a, - out_vlan_a, - in_vlan_z, - queue_id=self.queue_id, - ) - flows_a.append(push_flow) + if isinstance(in_vlan_a, list): + for in_mask_a in in_vlan_a: + push_flow = self._prepare_push_flow( + self.uni_a.interface, + endpoint_a, + in_mask_a, + out_vlan_a, + in_vlan_z, + queue_id=self.queue_id, + ) + flows_a.append(push_flow) + else: + push_flow = self._prepare_push_flow( + self.uni_a.interface, + endpoint_a, + in_vlan_a, + out_vlan_a, + in_vlan_z, + queue_id=self.queue_id, + ) + flows_a.append(push_flow) # Flow for the other direction, popping the service tag if not skip_out: @@ -1063,15 +1146,27 @@ def _prepare_uni_flows(self, path=None, skip_in=False, skip_out=False): # Flow for one direction, pushing the service tag if not skip_in: - push_flow = self._prepare_push_flow( - self.uni_z.interface, - endpoint_z, - in_vlan_z, - out_vlan_z, - in_vlan_a, - queue_id=self.queue_id, - ) - flows_z.append(push_flow) + if isinstance(in_vlan_z, list): + for in_mask_z in in_vlan_z: + push_flow = self._prepare_push_flow( + self.uni_z.interface, + endpoint_z, + in_mask_z, + out_vlan_z, + in_vlan_a, + queue_id=self.queue_id, + ) + flows_z.append(push_flow) + else: + push_flow = self._prepare_push_flow( + self.uni_z.interface, + endpoint_z, + in_vlan_z, + out_vlan_z, + in_vlan_a, + queue_id=self.queue_id, + ) + flows_z.append(push_flow) # Flow for the other direction, popping the service tag if not skip_out: @@ -1133,6 +1228,8 @@ def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict: @staticmethod def get_priority(vlan): """Return priority value depending on vlan value""" + if isinstance(vlan, list): + return settings.EPL_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: @@ -1185,7 +1282,7 @@ def _prepare_push_flow(self, *args, queue_id=None): Arguments: in_interface(str): Interface input. out_interface(str): Interface output. - in_vlan(str): Vlan input. + in_vlan(int,str,list,None): Vlan input. out_vlan(str): Vlan output. new_c_vlan(str): New client vlan. @@ -1209,7 +1306,8 @@ def _prepare_push_flow(self, *args, queue_id=None): # if in_vlan is set, it must be included in the match flow_mod["match"]["dl_vlan"] = in_vlan - if new_c_vlan not in self.special_cases and in_vlan != new_c_vlan: + if (not isinstance(new_c_vlan, list) and in_vlan != new_c_vlan and + new_c_vlan not in self.special_cases): # new_in_vlan is an integer but zero, action to set is required new_action = {"action_type": "set_vlan", "vlan_id": new_c_vlan} flow_mod["actions"].insert(0, new_action) @@ -1226,7 +1324,9 @@ def _prepare_push_flow(self, *args, queue_id=None): new_action = {"action_type": "pop_vlan"} flow_mod["actions"].insert(0, new_action) - elif not in_vlan and new_c_vlan not in self.special_cases: + elif (not in_vlan and + (not isinstance(new_c_vlan, list) and + new_c_vlan not in self.special_cases)): # new_in_vlan is an integer but zero and in_vlan is not set # then it is set now new_action = {"action_type": "push_vlan", "tag_type": "c"} @@ -1248,21 +1348,23 @@ def _prepare_pop_flow( return flow_mod @staticmethod - def run_bulk_sdntraces(uni_list): + def run_bulk_sdntraces( + uni_list: list[tuple[Interface, Union[str, int, None]]] + ) -> dict: """Run SDN traces on control plane starting from EVC UNIs.""" endpoint = f"{settings.SDN_TRACE_CP_URL}/traces" data = [] - for uni in uni_list: + for interface, tag_value in uni_list: data_uni = { "trace": { "switch": { - "dpid": uni.interface.switch.dpid, - "in_port": uni.interface.port_number, + "dpid": interface.switch.dpid, + "in_port": interface.port_number, } } } - if uni.user_tag: - uni_dl_vlan = map_dl_vlan(uni.user_tag.value) + if tag_value: + uni_dl_vlan = map_dl_vlan(tag_value) if uni_dl_vlan: data_uni["trace"]["eth"] = { "dl_type": 0x8100, @@ -1279,24 +1381,32 @@ def run_bulk_sdntraces(uni_list): return {"result": []} return response.json() - # pylint: disable=too-many-return-statements + # pylint: disable=too-many-return-statements, too-many-arguments @staticmethod - def check_trace(circuit, trace_a, trace_z): + def check_trace( + tag_a: Union[None, int, str], + tag_z: Union[None, int, str], + interface_a: Interface, + interface_z: Interface, + current_path: list, + trace_a: list, + trace_z: list + ) -> bool: """Auxiliar function to check an individual trace""" if ( - len(trace_a) != len(circuit.current_path) + 1 - or not compare_uni_out_trace(circuit.uni_z, trace_a[-1]) + len(trace_a) != len(current_path) + 1 + or not compare_uni_out_trace(tag_z, interface_z, trace_a[-1]) ): log.warning(f"Invalid trace from uni_a: {trace_a}") return False if ( - len(trace_z) != len(circuit.current_path) + 1 - or not compare_uni_out_trace(circuit.uni_a, trace_z[-1]) + len(trace_z) != len(current_path) + 1 + or not compare_uni_out_trace(tag_a, interface_a, trace_z[-1]) ): log.warning(f"Invalid trace from uni_z: {trace_z}") return False - for link, trace1, trace2 in zip(circuit.current_path, + for link, trace1, trace2 in zip(current_path, trace_a[1:], trace_z[:0:-1]): metadata_vlan = None @@ -1320,33 +1430,66 @@ def check_trace(circuit, trace_a, trace_z): return True @staticmethod - def check_list_traces(list_circuits): + def check_range(circuit, traces: list) -> bool: + """Check traces when for UNI with TAGRange""" + check = True + for i, mask in enumerate(circuit.uni_a.user_tag.mask_list): + trace_a = traces[i*2] + trace_z = traces[i*2+1] + check &= EVCDeploy.check_trace( + mask, mask, + circuit.uni_a.interface, + circuit.uni_z.interface, + circuit.current_path, + trace_a, trace_z, + ) + return check + + @staticmethod + def check_list_traces(list_circuits: list) -> dict: """Check if current_path is deployed comparing with SDN traces.""" if not list_circuits: return {} - uni_list = [] - for circuit in list_circuits: - uni_list.append(circuit.uni_a) - uni_list.append(circuit.uni_z) - - traces = EVCDeploy.run_bulk_sdntraces(uni_list) - traces = traces["result"] - circuits_checked = {} + uni_list = make_uni_list(list_circuits) + traces = EVCDeploy.run_bulk_sdntraces(uni_list)["result"] + if not traces: - return circuits_checked + return {} try: - for i, circuit in enumerate(list_circuits): - trace_a = traces[2*i] - trace_z = traces[2*i+1] - circuits_checked[circuit.id] = EVCDeploy.check_trace( - circuit, trace_a, trace_z + circuits_checked = {} + i = 0 + for circuit in list_circuits: + if isinstance(circuit.uni_a.user_tag, TAGRange): + length = len(circuit.uni_a.user_tag.mask_list) + circuits_checked[circuit.id] = EVCDeploy.check_range( + circuit, traces[0:length*2] ) + i += length*2 + else: + trace_a = traces[i] + trace_z = traces[i+1] + tag_a = None + if circuit.uni_a.user_tag: + tag_a = circuit.uni_a.user_tag.value + tag_z = None + if circuit.uni_z.user_tag: + tag_z = circuit.uni_z.user_tag.value + circuits_checked[circuit.id] = EVCDeploy.check_trace( + tag_a, + tag_z, + circuit.uni_a.interface, + circuit.uni_z.interface, + circuit.current_path, + trace_a, trace_z + ) + i += 2 except IndexError as err: log.error( f"Bulk sdntraces returned fewer items than expected." f"Error = {err}" ) + return {} return circuits_checked diff --git a/models/path.py b/models/path.py index 6a4b2203..ed917e20 100644 --- a/models/path.py +++ b/models/path.py @@ -3,6 +3,7 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity +from kytos.core.exceptions import KytosTagsNotInTagRanges from kytos.core.interface import TAG from kytos.core.link import Link from napps.kytos.mef_eline import settings @@ -44,14 +45,17 @@ def make_vlans_available(self, controller): """Make the VLANs used in a path available when undeployed.""" for link in self: tag = link.get_metadata("s_vlan") - result_a, result_b = link.make_tags_available( - controller, tag.value, link.id, tag.tag_type - ) - if result_a is False: - log.error(f"Tag {tag} was already available in" + try: + conflict_a, conflict_b = link.make_tags_available( + controller, tag.value, link.id, tag.tag_type + ) + except KytosTagsNotInTagRanges as err: + raise err + if conflict_a: + log.error(f"Tags {conflict_a} was already available in" f"{link.endpoint_a.id}") - if result_b is False: - log.error(f"Tag {tag} was already available in" + if conflict_b: + log.error(f"Tags {conflict_b} was already available in" f"{link.endpoint_b.id}") link.remove_metadata("s_vlan") diff --git a/openapi.yml b/openapi.yml index 863fa216..d796279a 100644 --- a/openapi.yml +++ b/openapi.yml @@ -695,9 +695,13 @@ components: - type: integer format: int32 - type: string - enum: - - any - - untagged + - type: array + minItems: 1 + items: + anyOf: + - type: array + - type: integer + example: [[1, 500], 2096, [3001]] CircuitSchedule: # Can be referenced via '#/components/schemas/CircuitSchedule' type: object diff --git a/requirements/dev.in b/requirements/dev.in index 0683acaa..59cfbe24 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -5,5 +5,5 @@ # pip-compile --output-file requirements/dev.txt requirements/dev.in # -e git+https://github.com/kytos-ng/python-openflow.git#egg=python-openflow --e git+https://github.com/kytos-ng/kytos.git#egg=kytos[dev] +-e git+https://github.com/kytos-ng/kytos.git@epic/vlan_range#egg=kytos[dev] -e . diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 9384ae0b..096c6637 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -397,6 +397,7 @@ async def test_circuit_with_invalid_id(self): expected_result = "circuit_id 3 not found" assert response.json()["description"] == expected_result + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -413,6 +414,7 @@ async def test_create_a_circuit_case_1( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test create a new circuit.""" @@ -422,6 +424,7 @@ async def test_create_a_circuit_case_1( mongo_controller_upsert_mock.return_value = True evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -613,6 +616,7 @@ async def test_create_a_circuit_invalid_queue_id(self, event_loop): assert response.status_code == 400 assert expected_data in current_data["description"] + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -629,6 +633,7 @@ async def test_create_circuit_already_enabled( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test create an already created circuit.""" @@ -639,6 +644,7 @@ async def test_create_circuit_already_enabled( sched_add_mock.return_value = True evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -676,10 +682,17 @@ async def test_create_circuit_already_enabled( assert current_data["description"] == expected_data assert 409 == response.status_code + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._uni_from_dict") - async def test_create_circuit_case_5(self, uni_from_dict_mock, event_loop): + async def test_create_circuit_case_5( + self, + uni_from_dict_mock, + mock_tags_equal, + event_loop + ): """Test when neither primary path nor dynamic_backup_path is set.""" self.napp.controller.loop = event_loop + mock_tags_equal.return_value = True url = f"{self.base_endpoint}/v2/evc/" uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) @@ -1441,6 +1454,7 @@ async def test_update_circuit( assert 409 == response.status_code assert "Can't update archived EVC" in response.json()["description"] + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1457,6 +1471,7 @@ async def test_update_circuit_invalid_json( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test update a circuit circuit.""" @@ -1466,6 +1481,7 @@ async def test_update_circuit_invalid_json( sched_add_mock.return_value = True evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -1509,6 +1525,7 @@ async def test_update_circuit_invalid_json( assert 400 == response.status_code assert "must have a primary path or" in current_data["description"] + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1529,6 +1546,7 @@ async def test_update_circuit_invalid_path( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test update a circuit circuit.""" @@ -1540,6 +1558,7 @@ async def test_update_circuit_invalid_path( evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True link_from_dict_mock.return_value = 1 + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -1681,6 +1700,7 @@ def test_uni_from_dict_non_existent_intf(self): with pytest.raises(ValueError): self.napp._uni_from_dict(uni_dict) + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1695,6 +1715,7 @@ async def test_update_evc_no_json_mime( sched_add_mock, evc_deploy_mock, mock_use_uni_tags, + mock_tags_equal, event_loop ): """Test update a circuit with wrong mimetype.""" @@ -1703,6 +1724,7 @@ async def test_update_evc_no_json_mime( sched_add_mock.return_value = True evc_deploy_mock.return_value = True mock_use_uni_tags.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) @@ -1751,6 +1773,7 @@ async def test_delete_no_evc(self): assert current_data["description"] == expected_data assert 404 == response.status_code + @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") @patch("napps.kytos.mef_eline.models.evc.EVC.remove_uni_tags") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.remove_current_flows") @@ -1771,6 +1794,7 @@ async def test_delete_archived_evc( remove_current_flows_mock, mock_remove_tags, mock_use_uni, + mock_tags_equal, event_loop ): """Try to delete an archived EVC""" @@ -1781,6 +1805,7 @@ async def test_delete_archived_evc( evc_deploy_mock.return_value = True remove_current_flows_mock.return_value = True mock_use_uni.return_value = True + mock_tags_equal.return_value = True uni1 = create_autospec(UNI) uni2 = create_autospec(UNI) uni1.interface = create_autospec(Interface) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index d965bf15..64267151 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -42,26 +42,25 @@ def test_compare_endpoint_trace(self, switch, expected): def test_compare_uni_out_trace(self): """Test compare_uni_out_trace method.""" # case1: trace without 'out' info, should return True - uni = MagicMock() - assert compare_uni_out_trace(uni, {}) + interface = MagicMock() + assert compare_uni_out_trace(None, interface, {}) # case2: trace with valid port and VLAN, should return True - uni.interface.port_number = 1 - uni.user_tag.value = 123 + interface.port_number = 1 + tag_value = 123 trace = {"out": {"port": 1, "vlan": 123}} - assert compare_uni_out_trace(uni, trace) + assert compare_uni_out_trace(tag_value, interface, trace) # case3: UNI has VLAN but trace dont have, should return False trace = {"out": {"port": 1}} - assert compare_uni_out_trace(uni, trace) is False + assert compare_uni_out_trace(tag_value, interface, trace) is False # case4: UNI and trace dont have VLAN should return True - uni.user_tag = None - assert compare_uni_out_trace(uni, trace) + assert compare_uni_out_trace(None, interface, trace) # case5: UNI dont have VLAN but trace has, should return False trace = {"out": {"port": 1, "vlan": 123}} - assert compare_uni_out_trace(uni, trace) is False + assert compare_uni_out_trace(None, interface, trace) is False def test_map_dl_vlan(self): """Test map_dl_vlan""" @@ -76,13 +75,13 @@ def test_map_dl_vlan(self): ( [[101, 200]], [ - "101/4095", + 101, "102/4094", "104/4088", "112/4080", "128/4032", "192/4088", - "200/4095", + 200, ] ), ( @@ -91,7 +90,7 @@ def test_map_dl_vlan(self): ), ( [[34, 34]], - ["34/4095"] + [34] ), ( [ @@ -100,8 +99,8 @@ def test_map_dl_vlan(self): [130, 135] ], [ - "34/4095", - "128/4095", + 34, + 128, "130/4094", "132/4092" ] diff --git a/utils.py b/utils.py index efe258e2..ff59ea8c 100644 --- a/utils.py +++ b/utils.py @@ -1,7 +1,9 @@ """Utility functions.""" +from typing import Union + from kytos.core.common import EntityStatus from kytos.core.events import KytosEvent -from kytos.core.interface import UNI +from kytos.core.interface import UNI, Interface, TAGRange from napps.kytos.mef_eline.exceptions import DisabledSwitch @@ -43,7 +45,7 @@ def compare_endpoint_trace(endpoint, vlan, trace): ) -def map_dl_vlan(value): +def map_dl_vlan(value: Union[str, int]) -> bool: """Map dl_vlan value with the following criteria: dl_vlan = untagged or 0 -> None dl_vlan = any or "4096/4096" -> 1 @@ -59,16 +61,20 @@ def map_dl_vlan(value): return value & (mask & 4095) -def compare_uni_out_trace(uni, trace): +def compare_uni_out_trace( + tag_value: Union[None, int, str], + interface: Interface, + trace: dict +) -> bool: """Check if the trace last step (output) matches the UNI attributes.""" # keep compatibility for old versions of sdntrace-cp if "out" not in trace: return True if not isinstance(trace["out"], dict): return False - uni_vlan = map_dl_vlan(uni.user_tag.value) if uni.user_tag else None + uni_vlan = map_dl_vlan(tag_value) if tag_value else None return ( - uni.interface.port_number == trace["out"].get("port") + interface.port_number == trace["out"].get("port") and uni_vlan == trace["out"].get("vlan") ) @@ -80,7 +86,7 @@ def max_power2_divisor(number: int, limit: int = 4096) -> int: return limit -def get_vlan_tags_and_masks(tag_ranges: list[list[int]]) -> list[str]: +def get_vlan_tags_and_masks(tag_ranges: list[list[int]]) -> list[int, str]: """Get a list of vlan/mask pairs for a given list of ranges.""" masks_list = [] for start, end in tag_ranges: @@ -89,7 +95,11 @@ def get_vlan_tags_and_masks(tag_ranges: list[list[int]]) -> list[str]: divisor = max_power2_divisor(start) while divisor > limit - start: divisor //= 2 - masks_list.append(f"{start}/{4096-divisor}") + mask = 4096 - divisor + if mask == 4095: + masks_list.append(start) + else: + masks_list.append(f"{start}/{mask}") start += divisor return masks_list @@ -107,3 +117,28 @@ def check_disabled_component(uni_a: UNI, uni_z: UNI): if uni_z.interface.status == EntityStatus.DISABLED: id_ = uni_z.interface.id raise DisabledSwitch(f"Interface {id_} is disabled") + + +def make_uni_list(list_circuits: list) -> list: + """Make uni list to be sent to sdntrace""" + uni_list = [] + for circuit in list_circuits: + if isinstance(circuit.uni_a.user_tag, TAGRange): + # TAGRange value from uni_a and uni_z are currently mirrored + for mask in circuit.uni_a.user_tag.mask_list: + uni_list.append((circuit.uni_a.interface, mask)) + uni_list.append((circuit.uni_z.interface, mask)) + else: + tag_a = None + if circuit.uni_a.user_tag: + tag_a = circuit.uni_a.user_tag.value + uni_list.append( + (circuit.uni_a.interface, tag_a) + ) + tag_z = None + if circuit.uni_z.user_tag: + tag_z = circuit.uni_z.user_tag.value + uni_list.append( + (circuit.uni_z.interface, tag_z) + ) + return uni_list From 2e6f94ee810b2b8c596a2963a0d56193b16cdffa Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 9 Nov 2023 03:05:49 -0500 Subject: [PATCH 22/28] Fixed trace - Deleted unnecessary raises --- db/models.py | 2 +- main.py | 24 ++++++++++++------------ models/evc.py | 41 +++++++++++++++++------------------------ models/path.py | 9 +++------ 4 files changed, 33 insertions(+), 43 deletions(-) diff --git a/db/models.py b/db/models.py index 346e36fe..18b2d17d 100644 --- a/db/models.py +++ b/db/models.py @@ -38,7 +38,7 @@ class CircuitScheduleDoc(BaseModel): class TAGDoc(BaseModel): """TAG model""" tag_type: str - value: Union[int, str, list] + value: Union[int, str, list[list[int]]] @validator('value') def validate_value(cls, value): diff --git a/main.py b/main.py index 1e46086b..80a197ec 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from kytos.core import KytosNApp, log, rest from kytos.core.events import KytosEvent -from kytos.core.exceptions import KytosInvalidRanges, KytosTagsAreNotAvailable +from kytos.core.exceptions import KytosInvalidRanges, KytosTagsAreNotAvailable, KytosTagError from kytos.core.helpers import (alisten_to, listen_to, load_spec, validate_openapi) from kytos.core.interface import TAG, UNI, TAGRange @@ -237,7 +237,9 @@ def create_circuit(self, request: Request) -> JSONResponse: except ValueError as exception: log.debug("create_circuit result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception - + except KytosTagError as exception: + log.debug("create_circuit result %s %s", exception, 400) + raise HTTPException(400, detail=str(exception)) from exception try: check_disabled_component(evc.uni_a, evc.uni_z) except DisabledSwitch as exception: @@ -314,10 +316,7 @@ def create_circuit(self, request: Request) -> JSONResponse: @staticmethod def _use_uni_tags(evc): uni_a = evc.uni_a - try: - evc._use_uni_vlan(uni_a) - except KytosTagsAreNotAvailable as err: - raise err + evc._use_uni_vlan(uni_a) try: uni_z = evc.uni_z evc._use_uni_vlan(uni_z) @@ -365,14 +364,14 @@ def update(self, request: Request) -> JSONResponse: enable, redeploy = evc.update( **self._evc_dict_with_instances(data) ) + except KytosTagError as exception: + raise HTTPException(400, detail=str(exception)) from exception except ValidationError as exception: raise HTTPException(400, detail=str(exception)) from exception except ValueError as exception: - log.error(exception) log.debug("update result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception except KytosTagsAreNotAvailable as exception: - log.error(exception) log.debug("update result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception except DisabledSwitch as exception: @@ -901,6 +900,10 @@ def _load_evc(self, circuit_dict): f"Could not load EVC: dict={circuit_dict} error={exception}" ) return None + except KytosTagError as exception: + log.error( + f"Could not load EVC: dict={circuit_dict} error={exception}" + ) if evc.archived: return None @@ -992,10 +995,7 @@ def _uni_from_dict(self, uni_dict): tag_type = tag_convert.get(tag_type, tag_type) tag_value = tag_dict.get("value") if isinstance(tag_value, list): - try: - tag_value = get_tag_ranges(tag_value) - except KytosInvalidRanges as err: - raise err + tag_value = get_tag_ranges(tag_value) mask_list = get_vlan_tags_and_masks(tag_value) tag = TAGRange(tag_type, tag_value, mask_list) else: diff --git a/models/evc.py b/models/evc.py index f901dd27..51fbdf4d 100644 --- a/models/evc.py +++ b/models/evc.py @@ -183,11 +183,7 @@ def _get_unis_use_tags(self, **kwargs) -> tuple[UNI, UNI]: uni_a_flag = False if uni_a and uni_a != self.uni_a: uni_a_flag = True - try: - # uni_a - self.uni_a - self._use_uni_vlan(uni_a, uni_dif=self.uni_a) - except KytosTagsAreNotAvailable as err: - raise err + self._use_uni_vlan(uni_a, uni_dif=self.uni_a) uni_z = kwargs.get("uni_z", None) if uni_z and uni_z != self.uni_z: @@ -314,12 +310,8 @@ def _tag_lists_equal(self, **kwargs): if (uni_z.user_tag and isinstance(uni_z.user_tag, TAGRange)): uni_z_list = True if uni_a_list and uni_z_list: - if uni_a.user_tag.value == uni_z.user_tag.value: - return True - return False - if uni_a_list == uni_z_list: - return True - return False + return uni_a.user_tag.value == uni_z.user_tag.value + return uni_a_list == uni_z_list def _validate_has_primary_or_dynamic( self, @@ -460,15 +452,14 @@ def _use_uni_vlan( if not tag or isinstance(tag, str): return tag_type = uni.user_tag.tag_type - if isinstance(tag, list) and uni_dif: - if isinstance(uni_dif.user_tag, list): - tag = range_difference(tag, uni_dif.user_tag.value) - try: - uni.interface.use_tags( - self._controller, tag, tag_type - ) - except KytosTagsAreNotAvailable as err: - raise err + if (uni_dif and isinstance(tag, list) and + isinstance(uni_dif.user_tag, list)): + tag = range_difference(tag, uni_dif.user_tag.value) + if not tag: + return + uni.interface.use_tags( + self._controller, tag, tag_type + ) def make_uni_vlan_available( self, @@ -482,9 +473,11 @@ def make_uni_vlan_available( if not tag or isinstance(tag, str): return tag_type = uni.user_tag.tag_type - if isinstance(tag, list) and uni_dif: - if isinstance(uni_dif.user_tag, list): - tag = range_difference(tag, uni_dif.user_tag.value) + if (uni_dif and isinstance(tag, list) and + isinstance(uni_dif.user_tag, list)): + tag = range_difference(tag, uni_dif.user_tag.value) + if not tag: + return try: conflict = uni.interface.make_tags_available( self._controller, tag, tag_type @@ -1463,7 +1456,7 @@ def check_list_traces(list_circuits: list) -> dict: if isinstance(circuit.uni_a.user_tag, TAGRange): length = len(circuit.uni_a.user_tag.mask_list) circuits_checked[circuit.id] = EVCDeploy.check_range( - circuit, traces[0:length*2] + circuit, traces[i:i+length*2] ) i += length*2 else: diff --git a/models/path.py b/models/path.py index ed917e20..e5a596ba 100644 --- a/models/path.py +++ b/models/path.py @@ -45,12 +45,9 @@ def make_vlans_available(self, controller): """Make the VLANs used in a path available when undeployed.""" for link in self: tag = link.get_metadata("s_vlan") - try: - conflict_a, conflict_b = link.make_tags_available( - controller, tag.value, link.id, tag.tag_type - ) - except KytosTagsNotInTagRanges as err: - raise err + conflict_a, conflict_b = link.make_tags_available( + controller, tag.value, link.id, tag.tag_type + ) if conflict_a: log.error(f"Tags {conflict_a} was already available in" f"{link.endpoint_a.id}") From 8d452307e5ce5835f10e9cc11f2b281a8e1d4bef Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 9 Nov 2023 16:19:30 -0500 Subject: [PATCH 23/28] Updated exceptions --- db/models.py | 1 + main.py | 30 +++++++++++++++++++++++------- models/evc.py | 15 ++++++--------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/db/models.py b/db/models.py index 18b2d17d..64695eff 100644 --- a/db/models.py +++ b/db/models.py @@ -39,6 +39,7 @@ class TAGDoc(BaseModel): """TAG model""" tag_type: str value: Union[int, str, list[list[int]]] + mask_list: Optional[list[str, int]] @validator('value') def validate_value(cls, value): diff --git a/main.py b/main.py index 80a197ec..931901d2 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from kytos.core import KytosNApp, log, rest from kytos.core.events import KytosEvent -from kytos.core.exceptions import KytosInvalidRanges, KytosTagsAreNotAvailable, KytosTagError +from kytos.core.exceptions import KytosTagError from kytos.core.helpers import (alisten_to, listen_to, load_spec, validate_openapi) from kytos.core.interface import TAG, UNI, TAGRange @@ -274,6 +274,11 @@ def create_circuit(self, request: Request) -> JSONResponse: detail=f"backup_path is not valid: {exception}" ) from exception + if self._is_duplicated_evc(evc): + result = "The EVC already exists." + log.debug("create_circuit result %s %s", result, 409) + raise HTTPException(409, detail=result) + if not evc._tag_lists_equal(): detail = "UNI_A and UNI_Z tag lists should be the same." raise HTTPException(400, detail=detail) @@ -285,7 +290,7 @@ def create_circuit(self, request: Request) -> JSONResponse: try: self._use_uni_tags(evc) - except KytosTagsAreNotAvailable as exception: + except KytosTagError as exception: raise HTTPException(400, detail=str(exception)) from exception # save circuit @@ -320,7 +325,7 @@ def _use_uni_tags(evc): try: uni_z = evc.uni_z evc._use_uni_vlan(uni_z) - except KytosTagsAreNotAvailable as err: + except KytosTagError as err: evc.make_uni_vlan_available(uni_a) raise err @@ -371,9 +376,6 @@ def update(self, request: Request) -> JSONResponse: except ValueError as exception: log.debug("update result %s %s", exception, 400) raise HTTPException(400, detail=str(exception)) from exception - except KytosTagsAreNotAvailable as exception: - log.debug("update result %s %s", exception, 400) - raise HTTPException(400, detail=str(exception)) from exception except DisabledSwitch as exception: log.debug("update result %s %s", exception, 409) raise HTTPException( @@ -709,6 +711,21 @@ def delete_schedule(self, request: Request) -> JSONResponse: log.debug("delete_schedule result %s %s", result, status) return JSONResponse(result, status_code=status) + def _is_duplicated_evc(self, evc): + """Verify if the circuit given is duplicated with the stored evcs. + + Args: + evc (EVC): circuit to be analysed. + + Returns: + boolean: True if the circuit is duplicated, otherwise False. + + """ + for circuit in tuple(self.circuits.values()): + if not circuit.archived and circuit.shares_uni(evc): + return True + return False + @listen_to("kytos/topology.link_up") def on_link_up(self, event): """Change circuit when link is up or end_maintenance.""" @@ -1003,7 +1020,6 @@ def _uni_from_dict(self, uni_dict): else: tag = None uni = UNI(interface, tag) - return uni def _link_from_dict(self, link_dict): diff --git a/models/evc.py b/models/evc.py index 51fbdf4d..b42f118a 100644 --- a/models/evc.py +++ b/models/evc.py @@ -15,8 +15,7 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity from kytos.core.exceptions import (KytosNoTagAvailableError, - KytosTagsAreNotAvailable, - KytosTagsNotInTagRanges) + KytosTagError) from kytos.core.helpers import get_time, now from kytos.core.interface import UNI, Interface, TAGRange from kytos.core.link import Link @@ -190,16 +189,14 @@ def _get_unis_use_tags(self, **kwargs) -> tuple[UNI, UNI]: try: self._use_uni_vlan(uni_z, uni_dif=self.uni_z) self.make_uni_vlan_available(self.uni_z, uni_dif=uni_z) - except KytosTagsAreNotAvailable as err: + except KytosTagError as err: if uni_a_flag: - # self.uni_a - uni_a self.make_uni_vlan_available(uni_a, uni_dif=self.uni_a) raise err else: uni_z = self.uni_z if uni_a_flag: - # self.uni_a - uni_a self.make_uni_vlan_available(self.uni_a, uni_dif=uni_a) else: uni_a = self.uni_a @@ -482,7 +479,7 @@ def make_uni_vlan_available( conflict = uni.interface.make_tags_available( self._controller, tag, tag_type ) - except KytosTagsNotInTagRanges as err: + except KytosTagError as err: log.error(f"Error in circuit {self._id}: {err}") if conflict: intf = uni.interface.id @@ -699,7 +696,7 @@ def remove_failover_flows(self, exclude_uni_switches=True, ) try: self.failover_path.make_vlans_available(self._controller) - except KytosTagsNotInTagRanges as err: + except KytosTagError as err: log.error(f"Error when removing failover flows: {err}") self.failover_path = Path([]) if sync: @@ -732,7 +729,7 @@ def remove_current_flows(self, current_path=None, force=True): ) try: current_path.make_vlans_available(self._controller) - except KytosTagsNotInTagRanges as err: + except KytosTagError as err: log.error(f"Error when removing current path flows: {err}") self.current_path = Path([]) self.deactivate() @@ -789,7 +786,7 @@ def remove_path_flows(self, path=None, force=True): ) try: path.make_vlans_available(self._controller) - except KytosTagsNotInTagRanges as err: + except KytosTagError as err: log.error(f"Error when removing path flows: {err}") @staticmethod From 6e714542c2d9c62323ffd654f3b6810baa746075 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 13 Nov 2023 02:35:13 -0500 Subject: [PATCH 24/28] Added unit tests - Fixed partial vlan list update --- main.py | 3 +- models/evc.py | 12 +- models/path.py | 1 - tests/helpers.py | 7 +- tests/unit/models/test_evc_base.py | 105 ++++++++++--- tests/unit/models/test_evc_deploy.py | 214 ++++++++++++++++++++++++--- tests/unit/test_db_models.py | 5 + tests/unit/test_main.py | 154 +++++++++++++++++-- 8 files changed, 440 insertions(+), 61 deletions(-) diff --git a/main.py b/main.py index 931901d2..99af8e4b 100644 --- a/main.py +++ b/main.py @@ -202,6 +202,7 @@ def get_circuit(self, request: Request) -> JSONResponse: log.debug("get_circuit result %s %s", circuit, status) return JSONResponse(circuit, status_code=status) + # pylint: disable=too-many-branches, too-many-statements @rest("/v2/evc/", methods=["POST"]) @validate_openapi(spec) def create_circuit(self, request: Request) -> JSONResponse: @@ -921,7 +922,7 @@ def _load_evc(self, circuit_dict): log.error( f"Could not load EVC: dict={circuit_dict} error={exception}" ) - + return None if evc.archived: return None diff --git a/models/evc.py b/models/evc.py index b42f118a..faeb5278 100644 --- a/models/evc.py +++ b/models/evc.py @@ -14,8 +14,7 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity -from kytos.core.exceptions import (KytosNoTagAvailableError, - KytosTagError) +from kytos.core.exceptions import KytosNoTagAvailableError, KytosTagError from kytos.core.helpers import get_time, now from kytos.core.interface import UNI, Interface, TAGRange from kytos.core.link import Link @@ -449,8 +448,8 @@ def _use_uni_vlan( if not tag or isinstance(tag, str): return tag_type = uni.user_tag.tag_type - if (uni_dif and isinstance(tag, list) and - isinstance(uni_dif.user_tag, list)): + if (uni_dif and isinstance(tag, list) and + isinstance(uni_dif.user_tag.value, list)): tag = range_difference(tag, uni_dif.user_tag.value) if not tag: return @@ -471,7 +470,7 @@ def make_uni_vlan_available( return tag_type = uni.user_tag.tag_type if (uni_dif and isinstance(tag, list) and - isinstance(uni_dif.user_tag, list)): + isinstance(uni_dif.user_tag.value, list)): tag = range_difference(tag, uni_dif.user_tag.value) if not tag: return @@ -481,6 +480,7 @@ def make_uni_vlan_available( ) except KytosTagError as err: log.error(f"Error in circuit {self._id}: {err}") + return if conflict: intf = uni.interface.id log.warning(f"Tags {conflict} was already available in {intf}") @@ -1219,7 +1219,7 @@ def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict: def get_priority(vlan): """Return priority value depending on vlan value""" if isinstance(vlan, list): - return settings.EPL_SB_PRIORITY + return settings.EVPL_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: diff --git a/models/path.py b/models/path.py index e5a596ba..936ba85e 100644 --- a/models/path.py +++ b/models/path.py @@ -3,7 +3,6 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity -from kytos.core.exceptions import KytosTagsNotInTagRanges from kytos.core.interface import TAG from kytos.core.link import Link from napps.kytos.mef_eline import settings diff --git a/tests/helpers.py b/tests/helpers.py index 388330be..341a257c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -4,7 +4,7 @@ from kytos.core import Controller from kytos.core.common import EntityStatus from kytos.core.config import KytosConfig -from kytos.core.interface import TAG, UNI, Interface +from kytos.core.interface import TAG, TAGRange, UNI, Interface from kytos.core.link import Link from kytos.core.switch import Switch from kytos.lib.helpers import get_interface_mock, get_switch_mock @@ -101,7 +101,10 @@ def get_uni_mocked(**kwargs): switch.id = kwargs.get("switch_id", "custom_switch_id") switch.dpid = kwargs.get("switch_dpid", "custom_switch_dpid") interface = Interface(interface_name, interface_port, switch) - tag = TAG(tag_type, tag_value) + if isinstance(tag_value, list): + tag = TAGRange(tag_type, tag_value) + else: + tag = TAG(tag_type, tag_value) uni = Mock(spec=UNI, interface=interface, user_tag=tag) uni.is_valid.return_value = is_valid uni.as_dict.return_value = { diff --git a/tests/unit/models/test_evc_base.py b/tests/unit/models/test_evc_base.py index bb7475ed..cb427962 100644 --- a/tests/unit/models/test_evc_base.py +++ b/tests/unit/models/test_evc_base.py @@ -1,6 +1,8 @@ """Module to test the EVCBase class.""" import sys from unittest.mock import MagicMock, patch, call +from kytos.core.exceptions import KytosTagError +from kytos.core.interface import TAGRange from napps.kytos.mef_eline.models import Path import pytest # pylint: disable=wrong-import-position @@ -253,6 +255,22 @@ def test_update_queue_null(self, _sync_mock): _, redeploy = evc.update(**update_dict) assert redeploy + def test_update_different_tag_lists(self): + """Test update when tag lists are different.""" + attributes = { + "controller": get_controller_mock(), + "name": "circuit_name", + "enable": True, + "dynamic_backup_path": True, + "uni_a": get_uni_mocked(is_valid=True), + "uni_z": get_uni_mocked(is_valid=True), + } + uni = MagicMock(user_tag=TAGRange("vlan", [[1, 10]])) + update_dict = {"uni_a": uni} + evc = EVC(**attributes) + with pytest.raises(ValueError): + evc.update(**update_dict) + def test_circuit_representation(self): """Test the method __repr__.""" attributes = { @@ -488,13 +506,19 @@ def test_get_unis_use_tags(self): unis = {"uni_a": new_uni_a, "uni_z": new_uni_z} evc._get_unis_use_tags(**unis) - expected = [call(new_uni_a), call(new_uni_z)] + expected = [ + call(new_uni_a, uni_dif=old_uni_a), + call(new_uni_z, uni_dif=old_uni_z) + ] evc._use_uni_vlan.assert_has_calls(expected) - expected = [call(old_uni_z), call(old_uni_a)] + expected = [ + call(old_uni_z, uni_dif=new_uni_z), + call(old_uni_a, uni_dif=new_uni_a) + ] evc.make_uni_vlan_available.assert_has_calls(expected) def test_get_unis_use_tags_error(self): - """Test _get_unis_use_tags with ValueError""" + """Test _get_unis_use_tags with KytosTagError""" old_uni_a = get_uni_mocked( interface_port=2, is_valid=True @@ -513,34 +537,38 @@ def test_get_unis_use_tags_error(self): evc = EVC(**attributes) evc._use_uni_vlan = MagicMock() - # UNI Z ValueError - evc._use_uni_vlan.side_effect = [None, ValueError()] + # UNI Z KytosTagError + evc._use_uni_vlan.side_effect = [None, KytosTagError("")] evc.make_uni_vlan_available = MagicMock() new_uni_a = get_uni_mocked(tag_value=200, is_valid=True) new_uni_z = get_uni_mocked(tag_value=200, is_valid=True) unis = {"uni_a": new_uni_a, "uni_z": new_uni_z} - with pytest.raises(ValueError): + with pytest.raises(KytosTagError): evc._get_unis_use_tags(**unis) - expected = [call(new_uni_a), call(new_uni_z)] + expected = [ + call(new_uni_a, uni_dif=old_uni_a), + call(new_uni_z, uni_dif=old_uni_z) + ] evc._use_uni_vlan.assert_has_calls(expected) assert evc.make_uni_vlan_available.call_count == 1 assert evc.make_uni_vlan_available.call_args[0][0] == new_uni_a - # UNI A ValueError + # UNI A KytosTagError evc = EVC(**attributes) evc._use_uni_vlan = MagicMock() - evc._use_uni_vlan.side_effect = [ValueError(), None] + evc._use_uni_vlan.side_effect = [KytosTagError(""), None] evc.make_uni_vlan_available = MagicMock() new_uni_a = get_uni_mocked(tag_value=200, is_valid=True) new_uni_z = get_uni_mocked(tag_value=200, is_valid=True) unis = {"uni_a": new_uni_a, "uni_z": new_uni_z} - with pytest.raises(ValueError): + with pytest.raises(KytosTagError): evc._get_unis_use_tags(**unis) assert evc._use_uni_vlan.call_count == 1 assert evc._use_uni_vlan.call_args[0][0] == new_uni_a assert evc.make_uni_vlan_available.call_count == 0 - def test_use_uni_vlan(self): + @patch("napps.kytos.mef_eline.models.evc.range_difference") + def test_use_uni_vlan(self, mock_difference): """Test _use_uni_vlan""" attributes = { "controller": get_controller_mock(), @@ -558,16 +586,31 @@ def test_use_uni_vlan(self): assert args[2] == uni.user_tag.tag_type assert uni.interface.use_tags.call_count == 1 - uni.interface.use_tags.return_value = False - with pytest.raises(ValueError): - evc._use_uni_vlan(uni) + uni.user_tag.value = "any" + evc._use_uni_vlan(uni) + assert uni.interface.use_tags.call_count == 1 + + uni.user_tag.value = [[1, 10]] + uni_dif = get_uni_mocked(tag_value=[[1, 2]]) + mock_difference.return_value = [[3, 10]] + evc._use_uni_vlan(uni, uni_dif) + assert uni.interface.use_tags.call_count == 2 + + mock_difference.return_value = [] + evc._use_uni_vlan(uni, uni_dif) assert uni.interface.use_tags.call_count == 2 + uni.interface.use_tags.side_effect = KytosTagError("") + with pytest.raises(KytosTagError): + evc._use_uni_vlan(uni) + assert uni.interface.use_tags.call_count == 3 + uni.user_tag = None evc._use_uni_vlan(uni) - assert uni.interface.use_tags.call_count == 2 + assert uni.interface.use_tags.call_count == 3 - def test_make_uni_vlan_available(self): + @patch("napps.kytos.mef_eline.models.evc.log") + def test_make_uni_vlan_available(self, mock_log): """Test make_uni_vlan_available""" attributes = { "controller": get_controller_mock(), @@ -586,13 +629,22 @@ def test_make_uni_vlan_available(self): assert args[2] == uni.user_tag.tag_type assert uni.interface.make_tags_available.call_count == 1 - uni.interface.make_tags_available.return_value = False + uni.user_tag.value = None evc.make_uni_vlan_available(uni) + assert uni.interface.make_tags_available.call_count == 1 + + uni.user_tag.value = [[1, 10]] + uni_dif = get_uni_mocked(tag_value=[[1, 2]]) + evc.make_uni_vlan_available(uni, uni_dif) assert uni.interface.make_tags_available.call_count == 2 + uni.interface.make_tags_available.side_effect = KytosTagError("") + evc.make_uni_vlan_available(uni) + assert mock_log.error.call_count == 1 + uni.user_tag = None evc.make_uni_vlan_available(uni) - assert uni.interface.make_tags_available.call_count == 2 + assert uni.interface.make_tags_available.call_count == 3 def test_remove_uni_tags(self): """Test remove_uni_tags""" @@ -607,3 +659,20 @@ def test_remove_uni_tags(self): evc.make_uni_vlan_available = MagicMock() evc.remove_uni_tags() assert evc.make_uni_vlan_available.call_count == 2 + + def test_tag_lists_equal(self): + """Test _tag_lists_equal""" + attributes = { + "controller": get_controller_mock(), + "name": "circuit_name", + "enable": True, + "uni_a": get_uni_mocked(is_valid=True), + "uni_z": get_uni_mocked(is_valid=True) + } + evc = EVC(**attributes) + uni = MagicMock(user_tag=TAGRange("vlan", [[1, 10]])) + update_dict = {"uni_z": uni} + assert evc._tag_lists_equal(**update_dict) is False + + update_dict = {"uni_a": uni, "uni_z": uni} + assert evc._tag_lists_equal(**update_dict) diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index e61c7df7..f920d98a 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -9,7 +9,7 @@ from kytos.core.exceptions import KytosNoTagAvailableError from kytos.core.interface import Interface from kytos.core.switch import Switch - +from requests.exceptions import Timeout # pylint: disable=wrong-import-position sys.path.insert(0, "/var/lib/kytos/napps/..") # pylint: enable=wrong-import-position @@ -193,6 +193,12 @@ def test_prepare_flow_mod(self): } assert expected_flow_mod == flow_mod + evc.sb_priority = 1234 + flow_mod = evc._prepare_flow_mod(interface_a, interface_z, 3) + assert flow_mod["priority"] == 1234 + assert flow_mod["actions"][1]["action_type"] == "set_queue" + assert flow_mod["actions"][1]["queue_id"] == 3 + def test_prepare_pop_flow(self): """Test prepare pop flow method.""" attributes = { @@ -1385,7 +1391,8 @@ def test_run_bulk_sdntraces(self, put_mock): } } ] - result = EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + arg_tuple = [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + result = EVCDeploy.run_bulk_sdntraces(arg_tuple) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1394,7 +1401,12 @@ def test_run_bulk_sdntraces(self, put_mock): assert result['result'] == "ok" response.status_code = 400 - result = EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + result = EVCDeploy.run_bulk_sdntraces(arg_tuple) + assert result == {"result": []} + + put_mock.side_effect = Timeout + response.status_code = 200 + result = EVCDeploy.run_bulk_sdntraces(arg_tuple) assert result == {"result": []} @patch("requests.put") @@ -1413,9 +1425,10 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): } } ] - evc.uni_a.user_tag.value = 'untagged' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1425,7 +1438,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): assert 'eth' not in args evc.uni_a.user_tag.value = 0 - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1435,7 +1450,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): assert 'eth' not in args evc.uni_a.user_tag.value = '5/2' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1446,7 +1463,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): expected_payload[0]['trace']['eth'] = {'dl_type': 0x8100, 'dl_vlan': 1} evc.uni_a.user_tag.value = 'any' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1456,7 +1475,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): assert args['eth'] == {'dl_type': 33024, 'dl_vlan': 1} evc.uni_a.user_tag.value = '4096/4096' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1470,7 +1491,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): 'dl_vlan': 10 } evc.uni_a.user_tag.value = '10/10' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1484,7 +1507,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): 'dl_vlan': 1 } evc.uni_a.user_tag.value = '5/3' - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1498,7 +1523,9 @@ def test_run_bulk_sdntraces_special_vlan(self, put_mock): 'dl_vlan': 10 } evc.uni_a.user_tag.value = 10 - EVCDeploy.run_bulk_sdntraces([evc.uni_a]) + EVCDeploy.run_bulk_sdntraces( + [(evc.uni_a.interface, evc.uni_a.user_tag.value)] + ) put_mock.assert_called_with( expected_endpoint, json=expected_payload, @@ -1836,6 +1863,45 @@ def test_check_list_traces_invalid_types(self, run_bulk_sdntraces_mock, _): # type loop assert result[evc.id] is False + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_range") + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.run_bulk_sdntraces") + def test_check_list_traces_vlan_list(self, *args): + """Test check_list_traces with vlan list""" + mock_bulk, mock_range, mock_trace = args + mask_list = [1, '2/4094', '4/4094'] + evc = self.create_evc_inter_switch([[1, 5]], [[1, 5]]) + evc.uni_a.user_tag.mask_list = mask_list + evc.uni_z.user_tag.mask_list = mask_list + mock_bulk.return_value = {"result": ["mock"] * 6} + mock_range.return_value = True + actual_return = EVC.check_list_traces([evc]) + assert actual_return == {evc._id: True} + assert mock_trace.call_count == 0 + assert mock_range.call_count == 1 + args = mock_range.call_args[0] + assert args[0] == evc + assert args[1] == ["mock"] * 6 + + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") + @patch("napps.kytos.mef_eline.models.evc.log") + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.run_bulk_sdntraces") + def test_check_list_traces_empty(self, mock_bulk, mock_log, mock_trace): + """Test check_list_traces with empty return""" + evc = self.create_evc_inter_switch(1, 1) + actual_return = EVC.check_list_traces([]) + assert not actual_return + + mock_bulk.return_value = {"result": []} + actual_return = EVC.check_list_traces([evc]) + assert not actual_return + + mock_bulk.return_value = {"result": ["mock"]} + mock_trace.return_value = True + actual_return = EVC.check_list_traces([evc]) + assert mock_log.error.call_count == 1 + assert not actual_return + @patch( "napps.kytos.mef_eline.models.path.DynamicPathManager" ".get_disjoint_paths" @@ -1865,22 +1931,28 @@ def test_is_eligible_for_failover_path(self): def test_get_value_from_uni_tag(self): """Test _get_value_from_uni_tag""" - uni = get_uni_mocked(tag_value=None) - value = EVC._get_value_from_uni_tag(uni) - assert value is None - uni = get_uni_mocked(tag_value="any") value = EVC._get_value_from_uni_tag(uni) assert value == "4096/4096" - uni = get_uni_mocked(tag_value="untagged") + uni.user_tag.value = "untagged" value = EVC._get_value_from_uni_tag(uni) assert value == 0 - uni = get_uni_mocked(tag_value=100) + uni.user_tag.value = 100 value = EVC._get_value_from_uni_tag(uni) assert value == 100 + uni.user_tag = None + value = EVC._get_value_from_uni_tag(uni) + assert value is None + + uni = get_uni_mocked(tag_value=[[12, 20]]) + uni.user_tag.mask_list = ['12/4092', '16/4092', '20/4094'] + + value = EVC._get_value_from_uni_tag(uni) + assert value == ['12/4092', '16/4092', '20/4094'] + def test_get_priority(self): """Test get_priority_from_vlan""" evpl_value = EVC.get_priority(100) @@ -1895,6 +1967,9 @@ def test_get_priority(self): epl_value = EVC.get_priority(None) assert epl_value == EPL_SB_PRIORITY + epl_value = EVC.get_priority([[1, 5]]) + assert epl_value == EVPL_SB_PRIORITY + def test_set_flow_table_group_id(self): """Test set_flow_table_group_id""" self.evc_deploy.table_group = {"epl": 3, "evpl": 4} @@ -1915,3 +1990,106 @@ def test_get_endpoint_by_id(self): assert result == link.endpoint_a result = self.evc_deploy.get_endpoint_by_id(link, "01", operator.ne) assert result == link.endpoint_b + + @patch("napps.kytos.mef_eline.models.evc.EVC._prepare_pop_flow") + @patch("napps.kytos.mef_eline.models.evc.EVC.get_endpoint_by_id") + @patch("napps.kytos.mef_eline.models.evc.EVC._prepare_push_flow") + def test_prepare_uni_flows(self, mock_push, mock_endpoint, _): + """Test _prepare_uni_flows""" + mask_list = [1, '2/4094', '4/4094'] + uni_a = get_uni_mocked(interface_port=1, tag_value=[[1, 5]]) + uni_a.user_tag.mask_list = mask_list + uni_z = get_uni_mocked(interface_port=2, tag_value=[[1, 5]]) + uni_z.user_tag.mask_list = mask_list + mock_endpoint.return_value = "mock_endpoint" + attributes = { + "table_group": {"evpl": 3, "epl": 4}, + "controller": get_controller_mock(), + "name": "custom_name", + "uni_a": uni_a, + "uni_z": uni_z, + } + evc = EVC(**attributes) + link = get_link_mocked() + evc._prepare_uni_flows(Path([link])) + call_list = [] + for i in range(0, 3): + call_list.append(call( + uni_a.interface, + "mock_endpoint", + mask_list[i], + None, + mask_list, + queue_id=-1 + )) + for i in range(0, 3): + call_list.append(call( + uni_z.interface, + "mock_endpoint", + mask_list[i], + None, + mask_list, + queue_id=-1 + )) + mock_push.assert_has_calls(call_list) + + def test_prepare_direct_uni_flows(self): + """Test _prepare_direct_uni_flows""" + mask_list = [1, '2/4094', '4/4094'] + uni_a = get_uni_mocked(interface_port=1, tag_value=[[1, 5]]) + uni_a.user_tag.mask_list = mask_list + uni_z = get_uni_mocked(interface_port=2, tag_value=[[1, 5]]) + uni_z.user_tag.mask_list = mask_list + attributes = { + "table_group": {"evpl": 3, "epl": 4}, + "controller": get_controller_mock(), + "name": "custom_name", + "uni_a": uni_a, + "uni_z": uni_z, + } + evc = EVC(**attributes) + flows = evc._prepare_direct_uni_flows()[1] + assert len(flows) == 6 + for i in range(0, 3): + assert flows[i]["match"]["in_port"] == 1 + assert flows[i]["match"]["dl_vlan"] == mask_list[i] + assert flows[i]["priority"] == EVPL_SB_PRIORITY + for i in range(3, 6): + assert flows[i]["match"]["in_port"] == 2 + assert flows[i]["match"]["dl_vlan"] == mask_list[i-3] + assert flows[i]["priority"] == EVPL_SB_PRIORITY + + @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") + def test_check_range(self, mock_check_range): + """Test check_range""" + mask_list = [1, '2/4094', '4/4094'] + uni_a = get_uni_mocked(interface_port=1, tag_value=[[1, 5]]) + uni_a.user_tag.mask_list = mask_list + uni_z = get_uni_mocked(interface_port=2, tag_value=[[1, 5]]) + uni_z.user_tag.mask_list = mask_list + attributes = { + "table_group": {"evpl": 3, "epl": 4}, + "controller": get_controller_mock(), + "name": "custom_name", + "uni_a": uni_a, + "uni_z": uni_z, + } + circuit = EVC(**attributes) + traces = list(range(0, 6)) + mock_check_range.return_value = True + check = EVC.check_range(circuit, traces) + call_list = [] + for i in range(0, 3): + call_list.append(call( + mask_list[i], mask_list[i], + uni_a.interface, + uni_z.interface, + circuit.current_path, + i*2, i*2+1 + )) + mock_check_range.assert_has_calls(call_list) + assert check + + mock_check_range.side_effect = [True, False, True] + check = EVC.check_range(circuit, traces) + assert check is False diff --git a/tests/unit/test_db_models.py b/tests/unit/test_db_models.py index d0a2848b..185a5167 100644 --- a/tests/unit/test_db_models.py +++ b/tests/unit/test_db_models.py @@ -108,6 +108,11 @@ def test_tagdoc_value(self): assert tag.tag_type == 'vlan' assert tag.value == "any" + tag_list = {"tag_type": 'vlan', "value": [[1, 10]]} + tag = TAGDoc(**tag_list) + assert tag.tag_type == 'vlan' + assert tag.value == [[1, 10]] + def test_tagdoc_fail(self): """Test TAGDoc value fail case""" tag_fail = {"tag_type": 'vlan', "value": "test_fail"} diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 096c6637..f7eaff13 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -6,7 +6,8 @@ from kytos.lib.helpers import get_controller_mock, get_test_client from kytos.core.common import EntityStatus from kytos.core.events import KytosEvent -from kytos.core.interface import UNI, Interface +from kytos.core.exceptions import KytosTagError +from kytos.core.interface import TAGRange, UNI, Interface from napps.kytos.mef_eline.exceptions import InvalidPath from napps.kytos.mef_eline.models import EVC from napps.kytos.mef_eline.tests.helpers import get_uni_mocked @@ -37,6 +38,13 @@ async def test_on_table_enabled(): await napp.on_table_enabled(event) assert controller.buffers.app.aput.call_count == 1 + # Failure with early return + content = {} + event = KytosEvent(name="kytos/of_multi_table.enable_table", + content=content) + await napp.on_table_enabled(event) + assert controller.buffers.app.aput.call_count == 1 + # pylint: disable=too-many-public-methods, too-many-lines # pylint: disable=too-many-arguments,too-many-locals @@ -397,7 +405,7 @@ async def test_circuit_with_invalid_id(self): expected_result = "circuit_id 3 not found" assert response.json()["description"] == expected_result - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -616,7 +624,7 @@ async def test_create_a_circuit_invalid_queue_id(self, event_loop): assert response.status_code == 400 assert expected_data in current_data["description"] - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -682,7 +690,7 @@ async def test_create_circuit_already_enabled( assert current_data["description"] == expected_data assert 409 == response.status_code - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._uni_from_dict") async def test_create_circuit_case_5( self, @@ -722,6 +730,98 @@ async def test_create_circuit_case_5( assert 400 == response.status_code, response.data assert current_data["description"] == expected_data + @patch("napps.kytos.mef_eline.main.Main._evc_from_dict") + async def test_create_circuit_case_6(self, mock_evc, event_loop): + """Test create_circuit with KytosTagError""" + self.napp.controller.loop = event_loop + url = f"{self.base_endpoint}/v2/evc/" + mock_evc.side_effect = KytosTagError("") + payload = { + "name": "my evc1", + "uni_a": { + "interface_id": "00:00:00:00:00:00:00:01:1", + }, + "uni_z": { + "interface_id": "00:00:00:00:00:00:00:02:2", + }, + } + response = await self.api_client.post(url, json=payload) + assert response.status_code == 400, response.data + + @patch("napps.kytos.mef_eline.main.check_disabled_component") + @patch("napps.kytos.mef_eline.main.Main._evc_from_dict") + async def test_create_circuit_case_7( + self, + mock_evc, + mock_check_disabled_component, + event_loop + ): + """Test create_circuit with InvalidPath""" + self.napp.controller.loop = event_loop + mock_check_disabled_component.return_value = True + url = f"{self.base_endpoint}/v2/evc/" + uni1 = get_uni_mocked() + uni2 = get_uni_mocked() + evc = MagicMock(uni_a=uni1, uni_z=uni2) + evc.primary_path = MagicMock() + evc.backup_path = MagicMock() + + # Backup_path invalid + evc.backup_path.is_valid = MagicMock(side_effect=InvalidPath) + mock_evc.return_value = evc + payload = { + "name": "my evc1", + "uni_a": { + "interface_id": "00:00:00:00:00:00:00:01:1", + }, + "uni_z": { + "interface_id": "00:00:00:00:00:00:00:02:2", + }, + } + response = await self.api_client.post(url, json=payload) + assert response.status_code == 400, response.data + + # Backup_path invalid + evc.primary_path.is_valid = MagicMock(side_effect=InvalidPath) + mock_evc.return_value = evc + + response = await self.api_client.post(url, json=payload) + assert response.status_code == 400, response.data + + @patch("napps.kytos.mef_eline.main.Main._is_duplicated_evc") + @patch("napps.kytos.mef_eline.main.check_disabled_component") + @patch("napps.kytos.mef_eline.main.Main._evc_from_dict") + async def test_create_circuit_case_8( + self, + mock_evc, + mock_check_disabled_component, + mock_duplicated, + event_loop + ): + """Test create_circuit wit no equal tag lists""" + self.napp.controller.loop = event_loop + mock_check_disabled_component.return_value = True + mock_duplicated.return_value = False + url = f"{self.base_endpoint}/v2/evc/" + uni1 = get_uni_mocked() + uni2 = get_uni_mocked() + evc = MagicMock(uni_a=uni1, uni_z=uni2) + evc._tag_lists_equal = MagicMock(return_value=False) + mock_evc.return_value = evc + payload = { + "name": "my evc1", + "uni_a": { + "interface_id": "00:00:00:00:00:00:00:01:1", + "tag": {"tag_type": 'vlan', "value": [[50, 100]]}, + }, + "uni_z": { + "interface_id": "00:00:00:00:00:00:00:02:2", + "tag": {"tag_type": 'vlan', "value": [[1, 10]]}, + }, + } + response = await self.api_client.post(url, json=payload) + assert response.status_code == 400, response.data + async def test_redeploy_evc(self): """Test endpoint to redeploy an EVC.""" evc1 = MagicMock() @@ -1454,7 +1554,7 @@ async def test_update_circuit( assert 409 == response.status_code assert "Can't update archived EVC" in response.json()["description"] - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1525,7 +1625,7 @@ async def test_update_circuit_invalid_json( assert 400 == response.status_code assert "must have a primary path or" in current_data["description"] - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1700,7 +1800,7 @@ def test_uni_from_dict_non_existent_intf(self): with pytest.raises(ValueError): self.napp._uni_from_dict(uni_dict) - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.deploy") @patch("napps.kytos.mef_eline.scheduler.Scheduler.add") @@ -1773,7 +1873,7 @@ async def test_delete_no_evc(self): assert current_data["description"] == expected_data assert 404 == response.status_code - @patch("napps.kytos.mef_eline.main.Main._tag_lists_equal") + @patch("napps.kytos.mef_eline.models.evc.EVC._tag_lists_equal") @patch("napps.kytos.mef_eline.models.evc.EVC.remove_uni_tags") @patch("napps.kytos.mef_eline.main.Main._use_uni_tags") @patch("napps.kytos.mef_eline.models.evc.EVC.remove_current_flows") @@ -2216,14 +2316,18 @@ def test_load_evc(self, evc_from_dict_mock): evc_dict = MagicMock() assert not self.napp._load_evc(evc_dict) - # case2: archived evc + # case 2: early return with KytosTagError exception + evc_from_dict_mock.side_effect = KytosTagError("") + assert not self.napp._load_evc(evc_dict) + + # case 3: archived evc evc = MagicMock() evc.archived = True evc_from_dict_mock.side_effect = None evc_from_dict_mock.return_value = evc assert not self.napp._load_evc(evc_dict) - # case3: success creating + # case 4: success creating evc.archived = False evc.id = 1 self.napp.sched = MagicMock() @@ -2268,7 +2372,12 @@ def test_uni_from_dict(self, _get_interface_by_id_mock): uni = self.napp._uni_from_dict(uni_dict) assert uni == uni_mock - # case4: success creation without tag + # case4: success creation of tag list + uni_dict["tag"]["value"] = [[1, 10]] + uni = self.napp._uni_from_dict(uni_dict) + assert isinstance(uni.user_tag, TAGRange) + + # case5: success creation without tag uni_mock.user_tag = None del uni_dict["tag"] uni = self.napp._uni_from_dict(uni_dict) @@ -2364,6 +2473,21 @@ async def test_delete_bulk_metadata(self, event_loop): assert calls == 1 assert evc_mock.remove_metadata.call_count == 1 + async def test_delete_bulk_metadata_error(self, event_loop): + """Test bulk_delete_metadata with ciruit erroring""" + self.napp.controller.loop = event_loop + evc_mock = create_autospec(EVC) + evcs = [evc_mock, evc_mock] + self.napp.circuits = dict(zip(["1", "2"], evcs)) + payload = {"circuit_ids": ["1", "2", "3"]} + response = await self.api_client.request( + "DELETE", + f"{self.base_endpoint}/v2/evc/metadata/metadata1", + json=payload + ) + assert response.status_code == 404, response.data + assert response.json()["description"] == ["3"] + async def test_use_uni_tags(self, event_loop): """Test _use_uni_tags""" self.napp.controller.loop = event_loop @@ -2375,14 +2499,14 @@ async def test_use_uni_tags(self, event_loop): assert evc_mock._use_uni_vlan.call_args[0][0] == evc_mock.uni_z # One UNI tag is not available - evc_mock._use_uni_vlan.side_effect = [ValueError(), None] - with pytest.raises(ValueError): + evc_mock._use_uni_vlan.side_effect = [KytosTagError(""), None] + with pytest.raises(KytosTagError): self.napp._use_uni_tags(evc_mock) assert evc_mock._use_uni_vlan.call_count == 3 assert evc_mock.make_uni_vlan_available.call_count == 0 - evc_mock._use_uni_vlan.side_effect = [None, ValueError()] - with pytest.raises(ValueError): + evc_mock._use_uni_vlan.side_effect = [None, KytosTagError("")] + with pytest.raises(KytosTagError): self.napp._use_uni_tags(evc_mock) assert evc_mock._use_uni_vlan.call_count == 5 assert evc_mock.make_uni_vlan_available.call_count == 1 From f8ac14d3f8cfcd58d45e926fe780422542c0ff86 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Tue, 14 Nov 2023 14:18:41 -0500 Subject: [PATCH 25/28] Added `RANGE_SB_PRIORITY` - Updated changelog --- CHANGELOG.rst | 2 ++ models/evc.py | 9 +++++---- settings.py | 1 + tests/unit/models/test_evc_deploy.py | 9 +++++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 679cfde8..c7a96b54 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,8 @@ Added - Added a UI button for redeploying an EVC. - UNI tag_type are now accepted as string. - EVCs now listen to ``switch.interface.(link_up|link_down|created|deleted)`` events for activation/deactivation +- Circuits with a vlan range are supported now. The ranges follows ``list[list[int]]`` format and both UNIs vlan should have the same ranges. +- Added default priority ``RANGE_SB_PRIORITY`` with value as ``18000`` for circuits with vlan ranges. Changed ======= diff --git a/models/evc.py b/models/evc.py index faeb5278..2a32a7f7 100644 --- a/models/evc.py +++ b/models/evc.py @@ -1219,7 +1219,7 @@ def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict: def get_priority(vlan): """Return priority value depending on vlan value""" if isinstance(vlan, list): - return settings.EVPL_SB_PRIORITY + return settings.RANGE_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: @@ -1272,9 +1272,9 @@ def _prepare_push_flow(self, *args, queue_id=None): Arguments: in_interface(str): Interface input. out_interface(str): Interface output. - in_vlan(int,str,list,None): Vlan input. + in_vlan(int,str,None): Vlan input. out_vlan(str): Vlan output. - new_c_vlan(str): New client vlan. + new_c_vlan(int,str,list,None): New client vlan. Return: dict: An python dictionary representing a FlowMod @@ -1282,8 +1282,9 @@ def _prepare_push_flow(self, *args, queue_id=None): """ # assign all arguments in_interface, out_interface, in_vlan, out_vlan, new_c_vlan = args + vlan_pri = in_vlan if not isinstance(new_c_vlan, list) else new_c_vlan flow_mod = self._prepare_flow_mod( - in_interface, out_interface, queue_id, in_vlan + in_interface, out_interface, queue_id, vlan_pri ) # the service tag must be always pushed new_action = {"action_type": "set_vlan", "vlan_id": out_vlan} diff --git a/settings.py b/settings.py index 68b81452..dcdc9a65 100644 --- a/settings.py +++ b/settings.py @@ -43,6 +43,7 @@ EPL_SB_PRIORITY = 10000 ANY_SB_PRIORITY = 15000 UNTAGGED_SB_PRIORITY = 20000 +RANGE_SB_PRIORITY = 18000 # Time (seconds) to check if an evc has been updated # or flows have been deleted. diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index f920d98a..4cb8388c 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -18,7 +18,8 @@ from napps.kytos.mef_eline.models import EVC, EVCDeploy, Path # NOQA from napps.kytos.mef_eline.settings import (ANY_SB_PRIORITY, # NOQA EPL_SB_PRIORITY, EVPL_SB_PRIORITY, - MANAGER_URL, SDN_TRACE_CP_URL, + MANAGER_URL, RANGE_SB_PRIORITY, + SDN_TRACE_CP_URL, UNTAGGED_SB_PRIORITY) from napps.kytos.mef_eline.tests.helpers import (get_link_mocked, # NOQA get_uni_mocked) @@ -1968,7 +1969,7 @@ def test_get_priority(self): assert epl_value == EPL_SB_PRIORITY epl_value = EVC.get_priority([[1, 5]]) - assert epl_value == EVPL_SB_PRIORITY + assert epl_value == RANGE_SB_PRIORITY def test_set_flow_table_group_id(self): """Test set_flow_table_group_id""" @@ -2053,11 +2054,11 @@ def test_prepare_direct_uni_flows(self): for i in range(0, 3): assert flows[i]["match"]["in_port"] == 1 assert flows[i]["match"]["dl_vlan"] == mask_list[i] - assert flows[i]["priority"] == EVPL_SB_PRIORITY + assert flows[i]["priority"] == RANGE_SB_PRIORITY for i in range(3, 6): assert flows[i]["match"]["in_port"] == 2 assert flows[i]["match"]["dl_vlan"] == mask_list[i-3] - assert flows[i]["priority"] == EVPL_SB_PRIORITY + assert flows[i]["priority"] == RANGE_SB_PRIORITY @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") def test_check_range(self, mock_check_range): From dcd59b0e75d571a9f342f1dbff92ac341f9e9315 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 16 Nov 2023 19:45:36 -0500 Subject: [PATCH 26/28] Using `_is_duplicated_evc()` when updating - Reverted RANGE_SB_PRIORITY adittion --- CHANGELOG.rst | 1 - main.py | 8 +++++++- models/evc.py | 2 +- settings.py | 1 - tests/unit/models/test_evc_deploy.py | 8 ++++---- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c7a96b54..698597ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,6 @@ Added - UNI tag_type are now accepted as string. - EVCs now listen to ``switch.interface.(link_up|link_down|created|deleted)`` events for activation/deactivation - Circuits with a vlan range are supported now. The ranges follows ``list[list[int]]`` format and both UNIs vlan should have the same ranges. -- Added default priority ``RANGE_SB_PRIORITY`` with value as ``18000`` for circuits with vlan ranges. Changed ======= diff --git a/main.py b/main.py index 99af8e4b..ddfb3433 100644 --- a/main.py +++ b/main.py @@ -384,6 +384,11 @@ def update(self, request: Request) -> JSONResponse: detail=f"Path is not valid: {exception}" ) from exception + if self._is_duplicated_evc(evc): + result = "The EVC already exists." + log.debug("create_circuit result %s %s", result, 409) + raise HTTPException(409, detail=result) + if evc.is_active(): if enable is False: # disable if active with evc.lock: @@ -723,7 +728,8 @@ def _is_duplicated_evc(self, evc): """ for circuit in tuple(self.circuits.values()): - if not circuit.archived and circuit.shares_uni(evc): + if (not circuit.archived and circuit._id != evc._id + and circuit.shares_uni(evc)): return True return False diff --git a/models/evc.py b/models/evc.py index 2a32a7f7..e6646f96 100644 --- a/models/evc.py +++ b/models/evc.py @@ -1219,7 +1219,7 @@ def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict: def get_priority(vlan): """Return priority value depending on vlan value""" if isinstance(vlan, list): - return settings.RANGE_SB_PRIORITY + return settings.EVPL_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: diff --git a/settings.py b/settings.py index dcdc9a65..68b81452 100644 --- a/settings.py +++ b/settings.py @@ -43,7 +43,6 @@ EPL_SB_PRIORITY = 10000 ANY_SB_PRIORITY = 15000 UNTAGGED_SB_PRIORITY = 20000 -RANGE_SB_PRIORITY = 18000 # Time (seconds) to check if an evc has been updated # or flows have been deleted. diff --git a/tests/unit/models/test_evc_deploy.py b/tests/unit/models/test_evc_deploy.py index 4cb8388c..5e7ee526 100644 --- a/tests/unit/models/test_evc_deploy.py +++ b/tests/unit/models/test_evc_deploy.py @@ -18,7 +18,7 @@ from napps.kytos.mef_eline.models import EVC, EVCDeploy, Path # NOQA from napps.kytos.mef_eline.settings import (ANY_SB_PRIORITY, # NOQA EPL_SB_PRIORITY, EVPL_SB_PRIORITY, - MANAGER_URL, RANGE_SB_PRIORITY, + MANAGER_URL, SDN_TRACE_CP_URL, UNTAGGED_SB_PRIORITY) from napps.kytos.mef_eline.tests.helpers import (get_link_mocked, # NOQA @@ -1969,7 +1969,7 @@ def test_get_priority(self): assert epl_value == EPL_SB_PRIORITY epl_value = EVC.get_priority([[1, 5]]) - assert epl_value == RANGE_SB_PRIORITY + assert epl_value == EVPL_SB_PRIORITY def test_set_flow_table_group_id(self): """Test set_flow_table_group_id""" @@ -2054,11 +2054,11 @@ def test_prepare_direct_uni_flows(self): for i in range(0, 3): assert flows[i]["match"]["in_port"] == 1 assert flows[i]["match"]["dl_vlan"] == mask_list[i] - assert flows[i]["priority"] == RANGE_SB_PRIORITY + assert flows[i]["priority"] == EVPL_SB_PRIORITY for i in range(3, 6): assert flows[i]["match"]["in_port"] == 2 assert flows[i]["match"]["dl_vlan"] == mask_list[i-3] - assert flows[i]["priority"] == RANGE_SB_PRIORITY + assert flows[i]["priority"] == EVPL_SB_PRIORITY @patch("napps.kytos.mef_eline.models.evc.EVCDeploy.check_trace") def test_check_range(self, mock_check_range): From f95145e2af1db0144dd95717493c9508766e095f Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 16 Nov 2023 20:02:56 -0500 Subject: [PATCH 27/28] Bypassed tag checking --- models/evc.py | 4 ++-- models/path.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/models/evc.py b/models/evc.py index e6646f96..62419000 100644 --- a/models/evc.py +++ b/models/evc.py @@ -454,7 +454,7 @@ def _use_uni_vlan( if not tag: return uni.interface.use_tags( - self._controller, tag, tag_type + self._controller, tag, tag_type, False ) def make_uni_vlan_available( @@ -476,7 +476,7 @@ def make_uni_vlan_available( return try: conflict = uni.interface.make_tags_available( - self._controller, tag, tag_type + self._controller, tag, tag_type, False ) except KytosTagError as err: log.error(f"Error in circuit {self._id}: {err}") diff --git a/models/path.py b/models/path.py index 936ba85e..e94f1bfc 100644 --- a/models/path.py +++ b/models/path.py @@ -45,7 +45,7 @@ def make_vlans_available(self, controller): for link in self: tag = link.get_metadata("s_vlan") conflict_a, conflict_b = link.make_tags_available( - controller, tag.value, link.id, tag.tag_type + controller, tag.value, link.id, tag.tag_type, False ) if conflict_a: log.error(f"Tags {conflict_a} was already available in" From 3c0204377741e96029588055126780a93b3d4c0b Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 20 Nov 2023 18:47:40 -0500 Subject: [PATCH 28/28] Updated arguments Interface tag functions --- models/evc.py | 14 +++++++++----- models/path.py | 3 ++- utils.py | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/models/evc.py b/models/evc.py index 62419000..1cd42b52 100644 --- a/models/evc.py +++ b/models/evc.py @@ -454,7 +454,7 @@ def _use_uni_vlan( if not tag: return uni.interface.use_tags( - self._controller, tag, tag_type, False + self._controller, tag, tag_type, use_lock=True, check_order=False ) def make_uni_vlan_available( @@ -476,7 +476,8 @@ def make_uni_vlan_available( return try: conflict = uni.interface.make_tags_available( - self._controller, tag, tag_type, False + self._controller, tag, tag_type, use_lock=True, + check_order=False ) except KytosTagError as err: log.error(f"Error in circuit {self._id}: {err}") @@ -1424,7 +1425,9 @@ def check_trace( def check_range(circuit, traces: list) -> bool: """Check traces when for UNI with TAGRange""" check = True - for i, mask in enumerate(circuit.uni_a.user_tag.mask_list): + mask_list = (circuit.uni_a.user_tag.mask_list or + circuit.uni_z.user_tag.mask_list) + for i, mask in enumerate(mask_list): trace_a = traces[i*2] trace_z = traces[i*2+1] check &= EVCDeploy.check_trace( @@ -1452,7 +1455,8 @@ def check_list_traces(list_circuits: list) -> dict: i = 0 for circuit in list_circuits: if isinstance(circuit.uni_a.user_tag, TAGRange): - length = len(circuit.uni_a.user_tag.mask_list) + length = (len(circuit.uni_a.user_tag.mask_list) or + len(circuit.uni_z.user_tag.mask_list)) circuits_checked[circuit.id] = EVCDeploy.check_range( circuit, traces[i:i+length*2] ) @@ -1657,7 +1661,7 @@ def handle_interface_link_up(self, interface: Interface): """ Handler for interface link_up events """ - if self.archived: # TODO: Remove when addressing issue #369 + if self.archived: return if self.is_active(): return diff --git a/models/path.py b/models/path.py index e94f1bfc..53e25ee0 100644 --- a/models/path.py +++ b/models/path.py @@ -45,7 +45,8 @@ def make_vlans_available(self, controller): for link in self: tag = link.get_metadata("s_vlan") conflict_a, conflict_b = link.make_tags_available( - controller, tag.value, link.id, tag.tag_type, False + controller, tag.value, link.id, tag.tag_type, + check_order=False ) if conflict_a: log.error(f"Tags {conflict_a} was already available in" diff --git a/utils.py b/utils.py index ff59ea8c..a7d5a41d 100644 --- a/utils.py +++ b/utils.py @@ -125,7 +125,9 @@ def make_uni_list(list_circuits: list) -> list: for circuit in list_circuits: if isinstance(circuit.uni_a.user_tag, TAGRange): # TAGRange value from uni_a and uni_z are currently mirrored - for mask in circuit.uni_a.user_tag.mask_list: + mask_list = (circuit.uni_a.user_tag.mask_list or + circuit.uni_z.user_tag.mask_list) + for mask in mask_list: uni_list.append((circuit.uni_a.interface, mask)) uni_list.append((circuit.uni_z.interface, mask)) else: