diff --git a/kytos/core/config.py b/kytos/core/config.py index 94e8b67c..d8fb124f 100644 --- a/kytos/core/config.py +++ b/kytos/core/config.py @@ -145,7 +145,6 @@ def parse_args(self): 'enable_entities_by_default': False, 'napps_pre_installed': [], 'authenticate_urls': [], - 'vlan_pool': {}, 'token_expiration_minutes': 180, 'thread_pool_max_workers': {}, 'database': '', @@ -206,7 +205,6 @@ def _parse_json(value): options.logger_decorators = _parse_json(options.logger_decorators) options.napps_pre_installed = _parse_json(options.napps_pre_installed) - options.vlan_pool = _parse_json(options.vlan_pool) options.authenticate_urls = _parse_json(options.authenticate_urls) thread_pool_max_workers = options.thread_pool_max_workers options.thread_pool_max_workers = _parse_json(thread_pool_max_workers) diff --git a/kytos/core/controller.py b/kytos/core/controller.py index d53a92bb..e6c6acc2 100644 --- a/kytos/core/controller.py +++ b/kytos/core/controller.py @@ -42,7 +42,6 @@ from kytos.core.events import KytosEvent from kytos.core.exceptions import KytosAPMInitException, KytosDBInitException from kytos.core.helpers import executors, now -from kytos.core.interface import Interface from kytos.core.logs import LogManager from kytos.core.napps.base import NApp from kytos.core.napps.manager import NAppsManager @@ -678,8 +677,6 @@ def get_switch_or_create(self, dpid, connection=None): event_name += 'new' else: event_name += 'reconnected' - - self.set_switch_options(dpid=dpid) event = KytosEvent(name=event_name, content={'switch': switch}) if connection: @@ -693,37 +690,6 @@ def get_switch_or_create(self, dpid, connection=None): return switch - def set_switch_options(self, dpid): - """Update the switch settings based on kytos.conf options. - - Args: - dpid (str): dpid used to identify a switch. - - """ - switch = self.switches.get(dpid) - if not switch: - return - - vlan_pool = {} - vlan_pool = self.options.vlan_pool - if not vlan_pool: - return - - if vlan_pool.get(dpid): - self.log.info("Loading vlan_pool configuration for dpid %s", dpid) - for intf_num, port_list in vlan_pool[dpid].items(): - if not switch.interfaces.get((intf_num)): - vlan_ids = set() - for vlan_range in port_list: - (vlan_begin, vlan_end) = (vlan_range[0:2]) - for vlan_id in range(vlan_begin, vlan_end): - vlan_ids.add(vlan_id) - intf_num = int(intf_num) - intf = Interface(name=intf_num, port_number=intf_num, - switch=switch) - intf.set_available_tags(vlan_ids) - switch.update_interface(intf) - def create_or_update_connection(self, connection): """Update a connection. diff --git a/kytos/core/exceptions.py b/kytos/core/exceptions.py index 93ceceda..ff5f3270 100644 --- a/kytos/core/exceptions.py +++ b/kytos/core/exceptions.py @@ -77,6 +77,10 @@ def __str__(self): return msg +class KytosResizingAvailableTagError(Exception): + """Exception raised when available_tag cannot be resized""" + + class KytosLinkCreationError(Exception): """Exception thrown when the link has an empty endpoint.""" diff --git a/kytos/core/interface.py b/kytos/core/interface.py index ef8bc1f0..8b495511 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -6,6 +6,7 @@ from enum import IntEnum from functools import reduce from threading import Lock +from typing import Optional from pyof.v0x01.common.phy_port import Port as PortNo01 from pyof.v0x01.common.phy_port import PortFeatures as PortFeatures01 @@ -13,6 +14,8 @@ from pyof.v0x04.common.port import PortNo as PortNo04 from kytos.core.common import EntityStatus, GenericEntity +from kytos.core.events import KytosEvent +from kytos.core.exceptions import KytosResizingAvailableTagError from kytos.core.helpers import now from kytos.core.id import InterfaceID @@ -107,8 +110,9 @@ def __init__(self, name, port_number, switch, address=None, state=None, self._id = InterfaceID(switch.id, port_number) self._custom_speed = speed self._tag_lock = Lock() - self.set_available_tags(range(1, 4096)) - + self.set_available_tags_tag_ranges( + {'1': [[1, 4095]]}, {'1': None} + ) super().__init__() def __repr__(self): @@ -173,19 +177,295 @@ def status_reason(self): super().status_reason ) - def set_available_tags(self, iterable): + @staticmethod + def range_intersection( + ranges_a: list[list[int]], + ranges_b: list[list[int]] + ) -> list[list[int]]: + """Returns the intersection between two list + of ranges""" + result = [] + 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) + result.append([intersection_start, intersection_end]) + + if snd_a < snd_b: + a_i += 1 + else: + b_i += 1 + return result + + def get_available_tags(self, tag_type: str = '1') -> list[list[int]]: + """Get available tags, if there is tag_ranges restriction + self.available_tags are not truly available""" + if self.tag_ranges[tag_type] is None: + return self.available_tags[tag_type] + return self.range_intersection(self.available_tags[tag_type], + self.tag_ranges[tag_type]) + + def verify_first_last_tag( + self, + tag_ranges: list[list[int, int]], + tag_type: str = '1' + ) -> Optional[str]: + """Verify that first tags [[1...] and last tags [[...4095]] are + included""" + available = self.available_tags[tag_type] + + if available[0][0] != 1: + if not (tag_ranges[0][0] == 1 and + tag_ranges[0][1] + 1 >= available[0][0]): + reason = f"There is a gap at the beginning: available_tag:"\ + f"{available[0]} and tag_ranges:"\ + f"{tag_ranges[0]}. Tags can be {[1, 4095]}." + return reason + if available[-1][1] != 4095: + if not (tag_ranges[-1][1] == 4095 and + tag_ranges[-1][0] <= available[-1][1] + 1): + reason = f"There is a gap at the end: available_tag:"\ + f"{available[-1]} and tag_ranges:"\ + f"{tag_ranges[-1]}. Tags can be {[1, 4095]}." + return reason + return None + + def not_applicable( + self, + tag_ranges: list[list[int, int]], + tag_type: str = '1' + ) -> Optional[str]: + """Check if tag_ranges can be applied when there is available + Returns an early False when a gap is detected + Simulates sliding window O(n+m)""" + available = self.available_tags[tag_type] + if not available: + return "Every tag is used." + + error_msg = self.verify_first_last_tag(tag_ranges) + if error_msg: + return error_msg + + # Corner case + ava_n = len(available) + if ava_n == 1: + return None + + rest_n = len(tag_ranges) + ava_i, rest_i = 0, 0 + while rest_i < rest_n: + if tag_ranges[rest_i][0] <= available[ava_i][1] + 1: + while ava_i + 1 < ava_n: + if tag_ranges[rest_i][1] + 1 >= available[ava_i+1][0]: + ava_i += 1 + elif tag_ranges[rest_i][1] <= available[ava_i][1]: + break + else: + reason = f"Gap detected available_tag:"\ + f"{[available[ava_i], available[ava_i+1]]}"\ + f" and tag_ranges:{tag_ranges[rest_i]}." + return reason + rest_i += 1 + else: + reason = f"Gap detected available_tag:"\ + f"{[available[ava_i], available[ava_i+1]]} and "\ + f"tag_ranges:{tag_ranges[rest_i]}." + return reason + return None + + def set_tag_ranges(self, tag_ranges: list[list[int]], tag_type: str): + """Set new restriction""" + with self._tag_lock: + reason = self.not_applicable(tag_ranges, tag_type) + if reason: + msg = f"tag_ranges: {tag_ranges} is not applicable. {reason}" + raise KytosResizingAvailableTagError(msg) + self.tag_ranges[tag_type] = tag_ranges + + def remove_tag_ranges(self, tag_type: str = '1'): + """Remove restriction set on tag_ranges""" + self.tag_ranges[tag_type] = None + + def get_next_available_tag(self) -> Optional[int]: + """Get the next available tag from the interface. + + Return the next available tag if exists and remove from the + available tags. + If no tag is available return False. + """ + try: + with self._tag_lock: + first_tag = self.available_tags[-1][1] + self.available_tags[-1][1] -= 1 + if self.available_tags[-1][0] > self.available_tags[-1][1]: + self.available_tags.pop() + return first_tag + except IndexError: + return False + + @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""" + low = 0 + high = len(available_tags) + while low < high: + mid = (low+high)//2 + if available_tags[mid][0] <= tag_range[0]: + if available_tags[mid][1] >= tag_range[1]: + return mid + low = mid + 1 + elif available_tags[mid][0] > tag_range[0]: + high = mid + return None + + def remove_tags(self, tags: list[int], tag_type: str) -> 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) + if index is None: + return False + # Resizing + if tags[0] == available[index][0]: + if tags[1] == available[index][1]: + available[tag_type].pop(index) + else: + available[index: index+1] = [[tags[1]+1, available[index][1]]] + elif tags[1] == available[index][1]: + available[index: index+1] = [[available[index][0], tags[0]-1]] + else: + available[index: index+1] = [ + [available[index][0], tags[0]-1], + [tags[1]+1, available[index][1]] + ] + return True + + def use_tags(self, tags: list[int], tag_type: str = '1') -> bool: + """Remove a specific tag from available_tags if it is there. + + Return False in case the tags is already removed. + """ + with self._tag_lock: + return self.remove_tags(tags, tag_type) + + @staticmethod + def find_index_add( + available_tags: list[list[int]], + tag_range: list[int] + ) -> Optional[int]: + """Find the index of tags in available_tags to be added""" + if available_tags[0][0] > tag_range[1]: + return 0 + low = 1 + high = len(available_tags) + while low < high: + mid = (low+high)//2 + if available_tags[mid][0] > tag_range[1]: + if available_tags[mid-1][1] < tag_range[0]: + return mid + high = mid + else: + low = mid + 1 + if available_tags[-1][1] < tag_range[0]: + return high + return None + + def add_tags(self, tags: list[int], tag_type: str) -> bool: + """Add tags, return True if they were added. + Returns False nothing nothing was added, True otherwise + Ensuring that ranges are not unnecessarily divided + available_tag e.g [[7, 10], [20, 30], [78, 92], [100, 109], [189, 200]] + tags examples are in each if statement. + """ + available = self.available_tags[tag_type] + index = self.find_index_add(available, tags) + if index is None: + return False + if index == 0: + # [1, 6] + if tags[1] == available[index][0] - 1: + available[index][0] = tags[0] + # [1, 2] + else: + available.insert(0, tags) + elif index == len(available): + # [201, 300] + if available[index-1][1] + 1 == tags[0]: + available[index-1][1] = tags[1] + # [250, 300] + else: + available.append(tags) + else: + # [11, 19] + if (available[index-1][1] + 1 == tags[0] and + available[index][0] - 1 == tags[1]): + available[index-1: index+1] = [ + [available[index-1][0], available[index][1]] + ] + # [11, 15] + elif available[index-1][1] + 1 == tags[0]: + available[index-1][1] = tags[1] + # [15, 19] + elif available[index][0] - 1 == tags[1]: + available[index][0] = tags[0] + # [15, 15] + else: + available.insert(index, tags) + return True + + def make_tags_available( + self, + tags: list[int], + tag_type: str = '1' + ) -> bool: + """Add a specific tag in available_tags.""" + with self._tag_lock: + return self.add_tags(tags, tag_type) + + @staticmethod + def tags_are_available( + available: list[list[int]], + tags: [list[int]] + ) -> bool: + """Determine if tags is in available""" + low = 0 + high = len(available) + while low < high: + mid = (low+high)//2 + if available[mid][0] <= tags[0]: + if available[mid][1] >= tags[1]: + return True + elif available[mid][0] > tags[0]: + high = mid + return False + + def set_available_tags_tag_ranges( + self, + available_tag: dict[str, list[list[int]]], + tag_ranges: dict[str, Optional[list[list[int]]]] + ): """Set a range of VLAN tags to be used by this Interface. Args: iterable ([int]): range of VLANs. """ with self._tag_lock: - self.available_tags = [] - - for i in iterable: - vlan = TAGType.VLAN - tag = TAG(vlan, i) - self.available_tags.append(tag) + self.available_tags = available_tag + self.tag_ranges = tag_ranges def enable(self): """Enable this interface instance. @@ -195,7 +475,7 @@ def enable(self): self.switch.enable() self._enabled = True - def use_tag(self, tag): + def use_tag(self, tag: int): """Remove a specific tag from available_tags if it is there. Return False in case the tag is already removed. @@ -207,32 +487,15 @@ def use_tag(self, tag): return False return True - def is_tag_available(self, tag): + def is_tag_available(self, tag: int, tag_type: str = '1'): """Check if a tag is available.""" with self._tag_lock: - return tag in self.available_tags - - def get_next_available_tag(self): - """Get the next available tag from the interface. - - Return the next available tag if exists and remove from the - available tags. - If no tag is available return False. - """ - try: - with self._tag_lock: - return self.available_tags.pop() - except IndexError: + if self.find_index_remove( + self.available_tags[tag_type], [tag, tag] + ) is not None: + return True return False - def make_tag_available(self, tag): - """Add a specific tag in available_tags.""" - if not self.is_tag_available(tag) and isinstance(tag, TAG): - with self._tag_lock: - self.available_tags.append(tag) - return True - return False - def get_endpoint(self, endpoint): """Return a tuple with existent endpoint, None otherwise. @@ -505,6 +768,18 @@ def as_json(self): """ return json.dumps(self.as_dict()) + def notify_link_available_tags(self, controller, src_func=None): + """Notify link available tags""" + name = "kytos/core.link_available_tags" + content = { + "interface_id": self.id, + "available_tags": self.available_tags, + "tag_ranges": self.tag_ranges, + "src_func": src_func + } + event = KytosEvent(name=name, content=content) + controller.buffers.app.put(event) + class UNI: """Class that represents an User-to-Network Interface.""" @@ -527,11 +802,12 @@ def _is_reserved_valid_tag(self) -> bool: def is_valid(self): """Check if TAG is possible for this interface TAG pool.""" + tag = self.user_tag.value if self.user_tag: - if isinstance(self.user_tag.value, str): + if isinstance(tag, str): return self._is_reserved_valid_tag() - if isinstance(self.user_tag.value, int): - return self.interface.is_tag_available(self.user_tag) + if isinstance(tag, int): + return self.interface.is_tag_available(tag) return True def as_dict(self): diff --git a/kytos/core/link.py b/kytos/core/link.py index 1684e05b..802e0624 100644 --- a/kytos/core/link.py +++ b/kytos/core/link.py @@ -6,6 +6,7 @@ import json import operator from collections import OrderedDict +from copy import deepcopy from functools import reduce from threading import Lock @@ -13,7 +14,7 @@ from kytos.core.exceptions import (KytosLinkCreationError, KytosNoTagAvailableError) from kytos.core.id import LinkID -from kytos.core.interface import TAGType +from kytos.core.interface import TAG, Interface, TAGType class Link(GenericEntity): @@ -130,56 +131,50 @@ def available_tags(self): return [tag for tag in self.endpoint_a.available_tags if tag in self.endpoint_b.available_tags] - def use_tag(self, tag): - """Remove a specific tag from available_tags if it is there. - - Deprecated: use only the get_next_available_tag method. - """ - if self.is_tag_available(tag): - self.endpoint_a.use_tag(tag) - self.endpoint_b.use_tag(tag) - return True - return False - def is_tag_available(self, tag): """Check if a tag is available.""" return (self.endpoint_a.is_tag_available(tag) and self.endpoint_b.is_tag_available(tag)) - def get_next_available_tag(self): + def get_next_available_tag(self, controller, tag_type: str = '1') -> TAG: """Return the next available tag if exists.""" with self._get_available_vlans_lock: # Copy the available tags because in case of error # we will remove and add elements to the available_tags - available_tags_a = self.endpoint_a.available_tags.copy() - available_tags_b = self.endpoint_b.available_tags.copy() - - for tag in available_tags_a: - # Tag does not exist in endpoint B. Try another tag. - if tag not in available_tags_b: - continue - - # Tag already in use. Try another tag. - if not self.endpoint_a.use_tag(tag): - continue - - # Tag already in use in B. Mark the tag as available again. - if not self.endpoint_b.use_tag(tag): - self.endpoint_a.make_tag_available(tag) - continue - - # Tag used successfully by both endpoints. Returning. - return tag + available_tags_a = deepcopy(self.endpoint_a.get_available_tags()) + available_tags_b = deepcopy(self.endpoint_b.get_available_tags()) + intersection_tags = Interface.range_intersection(available_tags_a, + available_tags_b) + for tag_range in intersection_tags: + for tag in range(tag_range[0], tag_range[1]+1): + # Tag already in use. Try another tag. + if not self.endpoint_a.use_tags([tag, tag]): + continue + + # Tag already in use in B. Mark the tag as available again. + if not self.endpoint_b.use_tags([tag, tag]): + self.endpoint_a.make_tags_available([tag, tag]) + continue + + # Tag used successfully by both endpoints. Returning. + self.endpoint_a.notify_link_available_tags(controller) + self.endpoint_b.notify_link_available_tags(controller) + return TAG(int(tag_type), tag) raise KytosNoTagAvailableError(self) - def make_tag_available(self, tag): + def make_tag_available( + self, + controller, + tag: int, + tag_type: str = '1' + ) -> (bool, bool): """Add a specific tag in available_tags.""" - if not self.is_tag_available(tag): - self.endpoint_a.make_tag_available(tag) - self.endpoint_b.make_tag_available(tag) - return True - return False + result_a = self.endpoint_a.make_tags_available([tag, tag], tag_type) + result_b = self.endpoint_b.make_tags_available([tag, tag], tag_type) + self.endpoint_a.notify_link_available_tags(controller) + self.endpoint_b.notify_link_available_tags(controller) + return result_a, result_b def available_vlans(self): """Get all available vlans from each interface in the link.""" diff --git a/kytos/templates/kytos.conf.template b/kytos/templates/kytos.conf.template index bb5284bd..dec7198c 100644 --- a/kytos/templates/kytos.conf.template +++ b/kytos/templates/kytos.conf.template @@ -64,17 +64,6 @@ napps_repositories = [ # Use double quotes in each NApp in the list, e.g., ["username/napp"]. napps_pre_installed = [] -# VLAN pool settings -# -# The VLAN pool settings is a dictionary of datapath id, which contains -# a dictionary of of_port numbers with the respective vlan pool range -# for each port number. See the example below, which sets the vlan range -# [1, 5, 6, 7, 8, 9] on port 1 and vlan range [3] on port 4 of a switch -# that has a dpid '00:00:00:00:00:00:00:01' - -vlan_pool = {} -# vlan_pool = {"00:00:00:00:00:00:00:01": {"1": [[1, 2], [5, 10]], "4": [[3, 4]]}} - # The jwt_secret parameter is responsible for signing JSON Web Tokens. jwt_secret = {{ jwt_secret }}