Skip to content

Commit

Permalink
Merge pull request #427 from kytos-ng/disjointment
Browse files Browse the repository at this point in the history
Adding switches to disjointedness algorithm
  • Loading branch information
Alopalao authored Feb 7, 2024
2 parents 6305644 + 9f6facf commit 47e4cd4
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Changed
- Improved log for invalid traces by adding ``From EVC(evc_id) named 'evc_name'``
- An inactive and enabled EVC will be redeploy if an attribute from ``attributes_requiring_redeploy`` is updated.
- If a KytosEvent can't be put on ``buffers.app`` during ``setup()``, it'll make the NApp to fail to start
- Disjointedness algorithm now takes into account switches, excepting the UNIs switches. Unwanted switches have the same value as the unwanted links.

Deprecated
==========
Expand Down
69 changes: 49 additions & 20 deletions models/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,16 +185,17 @@ def get_disjoint_paths(
"""Computes the maximum disjoint paths from the unwanted_path for a EVC
Maximum disjoint paths from the unwanted_path are the paths from the
source node to the target node that share the minimum number os links
contained in unwanted_path. In other words, unwanted_path is the path
we want to avoid: we want the maximum possible disjoint path from it.
The disjointness of a path in regards to unwanted_path is calculated
by the complementary percentage of shared links between them. As an
example, if the unwanted_path has 3 links, a given path P1 has 1 link
shared with unwanted_path, and a given path P2 has 2 links shared with
unwanted_path, then the disjointness of P1 is 0.67 and the disjointness
of P2 is 0.33. In this example, P1 is preferable over P2 because it
offers a better disjoint path. When two paths have the same
source node to the target node that share the minimum number of links
and switches contained in unwanted_path. In other words, unwanted_path
is the path we want to avoid: we want the maximum possible disjoint
path from it. The disjointness of a path in regards to unwanted_path
is calculated by the complementary percentage of shared links and
switches between them. As an example, if the unwanted_path has 3
links and 2 switches, a given path P1 has 1 link shared with
unwanted_path, and a given path P2 has 2 links and 1 switch shared
with unwanted_path, then the disjointness of P1 is 0.8 and the
disjointness of P2 is 0.4. In this example, P1 is preferable over P2
because it offers a better disjoint path. When two paths have the same
disjointness they are ordered by 'cost' attributed as returned from
Pathfinder. When the disjointness of a path is equal to 0 (i.e., it
shares all the links with unwanted_path), that particular path is not
Expand Down Expand Up @@ -222,28 +223,56 @@ def get_disjoint_paths(
unwanted_links = [
(link.endpoint_a.id, link.endpoint_b.id) for link in unwanted_path
]
if not unwanted_links:
unwanted_switches = set()
for link in unwanted_path:
unwanted_switches.add(link.endpoint_a.switch.id)
unwanted_switches.add(link.endpoint_b.switch.id)
unwanted_switches.discard(circuit.uni_a.interface.switch.id)
unwanted_switches.discard(circuit.uni_z.interface.switch.id)

length_unwanted = (len(unwanted_links) + len(unwanted_switches))
if not unwanted_links or not unwanted_switches:
return None

paths = cls.get_paths(circuit, max_paths=cutoff,
**circuit.secondary_constraints)
for path in paths:
head = path["hops"][:-1]
tail = path["hops"][1:]
shared_edges = 0
for (endpoint_a, endpoint_b) in unwanted_links:
if ((endpoint_a, endpoint_b) in zip(head, tail)) or (
(endpoint_b, endpoint_a) in zip(head, tail)
):
shared_edges += 1
path["disjointness"] = 1 - shared_edges / len(unwanted_links)
links_n, switches_n = cls.get_shared_components(
path, unwanted_links, unwanted_switches
)
shared_components = links_n + switches_n
path["disjointness"] = 1 - shared_components / length_unwanted
paths = sorted(paths, key=lambda x: (-x['disjointness'], x['cost']))
for path in paths:
if path["disjointness"] == 0:
continue
yield cls.create_path(path["hops"])
return None

@staticmethod
def get_shared_components(
path: Path,
unwanted_links: list[tuple[str, str]],
unwanted_switches: set[str]
) -> tuple[int, int]:
"""Return the number of shared links
and switches found in path."""
head = path["hops"][:-1]
tail = path["hops"][1:]
shared_links = 0
for (endpoint_a, endpoint_b) in unwanted_links:
if ((endpoint_a, endpoint_b) in zip(head, tail)) or (
(endpoint_b, endpoint_a) in zip(head, tail)
):
shared_links += 1
copy_switches = unwanted_switches.copy()
shared_switches = 0
for component in path["hops"]:
if component in copy_switches:
shared_switches += 1
copy_switches.remove(component)
return shared_links, shared_switches

@classmethod
def create_path(cls, path):
"""Return the path containing only the interfaces."""
Expand Down
1 change: 1 addition & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def id_to_interface_mock(interface_id):
switch_id = ":".join(interface_id.split(":")[:-1])
port_id = int(interface_id.split(":")[-1])
switch = get_switch_mock(switch_id, 0x04)
switch.id = switch_id
interface = get_interface_mock(port_id, port_id, switch)
return interface

Expand Down
74 changes: 73 additions & 1 deletion tests/unit/models/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,14 @@ def test_get_best_paths_error(self, mock_requests_post, mock_log):
assert isinstance(res_paths, list)
assert mock_log.error.call_count == 1

# pylint: disable=too-many-statements, too-many-locals
@patch.object(
DynamicPathManager,
"get_shared_components",
side_effect=DynamicPathManager.get_shared_components
)
@patch("requests.post")
def test_get_disjoint_paths(self, mock_requests_post):
def test_get_disjoint_paths(self, mock_requests_post, mock_shared):
"""Test get_disjoint_paths method."""

controller = MagicMock()
Expand All @@ -506,6 +512,8 @@ def test_get_disjoint_paths(self, mock_requests_post):
}
evc.uni_a.interface.id = "1"
evc.uni_z.interface.id = "2"
evc.uni_a.interface.switch.id = "00:00:00:00:00:00:00:01"
evc.uni_z.interface.switch.id = "00:00:00:00:00:00:00:05"

# Topo0
paths1 = {
Expand Down Expand Up @@ -633,12 +641,25 @@ def test_get_disjoint_paths(self, mock_requests_post):
id_to_interface_mock("00:00:00:00:00:00:00:05:2")
),
]
path_links = [
("00:00:00:00:00:00:00:01:2", "00:00:00:00:00:00:00:02:2"),
("00:00:00:00:00:00:00:02:3", "00:00:00:00:00:00:00:04:2"),
("00:00:00:00:00:00:00:04:3", "00:00:00:00:00:00:00:05:2")
]
path_switches = {
"00:00:00:00:00:00:00:04",
"00:00:00:00:00:00:00:02"
}

# only one path available from pathfinder (precesilly the
# current_path), so the maximum disjoint path will be empty
mock_response.json.return_value = {"paths": paths1["paths"][0:1]}
mock_requests_post.return_value = mock_response
paths = list(DynamicPathManager.get_disjoint_paths(evc, current_path))
args = mock_shared.call_args[0]
assert args[0] == paths1["paths"][0]
assert args[1] == path_links
assert args[2] == path_switches
assert len(paths) == 0

expected_disjoint_path = [
Expand All @@ -664,6 +685,13 @@ def test_get_disjoint_paths(self, mock_requests_post):
mock_response.json.return_value = paths1
mock_requests_post.return_value = mock_response
paths = list(DynamicPathManager.get_disjoint_paths(evc, current_path))
args = mock_shared.call_args[0]
assert args[0] == paths1["paths"][-1]
assert args[1] == path_links
assert args[2] == {
"00:00:00:00:00:00:00:04",
"00:00:00:00:00:00:00:02"
}
assert len(paths) == 4
# for more information on the paths please refer to EP029
assert len(paths[0]) == 4 # path S-Z-W-I-D
Expand Down Expand Up @@ -692,6 +720,8 @@ def test_get_disjoint_paths(self, mock_requests_post):
mock_requests_post.assert_has_calls([expected_call])

# EP029 Topo2
evc.uni_a.interface.switch.id = "00:00:00:00:00:00:00:01"
evc.uni_z.interface.switch.id = "00:00:00:00:00:00:00:07"
paths2 = {
"paths": [
{
Expand Down Expand Up @@ -758,6 +788,17 @@ def test_get_disjoint_paths(self, mock_requests_post):
id_to_interface_mock("00:00:00:00:00:00:00:07:2")
),
]
path_interfaces = [
("00:00:00:00:00:00:00:01:2", "00:00:00:00:00:00:00:02:1"),
("00:00:00:00:00:00:00:02:2", "00:00:00:00:00:00:00:03:1"),
("00:00:00:00:00:00:00:03:2", "00:00:00:00:00:00:00:04:1"),
("00:00:00:00:00:00:00:04:2", "00:00:00:00:00:00:00:07:2")
]
path_switches = {
"00:00:00:00:00:00:00:02",
"00:00:00:00:00:00:00:03",
"00:00:00:00:00:00:00:04"
}

expected_disjoint_path = [
Link(
Expand Down Expand Up @@ -785,8 +826,39 @@ def test_get_disjoint_paths(self, mock_requests_post):
mock_response.json.return_value = {"paths": paths2["paths"]}
mock_requests_post.return_value = mock_response
paths = list(DynamicPathManager.get_disjoint_paths(evc, current_path))
args = mock_shared.call_args[0]
assert args[0] == paths2["paths"][-1]
assert args[1] == path_interfaces
assert args[2] == path_switches
assert len(paths) == 1
assert (
[link.id for link in paths[0]] ==
[link.id for link in expected_disjoint_path]
)

def test_get_shared_components(self):
"""Test get_shared_components"""
mock_path = {"hops": [
'00:00:00:00:00:00:00:01:1',
'00:00:00:00:00:00:00:01',
'00:00:00:00:00:00:00:01:4',
'00:00:00:00:00:00:00:05:2',
'00:00:00:00:00:00:00:05',
'00:00:00:00:00:00:00:05:3',
'00:00:00:00:00:00:00:02:4',
'00:00:00:00:00:00:00:02',
'00:00:00:00:00:00:00:02:3',
'00:00:00:00:00:00:00:03:2',
'00:00:00:00:00:00:00:03',
'00:00:00:00:00:00:00:03:1'
]}
mock_links = [
("00:00:00:00:00:00:00:01:2", "00:00:00:00:00:00:00:02:2"),
("00:00:00:00:00:00:00:02:3", "00:00:00:00:00:00:00:03:2")
]
mock_switches = {"00:00:00:00:00:00:00:02"}
actual_lk, actual_sw = DynamicPathManager.get_shared_components(
mock_path, mock_links, mock_switches
)
assert actual_lk == 1
assert actual_sw == 1

0 comments on commit 47e4cd4

Please sign in to comment.