Passed
Pull Request — master (#233)
by Vinicius
08:40 queued 05:16
created

build.models.path   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 239
Duplicated Lines 0 %

Test Coverage

Coverage 94.53%

Importance

Changes 0
Metric Value
wmc 53
eloc 147
dl 0
loc 239
ccs 121
cts 128
cp 0.9453
rs 6.96
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A Path.is_affected_by_link() 0 5 2
A Path.__eq__() 0 5 3
A Path.make_vlans_available() 0 5 2
A Path.choose_vlans() 0 5 2
A Path.link_affected_by_interface() 0 8 4
A DynamicPathManager.get_best_path() 0 7 2
A DynamicPathManager.get_best_paths() 0 5 2
B Path.status() 0 30 7
A DynamicPathManager.create_path() 0 17 5
A DynamicPathManager.set_controller() 0 4 1
C DynamicPathManager.get_disjoint_paths() 0 65 9
C Path.is_valid() 0 21 10
A DynamicPathManager.get_paths() 0 21 2
A Path.as_dict() 0 3 1
A DynamicPathManager._clear_path() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like build.models.path often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Classes related to paths"""
2 1
import requests
3
4 1
from kytos.core import log
5 1
from kytos.core.common import EntityStatus, GenericEntity
6 1
from kytos.core.link import Link
7 1
from napps.kytos.mef_eline import settings
8 1
from napps.kytos.mef_eline.exceptions import InvalidPath
9
10
11 1
class Path(list, GenericEntity):
12
    """Class to represent a Path."""
13
14 1
    def __eq__(self, other=None):
15
        """Compare paths."""
16 1
        if not other or not isinstance(other, Path):
17 1
            return False
18 1
        return super().__eq__(other)
19
20 1
    def is_affected_by_link(self, link=None):
21
        """Verify if the current path is affected by link."""
22 1
        if not link:
23 1
            return False
24 1
        return link in self
25
26 1
    def link_affected_by_interface(self, interface=None):
27
        """Return the link using this interface, if any, or None otherwise."""
28 1
        if not interface:
29 1
            return None
30 1
        for link in self:
31 1
            if interface in (link.endpoint_a, link.endpoint_b):
32 1
                return link
33
        return None
34
35 1
    def choose_vlans(self):
36
        """Choose the VLANs to be used for the circuit."""
37 1
        for link in self:
38 1
            tag = link.get_next_available_tag()
39 1
            link.add_metadata("s_vlan", tag)
40
41 1
    def make_vlans_available(self):
42
        """Make the VLANs used in a path available when undeployed."""
43 1
        for link in self:
44 1
            link.make_tag_available(link.get_metadata("s_vlan"))
45 1
            link.remove_metadata("s_vlan")
46
47 1
    def is_valid(self, switch_a, switch_z, is_scheduled=False):
48
        """Check if this is a valid path."""
49 1
        if not self:
50 1
            return True
51 1
        previous = switch_a
52 1
        for link in self:
53 1
            if link.endpoint_a.switch != previous:
54 1
                raise InvalidPath(
55
                    f"{link.endpoint_a} switch is different" f" from previous."
56
                )
57 1
            if is_scheduled is False and (
58
                link.endpoint_a.link is None
59
                or link.endpoint_a.link != link
60
                or link.endpoint_b.link is None
61
                or link.endpoint_b.link != link
62
            ):
63
                raise InvalidPath(f"Link {link} is not available.")
64 1
            previous = link.endpoint_b.switch
65 1
        if previous == switch_z:
66 1
            return True
67
        raise InvalidPath("Last endpoint is different from uni_z")
68
69 1
    @property
70 1
    def status(self):
71
        """Check for the  status of a path.
72
73
        If any link in this path is down, the path is considered down.
74
        """
75 1
        if not self:
76 1
            return EntityStatus.DISABLED
77
78 1
        endpoint = f"{settings.TOPOLOGY_URL}/links"
79 1
        api_reply = requests.get(endpoint)
80 1
        if api_reply.status_code != getattr(requests.codes, "ok"):
81
            log.error(
82
                "Failed to get links at %s. Returned %s",
83
                endpoint,
84
                api_reply.status_code,
85
            )
86
            return None
87 1
        links = api_reply.json()["links"]
88 1
        return_status = EntityStatus.UP
89 1
        for path_link in self:
90 1
            try:
91 1
                link = links[path_link.id]
92
            except KeyError:
93
                return EntityStatus.DISABLED
94 1
            if link["enabled"] is False:
95 1
                return EntityStatus.DISABLED
96 1
            if link["active"] is False:
97 1
                return_status = EntityStatus.DOWN
98 1
        return return_status
99
100 1
    def as_dict(self):
101
        """Return list comprehension of links as_dict."""
102 1
        return [link.as_dict() for link in self if link]
103
104
105 1
class DynamicPathManager:
106
    """Class to handle and create paths."""
107
108 1
    controller = None
109
110 1
    @classmethod
111 1
    def set_controller(cls, controller=None):
112
        """Set the controller to discovery news paths."""
113 1
        cls.controller = controller
114
115 1
    @staticmethod
116 1
    def get_paths(circuit, max_paths=2, **kwargs):
117
        """Get a valid path for the circuit from the Pathfinder."""
118 1
        endpoint = settings.PATHFINDER_URL
119 1
        request_data = {
120
            "source": circuit.uni_a.interface.id,
121
            "destination": circuit.uni_z.interface.id,
122
            "spf_max_paths": max_paths,
123
        }
124 1
        request_data.update(kwargs)
125 1
        api_reply = requests.post(endpoint, json=request_data)
126
127 1
        if api_reply.status_code != getattr(requests.codes, "ok"):
128 1
            log.error(
129
                "Failed to get paths at %s. Returned %s",
130
                endpoint,
131
                api_reply.status_code,
132
            )
133 1
            return None
134 1
        reply_data = api_reply.json()
135 1
        return reply_data.get("paths")
136
137 1
    @staticmethod
138 1
    def _clear_path(path):
139
        """Remove switches from a path, returning only interfaces."""
140 1
        return [endpoint for endpoint in path if len(endpoint) > 23]
141
142 1
    @classmethod
143 1
    def get_best_path(cls, circuit):
144
        """Return the best path available for a circuit, if exists."""
145 1
        paths = cls.get_paths(circuit)
146 1
        if paths:
147 1
            return cls.create_path(cls.get_paths(circuit)[0]["hops"])
148 1
        return None
149
150 1
    @classmethod
151 1
    def get_best_paths(cls, circuit, **kwargs):
152
        """Return the best paths available for a circuit, if they exist."""
153 1
        for path in cls.get_paths(circuit, **kwargs):
154 1
            yield cls.create_path(path["hops"])
155
156 1
    @classmethod
157 1
    def get_disjoint_paths(
158
        cls, circuit, unwanted_path, cutoff=settings.DISJOINT_PATH_CUTOFF
159
    ):
160
        """Computes the maximum disjoint paths from the unwanted_path for a EVC
161
162
        Maximum disjoint paths from the unwanted_path are the paths from the
163
        source node to the target node that share the minimum number os links
164
        contained in unwanted_path. In other words, unwanted_path is the path
165
        we want to avoid: we want the maximum possible disjoint path from it.
166
        The disjointness of a path in regards to unwanted_path is calculated
167
        by the complementary percentage of shared links between them. As an
168
        example, if the unwanted_path has 3 links, a given path P1 has 1 link
169
        shared with unwanted_path, and a given path P2 has 2 links shared with
170
        unwanted_path, then the disjointness of P1 is 0.67 and the disjointness
171
        of P2 is 0.33. In this example, P1 is preferable over P2 because it
172
        offers a better disjoint path. When two paths have the same
173
        disjointness they are ordered by 'cost' attributed as returned from
174
        Pathfinder. When the disjointness of a path is equal to 0 (i.e., it
175
        shares all the links with unwanted_path), that particular path is not
176
        considered a candidate.
177
178
        Parameters:
179
        -----------
180
181
        circuit : EVC
182
            The EVC providing source node (uni_a) and target node (uni_z)
183
184
        unwanted_path : Path
185
            The Path which we want to avoid.
186
187
        cutoff: int
188
            Maximum number of paths to consider when calculating the disjoint
189
            paths (number of paths to request from pathfinder)
190
191
        Returns:
192
        --------
193
        paths : generator
194
            Generator of unwanted_path disjoint paths. If unwanted_path is
195
            not provided or empty, we return an empty list.
196
        """
197 1
        unwanted_links = [
198
            (link.endpoint_a.id, link.endpoint_b.id) for link in unwanted_path
199
        ]
200 1
        if not unwanted_links:
201 1
            return None
202
203 1
        paths = cls.get_paths(circuit, max_paths=cutoff,
204
                              **circuit.secondary_constraints)
205 1
        for path in paths:
206 1
            head = path["hops"][:-1]
207 1
            tail = path["hops"][1:]
208 1
            shared_edges = 0
209 1
            for (endpoint_a, endpoint_b) in unwanted_links:
210 1
                if ((endpoint_a, endpoint_b) in zip(head, tail)) or (
211
                    (endpoint_b, endpoint_a) in zip(head, tail)
212
                ):
213 1
                    shared_edges += 1
214 1
            path["disjointness"] = 1 - shared_edges / len(unwanted_links)
215 1
        paths = sorted(paths, key=lambda x: (-x['disjointness'], x['cost']))
216 1
        for path in paths:
217 1
            if path["disjointness"] == 0:
218 1
                continue
219 1
            yield cls.create_path(path["hops"])
220 1
        return None
221
222 1
    @classmethod
223 1
    def create_path(cls, path):
224
        """Return the path containing only the interfaces."""
225 1
        new_path = Path()
226 1
        clean_path = cls._clear_path(path)
227
228 1
        if len(clean_path) % 2:
229 1
            return None
230
231 1
        for link in zip(clean_path[1:-1:2], clean_path[2::2]):
232 1
            interface_a = cls.controller.get_interface_by_id(link[0])
233 1
            interface_b = cls.controller.get_interface_by_id(link[1])
234 1
            if interface_a is None or interface_b is None:
235 1
                return None
236 1
            new_path.append(Link(interface_a, interface_b))
237
238
        return new_path
239