Passed
Push — master ( e1b2c4...fc5519 )
by Vinicius
04:01 queued 10s
created

build.models.evc.EVCDeploy.check_trace()   D

Complexity

Conditions 12

Size

Total Lines 72
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 12.3775

Importance

Changes 0
Metric Value
cc 12
eloc 55
nop 9
dl 0
loc 72
ccs 25
cts 29
cp 0.8621
crap 12.3775
rs 4.8
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

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

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

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
"""Classes used in the main application."""  # pylint: disable=too-many-lines
2 1
import traceback
3 1
from collections import OrderedDict, defaultdict
4 1
from copy import deepcopy
5 1
from datetime import datetime
6 1
from operator import eq, ne
7 1
from threading import Lock
8 1
from typing import Union
9 1
from uuid import uuid4
10
11 1
import httpx
12 1
from glom import glom
13 1
from tenacity import (retry, retry_if_exception_type, stop_after_attempt,
14
                      wait_combine, wait_fixed, wait_random)
15
16 1
from kytos.core import log
17 1
from kytos.core.common import EntityStatus, GenericEntity
18 1
from kytos.core.exceptions import KytosNoTagAvailableError, KytosTagError
19 1
from kytos.core.helpers import get_time, now
20 1
from kytos.core.interface import UNI, Interface, TAGRange
21 1
from kytos.core.link import Link
22 1
from kytos.core.retry import before_sleep
23 1
from kytos.core.tag_ranges import range_difference
24 1
from napps.kytos.mef_eline import controllers, settings
25 1
from napps.kytos.mef_eline.exceptions import (ActivationError,
26
                                              DuplicatedNoTagUNI,
27
                                              EVCPathNotInstalled,
28
                                              FlowModException, InvalidPath)
29 1
from napps.kytos.mef_eline.utils import (_does_uni_affect_evc,
30
                                         compare_endpoint_trace,
31
                                         compare_uni_out_trace, emit_event,
32
                                         make_uni_list, map_dl_vlan,
33
                                         map_evc_event_content,
34
                                         merge_flow_dicts)
35
36 1
from .path import DynamicPathManager, Path
37
38
39 1
class EVCBase(GenericEntity):
40
    """Class to represent a circuit."""
41
42 1
    attributes_requiring_redeploy = [
43
        "primary_path",
44
        "backup_path",
45
        "dynamic_backup_path",
46
        "queue_id",
47
        "sb_priority",
48
        "primary_constraints",
49
        "secondary_constraints",
50
        "uni_a",
51
        "uni_z",
52
    ]
53 1
    required_attributes = ["name", "uni_a", "uni_z"]
54
55 1
    updatable_attributes = {
56
        "uni_a",
57
        "uni_z",
58
        "name",
59
        "start_date",
60
        "end_date",
61
        "queue_id",
62
        "bandwidth",
63
        "primary_path",
64
        "backup_path",
65
        "dynamic_backup_path",
66
        "primary_constraints",
67
        "secondary_constraints",
68
        "owner",
69
        "sb_priority",
70
        "service_level",
71
        "circuit_scheduler",
72
        "metadata",
73
        "enabled",
74
        "max_paths",
75
    }
76
77
    # pylint: disable=too-many-statements
78 1
    def __init__(self, controller, **kwargs):
79
        """Create an EVC instance with the provided parameters.
80
81
        Args:
82
            id(str): EVC identifier. Whether it's None an ID will be genereted.
83
                     Only the first 14 bytes passed will be used.
84
            name: represents an EVC name.(Required)
85
            uni_a (UNI): Endpoint A for User Network Interface.(Required)
86
            uni_z (UNI): Endpoint Z for User Network Interface.(Required)
87
            start_date(datetime|str): Date when the EVC was registred.
88
                                      Default is now().
89
            end_date(datetime|str): Final date that the EVC will be fineshed.
90
                                    Default is None.
91
            bandwidth(int): Bandwidth used by EVC instance. Default is 0.
92
            primary_links(list): Primary links used by evc. Default is []
93
            backup_links(list): Backups links used by evc. Default is []
94
            current_path(list): Circuit being used at the moment if this is an
95
                                active circuit. Default is [].
96
            failover_path(list): Path being used to provide EVC protection via
97
                                failover during link failures. Default is [].
98
            primary_path(list): primary circuit offered to user IF one or more
99
                                links were provided. Default is [].
100
            backup_path(list): backup circuit offered to the user IF one or
101
                               more links were provided. Default is [].
102
            dynamic_backup_path(bool): Enable computer backup path dynamically.
103
                                       Dafault is False.
104
            creation_time(datetime|str): datetime when the circuit should be
105
                                         activated. default is now().
106
            enabled(Boolean): attribute to indicate the administrative state;
107
                              default is False.
108
            active(Boolean): attribute to indicate the operational state;
109
                             default is False.
110
            archived(Boolean): indicate the EVC has been deleted and is
111
                               archived; default is False.
112
            owner(str): The EVC owner. Default is None.
113
            sb_priority(int): Service level provided in the request.
114
                              Default is None.
115
            service_level(int): Service level provided. The higher the better.
116
                                Default is 0.
117
118
        Raises:
119
            ValueError: raised when object attributes are invalid.
120
121
        """
122 1
        self._controller = controller
123 1
        self._validate(**kwargs)
124 1
        super().__init__()
125
126
        # required attributes
127 1
        self._id = kwargs.get("id", uuid4().hex)[:14]
128 1
        self.uni_a: UNI = kwargs.get("uni_a")
129 1
        self.uni_z: UNI = kwargs.get("uni_z")
130 1
        self.name = kwargs.get("name")
131
132
        # optional attributes
133 1
        self.start_date = get_time(kwargs.get("start_date")) or now()
134 1
        self.end_date = get_time(kwargs.get("end_date")) or None
135 1
        self.queue_id = kwargs.get("queue_id", -1)
136
137 1
        self.bandwidth = kwargs.get("bandwidth", 0)
138 1
        self.primary_links = Path(kwargs.get("primary_links", []))
139 1
        self.backup_links = Path(kwargs.get("backup_links", []))
140 1
        self.current_path = Path(kwargs.get("current_path", []))
141 1
        self.failover_path = Path(kwargs.get("failover_path", []))
142 1
        self.primary_path = Path(kwargs.get("primary_path", []))
143 1
        self.backup_path = Path(kwargs.get("backup_path", []))
144 1
        self.dynamic_backup_path = kwargs.get("dynamic_backup_path", False)
145 1
        self.primary_constraints = kwargs.get("primary_constraints", {})
146 1
        self.secondary_constraints = kwargs.get("secondary_constraints", {})
147 1
        self.creation_time = get_time(kwargs.get("creation_time")) or now()
148 1
        self.owner = kwargs.get("owner", None)
149 1
        self.sb_priority = kwargs.get("sb_priority", None) or kwargs.get(
150
            "priority", None
151
        )
152 1
        self.service_level = kwargs.get("service_level", 0)
153 1
        self.circuit_scheduler = kwargs.get("circuit_scheduler", [])
154 1
        self.flow_removed_at = get_time(kwargs.get("flow_removed_at")) or None
155 1
        self.updated_at = get_time(kwargs.get("updated_at")) or now()
156 1
        self.execution_rounds = kwargs.get("execution_rounds", 0)
157 1
        self.current_links_cache = set()
158 1
        self.primary_links_cache = set()
159 1
        self.backup_links_cache = set()
160 1
        self.old_path = Path([])
161 1
        self.max_paths = kwargs.get("max_paths", 2)
162
163 1
        self.lock = Lock()
164
165 1
        self.archived = kwargs.get("archived", False)
166
167 1
        self.metadata = kwargs.get("metadata", {})
168
169 1
        self._mongo_controller = controllers.ELineController()
170
171 1
        if kwargs.get("active", False):
172 1
            self.activate()
173
        else:
174 1
            self.deactivate()
175
176 1
        if kwargs.get("enabled", False):
177 1
            self.enable()
178
        else:
179 1
            self.disable()
180
181
        # datetime of user request for a EVC (or datetime when object was
182
        # created)
183 1
        self.request_time = kwargs.get("request_time", now())
184
        # dict with the user original request (input)
185 1
        self._requested = kwargs
186
187
        # Special cases: No tag, any, untagged
188 1
        self.special_cases = {None, "4096/4096", 0}
189 1
        self.table_group = kwargs.get("table_group")
190
191 1
    def sync(self, keys: set = None):
192
        """Sync this EVC in the MongoDB."""
193 1
        self.updated_at = now()
194 1
        if keys:
195 1
            self._mongo_controller.update_evc(self.as_dict(keys))
196 1
            return
197 1
        self._mongo_controller.upsert_evc(self.as_dict())
198
199 1
    def _get_unis_use_tags(self, **kwargs) -> tuple[UNI, UNI]:
200
        """Obtain both UNIs (uni_a, uni_z).
201
        If a UNI is changing, verify tags"""
202 1
        uni_a = kwargs.get("uni_a", None)
203 1
        uni_a_flag = False
204 1
        if uni_a and uni_a != self.uni_a:
205 1
            uni_a_flag = True
206 1
            self._use_uni_vlan(uni_a, uni_dif=self.uni_a)
207
208 1
        uni_z = kwargs.get("uni_z", None)
209 1
        if uni_z and uni_z != self.uni_z:
210 1
            try:
211 1
                self._use_uni_vlan(uni_z, uni_dif=self.uni_z)
212 1
                self.make_uni_vlan_available(self.uni_z, uni_dif=uni_z)
213 1
            except KytosTagError as err:
214 1
                if uni_a_flag:
215 1
                    self.make_uni_vlan_available(uni_a, uni_dif=self.uni_a)
216 1
                raise err
217
        else:
218 1
            uni_z = self.uni_z
219
220 1
        if uni_a_flag:
221 1
            self.make_uni_vlan_available(self.uni_a, uni_dif=uni_a)
222
        else:
223 1
            uni_a = self.uni_a
224 1
        return uni_a, uni_z
225
226 1
    def update(self, **kwargs):
227
        """Update evc attributes.
228
229
        This method will raises an error trying to change the following
230
        attributes: [creation_time, active, current_path, failover_path,
231
        _id, archived]
232
        [name, uni_a and uni_z]
233
234
        Returns:
235
            the values for enable and a redeploy attribute, if exists and None
236
            otherwise
237
        Raises:
238
            ValueError: message with error detail.
239
240
        """
241 1
        enable, redeploy = (None, None)
242 1
        if not self._tag_lists_equal(**kwargs):
243 1
            raise ValueError(
244
                "UNI_A and UNI_Z tag lists should be the same."
245
            )
246 1
        uni_a, uni_z = self._get_unis_use_tags(**kwargs)
247 1
        self._validate_has_primary_or_dynamic(
248
            primary_path=kwargs.get("primary_path"),
249
            dynamic_backup_path=kwargs.get("dynamic_backup_path"),
250
            uni_a=uni_a,
251
            uni_z=uni_z,
252
        )
253 1
        for attribute, value in kwargs.items():
254 1
            if attribute not in self.updatable_attributes:
255 1
                raise ValueError(f"{attribute} can't be updated.")
256 1
            if attribute in ("primary_path", "backup_path"):
257 1
                try:
258 1
                    value.is_valid(
259
                        uni_a.interface.switch, uni_z.interface.switch
260
                    )
261 1
                except InvalidPath as exception:
262 1
                    raise ValueError(  # pylint: disable=raise-missing-from
263
                        f"{attribute} is not a " f"valid path: {exception}"
264
                    )
265 1
        for attribute, value in kwargs.items():
266 1
            if attribute == "enabled":
267 1
                if value:
268 1
                    self.enable()
269
                else:
270 1
                    self.disable()
271 1
                enable = value
272
            else:
273 1
                setattr(self, attribute, value)
274 1
                if attribute in self.attributes_requiring_redeploy:
275 1
                    redeploy = True
276 1
        self.sync(set(kwargs.keys()))
277 1
        return enable, redeploy
278
279 1
    def set_flow_removed_at(self):
280
        """Update flow_removed_at attribute."""
281
        self.flow_removed_at = now()
282
283 1
    def has_recent_removed_flow(self, setting=settings):
284
        """Check if any flow has been removed from the evc"""
285
        if self.flow_removed_at is None:
286
            return False
287
        res_seconds = (now() - self.flow_removed_at).seconds
288
        return res_seconds < setting.TIME_RECENT_DELETED_FLOWS
289
290 1
    def is_recent_updated(self, setting=settings):
291
        """Check if the evc has been updated recently"""
292
        res_seconds = (now() - self.updated_at).seconds
293
        return res_seconds < setting.TIME_RECENT_UPDATED
294
295 1
    def __repr__(self):
296
        """Repr method."""
297 1
        return f"EVC({self._id}, {self.name})"
298
299 1
    def _validate(self, **kwargs):
300
        """Do Basic validations.
301
302
        Verify required attributes: name, uni_a, uni_z
303
304
        Raises:
305
            ValueError: message with error detail.
306
307
        """
308 1
        for attribute in self.required_attributes:
309
310 1
            if attribute not in kwargs:
311 1
                raise ValueError(f"{attribute} is required.")
312
313 1
            if "uni" in attribute:
314 1
                uni = kwargs.get(attribute)
315 1
                if not isinstance(uni, UNI):
316
                    raise ValueError(f"{attribute} is an invalid UNI.")
317
318 1
    def _tag_lists_equal(self, **kwargs):
319
        """Verify that tag lists are the same."""
320 1
        uni_a = kwargs.get("uni_a") or self.uni_a
321 1
        uni_z = kwargs.get("uni_z") or self.uni_z
322 1
        uni_a_list = uni_z_list = False
323 1
        if (uni_a.user_tag and isinstance(uni_a.user_tag, TAGRange)):
324 1
            uni_a_list = True
325 1
        if (uni_z.user_tag and isinstance(uni_z.user_tag, TAGRange)):
326 1
            uni_z_list = True
327 1
        if uni_a_list and uni_z_list:
328 1
            return uni_a.user_tag.value == uni_z.user_tag.value
329 1
        return uni_a_list == uni_z_list
330
331 1
    def _validate_has_primary_or_dynamic(
332
        self,
333
        primary_path=None,
334
        dynamic_backup_path=None,
335
        uni_a=None,
336
        uni_z=None,
337
    ) -> None:
338
        """Validate that it must have a primary path or allow dynamic paths."""
339 1
        primary_path = (
340
            primary_path
341
            if primary_path is not None
342
            else self.primary_path
343
        )
344 1
        dynamic_backup_path = (
345
            dynamic_backup_path
346
            if dynamic_backup_path is not None
347
            else self.dynamic_backup_path
348
        )
349 1
        uni_a = uni_a if uni_a is not None else self.uni_a
350 1
        uni_z = uni_z if uni_z is not None else self.uni_z
351 1
        if (
352
            not primary_path
353
            and not dynamic_backup_path
354
            and uni_a and uni_z
355
            and uni_a.interface.switch != uni_z.interface.switch
356
        ):
357 1
            msg = "The EVC must have a primary path or allow dynamic paths."
358 1
            raise ValueError(msg)
359
360 1
    def __eq__(self, other):
361
        """Override the default implementation."""
362 1
        if not isinstance(other, EVC):
363
            return False
364
365 1
        attrs_to_compare = ["name", "uni_a", "uni_z", "owner", "bandwidth"]
366 1
        for attribute in attrs_to_compare:
367 1
            if getattr(other, attribute) != getattr(self, attribute):
368 1
                return False
369 1
        return True
370
371 1
    def is_intra_switch(self):
372
        """Check if the UNIs are in the same switch."""
373 1
        return self.uni_a.interface.switch == self.uni_z.interface.switch
374
375 1
    def check_no_tag_duplicate(self, other_uni: UNI):
376
        """Check if a no tag UNI is duplicated."""
377 1
        if other_uni in (self.uni_a, self.uni_z):
378 1
            msg = f"UNI with interface {other_uni.interface.id} is"\
379
                  f" duplicated with {self}."
380 1
            raise DuplicatedNoTagUNI(msg)
381
382 1
    def as_dict(self, keys: set = None):
383
        """Return a dictionary representing an EVC object.
384
            keys: Only fields on this variable will be
385
                  returned in the dictionary"""
386 1
        evc_dict = {
387
            "id": self.id,
388
            "name": self.name,
389
            "uni_a": self.uni_a.as_dict(),
390
            "uni_z": self.uni_z.as_dict(),
391
        }
392
393 1
        time_fmt = "%Y-%m-%dT%H:%M:%S"
394
395 1
        evc_dict["start_date"] = self.start_date
396 1
        if isinstance(self.start_date, datetime):
397 1
            evc_dict["start_date"] = self.start_date.strftime(time_fmt)
398
399 1
        evc_dict["end_date"] = self.end_date
400 1
        if isinstance(self.end_date, datetime):
401 1
            evc_dict["end_date"] = self.end_date.strftime(time_fmt)
402
403 1
        evc_dict["queue_id"] = self.queue_id
404 1
        evc_dict["bandwidth"] = self.bandwidth
405 1
        evc_dict["primary_links"] = self.primary_links.as_dict()
406 1
        evc_dict["backup_links"] = self.backup_links.as_dict()
407 1
        evc_dict["current_path"] = self.current_path.as_dict()
408 1
        evc_dict["failover_path"] = self.failover_path.as_dict()
409 1
        evc_dict["primary_path"] = self.primary_path.as_dict()
410 1
        evc_dict["backup_path"] = self.backup_path.as_dict()
411 1
        evc_dict["dynamic_backup_path"] = self.dynamic_backup_path
412 1
        evc_dict["metadata"] = self.metadata
413
414 1
        evc_dict["request_time"] = self.request_time
415 1
        if isinstance(self.request_time, datetime):
416 1
            evc_dict["request_time"] = self.request_time.strftime(time_fmt)
417
418 1
        time = self.creation_time.strftime(time_fmt)
419 1
        evc_dict["creation_time"] = time
420
421 1
        evc_dict["owner"] = self.owner
422 1
        evc_dict["circuit_scheduler"] = [
423
            sc.as_dict() for sc in self.circuit_scheduler
424
        ]
425
426 1
        evc_dict["active"] = self.is_active()
427 1
        evc_dict["enabled"] = self.is_enabled()
428 1
        evc_dict["archived"] = self.archived
429 1
        evc_dict["sb_priority"] = self.sb_priority
430 1
        evc_dict["service_level"] = self.service_level
431 1
        evc_dict["primary_constraints"] = self.primary_constraints
432 1
        evc_dict["secondary_constraints"] = self.secondary_constraints
433 1
        evc_dict["flow_removed_at"] = self.flow_removed_at
434 1
        evc_dict["updated_at"] = self.updated_at
435 1
        evc_dict["max_paths"] = self.max_paths
436
437 1
        if keys:
438 1
            selected = {}
439 1
            for key in keys:
440 1
                selected[key] = evc_dict[key]
441 1
            selected["id"] = evc_dict["id"]
442 1
            return selected
443 1
        return evc_dict
444
445 1
    @property
446 1
    def id(self):  # pylint: disable=invalid-name
447
        """Return this EVC's ID."""
448 1
        return self._id
449
450 1
    def archive(self):
451
        """Archive this EVC on deletion."""
452 1
        self.archived = True
453
454 1
    def _use_uni_vlan(
455
        self,
456
        uni: UNI,
457
        uni_dif: Union[None, UNI] = None
458
    ):
459
        """Use tags from UNI"""
460 1
        if uni.user_tag is None:
461 1
            return
462 1
        tag = uni.user_tag.value
463 1
        tag_type = uni.user_tag.tag_type
464 1
        if (uni_dif and isinstance(tag, list) and
465
                isinstance(uni_dif.user_tag.value, list)):
466 1
            tag = range_difference(tag, uni_dif.user_tag.value)
467 1
            if not tag:
468 1
                return
469 1
        uni.interface.use_tags(
470
            self._controller, tag, tag_type, use_lock=True, check_order=False
471
        )
472
473 1
    def make_uni_vlan_available(
474
        self,
475
        uni: UNI,
476
        uni_dif: Union[None, UNI] = None,
477
    ):
478
        """Make available tag from UNI"""
479 1
        if uni.user_tag is None:
480 1
            return
481 1
        tag = uni.user_tag.value
482 1
        tag_type = uni.user_tag.tag_type
483 1
        if (uni_dif and isinstance(tag, list) and
484
                isinstance(uni_dif.user_tag.value, list)):
485 1
            tag = range_difference(tag, uni_dif.user_tag.value)
486 1
            if not tag:
487
                return
488 1
        try:
489 1
            conflict = uni.interface.make_tags_available(
490
                self._controller, tag, tag_type, use_lock=True,
491
                check_order=False
492
            )
493 1
        except KytosTagError as err:
494 1
            log.error(f"Error in {self}: {err}")
495 1
            return
496 1
        if conflict:
497 1
            intf = uni.interface.id
498 1
            log.warning(f"Tags {conflict} was already available in {intf}")
499
500 1
    def remove_uni_tags(self):
501
        """Remove both UNI usage of a tag"""
502 1
        self.make_uni_vlan_available(self.uni_a)
503 1
        self.make_uni_vlan_available(self.uni_z)
504
505
506
# pylint: disable=fixme, too-many-public-methods
507 1
class EVCDeploy(EVCBase):
508
    """Class to handle the deploy procedures."""
509
510 1
    def create(self):
511
        """Create a EVC."""
512
513 1
    def discover_new_paths(self):
514
        """Discover new paths to satisfy this circuit and deploy it."""
515
        return DynamicPathManager.get_best_paths(self, self.max_paths,
516
                                                 **self.primary_constraints)
517
518 1
    def get_failover_path_candidates(self):
519
        """Get failover paths to satisfy this EVC."""
520
        # in the future we can return primary/backup paths as well
521
        # we just have to properly handle link_up and failover paths
522
        # if (
523
        #     self.is_using_primary_path() and
524
        #     self.backup_path.status is EntityStatus.UP
525
        # ):
526
        #     yield self.backup_path
527 1
        return DynamicPathManager.get_disjoint_paths(self, self.current_path)
528
529 1
    def change_path(self):
530
        """Change EVC path."""
531
532 1
    def reprovision(self):
533
        """Force the EVC (re-)provisioning."""
534
535 1
    def is_affected_by_link(self, link):
536
        """Return True if this EVC has the given link on its current path."""
537 1
        return link in self.current_path
538
539 1
    def link_affected_by_interface(self, interface):
540
        """Return True if this EVC has the given link on its current path."""
541
        return self.current_path.link_affected_by_interface(interface)
542
543 1
    def is_backup_path_affected_by_link(self, link):
544
        """Return True if the backup path of this EVC uses the given link."""
545 1
        return link in self.backup_path
546
547
    # pylint: disable=invalid-name
548 1
    def is_primary_path_affected_by_link(self, link):
549
        """Return True if the primary path of this EVC uses the given link."""
550 1
        return link in self.primary_path
551
552 1
    def is_failover_path_affected_by_link(self, link):
553
        """Return True if this EVC has the given link on its failover path."""
554 1
        return link in self.failover_path
555
556 1
    def is_eligible_for_failover_path(self):
557
        """Verify if this EVC is eligible for failover path (EP029)"""
558
        # In the future this function can be augmented to consider
559
        # primary/backup, primary/dynamic, and other path combinations
560 1
        return (
561
            self.dynamic_backup_path and
562
            not self.primary_path and not self.backup_path
563
        )
564
565 1
    def is_using_primary_path(self):
566
        """Verify if the current deployed path is self.primary_path."""
567 1
        return self.primary_path and (self.current_path == self.primary_path)
568
569 1
    def is_using_backup_path(self):
570
        """Verify if the current deployed path is self.backup_path."""
571 1
        return self.backup_path and (self.current_path == self.backup_path)
572
573 1
    def is_using_dynamic_path(self):
574
        """Verify if the current deployed path is a dynamic path."""
575 1
        if (
576
            self.current_path
577
            and not self.is_using_primary_path()
578
            and not self.is_using_backup_path()
579
            and self.current_path.status == EntityStatus.UP
580
        ):
581
            return True
582 1
        return False
583
584 1
    def deploy_to_backup_path(self, old_path_dict: dict = None):
585
        """Deploy the backup path into the datapaths of this circuit.
586
587
        If the backup_path attribute is valid and up, this method will try to
588
        deploy this backup_path.
589
590
        If everything fails and dynamic_backup_path is True, then tries to
591
        deploy a dynamic path.
592
        """
593
        # TODO: Remove flows from current (cookies)
594 1
        if self.is_using_backup_path():
595
            # TODO: Log to say that cannot move backup to backup
596
            return True
597
598 1
        success = False
599 1
        if self.backup_path.status is EntityStatus.UP:
600 1
            success = self.deploy_to_path(self.backup_path, old_path_dict)
601
602 1
        if success:
603 1
            return True
604
605 1
        if self.dynamic_backup_path or self.is_intra_switch():
606 1
            return self.deploy_to_path(old_path_dict=old_path_dict)
607
608
        return False
609
610 1
    def deploy_to_primary_path(self, old_path_dict: dict = None):
611
        """Deploy the primary path into the datapaths of this circuit.
612
613
        If the primary_path attribute is valid and up, this method will try to
614
        deploy this primary_path.
615
        """
616
        # TODO: Remove flows from current (cookies)
617 1
        if self.is_using_primary_path():
618
            # TODO: Log to say that cannot move primary to primary
619
            return True
620
621 1
        if self.primary_path.status is EntityStatus.UP:
622 1
            return self.deploy_to_path(self.primary_path, old_path_dict)
623
        return False
624
625 1
    def deploy(self, old_path_dict: dict = None):
626
        """Deploy EVC to best path.
627
628
        Best path can be the primary path, if available. If not, the backup
629
        path, and, if it is also not available, a dynamic path.
630
        """
631 1
        if self.archived:
632 1
            return False
633 1
        self.enable()
634 1
        success = self.deploy_to_primary_path(old_path_dict)
635 1
        if not success:
636 1
            success = self.deploy_to_backup_path(old_path_dict)
637
638 1
        if success:
639 1
            emit_event(self._controller, "deployed",
640
                       content=map_evc_event_content(self))
641 1
        return success
642
643 1
    @staticmethod
644 1
    def get_path_status(path):
645
        """Check for the current status of a path.
646
647
        If any link in this path is down, the path is considered down.
648
        """
649 1
        if not path:
650 1
            return EntityStatus.DISABLED
651
652 1
        for link in path:
653 1
            if link.status is not EntityStatus.UP:
654 1
                return link.status
655 1
        return EntityStatus.UP
656
657
    #    def discover_new_path(self):
658
    #        # TODO: discover a new path to satisfy this circuit and deploy
659
660 1
    def remove(self):
661
        """Remove EVC path and disable it."""
662 1
        self.remove_current_flows(sync=False)
663 1
        self.remove_failover_flows(sync=False)
664 1
        self.disable()
665 1
        self.sync()
666 1
        emit_event(self._controller, "undeployed",
667
                   content=map_evc_event_content(self))
668
669 1
    def remove_failover_flows(self, exclude_uni_switches=True,
670
                              force=True, sync=True) -> None:
671
        """Remove failover_flows.
672
673
        By default, it'll exclude UNI switches, if mef_eline has already
674
        called remove_current_flows before then this minimizes the number
675
        of FlowMods and IO.
676
        """
677 1
        if not self.failover_path:
678 1
            return
679 1
        switches, cookie, excluded = set(), self.get_cookie(), set()
680 1
        if exclude_uni_switches:
681 1
            excluded.add(self.uni_a.interface.switch.id)
682 1
            excluded.add(self.uni_z.interface.switch.id)
683 1
        for link in self.failover_path:
684 1
            if link.endpoint_a.switch.id not in excluded:
685 1
                switches.add(link.endpoint_a.switch.id)
686 1
            if link.endpoint_b.switch.id not in excluded:
687 1
                switches.add(link.endpoint_b.switch.id)
688 1
        flow_mods = {
689
            "switches": list(switches),
690
            "flows": [{
691
                "cookie": cookie,
692
                "cookie_mask": int(0xffffffffffffffff),
693
                "owner": "mef_eline",
694
            }]
695
        }
696 1
        try:
697 1
            self._send_flow_mods(
698
                flow_mods,
699
                "delete",
700
                force=force,
701
            )
702
        except FlowModException as err:
703
            log.error(f"Error deleting {self} failover_path flows, {err}")
704 1
        try:
705 1
            self.failover_path.make_vlans_available(self._controller)
706
        except KytosTagError as err:
707
            log.error(f"Error removing {self} failover_path: {err}")
708 1
        self.failover_path = Path([])
709 1
        if sync:
710 1
            self.sync()
711
712 1
    def remove_current_flows(
713
        self,
714
        current_path=None,
715
        force=True,
716
        sync=True,
717
        return_path=False
718
    ) -> dict[str, int]:
719
        """Remove all flows from current path or path intended for
720
         current path if exists."""
721 1
        switches, old_path_dict = set(), {}
722 1
        current_path = self.current_path if not current_path else current_path
723 1
        if not current_path and not self.is_intra_switch():
724 1
            return {}
725
726 1
        if return_path:
727 1
            for link in self.current_path:
728 1
                s_vlan = link.metadata.get("s_vlan")
729 1
                if s_vlan:
730 1
                    old_path_dict[link.id] = s_vlan.value
731
732 1
        for link in current_path:
733 1
            switches.add(link.endpoint_a.switch.id)
734 1
            switches.add(link.endpoint_b.switch.id)
735 1
        switches.add(self.uni_a.interface.switch.id)
736 1
        switches.add(self.uni_z.interface.switch.id)
737 1
        flow_mods = {
738
            "switches": list(switches),
739
            "flows": [{
740
                "cookie": self.get_cookie(),
741
                "cookie_mask": int(0xffffffffffffffff),
742
                "owner": "mef_eline",
743
            }]
744
        }
745
746 1
        try:
747 1
            self._send_flow_mods(flow_mods, "delete", force=force)
748 1
        except FlowModException as err:
749 1
            log.error(f"Error deleting {self} current_path flows, {err}")
750
751 1
        try:
752 1
            current_path.make_vlans_available(self._controller)
753 1
        except KytosTagError as err:
754 1
            log.error(f"Error removing {self} current_path: {err}")
755 1
        self.current_path = Path([])
756 1
        self.deactivate()
757 1
        if sync:
758 1
            self.sync()
759 1
        return old_path_dict
760
761 1
    def remove_path_flows(
762
        self, path=None, force=True
763
    ) -> dict[str, list[dict]]:
764
        """Remove all flows from path, and return the removed flows."""
765 1
        dpid_flows_match: dict[str, dict] = defaultdict(lambda: {"flows": []})
766 1
        out_flows: dict[str, list[dict]] = defaultdict(list)
767
768 1
        if not path:
769 1
            return dpid_flows_match
770
771 1
        try:
772 1
            nni_flows = self._prepare_nni_flows(path)
773
        # pylint: disable=broad-except
774
        except Exception:
775
            err = traceback.format_exc()
776
            log.error(f"Fail to remove NNI failover flows for {self}: {err}")
777
            nni_flows = {}
778
779 1
        for dpid, flows in nni_flows.items():
780 1
            for flow in flows:
781 1
                flow_mod = {
782
                    "cookie": flow["cookie"],
783
                    "match": flow["match"],
784
                    "owner": "mef_eline",
785
                    "cookie_mask": int(0xffffffffffffffff)
786
                }
787 1
                dpid_flows_match[dpid]["flows"].append(flow_mod)
788 1
                out_flows[dpid].append(flow_mod)
789
790 1
        try:
791 1
            uni_flows = self._prepare_uni_flows(path, skip_in=True)
792
        # pylint: disable=broad-except
793
        except Exception:
794
            err = traceback.format_exc()
795
            log.error(f"Fail to remove UNI failover flows for {self}: {err}")
796
            uni_flows = {}
797
798 1
        for dpid, flows in uni_flows.items():
799 1
            for flow in flows:
800 1
                flow_mod = {
801
                    "cookie": flow["cookie"],
802
                    "match": flow["match"],
803
                    "owner": "mef_eline",
804
                    "cookie_mask": int(0xffffffffffffffff)
805
                }
806 1
                dpid_flows_match[dpid]["flows"].append(flow_mod)
807 1
                out_flows[dpid].append(flow_mod)
808
809 1
        try:
810 1
            self._send_flow_mods(
811
                dpid_flows_match, 'delete', force=force, by_switch=True
812
            )
813 1
        except FlowModException as err:
814 1
            log.error(
815
                f"Error deleting {self} path flows, path:{path}, error={err}"
816
            )
817
818 1
        try:
819 1
            path.make_vlans_available(self._controller)
820
        except KytosTagError as err:
821
            log.error(f"Error removing {self} path: {err}")
822
823 1
        return out_flows
824
825 1
    @staticmethod
826 1
    def links_zipped(path=None):
827
        """Return an iterator which yields pairs of links in order."""
828 1
        if not path:
829 1
            return []
830 1
        return zip(path[:-1], path[1:])
831
832 1
    def should_deploy(self, path=None):
833
        """Verify if the circuit should be deployed."""
834 1
        if not path:
835 1
            log.debug("Path is empty.")
836 1
            return False
837
838 1
        if not self.is_enabled():
839 1
            log.debug(f"{self} is disabled.")
840 1
            return False
841
842 1
        if not self.is_active():
843 1
            log.debug(f"{self} will be deployed.")
844 1
            return True
845
846 1
        return False
847
848 1
    @staticmethod
849 1
    def is_uni_interface_active(
850
        *interfaces: Interface
851
    ) -> tuple[bool, dict]:
852
        """Whether UNIs are active and their status & status_reason."""
853 1
        active = True
854 1
        bad_interfaces = [
855
            interface
856
            for interface in interfaces
857
            if interface.status != EntityStatus.UP
858
        ]
859 1
        if bad_interfaces:
860 1
            active = False
861 1
            interfaces = bad_interfaces
862 1
        return active, {
863
            interface.id: {
864
                'status': interface.status.value,
865
                'status_reason': interface.status_reason,
866
            }
867
            for interface in interfaces
868
        }
869
870 1
    def try_to_activate(self) -> bool:
871
        """Try to activate the EVC."""
872 1
        if self.is_intra_switch():
873 1
            return self._try_to_activate_intra_evc()
874 1
        return self._try_to_activate_inter_evc()
875
876 1
    def _try_to_activate_intra_evc(self) -> bool:
877
        """Try to activate intra EVC."""
878 1
        intf_a, intf_z = self.uni_a.interface, self.uni_z.interface
879 1
        is_active, reason = self.is_uni_interface_active(intf_a, intf_z)
880 1
        if not is_active:
881 1
            raise ActivationError(
882
                f"Won't be able to activate {self} due to UNIs: {reason}"
883
            )
884 1
        self.activate()
885 1
        return True
886
887 1
    def _try_to_activate_inter_evc(self) -> bool:
888
        """Try to activate inter EVC."""
889 1
        intf_a, intf_z = self.uni_a.interface, self.uni_z.interface
890 1
        is_active, reason = self.is_uni_interface_active(intf_a, intf_z)
891 1
        if not is_active:
892 1
            raise ActivationError(
893
                f"Won't be able to activate {self} due to UNIs: {reason}"
894
            )
895 1
        if self.current_path.status != EntityStatus.UP:
896 1
            raise ActivationError(
897
                f"Won't be able to activate {self} due to current_path "
898
                f"status {self.current_path.status}"
899
            )
900 1
        self.activate()
901 1
        return True
902
903
    # pylint: disable=too-many-branches, too-many-statements
904 1
    def deploy_to_path(self, path=None, old_path_dict: dict = None):
905
        """Install the flows for this circuit.
906
907
        Procedures to deploy:
908
909
        0. Remove current flows installed
910
        1. Decide if will deploy "path" or discover a new path
911
        2. Choose vlan
912
        3. Install NNI flows
913
        4. Install UNI flows
914
        5. Activate
915
        6. Update current_path
916
        7. Update links caches(primary, current, backup)
917
918
        """
919 1
        self.remove_current_flows(sync=False)
920 1
        use_path = path or Path([])
921 1
        if not old_path_dict:
922 1
            old_path_dict = {}
923 1
        tag_errors = []
924 1
        no_valid_path = False
925 1
        if self.should_deploy(use_path):
926 1
            try:
927 1
                use_path.choose_vlans(self._controller, old_path_dict)
928 1
            except KytosNoTagAvailableError as e:
929 1
                tag_errors.append(str(e))
930 1
                use_path = None
931
        else:
932 1
            for use_path in self.discover_new_paths():
933 1
                if use_path is None:
934 1
                    no_valid_path = True
935 1
                    continue
936 1
                try:
937 1
                    use_path.choose_vlans(self._controller, old_path_dict)
938 1
                    break
939 1
                except KytosNoTagAvailableError as e:
940 1
                    tag_errors.append(str(e))
941
            else:
942 1
                use_path = None
943
944 1
        try:
945 1
            if use_path:
946 1
                self._install_flows(use_path)
947 1
            elif self.is_intra_switch():
948 1
                use_path = Path()
949 1
                self._install_direct_uni_flows()
950
            else:
951 1
                no_path_msg = "No available path was found."
952 1
                if no_valid_path:
953 1
                    no_path_msg = "No valid path was found, "\
954
                                  "try increasing `max_paths`"\
955
                                 f" from {self.max_paths}."
956 1
                msg = f"{self} was not deployed. {no_path_msg}"
957 1
                if tag_errors:
958 1
                    msg = self.add_tag_errors(msg, tag_errors)
959 1
                    log.error(msg)
960
                else:
961 1
                    log.warning(msg)
962 1
                return False
963 1
        except EVCPathNotInstalled as err:
964 1
            log.error(
965
                f"Error deploying EVC {self} when calling flow_manager: {err}"
966
            )
967 1
            self.remove_current_flows(use_path, sync=True)
968 1
            return False
969
970 1
        self.current_path = use_path
971 1
        msg = f"{self} was deployed."
972 1
        try:
973 1
            self.try_to_activate()
974
        except ActivationError as exc:
975
            msg = f"{msg} {str(exc)}"
976 1
        self.sync()
977 1
        log.info(msg)
978 1
        return True
979
980
    # pylint: disable=too-many-statements
981 1
    def setup_failover_path(self, warn_if_not_path=True):
982
        """Install flows for the failover path of this EVC.
983
984
        Procedures to deploy:
985
986
        0. Remove flows currently installed for failover_path (if any)
987
        1. Discover a disjoint path from current_path
988
        2. Choose vlans
989
        3. Install NNI flows
990
        4. Install UNI egress flows
991
        5. Update failover_path
992
        """
993
        # Intra-switch EVCs have no failover_path
994 1
        if self.is_intra_switch():
995 1
            return False
996
997
        # For not only setup failover path for totally dynamic EVCs
998 1
        if not self.is_eligible_for_failover_path():
999 1
            return False
1000
1001 1
        out_new_flows: dict[str, list[dict]] = {}
1002 1
        reason = ""
1003 1
        tag_errors = []
1004 1
        out_removed_flows = self.remove_path_flows(self.failover_path)
1005 1
        self.failover_path = Path([])
1006
1007 1
        for use_path in self.get_failover_path_candidates():
1008 1
            if not use_path:
1009 1
                continue
1010 1
            try:
1011 1
                use_path.choose_vlans(self._controller)
1012 1
                break
1013 1
            except KytosNoTagAvailableError as e:
1014 1
                tag_errors.append(str(e))
1015
        else:
1016 1
            use_path = Path([])
1017 1
            reason = "No available path was found"
1018
1019 1
        try:
1020 1
            if use_path:
1021 1
                out_new_flows = self._install_flows(
1022
                    use_path, skip_in=True
1023
                )
1024 1
        except EVCPathNotInstalled as err:
1025 1
            reason = "Error deploying failover path"
1026 1
            log.error(
1027
                f"{reason} for {self}. FlowManager error: {err}"
1028
            )
1029 1
            _rmed_flows = self.remove_path_flows(use_path)
1030 1
            out_removed_flows = merge_flow_dicts(
1031
                out_removed_flows, _rmed_flows
1032
            )
1033 1
            use_path = Path([])
1034
1035 1
        self.failover_path = use_path
1036 1
        self.sync()
1037
1038 1
        if out_new_flows or out_removed_flows:
1039 1
            emit_event(self._controller, "failover_deployed", content={
1040
                self.id: map_evc_event_content(
1041
                    self,
1042
                    flows=deepcopy(out_new_flows),
1043
                    removed_flows=deepcopy(out_removed_flows),
1044
                    error_reason=reason,
1045
                    current_path=self.current_path.as_dict(),
1046
                )
1047
            })
1048
1049 1
        if not use_path:
1050 1
            msg = f"Failover path for {self} was not deployed: {reason}."
1051 1
            if tag_errors:
1052 1
                msg = self.add_tag_errors(msg, tag_errors)
1053 1
                log.error(msg)
1054 1
            elif warn_if_not_path:
1055 1
                log.warning(msg)
1056 1
            return False
1057 1
        log.info(f"Failover path for {self} was deployed.")
1058 1
        return True
1059
1060 1
    @staticmethod
1061 1
    def add_tag_errors(msg: str, tag_errors: list):
1062
        """Add to msg the tag errors ecountered when chossing path."""
1063 1
        path = ['path', 'paths']
1064 1
        was = ['was', 'were']
1065 1
        message = ['message', 'messages']
1066
1067
        # Choose either singular(0) or plural(1) words
1068 1
        n = 1
1069 1
        if len(tag_errors) == 1:
1070 1
            n = 0
1071
1072 1
        msg += f" {len(tag_errors)} {path[n]} {was[n]} rejected"
1073 1
        msg += f" with {message[n]}: {tag_errors}"
1074 1
        return msg
1075
1076 1
    def get_failover_flows(self):
1077
        """Return the flows needed to make the failover path active, i.e. the
1078
        flows for ingress forwarding.
1079
1080
        Return:
1081
            dict: A dict of flows indexed by the switch_id will be returned, or
1082
                an empty dict if no failover_path is available.
1083
        """
1084 1
        if not self.failover_path:
1085 1
            return {}
1086 1
        return self._prepare_uni_flows(self.failover_path, skip_out=True)
1087
1088
    # pylint: disable=too-many-branches
1089 1
    def _prepare_direct_uni_flows(self):
1090
        """Prepare flows connecting two UNIs for intra-switch EVC."""
1091 1
        vlan_a = self._get_value_from_uni_tag(self.uni_a)
1092 1
        vlan_z = self._get_value_from_uni_tag(self.uni_z)
1093
1094 1
        flow_mod_az = self._prepare_flow_mod(
1095
            self.uni_a.interface, self.uni_z.interface,
1096
            self.queue_id, vlan_a
1097
        )
1098 1
        flow_mod_za = self._prepare_flow_mod(
1099
            self.uni_z.interface, self.uni_a.interface,
1100
            self.queue_id, vlan_z
1101
        )
1102
1103 1 View Code Duplication
        if not isinstance(vlan_z, list) and vlan_z not in self.special_cases:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1104 1
            flow_mod_az["actions"].insert(
1105
                0, {"action_type": "set_vlan", "vlan_id": vlan_z}
1106
            )
1107 1
            if not vlan_a:
1108 1
                flow_mod_az["actions"].insert(
1109
                    0, {"action_type": "push_vlan", "tag_type": "c"}
1110
                )
1111 1
            if vlan_a == 0:
1112 1
                flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"})
1113 1
        elif vlan_a == 0 and vlan_z == "4096/4096":
1114 1
            flow_mod_za["actions"].insert(0, {"action_type": "pop_vlan"})
1115
1116 1 View Code Duplication
        if not isinstance(vlan_a, list) and vlan_a not in self.special_cases:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1117 1
            flow_mod_za["actions"].insert(
1118
                    0, {"action_type": "set_vlan", "vlan_id": vlan_a}
1119
                )
1120 1
            if not vlan_z:
1121 1
                flow_mod_za["actions"].insert(
1122
                    0, {"action_type": "push_vlan", "tag_type": "c"}
1123
                )
1124 1
            if vlan_z == 0:
1125 1
                flow_mod_az["actions"].insert(0, {"action_type": "pop_vlan"})
1126 1
        elif vlan_a == "4096/4096" and vlan_z == 0:
1127 1
            flow_mod_az["actions"].insert(0, {"action_type": "pop_vlan"})
1128
1129 1
        flows = []
1130 1
        if isinstance(vlan_a, list):
1131 1
            for mask_a in vlan_a:
1132 1
                flow_aux = deepcopy(flow_mod_az)
1133 1
                flow_aux["match"]["dl_vlan"] = mask_a
1134 1
                flows.append(flow_aux)
1135
        else:
1136 1
            if vlan_a is not None:
1137 1
                flow_mod_az["match"]["dl_vlan"] = vlan_a
1138 1
            flows.append(flow_mod_az)
1139
1140 1
        if isinstance(vlan_z, list):
1141 1
            for mask_z in vlan_z:
1142 1
                flow_aux = deepcopy(flow_mod_za)
1143 1
                flow_aux["match"]["dl_vlan"] = mask_z
1144 1
                flows.append(flow_aux)
1145
        else:
1146 1
            if vlan_z is not None:
1147 1
                flow_mod_za["match"]["dl_vlan"] = vlan_z
1148 1
            flows.append(flow_mod_za)
1149 1
        return (
1150
            self.uni_a.interface.switch.id, flows
1151
        )
1152
1153 1
    def _install_direct_uni_flows(self):
1154
        """Install flows connecting two UNIs.
1155
1156
        This case happens when the circuit is between UNIs in the
1157
        same switch.
1158
        """
1159 1
        (dpid, flows) = self._prepare_direct_uni_flows()
1160 1
        flow_mods = {"switches": [dpid], "flows": flows}
1161 1
        try:
1162 1
            self._send_flow_mods(flow_mods, "install")
1163 1
        except FlowModException as err:
1164 1
            raise EVCPathNotInstalled(str(err)) from err
1165
1166 1
    def _prepare_nni_flows(self, path=None):
1167
        """Prepare NNI flows."""
1168 1
        nni_flows = OrderedDict()
1169 1
        previous = self.uni_a.interface.switch.dpid
1170 1
        for incoming, outcoming in self.links_zipped(path):
1171 1
            in_vlan = incoming.get_metadata("s_vlan").value
1172 1
            out_vlan = outcoming.get_metadata("s_vlan").value
1173 1
            in_endpoint = self.get_endpoint_by_id(incoming, previous, ne)
1174 1
            out_endpoint = self.get_endpoint_by_id(
1175
                outcoming, in_endpoint.switch.id, eq
1176
            )
1177
1178 1
            flows = []
1179
            # Flow for one direction
1180 1
            flows.append(
1181
                self._prepare_nni_flow(
1182
                    in_endpoint,
1183
                    out_endpoint,
1184
                    in_vlan,
1185
                    out_vlan,
1186
                    queue_id=self.queue_id,
1187
                )
1188
            )
1189
1190
            # Flow for the other direction
1191 1
            flows.append(
1192
                self._prepare_nni_flow(
1193
                    out_endpoint,
1194
                    in_endpoint,
1195
                    out_vlan,
1196
                    in_vlan,
1197
                    queue_id=self.queue_id,
1198
                )
1199
            )
1200 1
            previous = in_endpoint.switch.id
1201 1
            nni_flows[in_endpoint.switch.id] = flows
1202 1
        return nni_flows
1203
1204 1
    def _install_flows(
1205
        self, path=None, skip_in=False, skip_out=False
1206
    ) -> dict[str, list[dict]]:
1207
        """Install uni and nni flows"""
1208 1
        flows_by_switch = defaultdict(lambda: {"flows": []})
1209 1
        new_flows = defaultdict(list)
1210 1
        for dpid, flows in self._prepare_nni_flows(path).items():
1211 1
            flows_by_switch[dpid]["flows"].extend(flows)
1212 1
            new_flows[dpid].extend(flows)
1213 1
        for dpid, flows in self._prepare_uni_flows(
1214
            path, skip_in, skip_out
1215
        ).items():
1216 1
            flows_by_switch[dpid]["flows"].extend(flows)
1217 1
            new_flows[dpid].extend(flows)
1218
1219 1
        try:
1220 1
            self._send_flow_mods(flows_by_switch, "install", by_switch=True)
1221 1
        except FlowModException as err:
1222 1
            raise EVCPathNotInstalled(str(err)) from err
1223
1224 1
        return new_flows
1225
1226 1
    @staticmethod
1227 1
    def _get_value_from_uni_tag(uni: UNI):
1228
        """Returns the value from tag. In case of any and untagged
1229
        it should return 4096/4096 and 0 respectively"""
1230 1
        special = {"any": "4096/4096", "untagged": 0}
1231 1
        if uni.user_tag:
1232 1
            value = uni.user_tag.value
1233 1
            if isinstance(value, list):
1234 1
                return uni.user_tag.mask_list
1235 1
            return special.get(value, value)
1236 1
        return None
1237
1238
    # pylint: disable=too-many-locals
1239 1
    def _prepare_uni_flows(self, path=None, skip_in=False, skip_out=False):
1240
        """Prepare flows to install UNIs."""
1241 1
        uni_flows = {}
1242 1
        if not path:
1243
            return uni_flows
1244
1245
        # Determine VLANs
1246 1
        in_vlan_a = self._get_value_from_uni_tag(self.uni_a)
1247 1
        out_vlan_a = path[0].get_metadata("s_vlan").value
1248
1249 1
        in_vlan_z = self._get_value_from_uni_tag(self.uni_z)
1250 1
        out_vlan_z = path[-1].get_metadata("s_vlan").value
1251
1252
        # Get endpoints from path
1253 1
        endpoint_a = self.get_endpoint_by_id(
1254
            path[0], self.uni_a.interface.switch.id, eq
1255
        )
1256 1
        endpoint_z = self.get_endpoint_by_id(
1257
            path[-1], self.uni_z.interface.switch.id, eq
1258
        )
1259
1260
        # Flows for the first UNI
1261 1
        flows_a = []
1262
1263
        # Flow for one direction, pushing the service tag
1264 1
        if not skip_in:
1265 1
            if isinstance(in_vlan_a, list):
1266 1
                for in_mask_a in in_vlan_a:
1267 1
                    push_flow = self._prepare_push_flow(
1268
                        self.uni_a.interface,
1269
                        endpoint_a,
1270
                        in_mask_a,
1271
                        out_vlan_a,
1272
                        in_vlan_z,
1273
                        queue_id=self.queue_id,
1274
                    )
1275 1
                    flows_a.append(push_flow)
1276
            else:
1277
                push_flow = self._prepare_push_flow(
1278
                    self.uni_a.interface,
1279
                    endpoint_a,
1280
                    in_vlan_a,
1281
                    out_vlan_a,
1282
                    in_vlan_z,
1283
                    queue_id=self.queue_id,
1284
                )
1285
                flows_a.append(push_flow)
1286
1287
        # Flow for the other direction, popping the service tag
1288 1
        if not skip_out:
1289 1
            pop_flow = self._prepare_pop_flow(
1290
                endpoint_a,
1291
                self.uni_a.interface,
1292
                out_vlan_a,
1293
                in_vlan_a,
1294
                in_vlan_z,
1295
                queue_id=self.queue_id,
1296
            )
1297 1
            flows_a.append(pop_flow)
1298
1299 1
        uni_flows[self.uni_a.interface.switch.id] = flows_a
1300
1301
        # Flows for the second UNI
1302 1
        flows_z = []
1303
1304
        # Flow for one direction, pushing the service tag
1305 1
        if not skip_in:
1306 1
            if isinstance(in_vlan_z, list):
1307 1
                for in_mask_z in in_vlan_z:
1308 1
                    push_flow = self._prepare_push_flow(
1309
                        self.uni_z.interface,
1310
                        endpoint_z,
1311
                        in_mask_z,
1312
                        out_vlan_z,
1313
                        in_vlan_a,
1314
                        queue_id=self.queue_id,
1315
                    )
1316 1
                    flows_z.append(push_flow)
1317
            else:
1318
                push_flow = self._prepare_push_flow(
1319
                    self.uni_z.interface,
1320
                    endpoint_z,
1321
                    in_vlan_z,
1322
                    out_vlan_z,
1323
                    in_vlan_a,
1324
                    queue_id=self.queue_id,
1325
                )
1326
                flows_z.append(push_flow)
1327
1328
        # Flow for the other direction, popping the service tag
1329 1
        if not skip_out:
1330 1
            pop_flow = self._prepare_pop_flow(
1331
                endpoint_z,
1332
                self.uni_z.interface,
1333
                out_vlan_z,
1334
                in_vlan_z,
1335
                in_vlan_a,
1336
                queue_id=self.queue_id,
1337
            )
1338 1
            flows_z.append(pop_flow)
1339
1340 1
        uni_flows[self.uni_z.interface.switch.id] = flows_z
1341
1342 1
        return uni_flows
1343
1344 1
    @staticmethod
1345 1
    @retry(
1346
        stop=stop_after_attempt(3),
1347
        wait=wait_combine(wait_fixed(3), wait_random(min=2, max=7)),
1348
        retry=retry_if_exception_type(FlowModException),
1349
        before_sleep=before_sleep,
1350
        reraise=True,
1351
    )
1352 1
    def _send_flow_mods(
1353
        data_content: dict,
1354
        command="install",
1355
        force=False,
1356
        by_switch=False
1357
    ):
1358
        """Send a flow_mod list to a specific switch.
1359
1360
        Args:
1361
            dpid(str): The target of flows (i.e. Switch.id).
1362
            flow_mods(dict): Python dictionary with flow_mods.
1363
            command(str): By default is 'flows'. To remove a flow is 'remove'.
1364
            force(bool): True to send via consistency check in case of errors.
1365
            by_switch(bool): True to send to 'flows_by_switch' request instead.
1366
        """
1367 1
        if by_switch:
1368 1
            endpoint = f"{settings.MANAGER_URL}/flows_by_switch/?force={force}"
1369
        else:
1370 1
            endpoint = f"{settings.MANAGER_URL}/flows"
1371 1
            data_content["force"] = force
1372 1
        try:
1373 1
            if command == "install":
1374 1
                res = httpx.post(endpoint, json=data_content, timeout=30)
1375 1
            elif command == "delete":
1376 1
                res = httpx.request(
1377
                    "DELETE", endpoint, json=data_content, timeout=30
1378
                )
1379 1
        except httpx.RequestError as err:
1380 1
            raise FlowModException(str(err)) from err
1381 1
        if res.is_server_error or res.status_code >= 400:
1382 1
            raise FlowModException(res.text)
1383
1384 1
    def get_cookie(self):
1385
        """Return the cookie integer from evc id."""
1386 1
        return int(self.id, 16) + (settings.COOKIE_PREFIX << 56)
1387
1388 1
    @staticmethod
1389 1
    def get_id_from_cookie(cookie):
1390
        """Return the evc id given a cookie value."""
1391 1
        evc_id = cookie - (settings.COOKIE_PREFIX << 56)
1392 1
        return f"{evc_id:x}".zfill(14)
1393
1394 1
    def set_flow_table_group_id(self, flow_mod: dict, vlan) -> dict:
1395
        """Set table_group and table_id"""
1396 1
        table_group = "epl" if vlan is None else "evpl"
1397 1
        flow_mod["table_group"] = table_group
1398 1
        flow_mod["table_id"] = self.table_group[table_group]
1399 1
        return flow_mod
1400
1401 1
    @staticmethod
1402 1
    def get_priority(vlan):
1403
        """Return priority value depending on vlan value"""
1404 1
        if isinstance(vlan, list):
1405 1
            return settings.EVPL_SB_PRIORITY
1406 1
        if vlan not in {None, "4096/4096", 0}:
1407 1
            return settings.EVPL_SB_PRIORITY
1408 1
        if vlan == 0:
1409 1
            return settings.UNTAGGED_SB_PRIORITY
1410 1
        if vlan == "4096/4096":
1411 1
            return settings.ANY_SB_PRIORITY
1412 1
        return settings.EPL_SB_PRIORITY
1413
1414 1
    def _prepare_flow_mod(self, in_interface, out_interface,
1415
                          queue_id=None, vlan=True):
1416
        """Prepare a common flow mod."""
1417 1
        default_actions = [
1418
            {"action_type": "output", "port": out_interface.port_number}
1419
        ]
1420 1
        queue_id = settings.QUEUE_ID if queue_id == -1 else queue_id
1421 1
        if queue_id is not None:
1422 1
            default_actions.insert(
1423
                0,
1424
                {"action_type": "set_queue", "queue_id": queue_id}
1425
            )
1426
1427 1
        flow_mod = {
1428
            "match": {"in_port": in_interface.port_number},
1429
            "cookie": self.get_cookie(),
1430
            "actions": default_actions,
1431
            "owner": "mef_eline",
1432
        }
1433
1434 1
        self.set_flow_table_group_id(flow_mod, vlan)
1435 1
        if self.sb_priority:
1436 1
            flow_mod["priority"] = self.sb_priority
1437
        else:
1438 1
            flow_mod["priority"] = self.get_priority(vlan)
1439 1
        return flow_mod
1440
1441 1
    def _prepare_nni_flow(self, *args, queue_id=None):
1442
        """Create NNI flows."""
1443 1
        in_interface, out_interface, in_vlan, out_vlan = args
1444 1
        flow_mod = self._prepare_flow_mod(
1445
            in_interface, out_interface, queue_id
1446
        )
1447 1
        flow_mod["match"]["dl_vlan"] = in_vlan
1448 1
        new_action = {"action_type": "set_vlan", "vlan_id": out_vlan}
1449 1
        flow_mod["actions"].insert(0, new_action)
1450
1451 1
        return flow_mod
1452
1453 1
    def _prepare_push_flow(self, *args, queue_id=None):
1454
        """Prepare push flow.
1455
1456
        Arguments:
1457
            in_interface(Interface): Interface input.
1458
            out_interface(Interface): Interface output.
1459
            in_vlan(int,str,None): Vlan input.
1460
            out_vlan(int): Vlan output.
1461
            new_c_vlan(int,str,list,None): New client vlan.
1462
1463
        Return:
1464
            dict: An python dictionary representing a FlowMod
1465
1466
        """
1467
        # assign all arguments
1468 1
        in_interface, out_interface, in_vlan, out_vlan, new_c_vlan = args
1469 1
        vlan_pri = in_vlan if not isinstance(new_c_vlan, list) else new_c_vlan
1470 1
        flow_mod = self._prepare_flow_mod(
1471
            in_interface, out_interface, queue_id, vlan_pri
1472
        )
1473
        # the service tag must be always pushed
1474 1
        new_action = {"action_type": "set_vlan", "vlan_id": out_vlan}
1475 1
        flow_mod["actions"].insert(0, new_action)
1476
1477 1
        if (
1478
            not (in_vlan != new_c_vlan and isinstance(in_vlan, int) and
1479
                 isinstance(new_c_vlan, int))
1480
        ):
1481
            # Add service VLAN header when it does NOT fall into this
1482
            # statement: Both VLANs should be integer and different.
1483 1
            new_action = {"action_type": "push_vlan", "tag_type": "s"}
1484 1
            flow_mod["actions"].insert(0, new_action)
1485
1486 1
        if in_vlan is not None:
1487
            # if in_vlan is set, it must be included in the match
1488 1
            flow_mod["match"]["dl_vlan"] = in_vlan
1489
1490 1
        if (
1491
            not isinstance(in_vlan, int) and isinstance(new_c_vlan, int) and
1492
            new_c_vlan != 0
1493
        ):
1494
            # new_in_vlan is an integer but zero, action to set is required
1495 1
            new_action = {"action_type": "set_vlan", "vlan_id": new_c_vlan}
1496 1
            flow_mod["actions"].insert(0, new_action)
1497
1498 1
        if in_vlan == "4096/4096" and new_c_vlan == 0:
1499
            # if in_vlan match with any tags and new_c_vlan does not,
1500
            # a pop action is required
1501 1
            new_action = {"action_type": "pop_vlan"}
1502 1
            flow_mod["actions"].insert(0, new_action)
1503
1504 1
        elif (not in_vlan and
1505
                (not isinstance(new_c_vlan, list) and
1506
                 new_c_vlan not in self.special_cases)):
1507
            # new_in_vlan is an integer but zero and in_vlan is a no-tag or
1508
            # untagged
1509 1
            new_action = {"action_type": "push_vlan", "tag_type": "c"}
1510 1
            flow_mod["actions"].insert(0, new_action)
1511
1512 1
        return flow_mod
1513
1514 1
    def _prepare_pop_flow(
1515
        self,
1516
        in_interface: Interface,
1517
        out_interface: Interface,
1518
        out_vlan: int,
1519
        in_vlan: Union[int, str, list, None],
1520
        new_c_vlan: Union[int, str, list, None],
1521
        queue_id=None,
1522
    ):
1523
        # pylint: disable=too-many-arguments
1524
        """Prepare pop flow."""
1525 1
        flow_mod = self._prepare_flow_mod(
1526
            in_interface, out_interface, queue_id
1527
        )
1528 1
        flow_mod["match"]["dl_vlan"] = out_vlan
1529 1
        if in_vlan == 0:
1530 1
            new_action = {"action_type": "pop_vlan"}
1531 1
            flow_mod["actions"].insert(0, new_action)
1532 1
        elif (
1533
            in_vlan != new_c_vlan and isinstance(in_vlan, int) and
1534
            isinstance(new_c_vlan, int)
1535
        ):
1536
            # If UNI VLANs are different and in_vlan is not 0
1537 1
            new_action = {"action_type": "set_vlan", "vlan_id": in_vlan}
1538 1
            flow_mod["actions"].insert(0, new_action)
1539
        else:
1540 1
            new_action = {"action_type": "pop_vlan"}
1541 1
            flow_mod["actions"].insert(0, new_action)
1542 1
        return flow_mod
1543
1544 1
    @staticmethod
1545 1
    def run_bulk_sdntraces(
1546
        uni_list: list[tuple[Interface, Union[str, int, None]]]
1547
    ) -> dict:
1548
        """Run SDN traces on control plane starting from EVC UNIs."""
1549 1
        endpoint = f"{settings.SDN_TRACE_CP_URL}/traces"
1550 1
        data = []
1551 1
        for interface, tag_value in uni_list:
1552 1
            data_uni = {
1553
                "trace": {
1554
                            "switch": {
1555
                                "dpid": interface.switch.dpid,
1556
                                "in_port": interface.port_number,
1557
                            }
1558
                        }
1559
                }
1560 1
            if tag_value:
1561 1
                uni_dl_vlan = map_dl_vlan(tag_value)
1562 1
                if uni_dl_vlan:
1563 1
                    data_uni["trace"]["eth"] = {
1564
                                            "dl_type": 0x8100,
1565
                                            "dl_vlan": uni_dl_vlan,
1566
                                            }
1567 1
            data.append(data_uni)
1568 1
        try:
1569 1
            response = httpx.put(endpoint, json=data, timeout=30)
1570 1
        except httpx.TimeoutException as exception:
1571 1
            log.error(f"Request has timed out: {exception}")
1572 1
            return {"result": []}
1573 1
        if response.status_code >= 400:
1574 1
            log.error(f"Failed to run sdntrace-cp: {response.text}")
1575 1
            return {"result": []}
1576 1
        return response.json()
1577
1578
    # pylint: disable=too-many-return-statements, too-many-arguments
1579 1
    @staticmethod
1580 1
    def check_trace(
1581
        evc_id: str,
1582
        evc_name: str,
1583
        tag_a: Union[None, int, str],
1584
        tag_z: Union[None, int, str],
1585
        interface_a: Interface,
1586
        interface_z: Interface,
1587
        current_path: list,
1588
        trace_a: list,
1589
        trace_z: list
1590
    ) -> bool:
1591
        """Auxiliar function to check an individual trace"""
1592 1
        if (
1593
            len(trace_a) != len(current_path) + 1
1594
            or not compare_uni_out_trace(tag_z, interface_z, trace_a[-1])
1595
        ):
1596 1
            log.warning(f"From EVC({evc_id}) named '{evc_name}'. "
1597
                        f"Invalid trace from uni_a: {trace_a}")
1598 1
            return False
1599 1
        if (
1600
            len(trace_z) != len(current_path) + 1
1601
            or not compare_uni_out_trace(tag_a, interface_a, trace_z[-1])
1602
        ):
1603 1
            log.warning(f"From EVC({evc_id}) named '{evc_name}'. "
1604
                        f"Invalid trace from uni_z: {trace_z}")
1605 1
            return False
1606
1607 1
        if not current_path:
1608
            return True
1609
1610 1
        first_link, trace_path_begin, trace_path_end = current_path[0], [], []
1611 1
        if (
1612
            first_link.endpoint_a.switch.id == trace_a[0]["dpid"]
1613
        ):
1614 1
            trace_path_begin, trace_path_end = trace_a, trace_z
1615 1
        elif (
1616
            first_link.endpoint_a.switch.id == trace_z[0]["dpid"]
1617
        ):
1618 1
            trace_path_begin, trace_path_end = trace_z, trace_a
1619
        else:
1620
            msg = (
1621
                f"first link {first_link} endpoint_a didn't match the first "
1622
                f"step of trace_a {trace_a} or trace_z {trace_z}"
1623
            )
1624
            log.warning(msg)
1625
            return False
1626
1627 1
        for link, trace1, trace2 in zip(current_path,
1628
                                        trace_path_begin[1:],
1629
                                        trace_path_end[:0:-1]):
1630 1
            metadata_vlan = None
1631 1
            if link.metadata:
1632 1
                metadata_vlan = glom(link.metadata, 's_vlan.value')
1633 1
            if compare_endpoint_trace(
1634
                                        link.endpoint_a,
1635
                                        metadata_vlan,
1636
                                        trace2
1637
                                    ) is False:
1638 1
                log.warning(f"From EVC({evc_id}) named '{evc_name}'. "
1639
                            f"Invalid trace from uni_a: {trace_a}")
1640 1
                return False
1641 1
            if compare_endpoint_trace(
1642
                                        link.endpoint_b,
1643
                                        metadata_vlan,
1644
                                        trace1
1645
                                    ) is False:
1646 1
                log.warning(f"From EVC({evc_id}) named '{evc_name}'. "
1647
                            f"Invalid trace from uni_z: {trace_z}")
1648 1
                return False
1649
1650 1
        return True
1651
1652 1
    @staticmethod
1653 1
    def check_range(circuit, traces: list) -> bool:
1654
        """Check traces when for UNI with TAGRange"""
1655 1
        check = True
1656 1
        for i, mask in enumerate(circuit.uni_a.user_tag.mask_list):
1657 1
            trace_a = traces[i*2]
1658 1
            trace_z = traces[i*2+1]
1659 1
            check &= EVCDeploy.check_trace(
1660
                circuit.id, circuit.name,
1661
                mask, mask,
1662
                circuit.uni_a.interface,
1663
                circuit.uni_z.interface,
1664
                circuit.current_path,
1665
                trace_a, trace_z,
1666
            )
1667 1
        return check
1668
1669 1
    @staticmethod
1670 1
    def check_list_traces(list_circuits: list) -> dict:
1671
        """Check if current_path is deployed comparing with SDN traces."""
1672 1
        if not list_circuits:
1673 1
            return {}
1674 1
        uni_list = make_uni_list(list_circuits)
1675 1
        traces = EVCDeploy.run_bulk_sdntraces(uni_list)["result"]
1676
1677 1
        if not traces:
1678 1
            return {}
1679
1680 1
        try:
1681 1
            circuits_checked = {}
1682 1
            i = 0
1683 1
            for circuit in list_circuits:
1684 1
                if isinstance(circuit.uni_a.user_tag, TAGRange):
1685 1
                    length = len(circuit.uni_a.user_tag.mask_list)
1686 1
                    circuits_checked[circuit.id] = EVCDeploy.check_range(
1687
                        circuit, traces[i:i+length*2]
1688
                    )
1689 1
                    i += length*2
1690
                else:
1691 1
                    trace_a = traces[i]
1692 1
                    trace_z = traces[i+1]
1693 1
                    tag_a = None
1694 1
                    if circuit.uni_a.user_tag:
1695 1
                        tag_a = circuit.uni_a.user_tag.value
1696 1
                    tag_z = None
1697 1
                    if circuit.uni_z.user_tag:
1698 1
                        tag_z = circuit.uni_z.user_tag.value
1699 1
                    circuits_checked[circuit.id] = EVCDeploy.check_trace(
1700
                        circuit.id, circuit.name,
1701
                        tag_a, tag_z,
1702
                        circuit.uni_a.interface,
1703
                        circuit.uni_z.interface,
1704
                        circuit.current_path,
1705
                        trace_a, trace_z
1706
                    )
1707 1
                    i += 2
1708 1
        except IndexError as err:
1709 1
            log.error(
1710
                f"Bulk sdntraces returned fewer items than expected."
1711
                f"Error = {err}"
1712
            )
1713 1
            return {}
1714
1715 1
        return circuits_checked
1716
1717 1
    @staticmethod
1718 1
    def get_endpoint_by_id(
1719
        link: Link,
1720
        id_: str,
1721
        operator: Union[eq, ne]
1722
    ) -> Interface:
1723
        """Return endpoint from link
1724
        either equal(eq) or not equal(ne) to id"""
1725 1
        if operator(link.endpoint_a.switch.id, id_):
1726 1
            return link.endpoint_a
1727 1
        return link.endpoint_b
1728
1729
1730 1
class LinkProtection(EVCDeploy):
1731
    """Class to handle link protection."""
1732
1733 1
    def is_affected_by_link(self, link=None):
1734
        """Verify if the current path is affected by link down event."""
1735
        return self.current_path.is_affected_by_link(link)
1736
1737 1
    def is_using_primary_path(self):
1738
        """Verify if the current deployed path is self.primary_path."""
1739 1
        return self.current_path == self.primary_path
1740
1741 1
    def is_using_backup_path(self):
1742
        """Verify if the current deployed path is self.backup_path."""
1743 1
        return self.current_path == self.backup_path
1744
1745 1
    def is_using_dynamic_path(self):
1746
        """Verify if the current deployed path is dynamic."""
1747 1
        if (
1748
            self.current_path
1749
            and not self.is_using_primary_path()
1750
            and not self.is_using_backup_path()
1751
            and self.current_path.status is EntityStatus.UP
1752
        ):
1753
            return True
1754 1
        return False
1755
1756 1
    def handle_link_up(self, link=None, interface=None):
1757
        """Handle circuit when link up.
1758
1759
        Args:
1760
            link(Link): Link affected by link.up event.
1761
1762
        """
1763 1
        condition_pairs = [
1764
            (
1765
                lambda me: me.is_using_primary_path(),
1766
                lambda _: (True, 'nothing')
1767
            ),
1768
            (
1769
                lambda me: me.is_intra_switch(),
1770
                lambda _: (True, 'nothing')
1771
            ),
1772
            (
1773
                lambda me: me.primary_path.is_affected_by_link(link),
1774
                lambda me: (me.deploy_to_primary_path(), 'redeploy')
1775
            ),
1776
            # For this special case, it reached this point because interface
1777
            # was previously confirmed to be a UNI and both UNI are UP
1778
            (
1779
                lambda me: (me.primary_path.status == EntityStatus.UP
1780
                            and interface),
1781
                lambda me: (me.deploy_to_primary_path(), 'redeploy')
1782
            ),
1783
            (
1784
                lambda me: (me.backup_path.status == EntityStatus.UP
1785
                            and interface),
1786
                lambda me: (me.deploy_to_backup_path(), 'redeploy')
1787
            ),
1788
            # We tried to deploy(primary_path) without success.
1789
            # And in this case is up by some how. Nothing to do.
1790
            (
1791
                lambda me: me.is_using_backup_path(),
1792
                lambda _: (True, 'nothing')
1793
            ),
1794
            (
1795
                lambda me:  me.is_using_dynamic_path(),
1796
                lambda _: (True, 'nothing')
1797
            ),
1798
            # In this case, probably the circuit is not being used and
1799
            # we can move to backup
1800
            (
1801
                lambda me: me.backup_path.is_affected_by_link(link),
1802
                lambda me: (me.deploy_to_backup_path(), 'redeploy')
1803
            ),
1804
            # In this case, the circuit is not being used and we should
1805
            # try a dynamic path
1806
            (
1807
                lambda me: me.dynamic_backup_path and not me.is_active(),
1808
                lambda me: (me.deploy_to_path(), 'redeploy')
1809
            )
1810
        ]
1811 1
        for predicate, action in condition_pairs:
1812 1
            if not predicate(self):
1813 1
                continue
1814 1
            success, succcess_type = action(self)
1815 1
            if success:
1816 1
                if succcess_type == 'redeploy':
1817 1
                    emit_event(
1818
                        self._controller,
1819
                        "redeployed_link_up",
1820
                        content=map_evc_event_content(self)
1821
                    )
1822 1
                return True
1823 1
        return False
1824
1825 1
    def handle_link_down(self):
1826
        """Handle circuit when link down.
1827
1828
        Returns:
1829
            bool: True if the re-deploy was successly otherwise False.
1830
1831
        """
1832 1
        success = False
1833 1
        if self.is_using_primary_path():
1834 1
            success = self.deploy_to_backup_path()
1835 1
        elif self.is_using_backup_path():
1836 1
            success = self.deploy_to_primary_path()
1837
1838 1
        if not success and self.dynamic_backup_path:
1839 1
            success = self.deploy_to_path()
1840
1841 1
        if success:
1842 1
            log.debug(f"{self} deployed after link down.")
1843
        else:
1844 1
            self.remove_current_flows(sync=False)
1845 1
            self.deactivate()
1846 1
            self.sync()
1847 1
            log.debug(f"Failed to re-deploy {self} after link down.")
1848
1849 1
        return success
1850
1851 1
    def are_unis_active(self) -> bool:
1852
        """Determine whether this EVC should be active"""
1853 1
        interface_a = self.uni_a.interface
1854 1
        interface_z = self.uni_z.interface
1855 1
        active, _ = self.is_uni_interface_active(interface_a, interface_z)
1856 1
        return active
1857
1858 1
    def try_to_handle_uni_as_link_up(self, interface: Interface) -> bool:
1859
        """Try to handle UNI as link_up to trigger deployment."""
1860
        if (
1861
            self.current_path.status != EntityStatus.UP
1862
            and not self.is_intra_switch()
1863
        ):
1864
            succeeded = self.handle_link_up(interface=interface)
1865
            if succeeded:
1866
                msg = (
1867
                    f"Activated {self} due to successful "
1868
                    f"deployment triggered by {interface}"
1869
                )
1870
            else:
1871
                msg = (
1872
                    f"Couldn't activate {self} due to unsuccessful "
1873
                    f"deployment triggered by {interface}"
1874
                )
1875
            log.info(msg)
1876
            return True
1877
        return False
1878
1879 1
    def handle_interface_link_up(self, interface: Interface):
1880
        """
1881
        Handler for interface link_up events
1882
        """
1883 1
        if not _does_uni_affect_evc(self, interface, "up"):
1884 1
            return
1885 1
        if self.try_to_handle_uni_as_link_up(interface):
1886
            return
1887
1888 1
        interface_dicts = {
1889
            interface.id: {
1890
                'status': interface.status.value,
1891
                'status_reason': interface.status_reason,
1892
            }
1893
            for interface in (self.uni_a.interface, self.uni_z.interface)
1894
        }
1895 1
        try:
1896 1
            self.try_to_activate()
1897 1
            log.info(
1898
                f"Activating {self}. Interfaces: "
1899
                f"{interface_dicts}."
1900
            )
1901 1
            emit_event(self._controller, "uni_active_updated",
1902
                       content=map_evc_event_content(self))
1903 1
            self.sync()
1904
        except ActivationError as exc:
1905
            # On this ctx, no ActivationError isn't expected since the
1906
            # activation pre-requisites states were checked, so handled as err
1907
            log.error(f"ActivationError: {str(exc)} when handling {interface}")
1908
1909 1
    def handle_interface_link_down(self, interface):
1910
        """
1911
        Handler for interface link_down events
1912
        """
1913 1
        if not _does_uni_affect_evc(self, interface, "down"):
1914 1
            return
1915 1
        interface_dicts = {
1916
            interface.id: {
1917
                'status': interface.status.value,
1918
                'status_reason': interface.status_reason,
1919
            }
1920
            for interface in (self.uni_a.interface, self.uni_z.interface)
1921
            if interface.status != EntityStatus.UP
1922
        }
1923 1
        self.deactivate()
1924 1
        log.info(
1925
            f"Deactivating {self}. Interfaces: "
1926
            f"{interface_dicts}."
1927
        )
1928 1
        emit_event(self._controller, "uni_active_updated",
1929
                   content=map_evc_event_content(self))
1930 1
        self.sync()
1931
1932
1933 1
class EVC(LinkProtection):
1934
    """Class that represents a E-Line Virtual Connection."""
1935