Passed
Pull Request — master (#226)
by
unknown
03:35
created

build.models.evc.EVCDeploy.run_bulk_sdntraces()   A

Complexity

Conditions 4

Size

Total Lines 25
Code Lines 20

Duplication

Lines 25
Ratio 100 %

Code Coverage

Tests 14
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 20
dl 25
loc 25
ccs 14
cts 14
cp 1
rs 9.4
c 0
b 0
f 0
cc 4
nop 1
crap 4
1
"""Classes used in the main application."""  # pylint: disable=too-many-lines
2 1
from collections import OrderedDict
3 1
from datetime import datetime
4 1
from threading import Lock
5 1
from uuid import uuid4
6
7 1
import requests
8 1
from glom import glom
9
10 1
from kytos.core import log
11 1
from kytos.core.common import EntityStatus, GenericEntity
12 1
from kytos.core.exceptions import KytosNoTagAvailableError
13 1
from kytos.core.helpers import get_time, now
14 1
from kytos.core.interface import UNI
15 1
from napps.kytos.mef_eline import controllers, settings
16 1
from napps.kytos.mef_eline.exceptions import FlowModException, InvalidPath
17 1
from napps.kytos.mef_eline.utils import (compare_endpoint_trace, emit_event,
18
                                         notify_link_available_tags)
19
20 1
from .path import DynamicPathManager, Path
21
22
23 1
class EVCBase(GenericEntity):
24
    """Class to represent a circuit."""
25
26 1
    read_only_attributes = [
27
        "creation_time",
28
        "active",
29
        "current_path",
30
        "failover_path",
31
        "_id",
32
        "archived",
33
    ]
34 1
    attributes_requiring_redeploy = [
35
        "primary_path",
36
        "backup_path",
37
        "dynamic_backup_path",
38
        "queue_id",
39
        "sb_priority",
40
        "primary_constraints",
41
        "secondary_constraints"
42
    ]
43 1
    required_attributes = ["name", "uni_a", "uni_z"]
44
45 1
    def __init__(self, controller, **kwargs):
46
        """Create an EVC instance with the provided parameters.
47
48
        Args:
49
            id(str): EVC identifier. Whether it's None an ID will be genereted.
50
                     Only the first 14 bytes passed will be used.
51
            name: represents an EVC name.(Required)
52
            uni_a (UNI): Endpoint A for User Network Interface.(Required)
53
            uni_z (UNI): Endpoint Z for User Network Interface.(Required)
54
            start_date(datetime|str): Date when the EVC was registred.
55
                                      Default is now().
56
            end_date(datetime|str): Final date that the EVC will be fineshed.
57
                                    Default is None.
58
            bandwidth(int): Bandwidth used by EVC instance. Default is 0.
59
            primary_links(list): Primary links used by evc. Default is []
60
            backup_links(list): Backups links used by evc. Default is []
61
            current_path(list): Circuit being used at the moment if this is an
62
                                active circuit. Default is [].
63
            failover_path(list): Path being used to provide EVC protection via
64
                                failover during link failures. Default is [].
65
            primary_path(list): primary circuit offered to user IF one or more
66
                                links were provided. Default is [].
67
            backup_path(list): backup circuit offered to the user IF one or
68
                               more links were provided. Default is [].
69
            dynamic_backup_path(bool): Enable computer backup path dynamically.
70
                                       Dafault is False.
71
            creation_time(datetime|str): datetime when the circuit should be
72
                                         activated. default is now().
73
            enabled(Boolean): attribute to indicate the administrative state;
74
                              default is False.
75
            active(Boolean): attribute to indicate the operational state;
76
                             default is False.
77
            archived(Boolean): indicate the EVC has been deleted and is
78
                               archived; default is False.
79
            owner(str): The EVC owner. Default is None.
80
            sb_priority(int): Service level provided in the request.
81
                              Default is None.
82
            service_level(int): Service level provided. The higher the better.
83
                                Default is 0.
84
85
        Raises:
86
            ValueError: raised when object attributes are invalid.
87
88
        """
89 1
        self._validate(**kwargs)
90 1
        super().__init__()
91
92
        # required attributes
93 1
        self._id = kwargs.get("id", uuid4().hex)[:14]
94 1
        self.uni_a = kwargs.get("uni_a")
95 1
        self.uni_z = kwargs.get("uni_z")
96 1
        self.name = kwargs.get("name")
97
98
        # optional attributes
99 1
        self.start_date = get_time(kwargs.get("start_date")) or now()
100 1
        self.end_date = get_time(kwargs.get("end_date")) or None
101 1
        self.queue_id = kwargs.get("queue_id", None)
102
103 1
        self.bandwidth = kwargs.get("bandwidth", 0)
104 1
        self.primary_links = Path(kwargs.get("primary_links", []))
105 1
        self.backup_links = Path(kwargs.get("backup_links", []))
106 1
        self.current_path = Path(kwargs.get("current_path", []))
107 1
        self.failover_path = Path(kwargs.get("failover_path", []))
108 1
        self.primary_path = Path(kwargs.get("primary_path", []))
109 1
        self.backup_path = Path(kwargs.get("backup_path", []))
110 1
        self.dynamic_backup_path = kwargs.get("dynamic_backup_path", False)
111 1
        self.primary_constraints = kwargs.get("primary_constraints", {})
112 1
        self.secondary_constraints = kwargs.get("secondary_constraints", {})
113 1
        self.creation_time = get_time(kwargs.get("creation_time")) or now()
114 1
        self.owner = kwargs.get("owner", None)
115 1
        self.sb_priority = kwargs.get("sb_priority", None) or kwargs.get(
116
            "priority", None
117
        )
118 1
        self.service_level = kwargs.get("service_level", 0)
119 1
        self.circuit_scheduler = kwargs.get("circuit_scheduler", [])
120 1
        self.flow_removed_at = get_time(kwargs.get("flow_removed_at")) or None
121 1
        self.updated_at = get_time(kwargs.get("updated_at")) or now()
122 1
        self.execution_rounds = kwargs.get("execution_rounds", 0)
123
124 1
        self.current_links_cache = set()
125 1
        self.primary_links_cache = set()
126 1
        self.backup_links_cache = set()
127
128 1
        self.lock = Lock()
129
130 1
        self.archived = kwargs.get("archived", False)
131
132 1
        self.metadata = kwargs.get("metadata", {})
133
134 1
        self._controller = controller
135 1
        self._mongo_controller = controllers.ELineController()
136
137 1
        if kwargs.get("active", False):
138 1
            self.activate()
139
        else:
140 1
            self.deactivate()
141
142 1
        if kwargs.get("enabled", False):
143 1
            self.enable()
144
        else:
145 1
            self.disable()
146
147
        # datetime of user request for a EVC (or datetime when object was
148
        # created)
149 1
        self.request_time = kwargs.get("request_time", now())
150
        # dict with the user original request (input)
151 1
        self._requested = kwargs
152
153 1
    def sync(self):
154
        """Sync this EVC in the MongoDB."""
155 1
        self.updated_at = now()
156 1
        self._mongo_controller.upsert_evc(self.as_dict())
157
158 1
    def update(self, **kwargs):
159
        """Update evc attributes.
160
161
        This method will raises an error trying to change the following
162
        attributes: [name, uni_a and uni_z]
163
164
        Returns:
165
            the values for enable and a redeploy attribute, if exists and None
166
            otherwise
167
        Raises:
168
            ValueError: message with error detail.
169
170
        """
171 1
        enable, redeploy = (None, None)
172 1
        uni_a = kwargs.get("uni_a") or self.uni_a
173 1
        uni_z = kwargs.get("uni_z") or self.uni_z
174 1
        for attribute, value in kwargs.items():
175 1
            if attribute in self.read_only_attributes:
176 1
                raise ValueError(f"{attribute} can't be updated.")
177 1
            if not hasattr(self, attribute):
178 1
                raise ValueError(f'The attribute "{attribute}" is invalid.')
179 1
            if attribute in ("primary_path", "backup_path"):
180 1
                try:
181 1
                    value.is_valid(
182
                        uni_a.interface.switch, uni_z.interface.switch
183
                    )
184 1
                except InvalidPath as exception:
185 1
                    raise ValueError(  # pylint: disable=raise-missing-from
186
                        f"{attribute} is not a " f"valid path: {exception}"
187
                    )
188 1
        for attribute, value in kwargs.items():
189 1
            if attribute in ("enable", "enabled"):
190 1
                if value:
191 1
                    self.enable()
192
                else:
193 1
                    self.disable()
194 1
                enable = value
195
            else:
196 1
                setattr(self, attribute, value)
197 1
                if attribute in self.attributes_requiring_redeploy:
198 1
                    redeploy = value
199 1
        self.sync()
200 1
        return enable, redeploy
201
202 1
    def set_flow_removed_at(self):
203
        """Update flow_removed_at attribute."""
204
        self.flow_removed_at = now()
205
206 1
    def has_recent_removed_flow(self, setting=settings):
207
        """Check if any flow has been removed from the evc"""
208
        if self.flow_removed_at is None:
209
            return False
210
        res_seconds = (now() - self.flow_removed_at).seconds
211
        return res_seconds < setting.TIME_RECENT_DELETED_FLOWS
212
213 1
    def is_recent_updated(self, setting=settings):
214
        """Check if the evc has been updated recently"""
215
        res_seconds = (now() - self.updated_at).seconds
216
        return res_seconds < setting.TIME_RECENT_UPDATED
217
218 1
    def __repr__(self):
219
        """Repr method."""
220 1
        return f"EVC({self._id}, {self.name})"
221
222 1
    def _validate(self, **kwargs):
223
        """Do Basic validations.
224
225
        Verify required attributes: name, uni_a, uni_z
226
        Verify if the attributes uni_a and uni_z are valid.
227
228
        Raises:
229
            ValueError: message with error detail.
230
231
        """
232 1
        for attribute in self.required_attributes:
233
234 1
            if attribute not in kwargs:
235 1
                raise ValueError(f"{attribute} is required.")
236
237 1
            if "uni" in attribute:
238 1
                uni = kwargs.get(attribute)
239 1
                if not isinstance(uni, UNI):
240
                    raise ValueError(f"{attribute} is an invalid UNI.")
241
242 1
                if not uni.is_valid():
243 1
                    tag = uni.user_tag.value
244 1
                    message = f"VLAN tag {tag} is not available in {attribute}"
245 1
                    raise ValueError(message)
246
247 1
    def __eq__(self, other):
248
        """Override the default implementation."""
249 1
        if not isinstance(other, EVC):
250
            return False
251
252 1
        attrs_to_compare = ["name", "uni_a", "uni_z", "owner", "bandwidth"]
253 1
        for attribute in attrs_to_compare:
254 1
            if getattr(other, attribute) != getattr(self, attribute):
255 1
                return False
256 1
        return True
257
258 1
    def shares_uni(self, other):
259
        """Check if two EVCs share an UNI."""
260 1
        if other.uni_a in (self.uni_a, self.uni_z) or other.uni_z in (
261
            self.uni_a,
262
            self.uni_z,
263
        ):
264 1
            return True
265
        return False
266
267 1
    def as_dict(self):
268
        """Return a dictionary representing an EVC object."""
269 1
        evc_dict = {
270
            "id": self.id,
271
            "name": self.name,
272
            "uni_a": self.uni_a.as_dict(),
273
            "uni_z": self.uni_z.as_dict(),
274
        }
275
276 1
        time_fmt = "%Y-%m-%dT%H:%M:%S"
277
278 1
        evc_dict["start_date"] = self.start_date
279 1
        if isinstance(self.start_date, datetime):
280 1
            evc_dict["start_date"] = self.start_date.strftime(time_fmt)
281
282 1
        evc_dict["end_date"] = self.end_date
283 1
        if isinstance(self.end_date, datetime):
284 1
            evc_dict["end_date"] = self.end_date.strftime(time_fmt)
285
286 1
        evc_dict["queue_id"] = self.queue_id
287 1
        evc_dict["bandwidth"] = self.bandwidth
288 1
        evc_dict["primary_links"] = self.primary_links.as_dict()
289 1
        evc_dict["backup_links"] = self.backup_links.as_dict()
290 1
        evc_dict["current_path"] = self.current_path.as_dict()
291 1
        evc_dict["failover_path"] = self.failover_path.as_dict()
292 1
        evc_dict["primary_path"] = self.primary_path.as_dict()
293 1
        evc_dict["backup_path"] = self.backup_path.as_dict()
294 1
        evc_dict["dynamic_backup_path"] = self.dynamic_backup_path
295 1
        evc_dict["metadata"] = self.metadata
296
297 1
        evc_dict["request_time"] = self.request_time
298 1
        if isinstance(self.request_time, datetime):
299 1
            evc_dict["request_time"] = self.request_time.strftime(time_fmt)
300
301 1
        time = self.creation_time.strftime(time_fmt)
302 1
        evc_dict["creation_time"] = time
303
304 1
        evc_dict["owner"] = self.owner
305 1
        evc_dict["circuit_scheduler"] = [
306
            sc.as_dict() for sc in self.circuit_scheduler
307
        ]
308
309 1
        evc_dict["active"] = self.is_active()
310 1
        evc_dict["enabled"] = self.is_enabled()
311 1
        evc_dict["archived"] = self.archived
312 1
        evc_dict["sb_priority"] = self.sb_priority
313 1
        evc_dict["service_level"] = self.service_level
314 1
        evc_dict["primary_constraints"] = self.primary_constraints
315 1
        evc_dict["flow_removed_at"] = self.flow_removed_at
316 1
        evc_dict["updated_at"] = self.updated_at
317
318 1
        return evc_dict
319
320 1
    @property
321 1
    def id(self):  # pylint: disable=invalid-name
322
        """Return this EVC's ID."""
323 1
        return self._id
324
325 1
    def archive(self):
326
        """Archive this EVC on deletion."""
327 1
        self.archived = True
328
329
330
# pylint: disable=fixme, too-many-public-methods
331 1
class EVCDeploy(EVCBase):
332
    """Class to handle the deploy procedures."""
333
334 1
    def create(self):
335
        """Create a EVC."""
336
337 1
    def discover_new_paths(self):
338
        """Discover new paths to satisfy this circuit and deploy it."""
339
        return DynamicPathManager.get_best_paths(self,
340
                                                 **self.primary_constraints)
341
342 1
    def get_failover_path_candidates(self):
343
        """Get failover paths to satisfy this EVC."""
344
        # in the future we can return primary/backup paths as well
345
        # we just have to properly handle link_up and failover paths
346
        # if (
347
        #     self.is_using_primary_path() and
348
        #     self.backup_path.status is EntityStatus.UP
349
        # ):
350
        #     yield self.backup_path
351 1
        return DynamicPathManager.get_disjoint_paths(self, self.current_path)
352
353 1
    def change_path(self):
354
        """Change EVC path."""
355
356 1
    def reprovision(self):
357
        """Force the EVC (re-)provisioning."""
358
359 1
    def is_affected_by_link(self, link):
360
        """Return True if this EVC has the given link on its current path."""
361 1
        return link in self.current_path
362
363 1
    def link_affected_by_interface(self, interface):
364
        """Return True if this EVC has the given link on its current path."""
365
        return self.current_path.link_affected_by_interface(interface)
366
367 1
    def is_backup_path_affected_by_link(self, link):
368
        """Return True if the backup path of this EVC uses the given link."""
369 1
        return link in self.backup_path
370
371
    # pylint: disable=invalid-name
372 1
    def is_primary_path_affected_by_link(self, link):
373
        """Return True if the primary path of this EVC uses the given link."""
374 1
        return link in self.primary_path
375
376 1
    def is_failover_path_affected_by_link(self, link):
377
        """Return True if this EVC has the given link on its failover path."""
378 1
        return link in self.failover_path
379
380 1
    def is_eligible_for_failover_path(self):
381
        """Verify if this EVC is eligible for failover path (EP029)"""
382
        # In the future this function can be augmented to consider
383
        # primary/backup, primary/dynamic, and other path combinations
384 1
        return (
385
            self.dynamic_backup_path and
386
            not self.primary_path and not self.backup_path
387
        )
388
389 1
    def is_using_primary_path(self):
390
        """Verify if the current deployed path is self.primary_path."""
391 1
        return self.primary_path and (self.current_path == self.primary_path)
392
393 1
    def is_using_backup_path(self):
394
        """Verify if the current deployed path is self.backup_path."""
395 1
        return self.backup_path and (self.current_path == self.backup_path)
396
397 1
    def is_using_dynamic_path(self):
398
        """Verify if the current deployed path is a dynamic path."""
399 1
        if (
400
            self.current_path
401
            and not self.is_using_primary_path()
402
            and not self.is_using_backup_path()
403
            and self.current_path.status == EntityStatus.UP
404
        ):
405
            return True
406 1
        return False
407
408 1
    def deploy_to_backup_path(self):
409
        """Deploy the backup path into the datapaths of this circuit.
410
411
        If the backup_path attribute is valid and up, this method will try to
412
        deploy this backup_path.
413
414
        If everything fails and dynamic_backup_path is True, then tries to
415
        deploy a dynamic path.
416
        """
417
        # TODO: Remove flows from current (cookies)
418 1
        if self.is_using_backup_path():
419
            # TODO: Log to say that cannot move backup to backup
420
            return True
421
422 1
        success = False
423 1
        if self.backup_path.status is EntityStatus.UP:
424 1
            success = self.deploy_to_path(self.backup_path)
425
426 1
        if success:
427 1
            return True
428
429 1
        if (
430
            self.dynamic_backup_path
431
            or self.uni_a.interface.switch == self.uni_z.interface.switch
432
        ):
433 1
            return self.deploy_to_path()
434
435
        return False
436
437 1
    def deploy_to_primary_path(self):
438
        """Deploy the primary path into the datapaths of this circuit.
439
440
        If the primary_path attribute is valid and up, this method will try to
441
        deploy this primary_path.
442
        """
443
        # TODO: Remove flows from current (cookies)
444 1
        if self.is_using_primary_path():
445
            # TODO: Log to say that cannot move primary to primary
446
            return True
447
448 1
        if self.primary_path.status is EntityStatus.UP:
449 1
            return self.deploy_to_path(self.primary_path)
450
        return False
451
452 1
    def deploy(self):
453
        """Deploy EVC to best path.
454
455
        Best path can be the primary path, if available. If not, the backup
456
        path, and, if it is also not available, a dynamic path.
457
        """
458 1
        if self.archived:
459 1
            return False
460 1
        self.enable()
461 1
        success = self.deploy_to_primary_path()
462 1
        if not success:
463 1
            success = self.deploy_to_backup_path()
464
465 1
        if success:
466 1
            emit_event(self._controller, "deployed", evc_id=self.id)
467 1
        return success
468
469 1
    @staticmethod
470 1
    def get_path_status(path):
471
        """Check for the current status of a path.
472
473
        If any link in this path is down, the path is considered down.
474
        """
475 1
        if not path:
476 1
            return EntityStatus.DISABLED
477
478 1
        for link in path:
479 1
            if link.status is not EntityStatus.UP:
480 1
                return link.status
481 1
        return EntityStatus.UP
482
483
    #    def discover_new_path(self):
484
    #        # TODO: discover a new path to satisfy this circuit and deploy
485
486 1
    def remove(self):
487
        """Remove EVC path and disable it."""
488 1
        self.remove_current_flows()
489 1
        self.remove_failover_flows()
490 1
        self.disable()
491 1
        self.sync()
492 1
        emit_event(self._controller, "undeployed", evc_id=self.id)
493
494 1
    def remove_failover_flows(self, exclude_uni_switches=True,
495
                              force=True, sync=True) -> None:
496
        """Remove failover_flows.
497
498
        By default, it'll exclude UNI switches, if mef_eline has already
499
        called remove_current_flows before then this minimizes the number
500
        of FlowMods and IO.
501
        """
502 1
        if not self.failover_path:
503 1
            return
504 1
        switches, cookie, excluded = OrderedDict(), self.get_cookie(), set()
505 1
        links = set()
506 1
        if exclude_uni_switches:
507 1
            excluded.add(self.uni_a.interface.switch.id)
508 1
            excluded.add(self.uni_z.interface.switch.id)
509 1
        for link in self.failover_path:
510 1
            if link.endpoint_a.switch.id not in excluded:
511 1
                switches[link.endpoint_a.switch.id] = link.endpoint_a.switch
512 1
                links.add(link)
513 1
            if link.endpoint_b.switch.id not in excluded:
514 1
                switches[link.endpoint_b.switch.id] = link.endpoint_b.switch
515 1
                links.add(link)
516 1
        for switch in switches.values():
517 1
            try:
518 1
                self._send_flow_mods(
519
                    switch.id,
520
                    [
521
                        {
522
                            "cookie": cookie,
523
                            "cookie_mask": int(0xffffffffffffffff),
524
                        }
525
                    ],
526
                    "delete",
527
                    force=force,
528
                )
529
            except FlowModException as err:
530
                log.error(
531
                    f"Error removing flows from switch {switch.id} for"
532
                    f"EVC {self}: {err}"
533
                )
534 1
        for link in links:
535 1
            link.make_tag_available(link.get_metadata("s_vlan"))
536 1
            link.remove_metadata("s_vlan")
537 1
            notify_link_available_tags(self._controller, link)
538 1
        self.failover_path = Path([])
539 1
        if sync:
540 1
            self.sync()
541
542 1
    def remove_current_flows(self, current_path=None, force=True):
543
        """Remove all flows from current path."""
544 1
        switches = set()
545
546 1
        switches.add(self.uni_a.interface.switch)
547 1
        switches.add(self.uni_z.interface.switch)
548 1
        if not current_path:
549 1
            current_path = self.current_path
550 1
        for link in current_path:
551 1
            switches.add(link.endpoint_a.switch)
552 1
            switches.add(link.endpoint_b.switch)
553
554 1
        match = {
555
            "cookie": self.get_cookie(),
556
            "cookie_mask": int(0xffffffffffffffff)
557
        }
558
559 1
        for switch in switches:
560 1
            try:
561 1
                self._send_flow_mods(switch.id, [match], 'delete', force=force)
562 1
            except FlowModException as err:
563 1
                log.error(
564
                    f"Error removing flows from switch {switch.id} for"
565
                    f"EVC {self}: {err}"
566
                )
567
568 1
        current_path.make_vlans_available()
569 1
        for link in current_path:
570 1
            notify_link_available_tags(self._controller, link)
571 1
        self.current_path = Path([])
572 1
        self.deactivate()
573 1
        self.sync()
574
575 1
    def remove_path_flows(self, path=None, force=True):
576
        """Remove all flows from path."""
577 1
        if not path:
578 1
            return
579
580 1
        dpid_flows_match = {}
581 1
        for dpid, flows in self._prepare_nni_flows(path).items():
582 1
            dpid_flows_match.setdefault(dpid, [])
583 1
            for flow in flows:
584 1
                dpid_flows_match[dpid].append({
585
                    "cookie": flow["cookie"],
586
                    "match": flow["match"],
587
                    "cookie_mask": int(0xffffffffffffffff)
588
                })
589 1
        for dpid, flows in self._prepare_uni_flows(path, skip_in=True).items():
590 1
            dpid_flows_match.setdefault(dpid, [])
591 1
            for flow in flows:
592 1
                dpid_flows_match[dpid].append({
593
                    "cookie": flow["cookie"],
594
                    "match": flow["match"],
595
                    "cookie_mask": int(0xffffffffffffffff)
596
                })
597
598 1
        for dpid, flows in dpid_flows_match.items():
599 1
            try:
600 1
                self._send_flow_mods(dpid, flows, 'delete', force=force)
601 1
            except FlowModException as err:
602 1
                log.error(
603
                    "Error removing failover flows: "
604
                    f"dpid={dpid} evc={self} error={err}"
605
                )
606
607 1
        path.make_vlans_available()
608 1
        for link in path:
609 1
            notify_link_available_tags(self._controller, link)
610
611 1
    @staticmethod
612 1
    def links_zipped(path=None):
613
        """Return an iterator which yields pairs of links in order."""
614 1
        if not path:
615
            return []
616 1
        return zip(path[:-1], path[1:])
617
618 1
    def should_deploy(self, path=None):
619
        """Verify if the circuit should be deployed."""
620 1
        if not path:
621 1
            log.debug("Path is empty.")
622 1
            return False
623
624 1
        if not self.is_enabled():
625 1
            log.debug(f"{self} is disabled.")
626 1
            return False
627
628 1
        if not self.is_active():
629 1
            log.debug(f"{self} will be deployed.")
630 1
            return True
631
632 1
        return False
633
634 1
    def deploy_to_path(self, path=None):  # pylint: disable=too-many-branches
635
        """Install the flows for this circuit.
636
637
        Procedures to deploy:
638
639
        0. Remove current flows installed
640
        1. Decide if will deploy "path" or discover a new path
641
        2. Choose vlan
642
        3. Install NNI flows
643
        4. Install UNI flows
644
        5. Activate
645
        6. Update current_path
646
        7. Update links caches(primary, current, backup)
647
648
        """
649 1
        self.remove_current_flows()
650 1
        use_path = path
651 1
        if self.should_deploy(use_path):
652 1
            try:
653 1
                use_path.choose_vlans()
654 1
                for link in use_path:
655 1
                    notify_link_available_tags(self._controller, link)
656 1
            except KytosNoTagAvailableError:
657 1
                use_path = None
658
        else:
659 1
            for use_path in self.discover_new_paths():
660 1
                if use_path is None:
661
                    continue
662 1
                try:
663 1
                    use_path.choose_vlans()
664 1
                    for link in use_path:
665 1
                        notify_link_available_tags(self._controller, link)
666 1
                    break
667 1
                except KytosNoTagAvailableError:
668 1
                    pass
669
            else:
670 1
                use_path = None
671
672 1
        try:
673 1
            if use_path:
674 1
                self._install_nni_flows(use_path)
675 1
                self._install_uni_flows(use_path)
676 1
            elif self.uni_a.interface.switch == self.uni_z.interface.switch:
677 1
                use_path = Path()
678 1
                self._install_direct_uni_flows()
679
            else:
680 1
                log.warning(
681
                    f"{self} was not deployed. " "No available path was found."
682
                )
683 1
                return False
684 1
        except FlowModException as err:
685 1
            log.error(
686
                f"Error deploying EVC {self} when calling flow_manager: {err}"
687
            )
688 1
            self.remove_current_flows(use_path)
689 1
            return False
690 1
        self.activate()
691 1
        self.current_path = use_path
692 1
        self.sync()
693 1
        log.info(f"{self} was deployed.")
694 1
        return True
695
696 1
    def setup_failover_path(self):
697
        """Install flows for the failover path of this EVC.
698
699
        Procedures to deploy:
700
701
        0. Remove flows currently installed for failover_path (if any)
702
        1. Discover a disjoint path from current_path
703
        2. Choose vlans
704
        3. Install NNI flows
705
        4. Install UNI egress flows
706
        5. Update failover_path
707
        """
708
        # Intra-switch EVCs have no failover_path
709 1
        if self.uni_a.interface.switch == self.uni_z.interface.switch:
710 1
            return False
711
712
        # For not only setup failover path for totally dynamic EVCs
713 1
        if not self.is_eligible_for_failover_path():
714 1
            return False
715
716 1
        reason = ""
717 1
        self.remove_path_flows(self.failover_path)
718 1
        for use_path in self.get_failover_path_candidates():
719 1
            if not use_path:
720 1
                continue
721 1
            try:
722 1
                use_path.choose_vlans()
723 1
                for link in use_path:
724 1
                    notify_link_available_tags(self._controller, link)
725 1
                break
726 1
            except KytosNoTagAvailableError:
727 1
                pass
728
        else:
729 1
            use_path = Path([])
730 1
            reason = "No available path was found"
731
732 1
        try:
733 1
            if use_path:
734 1
                self._install_nni_flows(use_path)
735 1
                self._install_uni_flows(use_path, skip_in=True)
736 1
        except FlowModException as err:
737 1
            reason = "Error deploying failover path"
738 1
            log.error(
739
                f"{reason} for {self}. FlowManager error: {err}"
740
            )
741 1
            self.remove_path_flows(use_path)
742 1
            use_path = Path([])
743
744 1
        self.failover_path = use_path
745 1
        self.sync()
746
747 1
        if not use_path:
748 1
            log.warning(
749
                f"Failover path for {self} was not deployed: {reason}"
750
            )
751 1
            return False
752 1
        log.info(f"Failover path for {self} was deployed.")
753 1
        return True
754
755 1
    def get_failover_flows(self):
756
        """Return the flows needed to make the failover path active, i.e. the
757
        flows for ingress forwarding.
758
759
        Return:
760
            dict: A dict of flows indexed by the switch_id will be returned, or
761
                an empty dict if no failover_path is available.
762
        """
763 1
        if not self.failover_path:
764 1
            return {}
765 1
        return self._prepare_uni_flows(self.failover_path, skip_out=True)
766
767 1
    def _prepare_direct_uni_flows(self):
768
        """Prepare flows connecting two UNIs for intra-switch EVC."""
769 1
        vlan_a = self.uni_a.user_tag.value if self.uni_a.user_tag else None
770 1
        vlan_z = self.uni_z.user_tag.value if self.uni_z.user_tag else None
771
772 1
        is_EVPL = (vlan_a is not None)
773 1
        flow_mod_az = self._prepare_flow_mod(
774
            self.uni_a.interface, self.uni_z.interface,
775
            self.queue_id, is_EVPL
776
        )
777 1
        is_EVPL = (vlan_z is not None)
778 1
        flow_mod_za = self._prepare_flow_mod(
779
            self.uni_z.interface, self.uni_a.interface,
780
            self.queue_id, is_EVPL
781
        )
782
783 1
        if vlan_a and vlan_z:
784 1
            flow_mod_az["match"]["dl_vlan"] = vlan_a
785 1
            flow_mod_za["match"]["dl_vlan"] = vlan_z
786 1
            flow_mod_az["actions"].insert(
787
                0, {"action_type": "set_vlan", "vlan_id": vlan_z}
788
            )
789 1
            flow_mod_za["actions"].insert(
790
                0, {"action_type": "set_vlan", "vlan_id": vlan_a}
791
            )
792 1
        elif vlan_a:
793 1
            flow_mod_az["match"]["dl_vlan"] = vlan_a
794 1
            flow_mod_az["actions"].insert(0, {"action_type": "pop_vlan"})
795 1
            flow_mod_za["actions"].insert(
796
                0, {"action_type": "set_vlan", "vlan_id": vlan_a}
797
            )
798 1
        elif vlan_z:
799 1
            flow_mod_za["match"]["dl_vlan"] = vlan_z
800 1
            flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"})
801 1
            flow_mod_az["actions"].insert(
802
                0, {"action_type": "set_vlan", "vlan_id": vlan_z}
803
            )
804 1
        return (
805
            self.uni_a.interface.switch.id, [flow_mod_az, flow_mod_za]
806
        )
807
808 1
    def _install_direct_uni_flows(self):
809
        """Install flows connecting two UNIs.
810
811
        This case happens when the circuit is between UNIs in the
812
        same switch.
813
        """
814 1
        (dpid, flows) = self._prepare_direct_uni_flows()
815 1
        self._send_flow_mods(dpid, flows)
816
817 1
    def _prepare_nni_flows(self, path=None):
818
        """Prepare NNI flows."""
819 1
        nni_flows = OrderedDict()
820 1
        for incoming, outcoming in self.links_zipped(path):
821 1
            in_vlan = incoming.get_metadata("s_vlan").value
822 1
            out_vlan = outcoming.get_metadata("s_vlan").value
823
824 1
            flows = []
825
            # Flow for one direction
826 1
            flows.append(
827
                self._prepare_nni_flow(
828
                    incoming.endpoint_b,
829
                    outcoming.endpoint_a,
830
                    in_vlan,
831
                    out_vlan,
832
                    queue_id=self.queue_id,
833
                )
834
            )
835
836
            # Flow for the other direction
837 1
            flows.append(
838
                self._prepare_nni_flow(
839
                    outcoming.endpoint_a,
840
                    incoming.endpoint_b,
841
                    out_vlan,
842
                    in_vlan,
843
                    queue_id=self.queue_id,
844
                )
845
            )
846 1
            nni_flows[incoming.endpoint_b.switch.id] = flows
847 1
        return nni_flows
848
849 1
    def _install_nni_flows(self, path=None):
850
        """Install NNI flows."""
851 1
        for dpid, flows in self._prepare_nni_flows(path).items():
852 1
            self._send_flow_mods(dpid, flows)
853
854 1
    def _prepare_uni_flows(self, path=None, skip_in=False, skip_out=False):
855
        """Prepare flows to install UNIs."""
856 1
        uni_flows = {}
857 1
        if not path:
858 1
            log.info("install uni flows without path.")
859 1
            return uni_flows
860
861
        # Determine VLANs
862 1
        in_vlan_a = self.uni_a.user_tag.value if self.uni_a.user_tag else None
863 1
        out_vlan_a = path[0].get_metadata("s_vlan").value
864
865 1
        in_vlan_z = self.uni_z.user_tag.value if self.uni_z.user_tag else None
866 1
        out_vlan_z = path[-1].get_metadata("s_vlan").value
867
868
        # Flows for the first UNI
869 1
        flows_a = []
870
871
        # Flow for one direction, pushing the service tag
872 1
        if not skip_in:
873 1
            push_flow = self._prepare_push_flow(
874
                self.uni_a.interface,
875
                path[0].endpoint_a,
876
                in_vlan_a,
877
                out_vlan_a,
878
                in_vlan_z,
879
                queue_id=self.queue_id,
880
            )
881 1
            flows_a.append(push_flow)
882
883
        # Flow for the other direction, popping the service tag
884 1
        if not skip_out:
885 1
            pop_flow = self._prepare_pop_flow(
886
                path[0].endpoint_a,
887
                self.uni_a.interface,
888
                out_vlan_a,
889
                queue_id=self.queue_id,
890
            )
891 1
            flows_a.append(pop_flow)
892
893 1
        uni_flows[self.uni_a.interface.switch.id] = flows_a
894
895
        # Flows for the second UNI
896 1
        flows_z = []
897
898
        # Flow for one direction, pushing the service tag
899 1
        if not skip_in:
900 1
            push_flow = self._prepare_push_flow(
901
                self.uni_z.interface,
902
                path[-1].endpoint_b,
903
                in_vlan_z,
904
                out_vlan_z,
905
                in_vlan_a,
906
                queue_id=self.queue_id,
907
            )
908 1
            flows_z.append(push_flow)
909
910
        # Flow for the other direction, popping the service tag
911 1
        if not skip_out:
912 1
            pop_flow = self._prepare_pop_flow(
913
                path[-1].endpoint_b,
914
                self.uni_z.interface,
915
                out_vlan_z,
916
                queue_id=self.queue_id,
917
            )
918 1
            flows_z.append(pop_flow)
919
920 1
        uni_flows[self.uni_z.interface.switch.id] = flows_z
921
922 1
        return uni_flows
923
924 1
    def _install_uni_flows(self, path=None, skip_in=False, skip_out=False):
925
        """Install UNI flows."""
926 1
        uni_flows = self._prepare_uni_flows(path, skip_in, skip_out)
927
928 1
        for (dpid, flows) in uni_flows.items():
929 1
            self._send_flow_mods(dpid, flows)
930
931 1
    @staticmethod
932 1
    def _send_flow_mods(dpid, flow_mods, command='flows', force=False):
933
        """Send a flow_mod list to a specific switch.
934
935
        Args:
936
            dpid(str): The target of flows (i.e. Switch.id).
937
            flow_mods(dict): Python dictionary with flow_mods.
938
            command(str): By default is 'flows'. To remove a flow is 'remove'.
939
            force(bool): True to send via consistency check in case of errors
940
941
        """
942
943 1
        endpoint = f"{settings.MANAGER_URL}/{command}/{dpid}"
944
945 1
        data = {"flows": flow_mods, "force": force}
946 1
        response = requests.post(endpoint, json=data)
947 1
        if response.status_code >= 400:
948 1
            raise FlowModException(str(response.text))
949
950 1
    def get_cookie(self):
951
        """Return the cookie integer from evc id."""
952 1
        return int(self.id, 16) + (settings.COOKIE_PREFIX << 56)
953
954 1
    @staticmethod
955 1
    def get_id_from_cookie(cookie):
956
        """Return the evc id given a cookie value."""
957 1
        evc_id = cookie - (settings.COOKIE_PREFIX << 56)
958 1
        return f"{evc_id:x}".zfill(14)
959
960 1
    def _prepare_flow_mod(self, in_interface, out_interface,
961
                          queue_id=None, is_EVPL=True):
962
        """Prepare a common flow mod."""
963 1
        default_actions = [
964
            {"action_type": "output", "port": out_interface.port_number}
965
        ]
966 1
        if queue_id is not None:
967
            default_actions.append(
968
                {"action_type": "set_queue", "queue_id": queue_id}
969
            )
970
971 1
        flow_mod = {
972
            "match": {"in_port": in_interface.port_number},
973
            "cookie": self.get_cookie(),
974
            "actions": default_actions,
975
        }
976 1
        if self.sb_priority:
977
            flow_mod["priority"] = self.sb_priority
978
        else:
979 1
            if is_EVPL:
980 1
                flow_mod["priority"] = settings.EVPL_SB_PRIORITY
981
            else:
982 1
                flow_mod["priority"] = settings.EPL_SB_PRIORITY
983 1
        return flow_mod
984
985 1
    def _prepare_nni_flow(self, *args, queue_id=None):
986
        """Create NNI flows."""
987 1
        in_interface, out_interface, in_vlan, out_vlan = args
988 1
        flow_mod = self._prepare_flow_mod(
989
            in_interface, out_interface, queue_id
990
        )
991 1
        flow_mod["match"]["dl_vlan"] = in_vlan
992
993 1
        new_action = {"action_type": "set_vlan", "vlan_id": out_vlan}
994 1
        flow_mod["actions"].insert(0, new_action)
995
996 1
        return flow_mod
997
998
    # pylint: disable=too-many-arguments
999 1
    def _prepare_push_flow(self, *args, queue_id=None):
1000
        """Prepare push flow.
1001
1002
        Arguments:
1003
            in_interface(str): Interface input.
1004
            out_interface(str): Interface output.
1005
            in_vlan(str): Vlan input.
1006
            out_vlan(str): Vlan output.
1007
            new_c_vlan(str): New client vlan.
1008
1009
        Return:
1010
            dict: An python dictionary representing a FlowMod
1011
1012
        """
1013
        # assign all arguments
1014 1
        in_interface, out_interface, in_vlan, out_vlan, new_c_vlan = args
1015 1
        is_EVPL = (in_vlan is not None)
1016 1
        flow_mod = self._prepare_flow_mod(
1017
            in_interface, out_interface, queue_id, is_EVPL
1018
        )
1019
1020
        # the service tag must be always pushed
1021 1
        new_action = {"action_type": "set_vlan", "vlan_id": out_vlan}
1022 1
        flow_mod["actions"].insert(0, new_action)
1023
1024 1
        new_action = {"action_type": "push_vlan", "tag_type": "s"}
1025 1
        flow_mod["actions"].insert(0, new_action)
1026
1027 1
        if in_vlan:
1028
            # if in_vlan is set, it must be included in the match
1029 1
            flow_mod["match"]["dl_vlan"] = in_vlan
1030 1
        if new_c_vlan:
1031
            # new_in_vlan is set, so an action to set it is necessary
1032 1
            new_action = {"action_type": "set_vlan", "vlan_id": new_c_vlan}
1033 1
            flow_mod["actions"].insert(0, new_action)
1034 1
            if not in_vlan:
1035
                # new_in_vlan is set, but in_vlan is not, so there was no
1036
                # vlan set; then it is set now
1037 1
                new_action = {"action_type": "push_vlan", "tag_type": "c"}
1038 1
                flow_mod["actions"].insert(0, new_action)
1039 1
        elif in_vlan:
1040
            # in_vlan is set, but new_in_vlan is not, so the existing vlan
1041
            # must be removed
1042 1
            new_action = {"action_type": "pop_vlan"}
1043 1
            flow_mod["actions"].insert(0, new_action)
1044 1
        return flow_mod
1045
1046 1
    def _prepare_pop_flow(
1047
        self, in_interface, out_interface, out_vlan, queue_id=None
1048
    ):
1049
        # pylint: disable=too-many-arguments
1050
        """Prepare pop flow."""
1051 1
        flow_mod = self._prepare_flow_mod(
1052
            in_interface, out_interface, queue_id
1053
        )
1054 1
        flow_mod["match"]["dl_vlan"] = out_vlan
1055 1
        new_action = {"action_type": "pop_vlan"}
1056 1
        flow_mod["actions"].insert(0, new_action)
1057 1
        return flow_mod
1058
1059 1 View Code Duplication
    @staticmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1060 1
    def run_sdntrace(uni):
1061
        """Run SDN trace on control plane starting from EVC UNIs."""
1062 1
        endpoint = f"{settings.SDN_TRACE_CP_URL}/trace"
1063 1
        data_uni = {
1064
            "trace": {
1065
                "switch": {
1066
                    "dpid": uni.interface.switch.dpid,
1067
                    "in_port": uni.interface.port_number,
1068
                }
1069
            }
1070
        }
1071 1
        if uni.user_tag:
1072 1
            data_uni["trace"]["eth"] = {
1073
                "dl_type": 0x8100,
1074
                "dl_vlan": uni.user_tag.value,
1075
            }
1076 1
        response = requests.put(endpoint, json=data_uni)
1077 1
        if response.status_code >= 400:
1078 1
            log.error(f"Failed to run sdntrace-cp: {response.text}")
1079 1
            return []
1080 1
        return response.json().get('result', [])
1081
1082 1 View Code Duplication
    @staticmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1083 1
    def run_bulk_sdntraces(uni_list):
1084
        """Run SDN traces on control plane starting from EVC UNIs."""
1085 1
        endpoint = f"{settings.SDN_TRACE_CP_URL}/traces"
1086 1
        data = []
1087 1
        for uni in uni_list:
1088 1
            data_uni = {
1089
                "trace": {
1090
                            "switch": {
1091
                                "dpid": uni.interface.switch.dpid,
1092
                                "in_port": uni.interface.port_number,
1093
                            }
1094
                        }
1095
                }
1096 1
            if uni.user_tag:
1097 1
                data_uni["trace"]["eth"] = {
1098
                                            "dl_type": 0x8100,
1099
                                            "dl_vlan": uni.user_tag.value,
1100
                                            }
1101 1
            data.append(data_uni)
1102 1
        response = requests.put(endpoint, json=data, timeout=30)
1103 1
        if response.status_code >= 400:
1104 1
            log.error(f"Failed to run sdntrace-cp: {response.text}")
1105 1
            return []
1106 1
        return response.json()
1107
1108 1
    @staticmethod
1109
    # pylint: disable=too-many-locals
1110 1
    def check_list_traces(list_circuits):
1111
        """Check if current_path is deployed comparing with SDN traces."""
1112 1
        uni_list = []
1113 1
        circuit_data = {}
1114 1
        for circuit_id in list_circuits:
1115 1
            circuit = list_circuits[circuit_id]
1116 1
            uni_list.append(circuit.uni_a)
1117 1
            uni_list.append(circuit.uni_z)
1118 1
            dpid_a = circuit.uni_a.interface.switch.dpid
1119 1
            port_a = circuit.uni_a.interface.port_number
1120 1
            interface_a = str(dpid_a) + ':' + str(port_a)
1121 1
            circuit_data[interface_a] = {
1122
                                            'circuit_id': circuit.id,
1123
                                            'trace_name': 'trace_a'
1124
                                        }
1125 1
            dpid_z = circuit.uni_z.interface.switch.dpid
1126 1
            port_z = circuit.uni_z.interface.port_number
1127 1
            interface_z = str(dpid_z) + ':' + str(port_z)
1128 1
            circuit_data[interface_z] = {
1129
                                            'circuit_id': circuit.id,
1130
                                            'trace_name': 'trace_z'
1131
                                        }
1132 1
        if len(uni_list) == 0:
1133
            return {}
1134 1
        traces = EVCDeploy.run_bulk_sdntraces(uni_list)
1135 1
        del uni_list
1136 1
        circuit_by_traces = {}
1137 1
        circuits_checked = {}
1138
1139 1
        for trace_switch in traces:
1140 1
            for trace in traces[trace_switch]:
1141 1
                id_trace = str(trace[0]['dpid']) + ':' + str(trace[0]['port'])
1142 1
                circuit_from_data = circuit_data.get(id_trace)
1143 1
                if circuit_from_data is None:
1144
                    continue
1145 1
                circuit_id = circuit_from_data['circuit_id']
1146 1
                trace_name = circuit_from_data['trace_name']
1147 1
                if circuit_id not in circuit_by_traces:
1148 1
                    circuit_by_traces[circuit_id] = {}
1149 1
                circuit_by_traces[circuit_id][trace_name] = trace
1150 1
                if 'trace_a' in circuit_by_traces[circuit_id] \
1151
                        and 'trace_z' in circuit_by_traces[circuit_id]:
1152 1
                    circuits_checked[circuit.id] = True
0 ignored issues
show
introduced by
The variable circuit does not seem to be defined in case the for loop on line 1114 is not entered. Are you sure this can never be the case?
Loading history...
1153 1
                    trace_a = circuit_by_traces[circuit_id]['trace_a']
1154 1
                    trace_z = circuit_by_traces[circuit_id]['trace_z']
1155 1
                    if len(trace_a) != len(circuit.current_path) + 1:
1156 1
                        log.warning(f"Invalid trace from uni_a: {trace_a}")
1157 1
                        circuits_checked[circuit.id] = False
1158 1
                    if len(trace_z) != len(circuit.current_path) + 1:
1159
                        log.warning(f"Invalid trace from uni_z: {trace_z}")
1160
                        circuits_checked[circuit.id] = False
1161 1
                    for link, trace1, trace2 in zip(circuit.current_path,
1162
                                                    trace_a[1:],
1163
                                                    trace_z[:0:-1]):
1164 1
                        if compare_endpoint_trace(
1165
                                                    link.endpoint_a,
1166
                                                    glom(
1167
                                                            link.metadata,
1168
                                                            's_vlan.value'),
1169
                                                    trace2
1170
                                                ) is False:
1171
                            log.warning(f"Invalid trace from uni_a: {trace_a}")
1172
                            circuits_checked[circuit.id] = False
1173 1
                        if compare_endpoint_trace(
1174
                                                    link.endpoint_b,
1175
                                                    glom(
1176
                                                            link.metadata,
1177
                                                            's_vlan.value'),
1178
                                                    trace1
1179
                                                ) is False:
1180 1
                            log.warning(f"Invalid trace from uni_z: {trace_z}")
1181 1
                            circuits_checked[circuit.id] = False
1182 1
        return circuits_checked
1183
1184
1185 1
class LinkProtection(EVCDeploy):
1186
    """Class to handle link protection."""
1187
1188 1
    def is_affected_by_link(self, link=None):
1189
        """Verify if the current path is affected by link down event."""
1190
        return self.current_path.is_affected_by_link(link)
1191
1192 1
    def is_using_primary_path(self):
1193
        """Verify if the current deployed path is self.primary_path."""
1194 1
        return self.current_path == self.primary_path
1195
1196 1
    def is_using_backup_path(self):
1197
        """Verify if the current deployed path is self.backup_path."""
1198 1
        return self.current_path == self.backup_path
1199
1200 1
    def is_using_dynamic_path(self):
1201
        """Verify if the current deployed path is dynamic."""
1202 1
        if (
1203
            self.current_path
1204
            and not self.is_using_primary_path()
1205
            and not self.is_using_backup_path()
1206
            and self.current_path.status is EntityStatus.UP
1207
        ):
1208
            return True
1209 1
        return False
1210
1211 1
    def deploy_to(self, path_name=None, path=None):
1212
        """Create a deploy to path."""
1213 1
        if self.current_path == path:
1214 1
            log.debug(f"{path_name} is equal to current_path.")
1215 1
            return True
1216
1217 1
        if path.status is EntityStatus.UP:
1218 1
            return self.deploy_to_path(path)
1219
1220 1
        return False
1221
1222 1
    def handle_link_up(self, link):
1223
        """Handle circuit when link down.
1224
1225
        Args:
1226
            link(Link): Link affected by link.down event.
1227
1228
        """
1229 1
        if self.is_using_primary_path():
1230 1
            return True
1231
1232 1
        success = False
1233 1
        if self.primary_path.is_affected_by_link(link):
1234 1
            success = self.deploy_to_primary_path()
1235
1236 1
        if success:
1237 1
            return True
1238
1239
        # We tried to deploy(primary_path) without success.
1240
        # And in this case is up by some how. Nothing to do.
1241 1
        if self.is_using_backup_path() or self.is_using_dynamic_path():
1242 1
            return True
1243
1244
        # In this case, probably the circuit is not being used and
1245
        # we can move to backup
1246 1
        if self.backup_path.is_affected_by_link(link):
1247 1
            success = self.deploy_to_backup_path()
1248
1249
        # In this case, the circuit is not being used and we should
1250
        # try a dynamic path
1251 1
        if not success and self.dynamic_backup_path:
1252 1
            success = self.deploy_to_path()
1253
1254 1
        if success:
1255 1
            emit_event(self._controller, "redeployed_link_up", evc_id=self.id)
1256 1
            return True
1257
1258 1
        return True
1259
1260 1
    def handle_link_down(self):
1261
        """Handle circuit when link down.
1262
1263
        Returns:
1264
            bool: True if the re-deploy was successly otherwise False.
1265
1266
        """
1267 1
        success = False
1268 1
        if self.is_using_primary_path():
1269 1
            success = self.deploy_to_backup_path()
1270 1
        elif self.is_using_backup_path():
1271 1
            success = self.deploy_to_primary_path()
1272
1273 1
        if not success and self.dynamic_backup_path:
1274 1
            success = self.deploy_to_path()
1275
1276 1
        if success:
1277 1
            log.debug(f"{self} deployed after link down.")
1278
        else:
1279 1
            self.deactivate()
1280 1
            self.current_path = Path([])
1281 1
            self.sync()
1282 1
            log.debug(f"Failed to re-deploy {self} after link down.")
1283
1284 1
        return success
1285
1286
1287 1
class EVC(LinkProtection):
1288
    """Class that represents a E-Line Virtual Connection."""
1289