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