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