Passed
Pull Request — master (#1267)
by
unknown
08:50
created

kytos.core.interface.Interface._add_tags()   F

Complexity

Conditions 14

Size

Total Lines 54
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 31
nop 3
dl 0
loc 54
rs 3.6
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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 kytos.core.interface.Interface._add_tags() 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.

1
"""Module with main classes related to Interfaces."""
2
import json
3
import logging
4
import operator
5
from collections import OrderedDict
6
from copy import deepcopy
7
from enum import Enum
8
from functools import reduce
9
from threading import Lock
10
from typing import Union
11
12
from pyof.v0x01.common.phy_port import Port as PortNo01
13
from pyof.v0x01.common.phy_port import PortFeatures as PortFeatures01
14
from pyof.v0x04.common.port import PortFeatures as PortFeatures04
15
from pyof.v0x04.common.port import PortNo as PortNo04
16
17
from kytos.core.common import EntityStatus, GenericEntity
18
from kytos.core.events import KytosEvent
19
from kytos.core.exceptions import (KytosSetTagRangeError,
20
                                   KytosTagsAreNotAvailable,
21
                                   KytosTagsNotInTagRanges,
22
                                   KytosTagtypeNotSupported)
23
from kytos.core.helpers import now
24
from kytos.core.id import InterfaceID
25
from kytos.core.tag_ranges import (find_index_add, find_index_remove,
26
                                   get_special_tags, get_validated_tags,
27
                                   range_addition, range_difference)
28
29
__all__ = ('Interface',)
30
31
LOG = logging.getLogger(__name__)
32
33
34
class TAGType(Enum):
35
    """Class that represents a TAG Type."""
36
37
    VLAN = 'vlan'
38
    VLAN_QINQ = 'vlan_qinq'
39
    MPLS = 'mpls'
40
41
42
class TAG:
43
    """Class that represents a TAG."""
44
45
    def __init__(self, tag_type: str, value: int):
46
        self.tag_type = TAGType(tag_type).value
47
        self.value = value
48
49
    def __eq__(self, other):
50
        if not other:
51
            return False
52
        return self.tag_type == other.tag_type and self.value == other.value
53
54
    def as_dict(self):
55
        """Return a dictionary representating a tag object."""
56
        return {'tag_type': self.tag_type, 'value': self.value}
57
58
    @classmethod
59
    def from_dict(cls, tag_dict):
60
        """Return a TAG instance from python dictionary."""
61
        return cls(tag_dict.get('tag_type'), tag_dict.get('value'))
62
63
    @classmethod
64
    def from_json(cls, tag_json):
65
        """Return a TAG instance from json."""
66
        return cls.from_dict(json.loads(tag_json))
67
68
    def as_json(self):
69
        """Return a json representating a tag object."""
70
        return json.dumps(self.as_dict())
71
72
    def __repr__(self):
73
        return f"TAG({self.tag_type!r}, {self.value!r})"
74
75
76
# pylint: disable=super-init-not-called
77
class TAGRange(TAG):
78
    """Class that represents an User-to-Network Interface with
79
     a tag value as a list."""
80
81
    def __init__(
82
        self,
83
        tag_type: str,
84
        value: list[list[int]],
85
        mask_list: list[Union[str, int]] = None
86
    ):
87
        self.tag_type = TAGType(tag_type).value
88
        self.value = value
89
        self.mask_list = mask_list or []
90
91
    def as_dict(self):
92
        """Return a dictionary representating a tag range object."""
93
        return {
94
            'tag_type': self.tag_type,
95
            'value': self.value,
96
            'mask_list': self.mask_list
97
        }
98
99
100
class Interface(GenericEntity):  # pylint: disable=too-many-instance-attributes
101
    """Interface Class used to abstract the network interfaces."""
102
103
    status_funcs = OrderedDict()
104
    status_reason_funcs = OrderedDict()
105
106
    # pylint: disable=too-many-arguments, too-many-public-methods
107
    def __init__(self, name, port_number, switch, address=None, state=None,
108
                 features=None, speed=None, config=None):
109
        """Assign the parameters to instance attributes.
110
111
        Args:
112
            name (string): name from this interface.
113
            port_number (int): port number from this interface.
114
            switch (:class:`~.core.switch.Switch`): Switch with this interface.
115
            address (|hw_address|): Port address from this interface.
116
            state (|port_stats|): Port Stat from interface. It will be
117
            deprecated.
118
            features (|port_features|): Port feature used to calculate link
119
                utilization from this interface. It will be deprecated.
120
            speed (int, float): Interface speed in bytes per second. Defaults
121
                to what is informed by the switch. Return ``None`` if not set
122
                and switch does not inform the speed.
123
            config(|port_config|): Port config used to indicate interface
124
                behavior. In general, the port config bits are set by the
125
                controller and are not changed by the switch. Options
126
                are: administratively down, ignore received packets, drop
127
                forwarded packets, and/or do not send packet-in messages.
128
129
        Attributes:
130
            available_tags (dict[str, list[list[int]]]): Contains the available
131
                tags integers in the current interface, the availability is
132
                represented as a list of ranges. These ranges are
133
                [inclusive, inclusive]. For example, [1, 5] represents
134
                [1, 2, 3, 4, 5].
135
            tag_ranges (dict[str, list[list[int]]]): Contains restrictions for
136
                available_tags. The list of ranges is the same type as in
137
                available_tags. Setting a new tag_ranges will required for
138
                available_tags to be resize.
139
140
        """
141
        self.name = name
142
        self.port_number = int(port_number)
143
        self.switch = switch
144
        self.address = address
145
        self.state = state
146
        self.features = features
147
        self.config = config
148
        self.nni = False
149
        self.endpoints = []
150
        self.stats = None
151
        self.link = None
152
        self.lldp = True
153
        self._id = InterfaceID(switch.id, port_number)
154
        self._custom_speed = speed
155
        self._tag_lock = Lock()
156
        self.available_tags = {'vlan': self.default_tag_values['vlan']}
157
        self.tag_ranges = {'vlan': self.default_tag_values['vlan']}
158
        self.special_available_tags = {'vlan': self.default_special_tags}
159
        self.special_tags = {'vlan': self.default_special_tags}
160
        self.set_available_tags_tag_ranges(
161
            self.available_tags, self.tag_ranges,
162
            self.special_available_tags, self.special_tags
163
        )
164
        super().__init__()
165
166
    def __repr__(self):
167
        return f"Interface('{self.name}', {self.port_number}, {self.switch!r})"
168
169
    def __eq__(self, other):
170
        """Compare Interface class with another instance."""
171
        if isinstance(other, str):
172
            return self.address == other
173
        if isinstance(other, Interface):
174
            return self.port_number == other.port_number and \
175
                self.switch.dpid == other.switch.dpid
176
        return False
177
178
    @property
179
    def id(self):  # pylint: disable=invalid-name
180
        """Return id from Interface instance.
181
182
        Returns:
183
            string: Interface id.
184
185
        """
186
        return self._id
187
188
    @property
189
    def uni(self):
190
        """Return if an interface is a user-to-network Interface."""
191
        return not self.nni
192
193 View Code Duplication
    @property
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
194
    def status(self):
195
        """Return the current status of the Entity."""
196
        state = super().status
197
        if state == EntityStatus.DISABLED:
198
            return state
199
200
        for status_func in self.status_funcs.values():
201
            if status_func(self) == EntityStatus.DOWN:
202
                return EntityStatus.DOWN
203
        return state
204
205
    @classmethod
206
    def register_status_func(cls, name: str, func):
207
        """Register status func given its name and a callable at setup time."""
208
        cls.status_funcs[name] = func
209
210
    @classmethod
211
    def register_status_reason_func(cls, name: str, func):
212
        """Register status reason func given its name
213
        and a callable at setup time."""
214
        cls.status_reason_funcs[name] = func
215
216
    @property
217
    def status_reason(self):
218
        """Return the reason behind the current status of the entity."""
219
        return reduce(
220
            operator.or_,
221
            map(
222
                lambda x: x(self),
223
                self.status_reason_funcs.values()
224
            ),
225
            super().status_reason
226
        )
227
228
    @property
229
    def default_tag_values(self) -> dict[str, list[list[int]]]:
230
        """Return a default list of ranges. Applicable to
231
        available_tags and tag_ranges."""
232
        default_values = {
233
            "vlan": [[1, 4095]],
234
            "vlan_qinq": [[1, 4095]],
235
            "mpls": [[1, 1048575]],
236
        }
237
        return default_values
238
239
    @property
240
    def default_special_tags(self) -> list[str]:
241
        """Reurn a default list of special tags. Applicable to
242
         special_available_tags and special_tags."""
243
        return ["untagged", "any"]
244
245
    def all_tags_available(self) -> bool:
246
        """Return True if all tags are avaiable (no tags used),
247
         False otherwise"""
248
        with self._tag_lock:
249
            return (self.available_tags == self.tag_ranges and
250
                    self.special_available_tags == self.special_tags)
251
252
    def set_tag_ranges(self, tag_ranges: list[list[int]], tag_type: str):
253
        """Set new restriction, tag_ranges."""
254
        if tag_type != TAGType.VLAN.value:
255
            msg = f"Tag type {tag_type} is not supported."
256
            raise KytosTagtypeNotSupported(msg)
257
        with self._tag_lock:
258
            used_tags = range_difference(
259
                self.tag_ranges[tag_type], self.available_tags[tag_type]
260
            )
261
            # Verify new tag_ranges
262
            missing = range_difference(used_tags, tag_ranges)
263
            if missing:
264
                msg = f"Missing tags in tag_range: {missing}"
265
                raise KytosSetTagRangeError(msg)
266
267
            # Resizing
268
            new_available_tags = range_difference(
269
                tag_ranges, used_tags
270
            )
271
            self.available_tags[tag_type] = new_available_tags
272
            self.tag_ranges[tag_type] = tag_ranges
273
274
    def remove_tag_ranges(self, tag_type: str):
275
        """Set tag_ranges[tag_type] to default value"""
276
        if tag_type != TAGType.VLAN.value:
277
            msg = f"Tag type {tag_type} is not supported."
278
            raise KytosTagtypeNotSupported(msg)
279
        with self._tag_lock:
280
            used_tags = range_difference(
281
                self.tag_ranges[tag_type], self.available_tags[tag_type]
282
            )
283
            self.available_tags[tag_type] = range_difference(
284
                self.default_tag_values[tag_type], used_tags
285
            )
286
            self.tag_ranges[tag_type] = self.default_tag_values[tag_type]
287
288
    def set_special_tags(
289
        self,
290
        tag_type: str,
291
        special_tags: list[str],
292
    ):
293
        """Set new restriction, special_tags"""
294
        # Verify values in special_tags
295
        tag_range = get_special_tags(
296
            special_tags, self.default_special_tags
297
        )
298
299
        if tag_type != TAGType.VLAN.value:
300
            msg = f"Tag type {tag_type} is not supported."
301
            raise KytosTagtypeNotSupported(msg)
302
        old_special_set = set(self.special_tags[tag_type])
303
304
        for tag in self.special_available_tags[tag_type]:
305
            old_special_set.remove(tag)  # Get special used tags
306
        used_special = old_special_set.copy()
307
308
        for tag in tag_range:
309
            used_special.discard(tag)
310
311
        # Missing used special used tags
312
        if used_special:
313
            msg = f"Missing tags in tag_range: {used_special}"
314
            raise KytosSetTagRangeError(msg)
315
316
        new_special_available = set(tag_range)
317
        self.special_available_tags[tag_type] = list(
318
            new_special_available - old_special_set
319
        )
320
        self.special_tags[tag_type] = tag_range
321
322
    def _remove_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool:
323
        """Remove tags by resizing available_tags
324
        Returns False if nothing was remove, True otherwise"""
325
        available = self.available_tags[tag_type]
326
        if not available:
327
            return False
328
        index = find_index_remove(available, tags)
329
        if index is None:
330
            return False
331
        # Resizing
332
        if tags[0] == available[index][0]:
333
            if tags[1] == available[index][1]:
334
                available.pop(index)
335
            else:
336
                available[index][0] = tags[1] + 1
337
        elif tags[1] == available[index][1]:
338
            available[index][1] = tags[0] - 1
339
        else:
340
            available[index: index+1] = [
341
                [available[index][0], tags[0]-1],
342
                [tags[1]+1, available[index][1]]
343
            ]
344
        return True
345
346
    def use_tags(
347
        self,
348
        controller,
349
        tags: Union[str, int, list[int], list[list[int]]],
350
        tag_type: str = 'vlan',
351
        use_lock: bool = True,
352
        check_order: bool = True,
353
    ):
354
        """Remove a specific tag from available_tags if it is there.
355
        Exception raised in case the tags were not able to be removed.
356
357
        Args:
358
            controller: Kytos controller
359
            tags: value to be removed, multiple types for compatibility:
360
                (str): Special vlan, "untagged" or "vlan"
361
                (int): Single tag
362
                (list[int]): Single range of tags
363
                (list[list[int]]): List of ranges of tags
364
            tag_type: TAG type value
365
            use_lock: Boolean to whether use a lock or not
366
            check_order: Boolean to whether validate tags(list). Check order,
367
                type and length. Set to false when invocated internally.
368
369
        Exceptions:
370
            KytosTagsAreNotAvailable from _use_tags()
371
        """
372
        if isinstance(tags, int):
373
            tags = [tags] * 2
374
        elif check_order and not isinstance(tags, str):
375
            tags = get_validated_tags(tags)
376
        if use_lock:
377
            with self._tag_lock:
378
                try:
379
                    self._use_tags(tags, tag_type)
380
                except KeyError as err:
381
                    msg = f"Tag type {err.args[0]} is not supported"
382
                    raise KytosTagtypeNotSupported(msg)
383
        else:
384
            try:
385
                self._use_tags(tags, tag_type)
386
            except KeyError as err:
387
                msg = f"Tag type {err.args[0]} is not supported"
388
                raise KytosTagtypeNotSupported(msg)
389
390
        self._notify_interface_tags(controller)
391
392
    def _use_tags(
393
        self,
394
        tags: Union[str, list[int], list[list[int]]],
395
        tag_type: str
396
    ):
397
        """Manage available_tags deletion changes."""
398
        if isinstance(tags[0], list):
399
            available_copy = deepcopy(self.available_tags[tag_type])
400
            for tag_range in tags:
401
                result = self._remove_tags(tag_range, tag_type)
402
                if result is False:
403
                    self.available_tags[tag_type] = available_copy
404
                    conflict = range_difference(tags, available_copy)
405
                    raise KytosTagsAreNotAvailable(conflict, self._id)
406
        elif isinstance(tags, list):
407
            result = self._remove_tags(tags, tag_type)
408
            if result is False:
409
                raise KytosTagsAreNotAvailable([tags], self._id)
410
        else:
411
            try:
412
                self.special_available_tags[tag_type].remove(tags)
413
            except ValueError:
414
                raise KytosTagsAreNotAvailable(tags, self.id)
415
416
    # pylint: disable=too-many-branches
417
    def _add_tags(self, tags: list[int], tag_type: str = 'vlan') -> bool:
418
        """Add tags, return True if they were added.
419
        Returns False when nothing was added, True otherwise
420
        Ensuring that ranges are not unnecessarily divided
421
        available_tag e.g [[7, 10], [20, 30], [78, 92], [100, 109], [189, 200]]
422
        tags examples are in each if statement.
423
        """
424
        if not tags[0] or not tags[1]:
425
            return False
426
427
        # Check if tags is within self.tag_ranges
428
        tag_ranges = self.tag_ranges[tag_type]
429
        if find_index_remove(tag_ranges, tags) is None:
430
            raise KytosTagsNotInTagRanges([tags], self._id)
431
432
        available = self.available_tags[tag_type]
433
        if not available:
434
            self.available_tags[tag_type] = [tags]
435
            return True
436
437
        index = find_index_add(available, tags)
438
        if index is None:
439
            return False
440
        if index == 0:
441
            # [1, 6]
442
            if tags[1] == available[index][0] - 1:
443
                available[index][0] = tags[0]
444
            # [1, 2]
445
            else:
446
                available.insert(0, tags)
447
        elif index == len(available):
448
            # [201, 300]
449
            if available[index-1][1] + 1 == tags[0]:
450
                available[index-1][1] = tags[1]
451
            # [250, 300]
452
            else:
453
                available.append(tags)
454
        else:
455
            # [11, 19]
456
            if (available[index-1][1] + 1 == tags[0] and
457
                    available[index][0] - 1 == tags[1]):
458
                available[index-1: index+1] = [
459
                    [available[index-1][0], available[index][1]]
460
                ]
461
            # [11, 15]
462
            elif available[index-1][1] + 1 == tags[0]:
463
                available[index-1][1] = tags[1]
464
            # [15, 19]
465
            elif available[index][0] - 1 == tags[1]:
466
                available[index][0] = tags[0]
467
            # [15, 15]
468
            else:
469
                available.insert(index, tags)
470
        return True
471
472
    def make_tags_available(
473
        self,
474
        controller,
475
        tags: Union[str,  int, list[int], list[list[int]]],
476
        tag_type: str = 'vlan',
477
        use_lock: bool = True,
478
        check_order: bool = True,
479
    ) -> list[list[int]]:
480
        """Add a tags in available_tags.
481
482
        Args:
483
            controller: Kytos controller
484
            tags: value to be added, multiple types for compatibility:
485
                (str): Special vlan, "untagged" or "vlan"
486
                (int): Single tag
487
                (list[int]): Single range of tags
488
                (list[list[int]]): List of ranges of tags
489
            tag_type: TAG type value
490
            use_lock: Boolean to whether use a lock or not
491
            check_order: Boolean to whether validate tags(list). Check order,
492
                type and length. Set to false when invocated internally.
493
494
        Return:
495
            conflict: Return any values that were not added.
496
497
        Exeptions:
498
            KytosTagsNotInTagRanges from _make_tags_available()
499
        """
500
        if isinstance(tags, int):
501
            tags = [tags] * 2
502
        elif check_order and not isinstance(tags, str):
503
            tags = get_validated_tags(tags)
504
        if isinstance(tags[0], int) and tags[0] != tags[1]:
505
            tags = [tags]
506
        if use_lock:
507
            with self._tag_lock:
508
                try:
509
                    conflict = self._make_tags_available(tags, tag_type)
510
                except KeyError as err:
511
                    msg = f"Tag type {err.args[0]} is not supported"
512
                    raise KytosTagtypeNotSupported(msg)
513
        else:
514
            try:
515
                conflict = self._make_tags_available(tags, tag_type)
516
            except KeyError as err:
517
                msg = f"Tag type {err.args[0]} is not supported"
518
                raise KytosTagtypeNotSupported(msg)
519
        self._notify_interface_tags(controller)
520
        return conflict
521
522
    def _make_tags_available(
523
        self,
524
        tags: Union[str, list[int], list[list[int]]],
525
        tag_type: str,
526
    ) -> Union[str, list[list[int]], None]:
527
        """Manage available_tags adittion changes
528
529
        Exceptions:
530
            KytosTagsNotInTagRanges from _add_tags()
531
        """
532
        if isinstance(tags[0], list):
533
            diff = range_difference(tags, self.tag_ranges[tag_type])
534
            if diff:
535
                raise KytosTagsNotInTagRanges(diff, self._id)
536
            available_tags = self.available_tags[tag_type]
537
            new_tags, conflict = range_addition(tags, available_tags)
538
            self.available_tags[tag_type] = new_tags
539
            return conflict
540
        if isinstance(tags, list):
541
            result = self._add_tags(tags, tag_type)
542
            if result is False:
543
                return [tags]
544
        if isinstance(tags, str):
545
            if tags not in self.special_tags[tag_type]:
546
                raise KytosTagsNotInTagRanges(tags, self._id)
547
            if tags not in self.special_available_tags[tag_type]:
548
                self.special_available_tags[tag_type].append(tags)
549
                return None
550
            return tags
551
        return None
552
553
    def set_available_tags_tag_ranges(
554
        self,
555
        available_tag: dict[str, list[list[int]]],
556
        tag_ranges: dict[str, list[list[int]]],
557
        special_available_tags: dict[str, list[str]],
558
        special_tags: dict[str, list[str]]
559
    ):
560
        """Set a range of VLAN tags to be used by this Interface.
561
562
        Args:
563
            available_tag: Available tags from each tag type
564
            tag_ranges: Restriction for each type of available tag
565
        """
566
        with self._tag_lock:
567
            self.available_tags = available_tag
568
            self.tag_ranges = tag_ranges
569
            self.special_available_tags = special_available_tags
570
            self.special_tags = special_tags
571
572
    def is_tag_available(self, tag: int, tag_type: str = 'vlan'):
573
        """Check if a tag is available."""
574
        with self._tag_lock:
575
            if find_index_remove(
576
                    self.available_tags[tag_type], [tag, tag]
577
            ) is not None:
578
                return True
579
            return False
580
581
    def get_endpoint(self, endpoint):
582
        """Return a tuple with existent endpoint, None otherwise.
583
584
        Args:
585
            endpoint(|hw_address|, :class:`.Interface`): endpoint instance.
586
587
        Returns:
588
            tuple: A tuple with endpoint and time of last update.
589
590
        """
591
        for item in self.endpoints:
592
            if endpoint == item[0]:
593
                return item
594
        return None
595
596
    def add_endpoint(self, endpoint):
597
        """Create a new endpoint to Interface instance.
598
599
        Args:
600
            endpoint(|hw_address|, :class:`.Interface`): A target endpoint.
601
        """
602
        exists = self.get_endpoint(endpoint)
603
        if not exists:
604
            self.endpoints.append((endpoint, now()))
605
606
    def delete_endpoint(self, endpoint):
607
        """Delete a existent endpoint in Interface instance.
608
609
        Args:
610
            endpoint (|hw_address|, :class:`.Interface`): A target endpoint.
611
        """
612
        exists = self.get_endpoint(endpoint)
613
        if exists:
614
            self.endpoints.remove(exists)
615
616
    def update_endpoint(self, endpoint):
617
        """Update or create new endpoint to Interface instance.
618
619
        Args:
620
            endpoint(|hw_address|, :class:`.Interface`): A target endpoint.
621
        """
622
        exists = self.get_endpoint(endpoint)
623
        if exists:
624
            self.delete_endpoint(endpoint)
625
        self.add_endpoint(endpoint)
626
627
    def update_link(self, link):
628
        """Update link for this interface in a consistent way.
629
630
        Verify of the other endpoint of the link has the same Link information
631
        attached to it, and change it if necessary.
632
633
        Warning: This method can potentially change information of other
634
        Interface instances. Use it with caution.
635
        """
636
        if self not in (link.endpoint_a, link.endpoint_b):
637
            return False
638
639
        if self.link is None or self.link != link:
640
            self.link = link
641
642
        if link.endpoint_a == self:
643
            endpoint = link.endpoint_b
644
        else:
645
            endpoint = link.endpoint_a
646
647
        if endpoint.link is None or endpoint.link != link:
648
            endpoint.link = link
649
650
        return True
651
652
    @property
653
    def speed(self):
654
        """Return the link speed in bytes per second, None otherwise.
655
656
        If the switch was disconnected, we have :attr:`features` and speed is
657
        still returned for common values between v0x01 and v0x04. For specific
658
        v0x04 values (40 Gbps, 100 Gbps and 1 Tbps), the connection must be
659
        active so we can make sure the protocol version is v0x04.
660
661
        Returns:
662
            int, None: Link speed in bytes per second or ``None``.
663
664
        """
665
        speed = self.get_of_features_speed()
666
667
        if speed is not None:
668
            return speed
669
670
        if self._custom_speed is not None:
671
            return self._custom_speed
672
673
        if self._is_v0x04() and self.port_number == PortNo04.OFPP_LOCAL:
674
            return 0
675
676
        if not self._is_v0x04() and self.port_number == PortNo01.OFPP_LOCAL:
677
            return 0
678
679
        # Warn unknown speed
680
        # Use shorter switch ID with its beginning and end
681
        if isinstance(self.switch.id, str) and len(self.switch.id) > 20:
682
            switch_id = self.switch.id[:3] + '...' + self.switch.id[-3:]
683
        else:
684
            switch_id = self.switch.id
685
        LOG.warning("Couldn't get port %s speed, sw %s, feats %s",
686
                    self.port_number, switch_id, self.features)
687
688
        return None
689
690
    def set_custom_speed(self, bytes_per_second):
691
        """Set a speed that overrides switch OpenFlow information.
692
693
        If ``None`` is given, :attr:`speed` becomes the one given by the
694
        switch.
695
        """
696
        self._custom_speed = bytes_per_second
697
698
    def get_custom_speed(self):
699
        """Return custom speed or ``None`` if not set."""
700
        return self._custom_speed
701
702
    def get_of_features_speed(self):
703
        """Return the link speed in bytes per second, None otherwise.
704
705
        If the switch was disconnected, we have :attr:`features` and speed is
706
        still returned for common values between v0x01 and v0x04. For specific
707
        v0x04 values (40 Gbps, 100 Gbps and 1 Tbps), the connection must be
708
        active so we can make sure the protocol version is v0x04.
709
710
        Returns:
711
            int, None: Link speed in bytes per second or ``None``.
712
713
        """
714
        speed = self._get_v0x01_v0x04_speed()
715
        # Don't use switch.is_connected() because we can have the protocol
716
        if speed is None and self._is_v0x04():
717
            speed = self._get_v0x04_speed()
718
        return speed
719
720
    def _is_v0x04(self):
721
        """Whether the switch is connected using OpenFlow 1.3."""
722
        return self.switch.is_connected() and \
723
            self.switch.connection.protocol.version == 0x04
724
725
    def _get_v0x01_v0x04_speed(self):
726
        """Check against all values of v0x01. They're part of v0x04."""
727
        fts = self.features
728
        pfts = PortFeatures01
729
        if fts and fts & pfts.OFPPF_10GB_FD:
730
            return 10 * 10**9 / 8
731
        if fts and fts & (pfts.OFPPF_1GB_HD | pfts.OFPPF_1GB_FD):
732
            return 10**9 / 8
733
        if fts and fts & (pfts.OFPPF_100MB_HD | pfts.OFPPF_100MB_FD):
734
            return 100 * 10**6 / 8
735
        if fts and fts & (pfts.OFPPF_10MB_HD | pfts.OFPPF_10MB_FD):
736
            return 10 * 10**6 / 8
737
        return None
738
739
    def _get_v0x04_speed(self):
740
        """Check against higher enums of v0x04.
741
742
        Must be called after :meth:`get_v0x01_speed` returns ``None``.
743
        """
744
        fts = self.features
745
        pfts = PortFeatures04
746
        if fts and fts & pfts.OFPPF_1TB_FD:
747
            return 10**12 / 8
748
        if fts and fts & pfts.OFPPF_100GB_FD:
749
            return 100 * 10**9 / 8
750
        if fts and fts & pfts.OFPPF_40GB_FD:
751
            return 40 * 10**9 / 8
752
        return None
753
754
    def get_hr_speed(self):
755
        """Return Human-Readable string for link speed.
756
757
        Returns:
758
            string: String with link speed. e.g: '350 Gbps' or '350 Mbps'.
759
760
        """
761
        speed = self.speed
762
        if speed is None:
763
            return ''
764
        speed *= 8
765
        if speed == 10**12:
766
            return '1 Tbps'
767
        if speed >= 10**9:
768
            return f"{round(speed / 10**9)} Gbps"
769
        return f"{round(speed / 10**6)} Mbps"
770
771
    def as_dict(self):
772
        """Return a dictionary with Interface attributes.
773
774
        Speed is in bytes/sec. Example of output (100 Gbps):
775
776
        .. code-block:: python3
777
778
            {'id': '00:00:00:00:00:00:00:01:2',
779
             'name': 'eth01',
780
             'port_number': 2,
781
             'mac': '00:7e:04:3b:c2:a6',
782
             'switch': '00:00:00:00:00:00:00:01',
783
             'type': 'interface',
784
             'nni': False,
785
             'uni': True,
786
             'speed': 12500000000,
787
             'metadata': {},
788
             'lldp': True,
789
             'active': True,
790
             'enabled': False,
791
             'status': 'DISABLED',
792
             'link': ""
793
            }
794
795
        Returns:
796
            dict: Dictionary filled with interface attributes.
797
798
        """
799
        iface_dict = {
800
            'id': self.id,
801
            'name': self.name,
802
            'port_number': self.port_number,
803
            'mac': self.address,
804
            'switch': self.switch.dpid,
805
            'type': 'interface',
806
            'nni': self.nni,
807
            'uni': self.uni,
808
            'speed': self.speed,
809
            'metadata': self.metadata,
810
            'lldp': self.lldp,
811
            'active': self.is_active(),
812
            'enabled': self.is_enabled(),
813
            'status': self.status.value,
814
            'status_reason': sorted(self.status_reason),
815
            'link': self.link.id if self.link else "",
816
        }
817
        if self.stats:
818
            iface_dict['stats'] = self.stats.as_dict()
819
        return iface_dict
820
821
    @classmethod
822
    def from_dict(cls, interface_dict):
823
        """Return a Interface instance from python dictionary."""
824
        return cls(interface_dict.get('name'),
825
                   interface_dict.get('port_number'),
826
                   interface_dict.get('switch'),
827
                   interface_dict.get('address'),
828
                   interface_dict.get('state'),
829
                   interface_dict.get('features'),
830
                   interface_dict.get('speed'))
831
832
    def as_json(self):
833
        """Return a json with Interfaces attributes.
834
835
        Example of output:
836
837
        .. code-block:: json
838
839
            {"mac": "00:7e:04:3b:c2:a6",
840
             "switch": "00:00:00:00:00:00:00:01",
841
             "type": "interface",
842
             "name": "eth01",
843
             "id": "00:00:00:00:00:00:00:01:2",
844
             "port_number": 2,
845
             "speed": "350 Mbps"}
846
847
        Returns:
848
            string: Json filled with interface attributes.
849
850
        """
851
        return json.dumps(self.as_dict())
852
853
    def _notify_interface_tags(self, controller):
854
        """Notify link available tags"""
855
        name = "kytos/core.interface_tags"
856
        content = {"interface": self}
857
        event = KytosEvent(name=name, content=content)
858
        controller.buffers.app.put(event)
859
860
861
class UNI:
862
    """Class that represents an User-to-Network Interface."""
863
864
    def __init__(
865
        self,
866
        interface: Interface,
867
        user_tag: Union[None, TAG, TAGRange]
868
    ):
869
        self.user_tag = user_tag
870
        self.interface = interface
871
872
    def __eq__(self, other):
873
        """Override the default implementation."""
874
        return (self.user_tag == other.user_tag and
875
                self.interface == other.interface)
876
877
    def _is_reserved_valid_tag(self) -> bool:
878
        """Check if TAG string is possible"""
879
        reserved_tag = {"any", "untagged"}
880
        if self.user_tag.value in reserved_tag:
881
            return True
882
        return False
883
884
    def is_valid(self):
885
        """Check if TAG is possible for this interface TAG pool."""
886
        if self.user_tag:
887
            tag = self.user_tag.value
888
            if isinstance(tag, str):
889
                return self._is_reserved_valid_tag()
890
            if isinstance(tag, int):
891
                return self.interface.is_tag_available(tag)
892
        return True
893
894
    def as_dict(self):
895
        """Return a dict representating a UNI object."""
896
        return {
897
            'interface_id': self.interface.id,
898
            'tag': self.user_tag.as_dict() if self.user_tag else None
899
            }
900
901
    @classmethod
902
    def from_dict(cls, uni):
903
        """Return a Uni instance from python dictionary."""
904
        return cls(uni.get('interface'),
905
                   uni.get('user_tag'))
906
907
    def as_json(self):
908
        """Return a json representating a UNI object."""
909
        return json.dumps(self.as_dict())
910
911
912
class NNI:
913
    """Class that represents an Network-to-Network Interface."""
914
915
    def __init__(self, interface):
916
        self.interface = interface
917
918
919
class VNNI(NNI):
920
    """Class that represents an Virtual Network-to-Network Interface."""
921
922
    def __init__(self, service_tag, *args, **kwargs):
923
        self.service_tag = service_tag
924
925
        super().__init__(*args, **kwargs)
926