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