Skip to content

Commit

Permalink
Made changes based on developer feedback
Browse files Browse the repository at this point in the history
 - Renamed and moved tests
 - test_graph now test_search_results
 - test_graph1, 2, 3 now test_search1_results1, 2, 3
 - fixed test_constrained_path9 in test_search_results1
 - class names changed to reflect new file names
 - moved files to unit folder

Fixed padding in graph.py

Cleaned up constrained_flexible_paths and added comments

 - compacted default_edge_list and flexible assignments
 - renamed some variables
 - changed loop from for to while

Removed failing tests for checking

Fixed formatting issues

Fixed more formatting issues

Minimized local variable count

 - 5 parameters instead of 6
 - 17 local vars instead of 18

Fixed all formatting errors

Fixed minor formatting error

Changed logic to support code reuse

Revamped parameter passing.

 - calling constrained flexible paths is now more user-friendly
 - test updated to reflect changes.

Changed REST API parameters

Fixed KytosGraph import in Main

Implement Humberto's fixes

Updated comments and changed endpoints for new UI

Added missing endpoint changes

Updated KytosGraph, TestResults and subclasses
 - KytosGraph
   - set_path_function removed.
 - TestResults
   - test_setup method removed.
   - setup method renamed to setUp to run at the start of every test
 - TestResultsSimple, TestResultsMetaData, TestResultsEdges
   - Removed explicit setup call

Gave better descriptions to test subclasses.

Undid KytosGraph import change in Main.

Linting fixes
 - TestResults
   - removed unused import.
 - TestResultsMetadata
   - fixed two methods with same name.

Fixed JSON serialization of error message

Re-added methods and reverted update_links

Fixed linting issue

changed extend back to append

Update tests/unit/test_results.py

Standardized decorators

Co-authored-by: Gleyberson Andrade <[email protected]>
Updated return values of shortest_constrained_path
   - Returns 400 BAD REQUEST if user provides an illegal attribute value

Remove space from decorator

Changed maximum misses variable to minimum hits
   - This is to reflect changes from kytos#62

Co-authored-by: Humberto Diógenes <[email protected]>

Set up mock graph with metadata

Updated testing structure and files
   - test_results and children were moved to integration folder
   - test_graph and test_main now test our added methods
   - test_filter added
   - methods added to test helper

Linting fixes
  • Loading branch information
MarvinTorres committed Aug 6, 2020
1 parent 982c7a4 commit 490a510
Show file tree
Hide file tree
Showing 14 changed files with 788 additions and 612 deletions.
150 changes: 86 additions & 64 deletions graph.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Module Graph of kytos/pathfinder Kytos Network Application."""

from itertools import combinations

from kytos.core import log

try:
Expand All @@ -9,47 +11,51 @@
PACKAGE = 'networkx>=2.2'
log.error(f"Package {PACKAGE} not found. Please 'pip install {PACKAGE}'")

from itertools import combinations

class Filter:
"""Class responsible for removing items with disqualifying values."""

def __init__(self, filter_type, filter_function):
self._filter_type = filter_type
self._filter_fun = filter_function
self._filter_function = filter_function

def run(self,value, items):
def run(self, value, items):
"""Filter out items."""
if isinstance(value, self._filter_type):
fun0 = self._filter_fun(value)
return filter(fun0, items)
else:
raise TypeError(f"Expected type: {self._filter_type}")
return filter(self._filter_function(value), items)

raise TypeError(f"Expected type: {self._filter_type}")


class KytosGraph:
"""Class responsible for the graph generation."""

def __init__(self):
self.graph = nx.Graph()
self._filter_fun_dict = {}
def filterLEQ(metric):# Lower values are better
return lambda x: (lambda y: y[2].get(metric,x) <= x)
def filterGEQ(metric):# Higher values are better
return lambda x: (lambda y: y[2].get(metric,x) >= x)
def filterEEQ(metric):# Equivalence
return lambda x: (lambda y: y[2].get(metric,x) == x)


self._filter_fun_dict["ownership"] = Filter(str,filterEEQ("ownership"))
self._filter_fun_dict["bandwidth"] = Filter((int,float),filterGEQ("bandwidth"))
self._filter_fun_dict["priority"] = Filter((int,float),filterGEQ("priority"))
self._filter_fun_dict["reliability"] = Filter((int,float),filterGEQ("reliability"))
self._filter_fun_dict["utilization"] = Filter((int,float),filterLEQ("utilization"))
self._filter_fun_dict["delay"] = Filter((int,float),filterLEQ("delay"))
self._path_fun = nx.all_shortest_paths


def set_path_fun(self, path_fun):
self._path_fun = path_fun
self._filter_functions = {}

def filter_leq(metric): # Lower values are better
return lambda x: (lambda y: y[2].get(metric, x) <= x)

def filter_geq(metric): # Higher values are better
return lambda x: (lambda y: y[2].get(metric, x) >= x)

def filter_eeq(metric): # Equivalence
return lambda x: (lambda y: y[2].get(metric, x) == x)

self._filter_functions["ownership"] = Filter(
str, filter_eeq("ownership"))
self._filter_functions["bandwidth"] = Filter(
(int, float), filter_geq("bandwidth"))
self._filter_functions["priority"] = Filter(
(int, float), filter_geq("priority"))
self._filter_functions["reliability"] = Filter(
(int, float), filter_geq("reliability"))
self._filter_functions["utilization"] = Filter(
(int, float), filter_leq("utilization"))
self._filter_functions["delay"] = Filter(
(int, float), filter_leq("delay"))
self._path_function = nx.all_shortest_paths

def clear(self):
"""Remove all nodes and links registered."""
Expand Down Expand Up @@ -86,9 +92,22 @@ def update_links(self, links):
endpoint_b = link.endpoint_b.id
self.graph[endpoint_a][endpoint_b][key] = value

def get_metadata_from_link(self, endpoint_a, endpoint_b):
self._set_default_metadata(keys)

def _set_default_metadata(self, keys):
"""Set metadata to all links.
Set the value to zero for inexistent metadata in a link to make those
irrelevant in pathfinding.
"""
for key in keys:
for endpoint_a, endpoint_b in self.graph.edges:
if key not in self.graph[endpoint_a][endpoint_b]:
self.graph[endpoint_a][endpoint_b][key] = 0

def get_link_metadata(self, endpoint_a, endpoint_b):
"""Return the metadata of a link."""
return self.graph.edges[endpoint_a, endpoint_b]
return self.graph.get_edge_data(endpoint_a, endpoint_b)

@staticmethod
def _remove_switch_hops(circuit):
Expand All @@ -100,44 +119,45 @@ def _remove_switch_hops(circuit):
def shortest_paths(self, source, destination, parameter=None):
"""Calculate the shortest paths and return them."""
try:
paths = list(self._path_fun(self.graph,
source, destination, parameter))
paths = list(nx.shortest_simple_paths(self.graph,
source, destination,
parameter))
except (NodeNotFound, NetworkXNoPath):
return []
return paths

def constrained_flexible_paths(self, source, destination, metrics, flexible_metrics, flexible = None):
default_edge_list = self.graph.edges(data=True)
default_edge_list = self._filter_edges(default_edge_list,**metrics)
default_edge_list = list(default_edge_list)
length = len(flexible_metrics)
if flexible is None:
flexible = length
flexible = max(0,flexible)
flexible = min(length,flexible)
def constrained_flexible_paths(self, source, destination,
minimum_hits=None, **metrics):
"""Calculate the constrained shortest paths with flexibility."""
base = metrics.get("base", {})
flexible = metrics.get("flexible", {})
first_pass_links = list(self._filter_links(self.graph.edges(data=True),
**base))
length = len(flexible)
if minimum_hits is None:
minimum_hits = length
minimum_hits = min(length, max(0, minimum_hits))
results = []
stop = False
for i in range(0,flexible+1):
if stop:
break
y = combinations(flexible_metrics.items(),length-i)
for x in y:
tempDict = {}
for k,v in x:
tempDict[k] = v
edges = self._filter_edges(default_edge_list,**tempDict)
edges = ((u,v) for u,v,d in edges)
res0 = self._constrained_shortest_paths(source,destination,edges)
if res0 != []:
results.append({"paths":res0, "metrics":{**metrics, **tempDict}})
stop = True
paths = []
i = 0
while (paths == [] and i in range(0, minimum_hits+1)):
for combo in combinations(flexible.items(), length-i):
additional = dict(combo)
paths = self._constrained_shortest_paths(
source, destination,
self._filter_links(first_pass_links,
metadata=False, **additional))
if paths != []:
results.append(
{"paths": paths, "metrics": {**base, **additional}})
i = i + 1
return results

def _constrained_shortest_paths(self, source, destination, edges):
def _constrained_shortest_paths(self, source, destination, links):
paths = []
try:
paths = list(self._path_fun(self.graph.edge_subgraph(edges),
source, destination))
paths = list(self._path_function(self.graph.edge_subgraph(links),
source, destination))
except NetworkXNoPath:
pass
except NodeNotFound:
Expand All @@ -146,12 +166,14 @@ def _constrained_shortest_paths(self, source, destination, edges):
paths = [[source]]
return paths

def _filter_edges(self, edges, **metrics):
def _filter_links(self, links, metadata=True, **metrics):
for metric, value in metrics.items():
fil = self._filter_fun_dict.get(metric, None)
if fil != None:
filter_ = self._filter_functions.get(metric, None)
if filter_ is not None:
try:
edges = fil.run(value,edges)
links = filter_.run(value, links)
except TypeError as err:
raise TypeError(f"Error in {metric} filter: {err}")
return edges
raise TypeError(f"Error in {metric} value: {err}")
if not metadata:
links = ((u, v) for u, v, d in links)
return links
30 changes: 8 additions & 22 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,38 +89,24 @@ def shortest_path(self):
paths = self._filter_paths(paths, desired, undesired)
return jsonify({'paths': paths})

@rest('v3/', methods=['POST'])
@rest('v2/best-constrained-paths', methods=['POST'])
def shortest_constrained_path(self):
"""Get the set of shortest paths between the source and destination."""
data = request.get_json()

source = data.get('source')
destination = data.get('destination')
flexible = data.get('flexible', 0)
metrics = data.get('metrics',{})
try:
paths = self.graph.constrained_flexible_paths(source, destination,{},metrics,flexible)
return jsonify(paths)
except TypeError as err:
return jsonify({"error":err})


@rest('v4/', methods=['POST'])
def shortest_constrained_path2(self):
"""Get the set of shortest paths between the source and destination."""
data = request.get_json()

source = data.get('source')
destination = data.get('destination')
metrics = data.get('metrics',{})
flexible_metrics = data.get('flexibleMetrics', {})
base_metrics = data.get('base_metrics', {})
fle_metrics = data.get('flexible_metrics', {})
minimum_hits = data.get('minimum_flexible_hits')
try:
paths = self.graph.constrained_flexible_paths(source, destination,
metrics, flexible_metrics)
minimum_hits,
base=base_metrics,
flexible=fle_metrics)
return jsonify(paths)
except TypeError as err:
return jsonify({"error":err})

return jsonify({"error": str(err)}), 400

@listen_to('kytos.topology.updated')
def update_topology(self, event):
Expand Down
104 changes: 104 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,107 @@ def get_topology_mock():
switch_b.dpid: switch_b,
switch_c.dpid: switch_c}
return topology


def get_topology_with_metadata_mock():
"""Create a topology with metadata."""
switches = {}
interfaces = {}
links = {}
i = 0

switches_to_interface_counts = {"S1": 2, "S2": 2, "S3": 6, "S4": 2,
"S5": 6, "S6": 5, "S7": 2, "S8": 8,
"S9": 4, "S10": 3, "S11": 3,
"User1": 4, "User2": 2,
"User3": 2, "User4": 3}

links_to_interfaces = [["S1:1", "S2:1"], ["S1:2", "User1:1"],
["S2:2", "User4:1"], ["S3:1", "S5:1"],
["S3:2", "S7:1"], ["S3:3", "S8:1"],
["S3:4", "S11:1"],
["S3:5", "User3:1"], ["S3:6", "User4:2"],
["S4:1", "S5:2"], ["S4:2", "User1:2"],
["S5:3", "S6:1"],
["S5:4", "S6:2"], ["S5:5", "S8:2"],
["S5:6", "User1:3"], ["S6:3", "S9:1"],
["S6:4", "S9:2"], ["S6:5", "S10:1"],
["S7:2", "S8:3"],
["S8:4", "S9:3"], ["S8:5", "S9:4"],
["S8:6", "S10:2"],
["S8:7", "S11:2"], ["S8:8", "User3:2"],
["S10:3", "User2:1"], ["S11:3", "User2:2"],
["User1:4", "User4:3"]]

links_to_metadata = [
{"reliability": 5, "bandwidth": 100, "delay": 105},
{"reliability": 5, "bandwidth": 100, "delay": 1},
{"reliability": 5, "bandwidth": 100, "delay": 10},
{"reliability": 5, "bandwidth": 10, "delay": 112},
{"reliability": 5, "bandwidth": 100, "delay": 1},
{"reliability": 5, "bandwidth": 100, "delay": 1},
{"reliability": 3, "bandwidth": 100, "delay": 6},
{"reliability": 5, "bandwidth": 100, "delay": 1},
{"reliability": 5, "bandwidth": 100, "delay": 10},
{"reliability": 1, "bandwidth": 100, "delay": 30, "ownership": "A"},
{"reliability": 3, "bandwidth": 100, "delay": 110, "ownership": "A"},
{"reliability": 1, "bandwidth": 100, "delay": 40},
{"reliability": 3, "bandwidth": 100, "delay": 40, "ownership": "A"},
{"reliability": 5, "bandwidth": 100, "delay": 112},
{"reliability": 3, "bandwidth": 100, "delay": 60},
{"reliability": 3, "bandwidth": 100, "delay": 60},
{"reliability": 5, "bandwidth": 100, "delay": 62},
{"bandwidth": 100, "delay": 108, "ownership": "A"},
{"reliability": 5, "bandwidth": 100, "delay": 1},
{"reliability": 3, "bandwidth": 100, "delay": 32},
{"reliability": 3, "bandwidth": 100, "delay": 110},
{"reliability": 5, "bandwidth": 100, "ownership": "A"},
{"reliability": 3, "bandwidth": 100, "delay": 7},
{"reliability": 5, "bandwidth": 100, "delay": 1},
{"reliability": 3, "bandwidth": 100, "delay": 10, "ownership": "A"},
{"reliability": 3, "bandwidth": 100, "delay": 6},
{"reliability": 5, "bandwidth": 10, "delay": 105}]

for switch in switches_to_interface_counts:
switches[switch] = get_switch_mock(switch)

for key, value in switches_to_interface_counts.items():
switches[key].interfaces = {}
for interface in _get_interfaces(value, switches[key]):
switches[key].interfaces[interface.id] = interface
interfaces[interface.id] = interface

for interfaces_str in links_to_interfaces:
interface_a = interfaces[interfaces_str[0]]
interface_b = interfaces[interfaces_str[1]]
links[str(i)] = get_link_mock(interface_a, interface_b)
links[str(i)].metadata = links_to_metadata[i]
i += 1

topology = MagicMock()
topology.links = links
topology.switches = switches
return topology


def _get_interfaces(count, switch):
"""Add a new interface to the list of interfaces."""
for i in range(1, count + 1):
yield get_interface_mock("", i, switch)


def get_filter_links_fake(links, metadata=True, **metrics):
"""Get test links with optional metadata."""
# pylint: disable=unused-argument
filtered_links = ["a", "b", "c"]
filtered_links_without_metadata = filtered_links[0:2]
if not metadata:
return filtered_links_without_metadata
return filtered_links

# pylint: enable=unused-argument


def get_test_filter_function():
"""Get minimum filter function."""
return lambda x: (lambda y: y >= x)
1 change: 1 addition & 0 deletions tests/integration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""kytos/pathfinder subsystem tests."""
Loading

0 comments on commit 490a510

Please sign in to comment.