From f651d861a5bfb097cba9d8bfd4ba377bd8ca6620 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 30 Oct 2023 10:58:07 -0400 Subject: [PATCH 01/13] Added support to `list[list[int]]` --- kytos/core/exceptions.py | 28 ++- kytos/core/interface.py | 216 +++++++++--------------- kytos/core/link.py | 29 ++-- kytos/core/tag_ranges.py | 160 ++++++++++++++++++ tests/unit/test_core/test_interface.py | 100 ++++------- tests/unit/test_core/test_tag_ranges.py | 93 ++++++++++ 6 files changed, 410 insertions(+), 216 deletions(-) create mode 100644 kytos/core/tag_ranges.py create mode 100644 tests/unit/test_core/test_tag_ranges.py diff --git a/kytos/core/exceptions.py b/kytos/core/exceptions.py index d4c342dc..fb8bbfe7 100644 --- a/kytos/core/exceptions.py +++ b/kytos/core/exceptions.py @@ -86,7 +86,33 @@ class KytosLinkCreationError(Exception): class KytosTagtypeNotSupported(Exception): - """Exception thronw when a not supported tag type is not supported""" + """Exception thrown when a not supported tag type is not supported""" + + +class KytosTagsNotInTagRanges(Exception): + """Exception thrown when a tag is outside of tag ranges""" + def __init__(self, conflict: list[list[int]]) -> None: + super().__init__() + self.conflict = conflict + + def __repr__(self): + return f"The tags {self.conflict} are outside tag_ranges" + + def __str__(self) -> str: + return f"The tags {self.conflict} are outside tag_ranges" + + +class KytosTagsAreNotAvailable(Exception): + """Exception thrown when a tag is not available.""" + def __init__(self, conflict: list[list[int]]) -> None: + super().__init__() + self.conflict = conflict + + def __repr__(self): + return f"The tags {self.conflict} are not available." + + def __str__(self) -> str: + return f"The tags {self.conflict} are not available." # Exceptions related to NApps diff --git a/kytos/core/interface.py b/kytos/core/interface.py index 02a2f6b6..acb61354 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -1,13 +1,13 @@ """Module with main classes related to Interfaces.""" -import bisect import json import logging import operator from collections import OrderedDict +from copy import deepcopy from enum import Enum from functools import reduce from threading import Lock -from typing import Iterator, Optional, Union +from typing import Optional, Union from pyof.v0x01.common.phy_port import Port as PortNo01 from pyof.v0x01.common.phy_port import PortFeatures as PortFeatures01 @@ -17,9 +17,13 @@ from kytos.core.common import EntityStatus, GenericEntity from kytos.core.events import KytosEvent from kytos.core.exceptions import (KytosSetTagRangeError, + KytosTagsAreNotAvailable, + KytosTagsNotInTagRanges, KytosTagtypeNotSupported) from kytos.core.helpers import now from kytos.core.id import InterfaceID +from kytos.core.tag_ranges import (find_index_add, find_index_remove, + range_addition, range_difference) __all__ = ('Interface',) @@ -203,97 +207,23 @@ def default_tag_values(self) -> dict[str, list[list[int]]]: } return default_values - @staticmethod - def range_intersection( - ranges_a: list[list[int]], - ranges_b: list[list[int]] - ) -> Iterator[list[int]]: - """Returns an iterator of an intersection between - two list of ranges""" - a_i, b_i = 0, 0 - while a_i < len(ranges_a) and b_i < len(ranges_b): - fst_a, snd_a = ranges_a[a_i] - fst_b, snd_b = ranges_b[b_i] - # Moving forward with non-intersection - if snd_a < fst_b: - a_i += 1 - elif snd_b < fst_a: - b_i += 1 - else: - # Intersection - intersection_start = max(fst_a, fst_b) - intersection_end = min(snd_a, snd_b) - yield [intersection_start, intersection_end] - if snd_a < snd_b: - a_i += 1 - else: - b_i += 1 - - @staticmethod - def range_difference( - ranges_a: list[list[int]], - ranges_b: list[list[int]] - ) -> list[list[int]]: - """The operation is (ranges_a - ranges_b). - This method simulates difference of sets. E.g.: - [[10, 15]] - [[4, 11], [14, 45]] = [[12, 13]] - """ - result = [] - a_i, b_i = 0, 0 - update = True - while a_i < len(ranges_a) and b_i < len(ranges_b): - if update: - start_a, end_a = ranges_a[a_i] - else: - update = True - start_b, end_b = ranges_b[b_i] - - # Moving forward with non-intersection - if end_a < start_b: - result.append([start_a, end_a]) - a_i += 1 - elif end_b < start_a: - b_i += 1 - else: - # Intersection - if start_a < start_b: - result.append([start_a, start_b - 1]) - - if end_a > end_b: - start_a = end_b + 1 - update = False - b_i += 1 - else: - a_i += 1 - - # Append last intersection and the rest of ranges_a - while a_i < len(ranges_a): - if update: - start_a, end_a = ranges_a[a_i] - else: - update = True - result.append([start_a, end_a]) - a_i += 1 - - return result - def set_tag_ranges(self, tag_ranges: list[list[int]], tag_type: str): """Set new restriction""" if tag_type != TAGType.VLAN.value: msg = f"Tag type {tag_type} is not supported." raise KytosTagtypeNotSupported(msg) with self._tag_lock: - used_tags = self.range_difference( + used_tags = range_difference( self.tag_ranges[tag_type], self.available_tags[tag_type] ) # Verify new tag_ranges - missing = self.range_difference(used_tags, tag_ranges) + missing = range_difference(used_tags, tag_ranges) if missing: msg = f"Missing tags in tag_range: {missing}" raise KytosSetTagRangeError(msg) # Resizing - new_tag_ranges = self.range_difference( + new_tag_ranges = range_difference( tag_ranges, used_tags ) self.available_tags[tag_type] = new_tag_ranges @@ -305,38 +235,19 @@ def remove_tag_ranges(self, tag_type: str): msg = f"Tag type {tag_type} is not supported." raise KytosTagtypeNotSupported(msg) with self._tag_lock: - used_tags = self.range_difference( + used_tags = range_difference( self.tag_ranges[tag_type], self.available_tags[tag_type] ) - self.available_tags[tag_type] = self.range_difference( + self.available_tags[tag_type] = range_difference( self.default_tag_values[tag_type], used_tags ) self.tag_ranges[tag_type] = self.default_tag_values[tag_type] - @staticmethod - def find_index_remove( - available_tags: list[list[int]], - tag_range: list[int] - ) -> Optional[int]: - """Find the index of tags in available_tags to be removed""" - index = bisect.bisect_left(available_tags, tag_range) - if (index < len(available_tags) and - tag_range[0] >= available_tags[index][0] and - tag_range[1] <= available_tags[index][1]): - return index - - if (index-1 > -1 and - tag_range[0] >= available_tags[index-1][0] and - tag_range[1] <= available_tags[index-1][1]): - return index - 1 - - return None - def remove_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: """Remove tags by resize available_tags Returns False if nothing was remove, True otherwise""" available = self.available_tags[tag_type] - index = self.find_index_remove(available, tags) + index = find_index_remove(available, tags) if index is None: return False # Resizing @@ -357,10 +268,10 @@ def remove_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: def use_tags( self, controller, - tags: Union[int, list[int]], + tags: Union[int, list[int], list[list[int]]], tag_type: str = 'vlan', use_lock: bool = True, - ) -> bool: + ): """Remove a specific tag from available_tags if it is there. Return False in case the tags were not able to be removed. @@ -374,27 +285,36 @@ def use_tags( tags = [tags] * 2 if use_lock: with self._tag_lock: - result = self.remove_tags(tags, tag_type) + try: + self._use_tags(tags, tag_type) + except KytosTagsAreNotAvailable as err: + raise err + else: + try: + self._use_tags(tags, tag_type) + except KytosTagsAreNotAvailable as err: + raise err + + self._notify_interface_tags(controller) + + def _use_tags( + self, + tags: Union[list[int], list[list[int]]], + tag_type: str + ): + """Manage available_tags deletion changes""" + if isinstance(tags[0], list): + available_copy = deepcopy(self.available_tags[tag_type]) + for tag_range in tags: + result = self.remove_tags(tag_range, tag_type) + if result is False: + self.available_tags[tag_type] = available_copy + conflict = range_difference(tags, available_copy) + raise KytosTagsAreNotAvailable(conflict) else: result = self.remove_tags(tags, tag_type) - if result: - self._notify_interface_tags(controller) - return result - - @staticmethod - def find_index_add( - available_tags: list[list[int]], - tags: list[int] - ) -> Optional[int]: - """Find the index of tags in available_tags to be added. - This method assumes that tags is within self.tag_ranges""" - index = bisect.bisect_left(available_tags, tags) - - if (index == 0 or tags[0] > available_tags[index-1][1]) and \ - (index == len(available_tags) or - tags[1] < available_tags[index][0]): - return index - return None + if result is False: + raise KytosTagsAreNotAvailable([tags]) # pylint: disable=too-many-branches def add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: @@ -409,11 +329,11 @@ def add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: # Check if tags is within self.tag_ranges tag_ranges = self.tag_ranges[tag_type] - if self.find_index_remove(tag_ranges, tags) is None: - return False + if find_index_remove(tag_ranges, tags) is None: + raise KytosTagsNotInTagRanges([tags]) available = self.available_tags[tag_type] - index = self.find_index_add(available, tags) + index = find_index_add(available, tags) if index is None: return False if index == 0: @@ -451,10 +371,10 @@ def add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: def make_tags_available( self, controller, - tags: Union[int, list[int]], + tags: Union[int, list[int], list[list[int]]], tag_type: str = 'vlan', use_lock: bool = True, - ) -> bool: + ) -> list[list[int]]: """Add a specific tag in available_tags. Args: @@ -462,17 +382,49 @@ def make_tags_available( tags: List of tags, there should be 2 items in the list tag_type: TAG type value use_lock: Boolean to whether use a lock or not + + Return: + conflict: Return any values that were not added. """ if isinstance(tags, int): tags = [tags] * 2 + if isinstance(tags[0], int) and tags[0] != tags[1]: + tags = [tags[0], tags[1]] if use_lock: with self._tag_lock: - result = self.add_tags(tags, tag_type) + try: + conflict = self._make_tags_available(tags, tag_type) + except KytosTagsNotInTagRanges as err: + raise err else: + try: + conflict = self._make_tags_available(tags, tag_type) + except KytosTagsNotInTagRanges as err: + raise err + self._notify_interface_tags(controller) + return conflict + + def _make_tags_available( + self, + tags: Union[list[int], list[list[int]]], + tag_type: str, + ) -> list[list[int]]: + """Manage available_tags adittion changes""" + if isinstance(tags[0], list): + diff = range_difference(tags, self.tag_ranges[tag_type]) + if diff: + raise KytosTagsNotInTagRanges(diff) + available_tags = self.available_tags[tag_type] + new_tags, conflict = range_addition(tags, available_tags) + self.available_tags[tag_type] = new_tags + return conflict + try: result = self.add_tags(tags, tag_type) - if result: - self._notify_interface_tags(controller) - return result + except KytosTagsNotInTagRanges as err: + raise err + if result is False: + return [tags] + return [] def set_available_tags_tag_ranges( self, @@ -499,7 +451,7 @@ def enable(self): def is_tag_available(self, tag: int, tag_type: str = 'vlan'): """Check if a tag is available.""" with self._tag_lock: - if self.find_index_remove( + if find_index_remove( self.available_tags[tag_type], [tag, tag] ) is not None: return True diff --git a/kytos/core/link.py b/kytos/core/link.py index 84e48988..952cd50a 100644 --- a/kytos/core/link.py +++ b/kytos/core/link.py @@ -12,9 +12,11 @@ from kytos.core.common import EntityStatus, GenericEntity from kytos.core.exceptions import (KytosLinkCreationError, - KytosNoTagAvailableError) + KytosNoTagAvailableError, + KytosTagsNotInTagRanges) from kytos.core.id import LinkID from kytos.core.interface import Interface, TAGType +from kytos.core.tag_ranges import range_intersection class Link(GenericEntity): @@ -127,7 +129,7 @@ def available_tags(self, tag_type: str = 'vlan') -> list[list[int]]: Based on the endpoint tags. """ - tag_iterator = Interface.range_intersection( + tag_iterator = range_intersection( self.endpoint_a.available_tags[tag_type], self.endpoint_b.available_tags[tag_type], ) @@ -151,8 +153,8 @@ def get_next_available_tag( with self.endpoint_b._tag_lock: ava_tags_a = self.endpoint_a.available_tags[tag_type] ava_tags_b = self.endpoint_b.available_tags[tag_type] - tags = Interface.range_intersection(ava_tags_a, - ava_tags_b) + tags = range_intersection(ava_tags_a, + ava_tags_b) try: tag, _ = next(tags) self.endpoint_a.use_tags( @@ -171,18 +173,21 @@ def make_tags_available( tags: Union[int, list[int]], link_id, tag_type: str = 'vlan' - ) -> tuple[bool, bool]: + ) -> tuple[list[list[int]], list[list[int]]]: """Add a specific tag in available_tags.""" with self._get_available_vlans_lock[link_id]: with self.endpoint_a._tag_lock: with self.endpoint_b._tag_lock: - result_a = self.endpoint_a.make_tags_available( - controller, tags, tag_type, False - ) - result_b = self.endpoint_b.make_tags_available( - controller, tags, tag_type, False - ) - return result_a, result_b + try: + conflict_a = self.endpoint_a.make_tags_available( + controller, tags, tag_type, False + ) + conflict_b = self.endpoint_b.make_tags_available( + controller, tags, tag_type, False + ) + except KytosTagsNotInTagRanges as err: + raise err + return conflict_a, conflict_b def available_vlans(self): """Get all available vlans from each interface in the link.""" diff --git a/kytos/core/tag_ranges.py b/kytos/core/tag_ranges.py new file mode 100644 index 00000000..2e705887 --- /dev/null +++ b/kytos/core/tag_ranges.py @@ -0,0 +1,160 @@ +"""Methods for list of ranges [inclusive, inclusive]""" +import bisect +from typing import Iterator, Optional + + +def range_intersection( + ranges_a: list[list[int]], + ranges_b: list[list[int]] +) -> Iterator[list[int]]: + """Returns an iterator of an intersection between + two list of ranges""" + a_i, b_i = 0, 0 + while a_i < len(ranges_a) and b_i < len(ranges_b): + fst_a, snd_a = ranges_a[a_i] + fst_b, snd_b = ranges_b[b_i] + # Moving forward with non-intersection + if snd_a < fst_b: + a_i += 1 + elif snd_b < fst_a: + b_i += 1 + else: + # Intersection + intersection_start = max(fst_a, fst_b) + intersection_end = min(snd_a, snd_b) + yield [intersection_start, intersection_end] + if snd_a < snd_b: + a_i += 1 + else: + b_i += 1 + + +def range_difference( + ranges_a: list[list[int]], + ranges_b: list[list[int]] +) -> list[list[int]]: + """The operation is (ranges_a - ranges_b). + This method simulates difference of sets. E.g.: + [[10, 15]] - [[4, 11], [14, 45]] = [[12, 13]] + """ + result = [] + a_i, b_i = 0, 0 + update = True + while a_i < len(ranges_a) and b_i < len(ranges_b): + if update: + start_a, end_a = ranges_a[a_i] + else: + update = True + start_b, end_b = ranges_b[b_i] + # Moving forward with non-intersection + if end_a < start_b: + result.append([start_a, end_a]) + a_i += 1 + elif end_b < start_a: + b_i += 1 + else: + # Intersection + if start_a < start_b: + result.append([start_a, start_b - 1]) + if end_a > end_b: + start_a = end_b + 1 + update = False + b_i += 1 + else: + a_i += 1 + # Append last intersection and the rest of ranges_a + while a_i < len(ranges_a): + if update: + start_a, end_a = ranges_a[a_i] + else: + update = True + result.append([start_a, end_a]) + a_i += 1 + return result + + +def range_addition( + ranges_a: list[list[int]], + ranges_b: list[list[int]] +) -> tuple[list[list[int]], list[list[int]]]: + """Addition between two ranges. + Simulates the addition between two sets. + Return[adittion product, intersection]""" + result = [] + conflict = [] + a_i = b_i = 0 + len_a = len(ranges_a) + len_b = len(ranges_b) + while a_i < len_a or b_i < len_b: + if (a_i < len_a and + (b_i >= len_b or ranges_a[a_i][1] < ranges_b[b_i][0] - 1)): + result.append(ranges_a[a_i]) + a_i += 1 + elif (b_i < len_b and + (a_i >= len_a or ranges_b[b_i][1] < ranges_a[a_i][0] - 1)): + result.append(ranges_b[b_i]) + b_i += 1 + # Intersection and continuos ranges + else: + fst = max(ranges_a[a_i][0], ranges_b[b_i][0]) + snd = min(ranges_a[a_i][1], ranges_b[b_i][1]) + if fst <= snd: + conflict.append([fst, snd]) + new_range = [ + min(ranges_a[a_i][0], ranges_b[b_i][0]), + max(ranges_a[a_i][1], ranges_b[b_i][1]) + ] + a_i += 1 + b_i += 1 + while a_i < len_a or b_i < len_b: + if a_i < len_a and (ranges_a[a_i][0] <= new_range[1] + 1): + if ranges_a[a_i][0] <= new_range[1]: + conflict.append([ + max(ranges_a[a_i][0], new_range[0]), + min(ranges_a[a_i][1], new_range[1]) + ]) + new_range[1] = max(ranges_a[a_i][1], new_range[1]) + a_i += 1 + elif b_i < len_b and (ranges_b[b_i][0] <= new_range[1] + 1): + if ranges_b[b_i][0] <= new_range[1]: + conflict.append([ + max(ranges_b[b_i][0], new_range[0]), + min(ranges_b[b_i][1], new_range[1]) + ]) + new_range[1] = max(ranges_b[b_i][1], new_range[1]) + b_i += 1 + else: + break + result.append(new_range) + return result, conflict + + +def find_index_remove( + available_tags: list[list[int]], + tag_range: list[int] +) -> Optional[int]: + """Find the index of tags in available_tags to be removed""" + index = bisect.bisect_left(available_tags, tag_range) + if (index < len(available_tags) and + tag_range[0] >= available_tags[index][0] and + tag_range[1] <= available_tags[index][1]): + return index + if (index - 1 > -1 and + tag_range[0] >= available_tags[index-1][0] and + tag_range[1] <= available_tags[index-1][1]): + return index - 1 + return None + + +def find_index_add( + available_tags: list[list[int]], + tags: list[int] +) -> Optional[int]: + """Find the index of tags in available_tags to be added. + This method assumes that tags is within self.tag_ranges""" + index = bisect.bisect_left(available_tags, tags) + if (index == 0 or tags[0] > available_tags[index-1][1]) and \ + (index == len(available_tags) or + tags[1] < available_tags[index][0]): + return index + return None diff --git a/tests/unit/test_core/test_interface.py b/tests/unit/test_core/test_interface.py index f80b8d9a..c929c10f 100644 --- a/tests/unit/test_core/test_interface.py +++ b/tests/unit/test_core/test_interface.py @@ -9,6 +9,8 @@ from kytos.core.common import EntityStatus from kytos.core.exceptions import (KytosSetTagRangeError, + KytosTagsAreNotAvailable, + KytosTagsNotInTagRanges, KytosTagtypeNotSupported) from kytos.core.interface import TAG, UNI, Interface from kytos.core.switch import Switch @@ -186,19 +188,25 @@ async def test_interface_use_tags(self, controller): self.iface._notify_interface_tags = MagicMock() tags = [100, 200] # check use tag for the first time - is_success = self.iface.use_tags(controller, tags) - assert is_success + self.iface.use_tags(controller, tags) assert self.iface._notify_interface_tags.call_count == 1 # check use tag for the second time - is_success = self.iface.use_tags(controller, tags) - assert is_success is False + with pytest.raises(KytosTagsAreNotAvailable): + self.iface.use_tags(controller, tags) assert self.iface._notify_interface_tags.call_count == 1 - # check use tag after returning the tag to the pool - self.iface.make_tags_available(controller, tags) - is_success = self.iface.use_tags(controller, tags) - assert is_success + # check use tag after returning the tag to the pool as list + self.iface.make_tags_available(controller, [tags]) + self.iface.use_tags(controller, [tags]) + assert self.iface._notify_interface_tags.call_count == 3 + + with pytest.raises(KytosTagsAreNotAvailable): + self.iface.use_tags(controller, [tags]) + assert self.iface._notify_interface_tags.call_count == 3 + + with pytest.raises(KytosTagsAreNotAvailable): + self.iface.use_tags(controller, [tags], use_lock=False) assert self.iface._notify_interface_tags.call_count == 3 async def test_enable(self): @@ -335,41 +343,24 @@ async def test_make_tags_available(self, controller) -> None: assert self.iface.available_tags == available assert self.iface.tag_ranges == tag_ranges - assert self.iface.make_tags_available(controller, [1, 20]) is False + with pytest.raises(KytosTagsNotInTagRanges): + self.iface.make_tags_available(controller, [1, 20]) + assert self.iface._notify_interface_tags.call_count == 0 + + with pytest.raises(KytosTagsNotInTagRanges): + self.iface.make_tags_available(controller, [[1, 20]]) assert self.iface._notify_interface_tags.call_count == 0 - assert self.iface.make_tags_available(controller, [250, 280]) + + assert not self.iface.make_tags_available(controller, [250, 280]) assert self.iface._notify_interface_tags.call_count == 1 + with pytest.raises(KytosTagsNotInTagRanges): + self.iface.make_tags_available(controller, [1, 1], use_lock=False) + # Test sanity safe guard none = [None, None] - assert self.iface.make_tags_available(controller, none) is False - assert None not in self.iface.available_tags - assert self.iface._notify_interface_tags.call_count == 1 - - async def test_range_intersection(self) -> None: - """Test range_intersection""" - tags_a = [ - [3, 5], [7, 9], [11, 16], [21, 23], [25, 25], [27, 28], [30, 30] - ] - tags_b = [ - [1, 3], [6, 6], [10, 10], [12, 13], [15, 15], [17, 20], [22, 30] - ] - result = [] - iterator_result = self.iface.range_intersection(tags_a, tags_b) - for tag_range in iterator_result: - result.append(tag_range) - expected = [ - [3, 3], [12, 13], [15, 15], [22, 23], [25, 25], [27, 28], [30, 30] - ] - assert result == expected - - async def test_range_difference(self) -> None: - """Test range_difference""" - ranges_a = [[2, 3], [7, 10], [12, 12], [14, 14], [17, 19], [25, 27]] - ranges_b = [[1, 1], [4, 5], [8, 9], [11, 14], [18, 21], [23, 26]] - expected = [[2, 3], [7, 7], [10, 10], [17, 17], [27, 27]] - actual = self.iface.range_difference(ranges_a, ranges_b) - assert expected == actual + assert self.iface.make_tags_available(controller, none) == [none] + assert self.iface._notify_interface_tags.call_count == 2 async def test_set_tag_ranges(self, controller) -> None: """Test set_tag_ranges""" @@ -401,39 +392,6 @@ async def test_remove_tag_ranges(self) -> None: default = [[1, 4095]] assert self.iface.tag_ranges['vlan'] == default - async def test_find_index_remove(self) -> None: - """Test find_index_remove""" - tag_ranges = [[20, 20], [200, 3000]] - self.iface.set_tag_ranges(tag_ranges, 'vlan') - assert self.iface.tag_ranges['vlan'] == tag_ranges - index = self.iface.find_index_remove(tag_ranges, [20, 20]) - assert index == 0 - index = self.iface.find_index_remove(tag_ranges, [10, 15]) - assert index is None - index = self.iface.find_index_remove(tag_ranges, [190, 201]) - assert index is None - index = self.iface.find_index_remove(tag_ranges, [200, 250]) - assert index == 1 - - tag_ranges = [[200, 3000]] - self.iface.set_tag_ranges(tag_ranges, 'vlan') - assert self.iface.tag_ranges['vlan'] == tag_ranges - index = self.iface.find_index_remove(tag_ranges, [200, 250]) - assert index == 0 - - async def test_find_index_add(self) -> None: - """Test find_index_add""" - tag_ranges = [[20, 20], [200, 3000]] - self.iface.set_tag_ranges(tag_ranges, 'vlan') - assert self.iface.tag_ranges['vlan'] == tag_ranges - - index = self.iface.find_index_add(tag_ranges, [10, 15]) - assert index == 0 - index = self.iface.find_index_add(tag_ranges, [3004, 4000]) - assert index == 2 - index = self.iface.find_index_add(tag_ranges, [50, 202]) - assert index is None - async def test_remove_tags(self) -> None: """Test remove_tags""" available_tag = [[20, 20], [200, 3000]] diff --git a/tests/unit/test_core/test_tag_ranges.py b/tests/unit/test_core/test_tag_ranges.py new file mode 100644 index 00000000..06794be4 --- /dev/null +++ b/tests/unit/test_core/test_tag_ranges.py @@ -0,0 +1,93 @@ +"""Test kytos.core.tag_ranges""" +from kytos.core.tag_ranges import (find_index_add, find_index_remove, + range_addition, range_difference, + range_intersection) + + +def test_range_intersection(): + """Test range_intersection""" + tags_a = [ + [3, 5], [7, 9], [11, 16], [21, 23], [25, 25], [27, 28], [30, 30] + ] + tags_b = [ + [1, 3], [6, 6], [10, 10], [12, 13], [15, 15], [17, 20], [22, 30] + ] + result = [] + iterator_result = range_intersection(tags_a, tags_b) + for tag_range in iterator_result: + result.append(tag_range) + expected = [ + [3, 3], [12, 13], [15, 15], [22, 23], [25, 25], [27, 28], [30, 30] + ] + assert result == expected + + +def test_range_difference(): + """Test range_difference""" + ranges_a = [[7, 10], [12, 12], [14, 14], [17, 19], [25, 27], [30, 30]] + ranges_b = [[1, 1], [4, 5], [8, 9], [11, 14], [18, 26]] + expected = [[7, 7], [10, 10], [17, 17], [27, 27], [30, 30]] + actual = range_difference(ranges_a, ranges_b) + assert expected == actual + + +def test_find_index_remove(): + """Test find_index_remove""" + tag_ranges = [[20, 50], [200, 3000]] + index = find_index_remove(tag_ranges, [20, 20]) + assert index == 0 + index = find_index_remove(tag_ranges, [10, 15]) + assert index is None + index = find_index_remove(tag_ranges, [25, 30]) + assert index == 0 + + +def test_find_index_add(): + """Test find_index_add""" + tag_ranges = [[20, 20], [200, 3000]] + index = find_index_add(tag_ranges, [10, 15]) + assert index == 0 + index = find_index_add(tag_ranges, [3004, 4000]) + assert index == 2 + index = find_index_add(tag_ranges, [50, 202]) + assert index is None + + +def test_range_addition(): + """Test range_addition""" + ranges_a = [ + [3, 10], [20, 50], [60, 70], [80, 90], [100, 110], + [112, 120], [130, 140], [150, 160] + ] + ranges_b = [ + [1, 1], [3, 3], [9, 22], [24, 55], [57, 62], + [81, 101], [123, 128] + ] + expected_add = [ + [1, 1], [3, 55], [57, 70], [80, 110], [112, 120], + [123, 128], [130, 140], [150, 160] + ] + expected_intersection = [ + [3, 3], [9, 10], [20, 22], [24, 50], [60, 62], + [81, 90], [100, 101] + ] + result = range_addition(ranges_a, ranges_b) + assert expected_add == result[0] + assert expected_intersection == result[1] + + ranges_a = [[1, 4], [9, 15]] + ranges_b = [[6, 7]] + expected_add = [[1, 4], [6, 7], [9, 15]] + expected_intersection = [] + result = range_addition(ranges_a, ranges_b) + assert expected_add == result[0] + assert expected_intersection == result[1] + + # Corner case, assuring not unnecessary divisions + ranges_a = [[1, 2], [6, 7]] + ranges_b = [[3, 4], [9, 10]] + expected_add = [[1, 4], [6, 7], [9, 10]] + expected_intersection = [] + result = range_addition(ranges_a, ranges_b) + assert expected_add == result[0] + assert expected_intersection == result[1] From 5ea3ce57b97f68163b4ac95803f042875ff0e565 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 30 Oct 2023 18:26:42 -0400 Subject: [PATCH 02/13] Added interface ID to exceptions --- kytos/core/exceptions.py | 18 ++++++++++++------ kytos/core/interface.py | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/kytos/core/exceptions.py b/kytos/core/exceptions.py index fb8bbfe7..33900269 100644 --- a/kytos/core/exceptions.py +++ b/kytos/core/exceptions.py @@ -91,28 +91,34 @@ class KytosTagtypeNotSupported(Exception): class KytosTagsNotInTagRanges(Exception): """Exception thrown when a tag is outside of tag ranges""" - def __init__(self, conflict: list[list[int]]) -> None: + def __init__(self, conflict: list[list[int]], intf_id: str) -> None: super().__init__() self.conflict = conflict + self.intf_id = intf_id def __repr__(self): - return f"The tags {self.conflict} are outside tag_ranges" + return f"The tags {self.conflict} are outside tag_ranges"\ + f" in {self.intf_id}" def __str__(self) -> str: - return f"The tags {self.conflict} are outside tag_ranges" + return f"The tags {self.conflict} are outside tag_ranges"\ + f" in {self.intf_id}" class KytosTagsAreNotAvailable(Exception): """Exception thrown when a tag is not available.""" - def __init__(self, conflict: list[list[int]]) -> None: + def __init__(self, conflict: list[list[int]], intf_id: str) -> None: super().__init__() self.conflict = conflict + self.intf_id = intf_id def __repr__(self): - return f"The tags {self.conflict} are not available." + return f"The tags {self.conflict} are not available."\ + f" in {self.intf_id}" def __str__(self) -> str: - return f"The tags {self.conflict} are not available." + return f"The tags {self.conflict} are not available."\ + f" in {self.intf_id}" # Exceptions related to NApps diff --git a/kytos/core/interface.py b/kytos/core/interface.py index acb61354..a0ce3816 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -310,11 +310,11 @@ def _use_tags( if result is False: self.available_tags[tag_type] = available_copy conflict = range_difference(tags, available_copy) - raise KytosTagsAreNotAvailable(conflict) + raise KytosTagsAreNotAvailable(conflict, self._id) else: result = self.remove_tags(tags, tag_type) if result is False: - raise KytosTagsAreNotAvailable([tags]) + raise KytosTagsAreNotAvailable([tags], self._id) # pylint: disable=too-many-branches def add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: @@ -330,7 +330,7 @@ def add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: # Check if tags is within self.tag_ranges tag_ranges = self.tag_ranges[tag_type] if find_index_remove(tag_ranges, tags) is None: - raise KytosTagsNotInTagRanges([tags]) + raise KytosTagsNotInTagRanges([tags], self._id) available = self.available_tags[tag_type] index = find_index_add(available, tags) @@ -413,7 +413,7 @@ def _make_tags_available( if isinstance(tags[0], list): diff = range_difference(tags, self.tag_ranges[tag_type]) if diff: - raise KytosTagsNotInTagRanges(diff) + raise KytosTagsNotInTagRanges(diff, self._id) available_tags = self.available_tags[tag_type] new_tags, conflict = range_addition(tags, available_tags) self.available_tags[tag_type] = new_tags From 3af1652f696f925d91f4ef5d28b707962c5527d2 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 6 Nov 2023 10:38:22 -0500 Subject: [PATCH 03/13] Added TAGRange - Moved get_tag_ranges() --- kytos/core/exceptions.py | 13 ++++++ kytos/core/interface.py | 24 ++++++++++- kytos/core/tag_ranges.py | 52 ++++++++++++++++++++++- tests/unit/test_core/test_tag_ranges.py | 56 +++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 3 deletions(-) diff --git a/kytos/core/exceptions.py b/kytos/core/exceptions.py index 33900269..72eb4bdd 100644 --- a/kytos/core/exceptions.py +++ b/kytos/core/exceptions.py @@ -121,6 +121,19 @@ def __str__(self) -> str: f" in {self.intf_id}" +class KytosInvalidRanges(Exception): + """Exception thrown when a tag is not available.""" + def __init__(self, message: str) -> None: + super().__init__() + self.message = message + + def __repr__(self): + return f"KytosInvalidRanges {self.message}" + + def __str__(self) -> str: + return f"KytosInvalidRanges {self.message}" + + # Exceptions related to NApps diff --git a/kytos/core/interface.py b/kytos/core/interface.py index a0ce3816..68ccb4a4 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -41,7 +41,7 @@ class TAGType(Enum): class TAG: """Class that represents a TAG.""" - def __init__(self, tag_type, value): + def __init__(self, tag_type: str, value: int): self.tag_type = TAGType(tag_type).value self.value = value @@ -72,6 +72,22 @@ def __repr__(self): return f"TAG({self.tag_type!r}, {self.value!r})" +# pylint: disable=super-init-not-called +class TAGRange(TAG): + """Class that represents an User-to-Network Interface with + a tag value as a list.""" + + def __init__( + self, + tag_type: str, + value: list[list[int]], + mask_list: list[str, int] + ): + self.tag_type = TAGType(tag_type).value + self.value = value + self.mask_list = mask_list + + class Interface(GenericEntity): # pylint: disable=too-many-instance-attributes """Interface Class used to abstract the network interfaces.""" @@ -740,7 +756,11 @@ def _notify_interface_tags(self, controller): class UNI: """Class that represents an User-to-Network Interface.""" - def __init__(self, interface, user_tag=None): + def __init__( + self, + interface: Interface, + user_tag: Union[None, TAG, TAGRange] + ): self.user_tag = user_tag self.interface = interface diff --git a/kytos/core/tag_ranges.py b/kytos/core/tag_ranges.py index 2e705887..a97e6f26 100644 --- a/kytos/core/tag_ranges.py +++ b/kytos/core/tag_ranges.py @@ -1,6 +1,56 @@ """Methods for list of ranges [inclusive, inclusive]""" import bisect -from typing import Iterator, Optional +from typing import Iterator, Optional, Union + +from kytos.core.exceptions import KytosInvalidRanges + + +def map_singular_values(tag_range: Union[int, list[int]]): + """Change integer or singular interger list to + list[int, int] when necessary""" + if isinstance(tag_range, int): + tag_range = [tag_range] * 2 + elif len(tag_range) == 1: + tag_range = [tag_range[0]] * 2 + return tag_range + + +def get_tag_ranges(ranges: list[list[int]]): + """Get tag_ranges and check validity: + - It should be ordered + - Not unnecessary partition (eg. [[10,20],[20,30]]) + - Singular intergers are changed to ranges (eg. [10] to [[10, 10]]) + The ranges are understood as [inclusive, inclusive]""" + if len(ranges) < 1: + msg = "tag_ranges is empty" + raise KytosInvalidRanges(msg) + last_tag = 0 + ranges_n = len(ranges) + for i in range(0, ranges_n): + ranges[i] = map_singular_values(ranges[i]) + if ranges[i][0] > ranges[i][1]: + msg = f"The range {ranges[i]} is not ordered" + raise KytosInvalidRanges(msg) + if last_tag and last_tag > ranges[i][0]: + msg = f"tag_ranges is not ordered. {last_tag}"\ + f" is higher than {ranges[i][0]}" + raise KytosInvalidRanges(msg) + if last_tag and last_tag == ranges[i][0] - 1: + msg = f"tag_ranges has an unnecessary partition. "\ + f"{last_tag} is before to {ranges[i][0]}" + raise KytosInvalidRanges(msg) + if last_tag and last_tag == ranges[i][0]: + msg = f"tag_ranges has repetition. {ranges[i-1]}"\ + f" have same values as {ranges[i]}" + raise KytosInvalidRanges(msg) + last_tag = ranges[i][1] + if ranges[-1][1] > 4095: + msg = "Maximum value for tag_ranges is 4095" + raise KytosInvalidRanges(msg) + if ranges[0][0] < 1: + msg = "Minimum value for tag_ranges is 1" + raise KytosInvalidRanges(msg) + return ranges def range_intersection( diff --git a/tests/unit/test_core/test_tag_ranges.py b/tests/unit/test_core/test_tag_ranges.py index 06794be4..0ad7830e 100644 --- a/tests/unit/test_core/test_tag_ranges.py +++ b/tests/unit/test_core/test_tag_ranges.py @@ -1,9 +1,65 @@ """Test kytos.core.tag_ranges""" +import pytest + +from kytos.core.exceptions import KytosInvalidRanges from kytos.core.tag_ranges import (find_index_add, find_index_remove, + get_tag_ranges, map_singular_values, range_addition, range_difference, range_intersection) +def test_map_singular_values(): + """Test map_singular_values""" + mock_tag = 201 + result = map_singular_values(mock_tag) + assert result == [201, 201] + mock_tag = [201] + result = map_singular_values(mock_tag) + assert result == [201, 201] + + +def test_get_tag_ranges(): + """Test _get_tag_ranges""" + mock_ranges = [100, [150], [200, 3000]] + result = get_tag_ranges(mock_ranges) + assert result == [[100, 100], [150, 150], [200, 3000]] + + # Empty + mock_ranges = [] + with pytest.raises(KytosInvalidRanges): + get_tag_ranges(mock_ranges) + + # Range not ordered + mock_ranges = [[20, 19]] + with pytest.raises(KytosInvalidRanges): + get_tag_ranges(mock_ranges) + + # Ranges not ordered + mock_ranges = [[20, 50], [30, 3000]] + with pytest.raises(KytosInvalidRanges): + get_tag_ranges(mock_ranges) + + # Unnecessary partition + mock_ranges = [[20, 50], [51, 3000]] + with pytest.raises(KytosInvalidRanges): + get_tag_ranges(mock_ranges) + + # Repeated tag + mock_ranges = [[20, 50], [50, 3000]] + with pytest.raises(KytosInvalidRanges): + get_tag_ranges(mock_ranges) + + # Over 4095 + mock_ranges = [[20, 50], [52, 4096]] + with pytest.raises(KytosInvalidRanges): + get_tag_ranges(mock_ranges) + + # Under 1 + mock_ranges = [[0, 50], [52, 3000]] + with pytest.raises(KytosInvalidRanges): + get_tag_ranges(mock_ranges) + + def test_range_intersection(): """Test range_intersection""" tags_a = [ From 74bb1b1e3b8d667da044de78aef018749be8e553 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 6 Nov 2023 17:55:30 -0500 Subject: [PATCH 04/13] Added early returns --- kytos/core/interface.py | 4 ++++ kytos/core/tag_ranges.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/kytos/core/interface.py b/kytos/core/interface.py index 68ccb4a4..e3078391 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -297,6 +297,8 @@ def use_tags( tag_type: TAG type value use_lock: Boolean to whether use a lock or not """ + if not tags: + return if isinstance(tags, int): tags = [tags] * 2 if use_lock: @@ -402,6 +404,8 @@ def make_tags_available( Return: conflict: Return any values that were not added. """ + if not tags: + return if isinstance(tags, int): tags = [tags] * 2 if isinstance(tags[0], int) and tags[0] != tags[1]: diff --git a/kytos/core/tag_ranges.py b/kytos/core/tag_ranges.py index a97e6f26..5a84c9c4 100644 --- a/kytos/core/tag_ranges.py +++ b/kytos/core/tag_ranges.py @@ -87,6 +87,11 @@ def range_difference( This method simulates difference of sets. E.g.: [[10, 15]] - [[4, 11], [14, 45]] = [[12, 13]] """ + #print(f"RANGES -> {ranges_a} - {ranges_b}") + if not ranges_a: + return [] + if not ranges_b: + return ranges_a result = [] a_i, b_i = 0, 0 update = True @@ -130,6 +135,10 @@ def range_addition( """Addition between two ranges. Simulates the addition between two sets. Return[adittion product, intersection]""" + if not ranges_b: + return ranges_a + if not ranges_a: + return ranges_b result = [] conflict = [] a_i = b_i = 0 From c4920604eed80302618401de4dbfdbce96db11fd Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Wed, 8 Nov 2023 12:40:02 -0500 Subject: [PATCH 05/13] Added KytosTagError exception - Updated docstrings --- kytos/core/exceptions.py | 35 ++++++----- kytos/core/interface.py | 82 ++++++++++++------------- kytos/core/link.py | 20 +++--- kytos/core/tag_ranges.py | 25 ++++---- tests/unit/test_core/test_interface.py | 1 - tests/unit/test_core/test_tag_ranges.py | 16 ++--- 6 files changed, 91 insertions(+), 88 deletions(-) diff --git a/kytos/core/exceptions.py b/kytos/core/exceptions.py index 72eb4bdd..158ad893 100644 --- a/kytos/core/exceptions.py +++ b/kytos/core/exceptions.py @@ -85,8 +85,28 @@ class KytosLinkCreationError(Exception): """Exception thrown when the link has an empty endpoint.""" -class KytosTagtypeNotSupported(Exception): +class KytosTagError(Exception): + """Exception to catch an error when setting tag type or value.""" + def __init__(self, msg: str) -> None: + self.msg = msg + + def __str__(self) -> str: + return f"{self.msg}" + + def __repr__(self) -> str: + return f"{self.msg}" + + +class KytosTagtypeNotSupported(KytosTagError): """Exception thrown when a not supported tag type is not supported""" + def __init__(self, msg: str) -> None: + super().__init__(f"KytosTagtypeNotSupported, {msg}") + + +class KytosInvalidTagRanges(KytosTagError): + """Exception thrown when a list of ranges is invalid.""" + def __init__(self, msg: str) -> None: + super().__init__(f"KytosInvalidTagRanges, {msg}") class KytosTagsNotInTagRanges(Exception): @@ -121,19 +141,6 @@ def __str__(self) -> str: f" in {self.intf_id}" -class KytosInvalidRanges(Exception): - """Exception thrown when a tag is not available.""" - def __init__(self, message: str) -> None: - super().__init__() - self.message = message - - def __repr__(self): - return f"KytosInvalidRanges {self.message}" - - def __str__(self) -> str: - return f"KytosInvalidRanges {self.message}" - - # Exceptions related to NApps diff --git a/kytos/core/interface.py b/kytos/core/interface.py index e3078391..d466c24b 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -81,7 +81,7 @@ def __init__( self, tag_type: str, value: list[list[int]], - mask_list: list[str, int] + mask_list: Optional[list[str, int]] = None ): self.tag_type = TAGType(tag_type).value self.value = value @@ -215,7 +215,8 @@ def status_reason(self): @property def default_tag_values(self) -> dict[str, list[list[int]]]: - """Return a default list of ranges""" + """Return a default list of ranges. Applicable to + available_tags and tag_ranges.""" default_values = { "vlan": [[1, 4095]], "vlan_qinq": [[1, 4095]], @@ -224,7 +225,7 @@ def default_tag_values(self) -> dict[str, list[list[int]]]: return default_values def set_tag_ranges(self, tag_ranges: list[list[int]], tag_type: str): - """Set new restriction""" + """Set new restriction, tag_ranges.""" if tag_type != TAGType.VLAN.value: msg = f"Tag type {tag_type} is not supported." raise KytosTagtypeNotSupported(msg) @@ -239,14 +240,14 @@ def set_tag_ranges(self, tag_ranges: list[list[int]], tag_type: str): raise KytosSetTagRangeError(msg) # Resizing - new_tag_ranges = range_difference( + new_available_tags = range_difference( tag_ranges, used_tags ) - self.available_tags[tag_type] = new_tag_ranges + self.available_tags[tag_type] = new_available_tags self.tag_ranges[tag_type] = tag_ranges def remove_tag_ranges(self, tag_type: str): - """Set tag_ranges[tag_type] to default [[1, 4095]]""" + """Set tag_ranges[tag_type] to default value""" if tag_type != TAGType.VLAN.value: msg = f"Tag type {tag_type} is not supported." raise KytosTagtypeNotSupported(msg) @@ -260,7 +261,7 @@ def remove_tag_ranges(self, tag_type: str): self.tag_ranges[tag_type] = self.default_tag_values[tag_type] def remove_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: - """Remove tags by resize available_tags + """Remove tags by resizing available_tags Returns False if nothing was remove, True otherwise""" available = self.available_tags[tag_type] index = find_index_remove(available, tags) @@ -289,29 +290,27 @@ def use_tags( use_lock: bool = True, ): """Remove a specific tag from available_tags if it is there. - Return False in case the tags were not able to be removed. + Exception raised in case the tags were not able to be removed. Args: controller: Kytos controller - tags: List of tags, there should be 2 items in the list + tags: value to be removed, multiple types for compatibility: + (int): Single tag + (list[int]): Single range of tags + (list[list[int]]): List of ranges of tags tag_type: TAG type value use_lock: Boolean to whether use a lock or not + + Exceptions: + KytosTagsAreNotAvailable from _use_tags() """ - if not tags: - return if isinstance(tags, int): tags = [tags] * 2 if use_lock: with self._tag_lock: - try: - self._use_tags(tags, tag_type) - except KytosTagsAreNotAvailable as err: - raise err - else: - try: self._use_tags(tags, tag_type) - except KytosTagsAreNotAvailable as err: - raise err + else: + self._use_tags(tags, tag_type) self._notify_interface_tags(controller) @@ -320,7 +319,7 @@ def _use_tags( tags: Union[list[int], list[list[int]]], tag_type: str ): - """Manage available_tags deletion changes""" + """Manage available_tags deletion changes.""" if isinstance(tags[0], list): available_copy = deepcopy(self.available_tags[tag_type]) for tag_range in tags: @@ -393,34 +392,33 @@ def make_tags_available( tag_type: str = 'vlan', use_lock: bool = True, ) -> list[list[int]]: - """Add a specific tag in available_tags. + """Add a tags in available_tags. Args: controller: Kytos controller - tags: List of tags, there should be 2 items in the list + tags: value to be added, multiple types for compatibility: + (int): Single tag + (list[int]): Single range of tags + (list[list[int]]): List of ranges of tags tag_type: TAG type value use_lock: Boolean to whether use a lock or not Return: conflict: Return any values that were not added. + + Exeptions: + KytosTagsNotInTagRanges from _make_tags_available() """ - if not tags: - return if isinstance(tags, int): tags = [tags] * 2 - if isinstance(tags[0], int) and tags[0] != tags[1]: - tags = [tags[0], tags[1]] + if (isinstance(tags, list) and len(tags) == 2 and + isinstance(tags[0], int) and tags[0] != tags[1]): + tags = [tags] if use_lock: with self._tag_lock: - try: - conflict = self._make_tags_available(tags, tag_type) - except KytosTagsNotInTagRanges as err: - raise err - else: - try: conflict = self._make_tags_available(tags, tag_type) - except KytosTagsNotInTagRanges as err: - raise err + else: + conflict = self._make_tags_available(tags, tag_type) self._notify_interface_tags(controller) return conflict @@ -429,7 +427,11 @@ def _make_tags_available( tags: Union[list[int], list[list[int]]], tag_type: str, ) -> list[list[int]]: - """Manage available_tags adittion changes""" + """Manage available_tags adittion changes + + Exceptions: + KytosTagsNotInTagRanges from add_tags() + """ if isinstance(tags[0], list): diff = range_difference(tags, self.tag_ranges[tag_type]) if diff: @@ -438,10 +440,7 @@ def _make_tags_available( new_tags, conflict = range_addition(tags, available_tags) self.available_tags[tag_type] = new_tags return conflict - try: - result = self.add_tags(tags, tag_type) - except KytosTagsNotInTagRanges as err: - raise err + result = self.add_tags(tags, tag_type) if result is False: return [tags] return [] @@ -449,12 +448,13 @@ def _make_tags_available( def set_available_tags_tag_ranges( self, available_tag: dict[str, list[list[int]]], - tag_ranges: dict[str, Optional[list[list[int]]]] + tag_ranges: dict[str, list[list[int]]] ): """Set a range of VLAN tags to be used by this Interface. Args: - iterable ([int]): range of VLANs. + available_tag: Available tags from each tag type + tag_ranges: Restriction for each type of available tag """ with self._tag_lock: self.available_tags = available_tag diff --git a/kytos/core/link.py b/kytos/core/link.py index 952cd50a..a37405ea 100644 --- a/kytos/core/link.py +++ b/kytos/core/link.py @@ -12,8 +12,7 @@ from kytos.core.common import EntityStatus, GenericEntity from kytos.core.exceptions import (KytosLinkCreationError, - KytosNoTagAvailableError, - KytosTagsNotInTagRanges) + KytosNoTagAvailableError) from kytos.core.id import LinkID from kytos.core.interface import Interface, TAGType from kytos.core.tag_ranges import range_intersection @@ -170,7 +169,7 @@ def get_next_available_tag( def make_tags_available( self, controller, - tags: Union[int, list[int]], + tags: Union[int, list[int], list[list[int]]], link_id, tag_type: str = 'vlan' ) -> tuple[list[list[int]], list[list[int]]]: @@ -178,15 +177,12 @@ def make_tags_available( with self._get_available_vlans_lock[link_id]: with self.endpoint_a._tag_lock: with self.endpoint_b._tag_lock: - try: - conflict_a = self.endpoint_a.make_tags_available( - controller, tags, tag_type, False - ) - conflict_b = self.endpoint_b.make_tags_available( - controller, tags, tag_type, False - ) - except KytosTagsNotInTagRanges as err: - raise err + conflict_a = self.endpoint_a.make_tags_available( + controller, tags, tag_type, False + ) + conflict_b = self.endpoint_b.make_tags_available( + controller, tags, tag_type, False + ) return conflict_a, conflict_b def available_vlans(self): diff --git a/kytos/core/tag_ranges.py b/kytos/core/tag_ranges.py index 5a84c9c4..e9c3d296 100644 --- a/kytos/core/tag_ranges.py +++ b/kytos/core/tag_ranges.py @@ -1,8 +1,10 @@ """Methods for list of ranges [inclusive, inclusive]""" +# pylint: disable=too-many-branches import bisect +from copy import deepcopy from typing import Iterator, Optional, Union -from kytos.core.exceptions import KytosInvalidRanges +from kytos.core.exceptions import KytosInvalidTagRanges def map_singular_values(tag_range: Union[int, list[int]]): @@ -23,33 +25,33 @@ def get_tag_ranges(ranges: list[list[int]]): The ranges are understood as [inclusive, inclusive]""" if len(ranges) < 1: msg = "tag_ranges is empty" - raise KytosInvalidRanges(msg) + raise KytosInvalidTagRanges(msg) last_tag = 0 ranges_n = len(ranges) for i in range(0, ranges_n): ranges[i] = map_singular_values(ranges[i]) if ranges[i][0] > ranges[i][1]: msg = f"The range {ranges[i]} is not ordered" - raise KytosInvalidRanges(msg) + raise KytosInvalidTagRanges(msg) if last_tag and last_tag > ranges[i][0]: msg = f"tag_ranges is not ordered. {last_tag}"\ f" is higher than {ranges[i][0]}" - raise KytosInvalidRanges(msg) + raise KytosInvalidTagRanges(msg) if last_tag and last_tag == ranges[i][0] - 1: msg = f"tag_ranges has an unnecessary partition. "\ f"{last_tag} is before to {ranges[i][0]}" - raise KytosInvalidRanges(msg) + raise KytosInvalidTagRanges(msg) if last_tag and last_tag == ranges[i][0]: msg = f"tag_ranges has repetition. {ranges[i-1]}"\ f" have same values as {ranges[i]}" - raise KytosInvalidRanges(msg) + raise KytosInvalidTagRanges(msg) last_tag = ranges[i][1] if ranges[-1][1] > 4095: msg = "Maximum value for tag_ranges is 4095" - raise KytosInvalidRanges(msg) + raise KytosInvalidTagRanges(msg) if ranges[0][0] < 1: msg = "Minimum value for tag_ranges is 1" - raise KytosInvalidRanges(msg) + raise KytosInvalidTagRanges(msg) return ranges @@ -87,11 +89,10 @@ def range_difference( This method simulates difference of sets. E.g.: [[10, 15]] - [[4, 11], [14, 45]] = [[12, 13]] """ - #print(f"RANGES -> {ranges_a} - {ranges_b}") if not ranges_a: return [] if not ranges_b: - return ranges_a + return deepcopy(ranges_a) result = [] a_i, b_i = 0, 0 update = True @@ -136,9 +137,9 @@ def range_addition( Simulates the addition between two sets. Return[adittion product, intersection]""" if not ranges_b: - return ranges_a + return deepcopy(ranges_a) if not ranges_a: - return ranges_b + return deepcopy(ranges_b) result = [] conflict = [] a_i = b_i = 0 diff --git a/tests/unit/test_core/test_interface.py b/tests/unit/test_core/test_interface.py index c929c10f..05e5ac67 100644 --- a/tests/unit/test_core/test_interface.py +++ b/tests/unit/test_core/test_interface.py @@ -370,7 +370,6 @@ async def test_set_tag_ranges(self, controller) -> None: self.iface.set_tag_ranges(tag_ranges, 'vlan') self.iface.use_tags(controller, [200, 250]) - self.iface.set_tag_ranges(tag_ranges, 'vlan') ava_expected = [[20, 20], [251, 3000]] assert self.iface.tag_ranges['vlan'] == tag_ranges assert self.iface.available_tags['vlan'] == ava_expected diff --git a/tests/unit/test_core/test_tag_ranges.py b/tests/unit/test_core/test_tag_ranges.py index 0ad7830e..80ee8dd7 100644 --- a/tests/unit/test_core/test_tag_ranges.py +++ b/tests/unit/test_core/test_tag_ranges.py @@ -1,7 +1,7 @@ """Test kytos.core.tag_ranges""" import pytest -from kytos.core.exceptions import KytosInvalidRanges +from kytos.core.exceptions import KytosInvalidTagRanges from kytos.core.tag_ranges import (find_index_add, find_index_remove, get_tag_ranges, map_singular_values, range_addition, range_difference, @@ -26,37 +26,37 @@ def test_get_tag_ranges(): # Empty mock_ranges = [] - with pytest.raises(KytosInvalidRanges): + with pytest.raises(KytosInvalidTagRanges): get_tag_ranges(mock_ranges) # Range not ordered mock_ranges = [[20, 19]] - with pytest.raises(KytosInvalidRanges): + with pytest.raises(KytosInvalidTagRanges): get_tag_ranges(mock_ranges) # Ranges not ordered mock_ranges = [[20, 50], [30, 3000]] - with pytest.raises(KytosInvalidRanges): + with pytest.raises(KytosInvalidTagRanges): get_tag_ranges(mock_ranges) # Unnecessary partition mock_ranges = [[20, 50], [51, 3000]] - with pytest.raises(KytosInvalidRanges): + with pytest.raises(KytosInvalidTagRanges): get_tag_ranges(mock_ranges) # Repeated tag mock_ranges = [[20, 50], [50, 3000]] - with pytest.raises(KytosInvalidRanges): + with pytest.raises(KytosInvalidTagRanges): get_tag_ranges(mock_ranges) # Over 4095 mock_ranges = [[20, 50], [52, 4096]] - with pytest.raises(KytosInvalidRanges): + with pytest.raises(KytosInvalidTagRanges): get_tag_ranges(mock_ranges) # Under 1 mock_ranges = [[0, 50], [52, 3000]] - with pytest.raises(KytosInvalidRanges): + with pytest.raises(KytosInvalidTagRanges): get_tag_ranges(mock_ranges) From 572185574aa7a18147df8333b33d71b39a6995d6 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 9 Nov 2023 03:07:58 -0500 Subject: [PATCH 06/13] Fixed some messages --- kytos/core/exceptions.py | 10 ++++++---- kytos/core/tag_ranges.py | 17 +++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/kytos/core/exceptions.py b/kytos/core/exceptions.py index 158ad893..1ad2b427 100644 --- a/kytos/core/exceptions.py +++ b/kytos/core/exceptions.py @@ -77,10 +77,6 @@ def __str__(self): return msg -class KytosSetTagRangeError(Exception): - """Exception raised when available_tag cannot be resized""" - - class KytosLinkCreationError(Exception): """Exception thrown when the link has an empty endpoint.""" @@ -109,6 +105,12 @@ def __init__(self, msg: str) -> None: super().__init__(f"KytosInvalidTagRanges, {msg}") +class KytosSetTagRangeError(KytosTagError): + """Exception raised when available_tag cannot be resized""" + def __init__(self, msg: str) -> None: + super().__init__(f"KytosSetTagRangeError, {msg}") + + class KytosTagsNotInTagRanges(Exception): """Exception thrown when a tag is outside of tag ranges""" def __init__(self, conflict: list[list[int]], intf_id: str) -> None: diff --git a/kytos/core/tag_ranges.py b/kytos/core/tag_ranges.py index e9c3d296..3ac1639c 100644 --- a/kytos/core/tag_ranges.py +++ b/kytos/core/tag_ranges.py @@ -22,9 +22,10 @@ def get_tag_ranges(ranges: list[list[int]]): - It should be ordered - Not unnecessary partition (eg. [[10,20],[20,30]]) - Singular intergers are changed to ranges (eg. [10] to [[10, 10]]) + The ranges are understood as [inclusive, inclusive]""" if len(ranges) < 1: - msg = "tag_ranges is empty" + msg = "Tag range is empty" raise KytosInvalidTagRanges(msg) last_tag = 0 ranges_n = len(ranges) @@ -34,23 +35,23 @@ def get_tag_ranges(ranges: list[list[int]]): msg = f"The range {ranges[i]} is not ordered" raise KytosInvalidTagRanges(msg) if last_tag and last_tag > ranges[i][0]: - msg = f"tag_ranges is not ordered. {last_tag}"\ + msg = f"Tag ranges are not ordered. {last_tag}"\ f" is higher than {ranges[i][0]}" raise KytosInvalidTagRanges(msg) if last_tag and last_tag == ranges[i][0] - 1: - msg = f"tag_ranges has an unnecessary partition. "\ + msg = f"Tag ranges have an unnecessary partition. "\ f"{last_tag} is before to {ranges[i][0]}" raise KytosInvalidTagRanges(msg) if last_tag and last_tag == ranges[i][0]: - msg = f"tag_ranges has repetition. {ranges[i-1]}"\ + msg = f"Tag ranges have repetition. {ranges[i-1]}"\ f" have same values as {ranges[i]}" raise KytosInvalidTagRanges(msg) last_tag = ranges[i][1] if ranges[-1][1] > 4095: - msg = "Maximum value for tag_ranges is 4095" + msg = "Maximum value for a tag is 4095" raise KytosInvalidTagRanges(msg) if ranges[0][0] < 1: - msg = "Minimum value for tag_ranges is 1" + msg = "Minimum value for a tag is 1" raise KytosInvalidTagRanges(msg) return ranges @@ -137,9 +138,9 @@ def range_addition( Simulates the addition between two sets. Return[adittion product, intersection]""" if not ranges_b: - return deepcopy(ranges_a) + return deepcopy(ranges_a), [] if not ranges_a: - return deepcopy(ranges_b) + return deepcopy(ranges_b), [] result = [] conflict = [] a_i = b_i = 0 From 21b1a85a803a929a45aba4a2aea85dbba88efef6 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 9 Nov 2023 15:57:56 -0500 Subject: [PATCH 07/13] Nested kytos exceptions --- kytos/core/exceptions.py | 30 ++++++------------------------ kytos/core/interface.py | 8 ++++++++ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/kytos/core/exceptions.py b/kytos/core/exceptions.py index 1ad2b427..36a47f81 100644 --- a/kytos/core/exceptions.py +++ b/kytos/core/exceptions.py @@ -111,36 +111,18 @@ def __init__(self, msg: str) -> None: super().__init__(f"KytosSetTagRangeError, {msg}") -class KytosTagsNotInTagRanges(Exception): +class KytosTagsNotInTagRanges(KytosTagError): """Exception thrown when a tag is outside of tag ranges""" def __init__(self, conflict: list[list[int]], intf_id: str) -> None: - super().__init__() - self.conflict = conflict - self.intf_id = intf_id - - def __repr__(self): - return f"The tags {self.conflict} are outside tag_ranges"\ - f" in {self.intf_id}" - - def __str__(self) -> str: - return f"The tags {self.conflict} are outside tag_ranges"\ - f" in {self.intf_id}" + msg = f"The tags {conflict} are outside tag_ranges in {intf_id}" + super().__init__(f"KytosSetTagRangeError, {msg}") -class KytosTagsAreNotAvailable(Exception): +class KytosTagsAreNotAvailable(KytosTagError): """Exception thrown when a tag is not available.""" def __init__(self, conflict: list[list[int]], intf_id: str) -> None: - super().__init__() - self.conflict = conflict - self.intf_id = intf_id - - def __repr__(self): - return f"The tags {self.conflict} are not available."\ - f" in {self.intf_id}" - - def __str__(self) -> str: - return f"The tags {self.conflict} are not available."\ - f" in {self.intf_id}" + msg = f"The tags {conflict} are not available in {intf_id}" + super().__init__(f"KytosSetTagRangeError, {msg}") # Exceptions related to NApps diff --git a/kytos/core/interface.py b/kytos/core/interface.py index d466c24b..5953bec3 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -87,6 +87,14 @@ def __init__( self.value = value self.mask_list = mask_list + def as_dict(self): + """Return a dictionary representating a tag range object.""" + return { + 'tag_type': self.tag_type, + 'value': self.value, + 'mask_list': self.mask_list + } + class Interface(GenericEntity): # pylint: disable=too-many-instance-attributes """Interface Class used to abstract the network interfaces.""" From 6b3caf9372f4872f2e866d95145a10c29ed43d54 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 16 Nov 2023 16:12:16 -0500 Subject: [PATCH 08/13] Added validation to Interface tags methods --- kytos/core/interface.py | 38 +++++++++++++++-- kytos/core/tag_ranges.py | 15 +++---- tests/unit/test_core/test_interface.py | 57 +++++++++++++++++++++++--- 3 files changed, 93 insertions(+), 17 deletions(-) diff --git a/kytos/core/interface.py b/kytos/core/interface.py index 5953bec3..2e8e0069 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -16,14 +16,16 @@ from kytos.core.common import EntityStatus, GenericEntity from kytos.core.events import KytosEvent -from kytos.core.exceptions import (KytosSetTagRangeError, +from kytos.core.exceptions import (KytosInvalidTagRanges, + KytosSetTagRangeError, KytosTagsAreNotAvailable, KytosTagsNotInTagRanges, KytosTagtypeNotSupported) from kytos.core.helpers import now from kytos.core.id import InterfaceID from kytos.core.tag_ranges import (find_index_add, find_index_remove, - range_addition, range_difference) + get_tag_ranges, range_addition, + range_difference) __all__ = ('Interface',) @@ -272,6 +274,8 @@ def remove_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: """Remove tags by resizing available_tags Returns False if nothing was remove, True otherwise""" available = self.available_tags[tag_type] + if not available: + return False index = find_index_remove(available, tags) if index is None: return False @@ -296,6 +300,7 @@ def use_tags( tags: Union[int, list[int], list[list[int]]], tag_type: str = 'vlan', use_lock: bool = True, + check_order: bool = True, ): """Remove a specific tag from available_tags if it is there. Exception raised in case the tags were not able to be removed. @@ -314,6 +319,8 @@ def use_tags( """ if isinstance(tags, int): tags = [tags] * 2 + elif check_order: + tags = self.get_verified_tags(tags) if use_lock: with self._tag_lock: self._use_tags(tags, tag_type) @@ -358,6 +365,10 @@ def add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: raise KytosTagsNotInTagRanges([tags], self._id) available = self.available_tags[tag_type] + if not available: + self.available_tags[tag_type] = [tags] + return True + index = find_index_add(available, tags) if index is None: return False @@ -399,6 +410,7 @@ def make_tags_available( tags: Union[int, list[int], list[list[int]]], tag_type: str = 'vlan', use_lock: bool = True, + check_order: bool = True, ) -> list[list[int]]: """Add a tags in available_tags. @@ -419,8 +431,9 @@ def make_tags_available( """ if isinstance(tags, int): tags = [tags] * 2 - if (isinstance(tags, list) and len(tags) == 2 and - isinstance(tags[0], int) and tags[0] != tags[1]): + elif check_order: + tags = self.get_verified_tags(tags) + if isinstance(tags[0], int) and tags[0] != tags[1]: tags = [tags] if use_lock: with self._tag_lock: @@ -453,6 +466,23 @@ def _make_tags_available( return [tags] return [] + @staticmethod + def get_verified_tags( + tags: Union[list[int], list[list[int]]] + ) -> Union[list[int], list[list[int]]]: + """Return tags which values are validated to be correct.""" + if isinstance(tags, list) and isinstance(tags[0], int): + if len(tags) == 1: + return [tags[0], tags[0]] + if len(tags) == 2 and tags[0] > tags[1]: + raise KytosInvalidTagRanges(f"Range out of order {tags}") + if len(tags) > 2: + raise KytosInvalidTagRanges(f"Range have 2 values {tags}") + return tags + if isinstance(tags, list) and isinstance(tags[0], list): + return get_tag_ranges(tags) + raise KytosInvalidTagRanges(f"Value type not recognized {tags}") + def set_available_tags_tag_ranges( self, available_tag: dict[str, list[list[int]]], diff --git a/kytos/core/tag_ranges.py b/kytos/core/tag_ranges.py index 3ac1639c..c092b2b1 100644 --- a/kytos/core/tag_ranges.py +++ b/kytos/core/tag_ranges.py @@ -61,7 +61,7 @@ def range_intersection( ranges_b: list[list[int]] ) -> Iterator[list[int]]: """Returns an iterator of an intersection between - two list of ranges""" + two validated list of ranges""" a_i, b_i = 0, 0 while a_i < len(ranges_a) and b_i < len(ranges_b): fst_a, snd_a = ranges_a[a_i] @@ -83,10 +83,11 @@ def range_intersection( def range_difference( - ranges_a: list[list[int]], - ranges_b: list[list[int]] + ranges_a: list[Optional[list[int]]], + ranges_b: list[Optional[list[int]]] ) -> list[list[int]]: - """The operation is (ranges_a - ranges_b). + """The operation is two validated list of ranges + (ranges_a - ranges_b). This method simulates difference of sets. E.g.: [[10, 15]] - [[4, 11], [14, 45]] = [[12, 13]] """ @@ -131,10 +132,10 @@ def range_difference( def range_addition( - ranges_a: list[list[int]], - ranges_b: list[list[int]] + ranges_a: list[Optional[list[int]]], + ranges_b: list[Optional[list[int]]] ) -> tuple[list[list[int]], list[list[int]]]: - """Addition between two ranges. + """Addition between two validated list of ranges. Simulates the addition between two sets. Return[adittion product, intersection]""" if not ranges_b: diff --git a/tests/unit/test_core/test_interface.py b/tests/unit/test_core/test_interface.py index 05e5ac67..7ac71043 100644 --- a/tests/unit/test_core/test_interface.py +++ b/tests/unit/test_core/test_interface.py @@ -2,13 +2,14 @@ # pylint: disable=attribute-defined-outside-init import logging import pickle -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock, Mock, patch import pytest from pyof.v0x04.common.port import PortFeatures from kytos.core.common import EntityStatus -from kytos.core.exceptions import (KytosSetTagRangeError, +from kytos.core.exceptions import (KytosInvalidTagRanges, + KytosSetTagRangeError, KytosTagsAreNotAvailable, KytosTagsNotInTagRanges, KytosTagtypeNotSupported) @@ -357,10 +358,7 @@ async def test_make_tags_available(self, controller) -> None: with pytest.raises(KytosTagsNotInTagRanges): self.iface.make_tags_available(controller, [1, 1], use_lock=False) - # Test sanity safe guard - none = [None, None] - assert self.iface.make_tags_available(controller, none) == [none] - assert self.iface._notify_interface_tags.call_count == 2 + assert self.iface.make_tags_available(controller, 300) == [[300, 300]] async def test_set_tag_ranges(self, controller) -> None: """Test set_tag_ranges""" @@ -412,6 +410,18 @@ async def test_remove_tags(self) -> None: assert self.iface.remove_tags([500, 3000]) assert self.iface.available_tags['vlan'] == ava_expected + async def test_remove_tags_empty(self) -> None: + """Test remove_tags when available_tags is empty""" + available_tag = [] + tag_ranges = [[1, 4095]] + parameters = { + "available_tag": {'vlan': available_tag}, + "tag_ranges": {'vlan': tag_ranges} + } + self.iface.set_available_tags_tag_ranges(**parameters) + assert self.iface.remove_tags([4, 6]) is False + assert self.iface.available_tags['vlan'] == [] + async def test_add_tags(self) -> None: """Test add_tags""" available_tag = [[7, 10], [20, 30]] @@ -455,6 +465,18 @@ async def test_add_tags(self) -> None: assert self.iface.add_tags([35, 98]) is False + async def test_add_tags_empty(self) -> None: + """Test add_tags when available_tags is empty""" + available_tag = [] + tag_ranges = [[1, 4095]] + parameters = { + "available_tag": {'vlan': available_tag}, + "tag_ranges": {'vlan': tag_ranges} + } + self.iface.set_available_tags_tag_ranges(**parameters) + assert self.iface.add_tags([4, 6]) + assert self.iface.available_tags['vlan'] == [[4, 6]] + async def test_notify_interface_tags(self, controller) -> None: """Test _notify_interface_tags""" name = "kytos/core.interface_tags" @@ -464,6 +486,29 @@ async def test_notify_interface_tags(self, controller) -> None: assert event.name == name assert event.content == content + @patch("kytos.core.interface.get_tag_ranges") + async def test_get_verified_tags(self, mock_get_tag_ranges) -> None: + """Test get_verified_tags""" + + result = Interface.get_verified_tags([1, 2]) + assert result == [1, 2] + + result = Interface.get_verified_tags([4]) + assert result == [4, 4] + + result = Interface.get_verified_tags([[1, 2], [4, 5]]) + assert mock_get_tag_ranges.call_count == 1 + assert mock_get_tag_ranges.call_args[0][0] == [[1, 2], [4, 5]] + + with pytest.raises(KytosInvalidTagRanges): + Interface.get_verified_tags("a") + + with pytest.raises(KytosInvalidTagRanges): + Interface.get_verified_tags([1, 2, 3]) + + with pytest.raises(KytosInvalidTagRanges): + Interface.get_verified_tags([5, 2]) + class TestUNI(): """UNI tests.""" From 00d354d97fd0a8e482ee75958fd4e4ae8bdceb05 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Thu, 16 Nov 2023 19:51:46 -0500 Subject: [PATCH 09/13] Adapted `make_tags_available()` --- kytos/core/link.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kytos/core/link.py b/kytos/core/link.py index a37405ea..2f9d41d1 100644 --- a/kytos/core/link.py +++ b/kytos/core/link.py @@ -171,17 +171,18 @@ def make_tags_available( controller, tags: Union[int, list[int], list[list[int]]], link_id, - tag_type: str = 'vlan' + tag_type: str = 'vlan', + check_order: bool = True, ) -> tuple[list[list[int]], list[list[int]]]: """Add a specific tag in available_tags.""" with self._get_available_vlans_lock[link_id]: with self.endpoint_a._tag_lock: with self.endpoint_b._tag_lock: conflict_a = self.endpoint_a.make_tags_available( - controller, tags, tag_type, False + controller, tags, tag_type, False, check_order ) conflict_b = self.endpoint_b.make_tags_available( - controller, tags, tag_type, False + controller, tags, tag_type, False, check_order ) return conflict_a, conflict_b From f80d202828284e2a229937ad1eba7356edf4a049 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 20 Nov 2023 18:31:55 -0500 Subject: [PATCH 10/13] Moved test_get_validated_tags() to tag_ranges.py - Updated docstrings --- kytos/core/interface.py | 47 +++++++---------- kytos/core/link.py | 10 ++-- kytos/core/tag_ranges.py | 44 ++++++++++++++-- tests/unit/test_core/test_interface.py | 68 ++++++++----------------- tests/unit/test_core/test_tag_ranges.py | 25 +++++++-- 5 files changed, 107 insertions(+), 87 deletions(-) diff --git a/kytos/core/interface.py b/kytos/core/interface.py index 2e8e0069..f270492c 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -7,7 +7,7 @@ from enum import Enum from functools import reduce from threading import Lock -from typing import Optional, Union +from typing import Union from pyof.v0x01.common.phy_port import Port as PortNo01 from pyof.v0x01.common.phy_port import PortFeatures as PortFeatures01 @@ -16,15 +16,14 @@ from kytos.core.common import EntityStatus, GenericEntity from kytos.core.events import KytosEvent -from kytos.core.exceptions import (KytosInvalidTagRanges, - KytosSetTagRangeError, +from kytos.core.exceptions import (KytosSetTagRangeError, KytosTagsAreNotAvailable, KytosTagsNotInTagRanges, KytosTagtypeNotSupported) from kytos.core.helpers import now from kytos.core.id import InterfaceID from kytos.core.tag_ranges import (find_index_add, find_index_remove, - get_tag_ranges, range_addition, + get_validated_tags, range_addition, range_difference) __all__ = ('Interface',) @@ -79,11 +78,12 @@ class TAGRange(TAG): """Class that represents an User-to-Network Interface with a tag value as a list.""" + # pylint: disable=dangerous-default-value def __init__( self, tag_type: str, value: list[list[int]], - mask_list: Optional[list[str, int]] = None + mask_list: list[Union[str, int]] = [] ): self.tag_type = TAGType(tag_type).value self.value = value @@ -270,7 +270,7 @@ def remove_tag_ranges(self, tag_type: str): ) self.tag_ranges[tag_type] = self.default_tag_values[tag_type] - def remove_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: + def _remove_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: """Remove tags by resizing available_tags Returns False if nothing was remove, True otherwise""" available = self.available_tags[tag_type] @@ -313,6 +313,8 @@ def use_tags( (list[list[int]]): List of ranges of tags tag_type: TAG type value use_lock: Boolean to whether use a lock or not + check_order: Boolean to whether validate tags(list). Check order, + type and length. Set to false when invocated internally. Exceptions: KytosTagsAreNotAvailable from _use_tags() @@ -320,7 +322,7 @@ def use_tags( if isinstance(tags, int): tags = [tags] * 2 elif check_order: - tags = self.get_verified_tags(tags) + tags = get_validated_tags(tags) if use_lock: with self._tag_lock: self._use_tags(tags, tag_type) @@ -338,18 +340,18 @@ def _use_tags( if isinstance(tags[0], list): available_copy = deepcopy(self.available_tags[tag_type]) for tag_range in tags: - result = self.remove_tags(tag_range, tag_type) + result = self._remove_tags(tag_range, tag_type) if result is False: self.available_tags[tag_type] = available_copy conflict = range_difference(tags, available_copy) raise KytosTagsAreNotAvailable(conflict, self._id) else: - result = self.remove_tags(tags, tag_type) + result = self._remove_tags(tags, tag_type) if result is False: raise KytosTagsAreNotAvailable([tags], self._id) # pylint: disable=too-many-branches - def add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: + def _add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool: """Add tags, return True if they were added. Returns False when nothing was added, True otherwise Ensuring that ranges are not unnecessarily divided @@ -422,6 +424,8 @@ def make_tags_available( (list[list[int]]): List of ranges of tags tag_type: TAG type value use_lock: Boolean to whether use a lock or not + check_order: Boolean to whether validate tags(list). Check order, + type and length. Set to false when invocated internally. Return: conflict: Return any values that were not added. @@ -432,7 +436,7 @@ def make_tags_available( if isinstance(tags, int): tags = [tags] * 2 elif check_order: - tags = self.get_verified_tags(tags) + tags = get_validated_tags(tags) if isinstance(tags[0], int) and tags[0] != tags[1]: tags = [tags] if use_lock: @@ -451,7 +455,7 @@ def _make_tags_available( """Manage available_tags adittion changes Exceptions: - KytosTagsNotInTagRanges from add_tags() + KytosTagsNotInTagRanges from _add_tags() """ if isinstance(tags[0], list): diff = range_difference(tags, self.tag_ranges[tag_type]) @@ -461,28 +465,11 @@ def _make_tags_available( new_tags, conflict = range_addition(tags, available_tags) self.available_tags[tag_type] = new_tags return conflict - result = self.add_tags(tags, tag_type) + result = self._add_tags(tags, tag_type) if result is False: return [tags] return [] - @staticmethod - def get_verified_tags( - tags: Union[list[int], list[list[int]]] - ) -> Union[list[int], list[list[int]]]: - """Return tags which values are validated to be correct.""" - if isinstance(tags, list) and isinstance(tags[0], int): - if len(tags) == 1: - return [tags[0], tags[0]] - if len(tags) == 2 and tags[0] > tags[1]: - raise KytosInvalidTagRanges(f"Range out of order {tags}") - if len(tags) > 2: - raise KytosInvalidTagRanges(f"Range have 2 values {tags}") - return tags - if isinstance(tags, list) and isinstance(tags[0], list): - return get_tag_ranges(tags) - raise KytosInvalidTagRanges(f"Value type not recognized {tags}") - def set_available_tags_tag_ranges( self, available_tag: dict[str, list[list[int]]], diff --git a/kytos/core/link.py b/kytos/core/link.py index 2f9d41d1..6a004694 100644 --- a/kytos/core/link.py +++ b/kytos/core/link.py @@ -157,10 +157,10 @@ def get_next_available_tag( try: tag, _ = next(tags) self.endpoint_a.use_tags( - controller, tag, use_lock=False + controller, tag, use_lock=False, check_order=False ) self.endpoint_b.use_tags( - controller, tag, use_lock=False + controller, tag, use_lock=False, check_order=False ) return tag except StopIteration: @@ -179,10 +179,12 @@ def make_tags_available( with self.endpoint_a._tag_lock: with self.endpoint_b._tag_lock: conflict_a = self.endpoint_a.make_tags_available( - controller, tags, tag_type, False, check_order + controller, tags, tag_type, use_lock=False, + check_order=check_order ) conflict_b = self.endpoint_b.make_tags_available( - controller, tags, tag_type, False, check_order + controller, tags, tag_type, use_lock=False, + check_order=check_order ) return conflict_a, conflict_b diff --git a/kytos/core/tag_ranges.py b/kytos/core/tag_ranges.py index c092b2b1..82b158a1 100644 --- a/kytos/core/tag_ranges.py +++ b/kytos/core/tag_ranges.py @@ -56,12 +56,36 @@ def get_tag_ranges(ranges: list[list[int]]): return ranges +def get_validated_tags( + tags: Union[list[int], list[list[int]]] +) -> Union[list[int], list[list[int]]]: + """Return tags which values are validated to be correct.""" + if isinstance(tags, list) and isinstance(tags[0], int): + if len(tags) == 1: + return [tags[0], tags[0]] + if len(tags) == 2 and tags[0] > tags[1]: + raise KytosInvalidTagRanges(f"Range out of order {tags}") + if len(tags) > 2: + raise KytosInvalidTagRanges(f"Range must have 2 values {tags}") + return tags + if isinstance(tags, list) and isinstance(tags[0], list): + return get_tag_ranges(tags) + raise KytosInvalidTagRanges(f"Value type not recognized {tags}") + + def range_intersection( ranges_a: list[list[int]], ranges_b: list[list[int]] ) -> Iterator[list[int]]: """Returns an iterator of an intersection between - two validated list of ranges""" + two validated list of ranges. + + Necessities: + The lists from argument need to be ordered and validated. + E.g. [[1, 2], [4, 60]] + Use get_tag_ranges() for list[list[int]] or + get_validated_tags() for also list[int] + """ a_i, b_i = 0, 0 while a_i < len(ranges_a) and b_i < len(ranges_b): fst_a, snd_a = ranges_a[a_i] @@ -88,8 +112,13 @@ def range_difference( ) -> list[list[int]]: """The operation is two validated list of ranges (ranges_a - ranges_b). - This method simulates difference of sets. E.g.: - [[10, 15]] - [[4, 11], [14, 45]] = [[12, 13]] + This method simulates difference of sets. + + Necessities: + The lists from argument need to be ordered and validated. + E.g. [[1, 2], [4, 60]] + Use get_tag_ranges() for list[list[int]] or + get_validated_tags() for also list[int] """ if not ranges_a: return [] @@ -137,7 +166,14 @@ def range_addition( ) -> tuple[list[list[int]], list[list[int]]]: """Addition between two validated list of ranges. Simulates the addition between two sets. - Return[adittion product, intersection]""" + Return[adittion product, intersection] + + Necessities: + The lists from argument need to be ordered and validated. + E.g. [[1, 2], [4, 60]] + Use get_tag_ranges() for list[list[int]] or + get_validated_tags() for also list[int] + """ if not ranges_b: return deepcopy(ranges_a), [] if not ranges_a: diff --git a/tests/unit/test_core/test_interface.py b/tests/unit/test_core/test_interface.py index 7ac71043..b90e7fcd 100644 --- a/tests/unit/test_core/test_interface.py +++ b/tests/unit/test_core/test_interface.py @@ -2,14 +2,13 @@ # pylint: disable=attribute-defined-outside-init import logging import pickle -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock import pytest from pyof.v0x04.common.port import PortFeatures from kytos.core.common import EntityStatus -from kytos.core.exceptions import (KytosInvalidTagRanges, - KytosSetTagRangeError, +from kytos.core.exceptions import (KytosSetTagRangeError, KytosTagsAreNotAvailable, KytosTagsNotInTagRanges, KytosTagtypeNotSupported) @@ -390,28 +389,28 @@ async def test_remove_tag_ranges(self) -> None: assert self.iface.tag_ranges['vlan'] == default async def test_remove_tags(self) -> None: - """Test remove_tags""" + """Test _remove_tags""" available_tag = [[20, 20], [200, 3000]] tag_ranges = [[1, 4095]] self.iface.set_available_tags_tag_ranges( {'vlan': available_tag}, {'vlan': tag_ranges} ) ava_expected = [[20, 20], [241, 3000]] - assert self.iface.remove_tags([200, 240]) + assert self.iface._remove_tags([200, 240]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[241, 3000]] - assert self.iface.remove_tags([20, 20]) + assert self.iface._remove_tags([20, 20]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[241, 250], [400, 3000]] - assert self.iface.remove_tags([251, 399]) + assert self.iface._remove_tags([251, 399]) assert self.iface.available_tags['vlan'] == ava_expected - assert self.iface.remove_tags([200, 240]) is False + assert self.iface._remove_tags([200, 240]) is False ava_expected = [[241, 250], [400, 499]] - assert self.iface.remove_tags([500, 3000]) + assert self.iface._remove_tags([500, 3000]) assert self.iface.available_tags['vlan'] == ava_expected async def test_remove_tags_empty(self) -> None: - """Test remove_tags when available_tags is empty""" + """Test _remove_tags when available_tags is empty""" available_tag = [] tag_ranges = [[1, 4095]] parameters = { @@ -419,11 +418,11 @@ async def test_remove_tags_empty(self) -> None: "tag_ranges": {'vlan': tag_ranges} } self.iface.set_available_tags_tag_ranges(**parameters) - assert self.iface.remove_tags([4, 6]) is False + assert self.iface._remove_tags([4, 6]) is False assert self.iface.available_tags['vlan'] == [] async def test_add_tags(self) -> None: - """Test add_tags""" + """Test _add_tags""" available_tag = [[7, 10], [20, 30]] tag_ranges = [[1, 4095]] parameters = { @@ -432,41 +431,41 @@ async def test_add_tags(self) -> None: } self.iface.set_available_tags_tag_ranges(**parameters) ava_expected = [[4, 10], [20, 30]] - assert self.iface.add_tags([4, 6]) + assert self.iface._add_tags([4, 6]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[1, 2], [4, 10], [20, 30]] - assert self.iface.add_tags([1, 2]) + assert self.iface._add_tags([1, 2]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[1, 2], [4, 10], [20, 35]] - assert self.iface.add_tags([31, 35]) + assert self.iface._add_tags([31, 35]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[1, 2], [4, 10], [20, 35], [90, 90]] - assert self.iface.add_tags([90, 90]) + assert self.iface._add_tags([90, 90]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[1, 2], [4, 10], [20, 90]] - assert self.iface.add_tags([36, 89]) + assert self.iface._add_tags([36, 89]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[1, 2], [4, 12], [20, 90]] - assert self.iface.add_tags([11, 12]) + assert self.iface._add_tags([11, 12]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[1, 2], [4, 12], [17, 90]] - assert self.iface.add_tags([17, 19]) + assert self.iface._add_tags([17, 19]) assert self.iface.available_tags['vlan'] == ava_expected ava_expected = [[1, 2], [4, 12], [15, 15], [17, 90]] - assert self.iface.add_tags([15, 15]) + assert self.iface._add_tags([15, 15]) assert self.iface.available_tags['vlan'] == ava_expected - assert self.iface.add_tags([35, 98]) is False + assert self.iface._add_tags([35, 98]) is False async def test_add_tags_empty(self) -> None: - """Test add_tags when available_tags is empty""" + """Test _add_tags when available_tags is empty""" available_tag = [] tag_ranges = [[1, 4095]] parameters = { @@ -474,7 +473,7 @@ async def test_add_tags_empty(self) -> None: "tag_ranges": {'vlan': tag_ranges} } self.iface.set_available_tags_tag_ranges(**parameters) - assert self.iface.add_tags([4, 6]) + assert self.iface._add_tags([4, 6]) assert self.iface.available_tags['vlan'] == [[4, 6]] async def test_notify_interface_tags(self, controller) -> None: @@ -486,29 +485,6 @@ async def test_notify_interface_tags(self, controller) -> None: assert event.name == name assert event.content == content - @patch("kytos.core.interface.get_tag_ranges") - async def test_get_verified_tags(self, mock_get_tag_ranges) -> None: - """Test get_verified_tags""" - - result = Interface.get_verified_tags([1, 2]) - assert result == [1, 2] - - result = Interface.get_verified_tags([4]) - assert result == [4, 4] - - result = Interface.get_verified_tags([[1, 2], [4, 5]]) - assert mock_get_tag_ranges.call_count == 1 - assert mock_get_tag_ranges.call_args[0][0] == [[1, 2], [4, 5]] - - with pytest.raises(KytosInvalidTagRanges): - Interface.get_verified_tags("a") - - with pytest.raises(KytosInvalidTagRanges): - Interface.get_verified_tags([1, 2, 3]) - - with pytest.raises(KytosInvalidTagRanges): - Interface.get_verified_tags([5, 2]) - class TestUNI(): """UNI tests.""" diff --git a/tests/unit/test_core/test_tag_ranges.py b/tests/unit/test_core/test_tag_ranges.py index 80ee8dd7..865fb70c 100644 --- a/tests/unit/test_core/test_tag_ranges.py +++ b/tests/unit/test_core/test_tag_ranges.py @@ -3,9 +3,9 @@ from kytos.core.exceptions import KytosInvalidTagRanges from kytos.core.tag_ranges import (find_index_add, find_index_remove, - get_tag_ranges, map_singular_values, - range_addition, range_difference, - range_intersection) + get_tag_ranges, get_validated_tags, + map_singular_values, range_addition, + range_difference, range_intersection) def test_map_singular_values(): @@ -147,3 +147,22 @@ def test_range_addition(): result = range_addition(ranges_a, ranges_b) assert expected_add == result[0] assert expected_intersection == result[1] + + +async def test_get_validated_tags() -> None: + """Test get_validated_tags""" + result = get_validated_tags([1, 2]) + assert result == [1, 2] + + result = get_validated_tags([4]) + assert result == [4, 4] + + result = get_validated_tags([[1, 2], [4, 5]]) + assert result == [[1, 2], [4, 5]] + + with pytest.raises(KytosInvalidTagRanges): + get_validated_tags("a") + with pytest.raises(KytosInvalidTagRanges): + get_validated_tags([1, 2, 3]) + with pytest.raises(KytosInvalidTagRanges): + get_validated_tags([5, 2]) From 526211c77b04495922f0e89da5086e30e3f5791d Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Mon, 20 Nov 2023 18:50:41 -0500 Subject: [PATCH 11/13] Fixed linter --- kytos/core/tag_ranges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kytos/core/tag_ranges.py b/kytos/core/tag_ranges.py index 82b158a1..23ba26c7 100644 --- a/kytos/core/tag_ranges.py +++ b/kytos/core/tag_ranges.py @@ -79,7 +79,7 @@ def range_intersection( ) -> Iterator[list[int]]: """Returns an iterator of an intersection between two validated list of ranges. - + Necessities: The lists from argument need to be ordered and validated. E.g. [[1, 2], [4, 60]] From cdc5d445bbbf9565db7b20eb68f09677243b227f Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Tue, 21 Nov 2023 11:35:41 -0500 Subject: [PATCH 12/13] Updated changelog --- CHANGELOG.rst | 3 +++ kytos/core/interface.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 69d434e2..08d1d281 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,12 +10,15 @@ Added ===== - Added ``Interface.tag_ranges`` as ``dict[str, list[list[int]]]`` as replacement for ``vlan_pool`` settings. - Added ``kytos/core.interface_tags`` event publication to notify any modification of ``Interface.tag_ranges`` or ``Interface.available_tags``. +- Added ``TAGRange`` class which is used when a ``UNI`` has a tag as a list of ranges. +- Added ``KytosTagError`` exception that cover other exceptions ``KytosTagtypeNotSupported``, ``KytosInvalidTagRanges``, ``KytosSetTagRangeError``, ``KytosTagsNotInTagRanges`` and ``KytosTagsAreNotAvailable`` all of which are related to TAGs. Changed ======= - Parametrized default ``maxTimeMS`` when creating an index via ``Mongo.boostrap_index`` via environment variable ``MONGO_IDX_TIMEOUTMS=30000``. The retries parameters reuse the same environment variables ``MONGO_AUTO_RETRY_STOP_AFTER_ATTEMPT=3``, ``MONGO_AUTO_RETRY_WAIT_RANDOM_MIN=0.1``, ``MONGO_AUTO_RETRY_WAIT_RANDOM_MAX=1`` that NApps controllers have been using. - ``kytosd`` process will exit if a NApp raises an exception during its ``setup()`` execution. - Change format for ``Interface.available_tags`` to ``dict[str, list[list[int]]]``. Storing ``tag_types`` as keys and a list of ranges for ``available_tags`` as values. +- ``Interface.use_tags`` and ``Interface.make_tags_available`` are compatible with list of ranges. Deprecated ========== diff --git a/kytos/core/interface.py b/kytos/core/interface.py index f270492c..ffdfa35e 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -83,11 +83,11 @@ def __init__( self, tag_type: str, value: list[list[int]], - mask_list: list[Union[str, int]] = [] + mask_list: list[Union[str, int]] = None ): self.tag_type = TAGType(tag_type).value self.value = value - self.mask_list = mask_list + self.mask_list = mask_list or [] def as_dict(self): """Return a dictionary representating a tag range object.""" From e722f777a4a33b82735034e2e8c70047d6ab6a40 Mon Sep 17 00:00:00 2001 From: Aldo Ortega Date: Tue, 21 Nov 2023 11:37:49 -0500 Subject: [PATCH 13/13] Deleted unnecessary disabled --- kytos/core/interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kytos/core/interface.py b/kytos/core/interface.py index ffdfa35e..a643ccf1 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -78,7 +78,6 @@ class TAGRange(TAG): """Class that represents an User-to-Network Interface with a tag value as a list.""" - # pylint: disable=dangerous-default-value def __init__( self, tag_type: str,