Passed
Push — master ( 61cfcd...d7abc2 )
by Vinicius
15:29 queued 13:28
created
1
"""Main module of kytos/pathfinder Kytos Network Application."""
2
3 1
from threading import Lock
4
5 1
from flask import jsonify, request
6 1
from kytos.core import KytosNApp, log, rest
7 1
from kytos.core.helpers import listen_to
8 1
from napps.kytos.pathfinder.graph import KytosGraph
9
# pylint: disable=import-error
10 1
from werkzeug.exceptions import BadRequest
11
12
13 1
class Main(KytosNApp):
14
    """
15
    Main class of kytos/pathfinder NApp.
16
17
    This class is the entry point for this napp.
18
    """
19
20 1
    def setup(self):
21
        """Create a graph to handle the nodes and edges."""
22 1
        self.graph = KytosGraph()
23 1
        self._topology = None
24 1
        self._lock = Lock()
25
26 1
    def execute(self):
27
        """Do nothing."""
28
29 1
    def shutdown(self):
30
        """Shutdown the napp."""
31
32 1
    def _filter_paths(self, paths, desired, undesired):
33
        """
34
        Apply filters to the paths list.
35
36
        Make sure that each path in the list has all the desired links and none
37
        of the undesired ones.
38
        """
39 1
        filtered_paths = []
40
41 1 View Code Duplication
        if desired:
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
42 1
            for link_id in desired:
43 1
                try:
44 1
                    endpoint_a = self._topology.links[link_id].endpoint_a.id
45 1
                    endpoint_b = self._topology.links[link_id].endpoint_b.id
46
                except KeyError:
47
                    return []
48
49 1
                for path in paths:
50 1
                    head = path["hops"][:-1]
51 1
                    tail = path["hops"][1:]
52 1
                    if ((endpoint_a, endpoint_b) in zip(head, tail)) or (
53
                        (endpoint_b, endpoint_a) in zip(head, tail)
54
                    ):
55 1
                        filtered_paths.append(path)
56
        else:
57 1
            filtered_paths = paths
58
59 1 View Code Duplication
        if undesired:
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
60 1
            for link_id in undesired:
61 1
                try:
62 1
                    endpoint_a = self._topology.links[link_id].endpoint_a.id
63 1
                    endpoint_b = self._topology.links[link_id].endpoint_b.id
64
                except KeyError:
65
                    continue
66
67 1
                for path in paths:
68 1
                    head = path["hops"][:-1]
69 1
                    tail = path["hops"][1:]
70 1
                    if ((endpoint_a, endpoint_b) in zip(head, tail)) or (
71
                        (endpoint_b, endpoint_a) in zip(head, tail)
72
                    ):
73
74 1
                        filtered_paths.remove(path)
75
76 1
        return filtered_paths
77
78 1
    def _filter_paths_le_cost(self, paths, max_cost):
79
        """Filter by paths where the cost is le <= max_cost."""
80 1
        if not max_cost:
81 1
            return paths
82 1
        return [path for path in paths if path["cost"] <= max_cost]
83
84 1
    def _validate_payload(self, data):
85
        """Validate shortest_path v2/ POST endpoint."""
86 1
        if data.get("desired_links"):
87 1
            if not isinstance(data["desired_links"], list):
88
                raise BadRequest(
89
                    f"TypeError: desired_links is supposed to be a list."
90
                    f" type: {type(data['desired_links'])}"
91
                )
92
93 1
        if data.get("undesired_links"):
94
            if not isinstance(data["undesired_links"], list):
95
                raise BadRequest(
96
                    f"TypeError: undesired_links is supposed to be a list."
97
                    f" type: {type(data['undesired_links'])}"
98
                )
99
100 1
        parameter = data.get("parameter")
101 1
        spf_attr = data.get("spf_attribute")
102 1
        if not spf_attr:
103 1
            spf_attr = parameter or "hop"
104 1
        data["spf_attribute"] = spf_attr
105
106 1
        if spf_attr not in self.graph.spf_edge_data_cbs:
107
            raise BadRequest(
108
                "Invalid 'spf_attribute'. Valid values: "
109
                f"{', '.join(self.graph.spf_edge_data_cbs.keys())}"
110
            )
111
112 1
        try:
113 1
            data["spf_max_paths"] = max(int(data.get("spf_max_paths", 2)), 1)
114
        except (TypeError, ValueError):
115
            raise BadRequest(
116
                f"spf_max_paths {data.get('spf_max_pahts')} must be an int"
117
            )
118
119 1
        spf_max_path_cost = data.get("spf_max_path_cost")
120 1
        if spf_max_path_cost:
121
            try:
122
                spf_max_path_cost = max(int(spf_max_path_cost), 1)
123
                data["spf_max_path_cost"] = spf_max_path_cost
124
            except (TypeError, ValueError):
125
                raise BadRequest(
126
                    f"spf_max_path_cost {data.get('spf_max_path_cost')} must"
127
                    " be an int"
128
                )
129
130 1
        data["mandatory_metrics"] = data.get("mandatory_metrics", {})
131 1
        data["flexible_metrics"] = data.get("flexible_metrics", {})
132
133 1
        try:
134 1
            minimum_hits = data.get("minimum_flexible_hits")
135 1
            if minimum_hits:
136 1
                minimum_hits = min(
137
                    len(data["flexible_metrics"]), max(0, int(minimum_hits))
138
                )
139 1
            data["minimum_flexible_hits"] = minimum_hits
140
        except (TypeError, ValueError):
141
            raise BadRequest(
142
                f"minimum_hits {data.get('minimum_flexible_hits')} must be an int"
143
            )
144
145 1
        return data
146
147 1
    @rest("v2/", methods=["POST"])
148 1
    def shortest_path(self):
149
        """Calculate the best path between the source and destination."""
150 1
        data = request.get_json()
151 1
        data = self._validate_payload(data)
152
153 1
        desired = data.get("desired_links")
154 1
        undesired = data.get("undesired_links")
155
156 1
        spf_attr = data.get("spf_attribute")
157 1
        spf_max_paths = data.get("spf_max_paths")
158 1
        spf_max_path_cost = data.get("spf_max_path_cost")
159 1
        mandatory_metrics = data.get("mandatory_metrics")
160 1
        flexible_metrics = data.get("flexible_metrics")
161 1
        minimum_hits = data.get("minimum_flexible_hits")
162 1
        log.debug(f"POST v2/ payload data: {data}")
163
164 1
        try:
165 1
            with self._lock:
166 1
                if any([mandatory_metrics, flexible_metrics]):
167 1
                    paths = self.graph.constrained_k_shortest_paths(
168
                        data["source"],
169
                        data["destination"],
170
                        weight=self.graph.spf_edge_data_cbs[spf_attr],
171
                        k=spf_max_paths,
172
                        minimum_hits=minimum_hits,
173
                        mandatory_metrics=mandatory_metrics,
174
                        flexible_metrics=flexible_metrics,
175
                    )
176
                else:
177 1
                    paths = self.graph.k_shortest_paths(
178
                        data["source"],
179
                        data["destination"],
180
                        weight=self.graph.spf_edge_data_cbs[spf_attr],
181
                        k=spf_max_paths,
182
                    )
183
184 1
                paths = self.graph.path_cost_builder(
185
                    paths,
186
                    weight=spf_attr,
187
                )
188 1
            log.debug(f"Found paths: {paths}")
189 1
        except TypeError as err:
190 1
            raise BadRequest(str(err))
191
192 1
        paths = self._filter_paths(paths, desired, undesired)
193 1
        paths = self._filter_paths_le_cost(paths, max_cost=spf_max_path_cost)
194 1
        log.debug(f"Filtered paths: {paths}")
195 1
        return jsonify({"paths": paths})
196
197 1
    @listen_to("kytos.topology.updated")
198 1
    def on_topology_updated(self, event):
199
        """Update the graph when the network topology is updated."""
200
        self.update_topology(event)
201
202 1
    def update_topology(self, event):
203
        """Update the graph when the network topology is updated."""
204 1
        if "topology" not in event.content:
205 1
            return
206 1
        topology = event.content["topology"]
207 1
        with self._lock:
208 1
            self._topology = topology
209 1
            self.graph.update_topology(topology)
210 1
        log.debug("Topology graph updated.")
211
212 1
    @listen_to("kytos/topology.links.metadata.(added|removed)")
213 1
    def on_links_metadata_changed(self, event):
214
        """Update the graph when links' metadata are added or removed."""
215
        link = event.content["link"]
216
        with self._lock:
217
            self.graph.update_link_metadata(link)
218
        metadata = event.content["metadata"]
219
        log.debug(f"Topology graph updated link id: {link.id} metadata: {metadata}")
220