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