diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 679cfde8..698597ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ 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. Changed ======= diff --git a/db/models.py b/db/models.py index a104b712..64695eff 100644 --- a/db/models.py +++ b/db/models.py @@ -38,11 +38,14 @@ class CircuitScheduleDoc(BaseModel): class TAGDoc(BaseModel): """TAG model""" tag_type: str - value: Union[int, str] + value: Union[int, str, list[list[int]]] + mask_list: Optional[list[str, int]] @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..ddfb3433 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 KytosTagError 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 @@ -199,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: @@ -234,7 +238,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: @@ -269,12 +275,15 @@ 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() except ValueError as exception: @@ -282,7 +291,7 @@ def create_circuit(self, request: Request) -> JSONResponse: try: self._use_uni_tags(evc) - except ValueError as exception: + except KytosTagError as exception: raise HTTPException(400, detail=str(exception)) from exception # save circuit @@ -313,14 +322,11 @@ 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 ValueError as err: - raise err + evc._use_uni_vlan(uni_a) try: uni_z = evc.uni_z evc._use_uni_vlan(uni_z) - except ValueError as err: + except KytosTagError as err: evc.make_uni_vlan_available(uni_a) raise err @@ -364,10 +370,11 @@ 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 DisabledSwitch as exception: @@ -377,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: @@ -716,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 @@ -911,7 +924,11 @@ 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}" + ) + return None if evc.archived: return None @@ -1001,11 +1018,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): + 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: + tag = TAG(tag_type, tag_value) 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 3d1056de..1cd42b52 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,18 @@ from kytos.core import log from kytos.core.common import EntityStatus, GenericEntity -from kytos.core.exceptions import KytosNoTagAvailableError +from kytos.core.exceptions import KytosNoTagAvailableError, KytosTagError 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,32 +174,29 @@ 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) uni_a_flag = False if uni_a and uni_a != self.uni_a: uni_a_flag = True - try: - self._use_uni_vlan(uni_a) - except ValueError 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: 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 KytosTagError as err: if uni_a_flag: - self.make_uni_vlan_available(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.make_uni_vlan_available(self.uni_a, uni_dif=uni_a) else: uni_a = self.uni_a return uni_a, uni_z @@ -217,6 +217,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 +281,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 +296,19 @@ 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: + 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, primary_path=None, @@ -420,33 +436,55 @@ 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( - self._controller, tag, tag_type - ) - if not result: - intf = uni.interface.id - raise ValueError(f"Tag {tag} is not available in {intf}") + 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 + uni.interface.use_tags( + self._controller, tag, tag_type, use_lock=True, check_order=False + ) - 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( - self._controller, tag, tag_type + 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 + try: + conflict = uni.interface.make_tags_available( + self._controller, tag, tag_type, use_lock=True, + check_order=False ) - if not result: - intf = uni.interface.id - log.warning(f"Tag {tag} was already available in {intf}") + 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}") def remove_uni_tags(self): """Remove both UNI usage of a tag""" @@ -657,7 +695,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 KytosTagError as err: + log.error(f"Error when removing failover flows: {err}") self.failover_path = Path([]) if sync: self.sync() @@ -687,8 +728,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 KytosTagError as err: + log.error(f"Error when removing current path flows: {err}") self.current_path = Path([]) self.deactivate() self.sync() @@ -742,8 +785,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 KytosTagError as err: + log.error(f"Error when removing path flows: {err}") @staticmethod def links_zipped(path=None): @@ -896,6 +941,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 +956,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 +964,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 +979,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 +1059,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 +1098,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 +1137,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 +1219,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.EVPL_SB_PRIORITY if vlan not in {None, "4096/4096", 0}: return settings.EVPL_SB_PRIORITY if vlan == 0: @@ -1185,9 +1273,9 @@ 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,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 @@ -1195,8 +1283,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} @@ -1209,7 +1298,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 +1316,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 +1340,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 +1373,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 +1422,69 @@ 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 + 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( + 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) or + len(circuit.uni_z.user_tag.mask_list)) + circuits_checked[circuit.id] = EVCDeploy.check_range( + circuit, traces[i:i+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 @@ -1523,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 6a4b2203..53e25ee0 100644 --- a/models/path.py +++ b/models/path.py @@ -44,14 +44,15 @@ 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 + conflict_a, conflict_b = link.make_tags_available( + controller, tag.value, link.id, tag.tag_type, + check_order=False ) - if result_a is False: - log.error(f"Tag {tag} was already available in" + 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/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..5e7ee526 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 @@ -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, + SDN_TRACE_CP_URL, UNTAGGED_SB_PRIORITY) from napps.kytos.mef_eline.tests.helpers import (get_link_mocked, # NOQA get_uni_mocked) @@ -193,6 +194,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 +1392,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 +1402,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 +1426,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 +1439,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 +1451,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 +1464,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 +1476,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 +1492,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 +1508,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 +1524,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 +1864,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 +1932,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 +1968,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 +1991,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 9384ae0b..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,6 +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.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") @@ -413,6 +422,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 +432,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 +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.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") @@ -629,6 +641,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 +652,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 +690,17 @@ async def test_create_circuit_already_enabled( assert current_data["description"] == expected_data assert 409 == response.status_code + @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, 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) @@ -709,6 +730,98 @@ async def test_create_circuit_case_5(self, uni_from_dict_mock, event_loop): 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() @@ -1441,6 +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.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") @@ -1457,6 +1571,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 +1581,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 +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.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") @@ -1529,6 +1646,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 +1658,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 +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.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") @@ -1695,6 +1815,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 +1824,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 +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.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") @@ -1771,6 +1894,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 +1905,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) @@ -2191,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() @@ -2243,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) @@ -2339,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 @@ -2350,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 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..a7d5a41d 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,30 @@ 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 + 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: + 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