Passed
Pull Request — master (#651)
by Carlos Eduardo
02:24
created

Interface.speed()   A

Complexity

Conditions 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 16
ccs 4
cts 4
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
"""Module with main classes related to Switches."""
2 1
import json
3 1
import logging
4
5 1
from pyof.v0x01.common.phy_port import PortFeatures as PortFeatures01
6 1
from pyof.v0x04.common.port import PortFeatures as PortFeatures04
7
8 1
from kytos.core.constants import CONNECTION_TIMEOUT, FLOOD_TIMEOUT
9 1
from kytos.core.helpers import now
10
11 1
__all__ = ('Interface', 'Switch')
12
13 1
LOG = logging.getLogger(__name__)
14
15
16 1
class Interface:  # pylint: disable=too-many-instance-attributes
17
    """Interface Class used to abstract the network interfaces."""
18
19
    # pylint: disable=too-many-arguments
20 1
    def __init__(self, name, port_number, switch, address=None, state=None,
21
                 features=None, speed=None):
22
        """Assign the parameters to instance attributes.
23
24
        Args:
25
            name (string): name from this interface.
26
            port_number (int): port number from this interface.
27
            switch (:class:`~.core.switch.Switch`): Switch with this interface.
28
            address (|hw_address|): Port address from this interface.
29
            state (|port_stats|): Port Stat from interface.
30
            features (|port_features|): Port feature used to calculate link
31
                utilization from this interface.
32
            speed (int, float): Interface speed in bytes per second. Defaults
33
                to what is informed by the switch. Return ``None`` if not set
34
                and switch does not inform the speed.
35
        """
36 1
        self.name = name
37 1
        self.port_number = int(port_number)
38 1
        self.switch = switch
39 1
        self.address = address
40 1
        self.state = state
41 1
        self.features = features
42 1
        self.nni = False
43 1
        self.endpoints = []
44 1
        self.stats = None
45 1
        self._custom_speed = speed
46
47 1
    def __eq__(self, other):
48
        """Compare Interface class with another instance."""
49
        if isinstance(other, str):
50
            return self.address == other
51
        elif isinstance(other, Interface):
52
            return self.port_number == other.port_number and \
53
                self.name == other.name and \
54
                self.address == other.address and \
55
                self.switch.dpid == other.switch.dpid
56
        return False
57
58 1
    @property
59
    def id(self):  # pylint: disable=invalid-name
60
        """Return id from Interface intance.
61
62
        Returns:
63
            string: Interface id.
64
65
        """
66
        return "{}:{}".format(self.switch.dpid, self.port_number)
67
68 1
    @property
69
    def uni(self):
70
        """Return if an interface is a user-to-network Interface."""
71
        return not self.nni
72
73 1
    def get_endpoint(self, endpoint):
74
        """Return a tuple with existent endpoint, None otherwise.
75
76
        Args:
77
            endpoint(|hw_address|, :class:`.Interface`): endpoint instance.
78
79
        Returns:
80
            tuple: A tuple with endpoint and time of last update.
81
82
        """
83
        for item in self.endpoints:
84
            if endpoint == item[0]:
85
                return item
86
        return None
87
88 1
    def add_endpoint(self, endpoint):
89
        """Create a new endpoint to Interface instance.
90
91
        Args:
92
            endpoint(|hw_address|, :class:`.Interface`): A target endpoint.
93
        """
94
        exists = self.get_endpoint(endpoint)
95
        if not exists:
96
            self.endpoints.append((endpoint, now()))
97
98 1
    def delete_endpoint(self, endpoint):
99
        """Delete a existent endpoint in Interface instance.
100
101
        Args:
102
            endpoint (|hw_address|, :class:`.Interface`): A target endpoint.
103
        """
104
        exists = self.get_endpoint(endpoint)
105
        if exists:
106
            self.endpoints.remove(exists)
107
108 1
    def update_endpoint(self, endpoint):
109
        """Update or create new endpoint to Interface instance.
110
111
        Args:
112
            endpoint(|hw_address|, :class:`.Interface`): A target endpoint.
113
        """
114
        exists = self.get_endpoint(endpoint)
115
        if exists:
116
            self.delete_endpoint(endpoint)
117
        self.add_endpoint(endpoint)
118
119 1
    @property
120
    def speed(self):
121
        """Return the link speed in bytes per second, None otherwise.
122
123
        If the switch was disconnected, we have :attr:`features` and speed is
124
        still returned for common values between v0x01 and v0x04. For specific
125
        v0x04 values (40 Gbps, 100 Gbps and 1 Tbps), the connection must be
126
        active so we can make sure the protocol version is v0x04.
127
128
        Returns:
129
            int, None: Link speed in bytes per second or ``None``.
130
131
        """
132 1
        if self._custom_speed is not None:
133 1
            return self._custom_speed
134 1
        return self.get_of_features_speed()
135
136 1
    def set_custom_speed(self, bytes_per_second):
137
        """Set a speed that overrides switch OpenFlow information.
138
139
        If ``None`` is given, :attr:`speed` becomes the one given by the
140
        switch.
141
        """
142 1
        self._custom_speed = bytes_per_second
143
144 1
    def get_custom_speed(self):
145
        """Return custom speed or ``None`` if not set."""
146
        return self._custom_speed
147
148 1
    def get_of_features_speed(self):
149
        """Return the link speed in bytes per second, None otherwise.
150
151
        If the switch was disconnected, we have :attr:`features` and speed is
152
        still returned for common values between v0x01 and v0x04. For specific
153
        v0x04 values (40 Gbps, 100 Gbps and 1 Tbps), the connection must be
154
        active so we can make sure the protocol version is v0x04.
155
156
        Returns:
157
            int, None: Link speed in bytes per second or ``None``.
158
159
        """
160 1
        speed = self._get_v0x01_v0x04_speed()
161
        # Don't use switch.is_connected() because we can have the protocol
162 1
        if speed is None and self._is_v0x04():
163 1
            speed = self._get_v0x04_speed()
164 1
        if speed is not None:
165 1
            return speed
166
        # Warn unknown speed
167
        # Use shorter switch ID with its beginning and end
168 1
        if isinstance(self.switch.id, str) and len(self.switch.id) > 20:
169
            switch_id = self.switch.id[:3] + '...' + self.switch.id[-3:]
170
        else:
171 1
            switch_id = self.switch.id
172 1
        LOG.warning("Couldn't get port %s speed, sw %s, feats %s",
173
                    self.port_number, switch_id, self.features)
174
175 1
    def _is_v0x04(self):
176
        """Whether the switch is connected using OpenFlow 1.3."""
177 1
        return self.switch.is_connected() and \
178
            self.switch.connection.protocol.version == 0x04
179
180 1
    def _get_v0x01_v0x04_speed(self):
181
        """Check against all values of v0x01. They're part of v0x04."""
182 1
        fts = self.features
183 1
        pfts = PortFeatures01
184 1
        if fts and fts & pfts.OFPPF_10GB_FD:
185 1
            return 10 * 10**9 / 8
186 1
        elif fts and fts & (pfts.OFPPF_1GB_HD | pfts.OFPPF_1GB_FD):
187 1
            return 10**9 / 8
188 1
        elif fts and fts & (pfts.OFPPF_100MB_HD | pfts.OFPPF_100MB_FD):
189 1
            return 100 * 10**6 / 8
190 1
        elif fts and fts & (pfts.OFPPF_10MB_HD | pfts.OFPPF_10MB_FD):
191 1
            return 10 * 10**6 / 8
192
193 1
    def _get_v0x04_speed(self):
194
        """Check against higher enums of v0x04.
195
196
        Must be called after :meth:`get_v0x01_speed` returns ``None``.
197
        """
198 1
        fts = self.features
199 1
        pfts = PortFeatures04
200 1
        if fts and fts & pfts.OFPPF_1TB_FD:
201 1
            return 10**12 / 8
202 1
        elif fts and fts & pfts.OFPPF_100GB_FD:
203 1
            return 100 * 10**9 / 8
204 1
        elif fts and fts & pfts.OFPPF_40GB_FD:
205 1
            return 40 * 10**9 / 8
206
207 1
    def get_hr_speed(self):
208
        """Return Human-Readable string for link speed.
209
210
        Returns:
211
            string: String with link speed. e.g: '350 Gbps' or '350 Mbps'.
212
213
        """
214 1
        speed = self.speed
215 1
        if speed is None:
216 1
            return ''
217 1
        speed *= 8
218 1
        if speed == 10**12:
219 1
            return '1 Tbps'
220 1
        if speed >= 10**9:
221 1
            return '{} Gbps'.format(round(speed / 10**9))
222 1
        return '{} Mbps'.format(round(speed / 10**6))
223
224 1
    def as_dict(self):
225
        """Return a dictionary with Interface attributes.
226
227
        Speed is in bytes/sec. Example of output (100 Gbps):
228
229
        .. code-block:: python3
230
231
            {'id': '00:00:00:00:00:00:00:01:2',
232
             'name': 'eth01',
233
             'port_number': 2,
234
             'mac': '00:7e:04:3b:c2:a6',
235
             'switch': '00:00:00:00:00:00:00:01',
236
             'type': 'interface',
237
             'nni': False,
238
             'uni': True,
239
             'speed': 12500000000}
240
241
        Returns:
242
            dict: Dictionary filled with interface attributes.
243
244
        """
245
        iface_dict = {'id': self.id,
246
                      'name': self.name,
247
                      'port_number': self.port_number,
248
                      'mac': self.address,
249
                      'switch': self.switch.dpid,
250
                      'type': 'interface',
251
                      'nni': self.nni,
252
                      'uni': self.uni,
253
                      'speed': self.speed}
254
        if self.stats:
255
            iface_dict['stats'] = self.stats.as_dict()
256
        return iface_dict
257
258 1
    def as_json(self):
259
        """Return a json with Interfaces attributes.
260
261
        Example of output:
262
263
        .. code-block:: json
264
265
            {"mac": "00:7e:04:3b:c2:a6",
266
             "switch": "00:00:00:00:00:00:00:01",
267
             "type": "interface",
268
             "name": "eth01",
269
             "id": "00:00:00:00:00:00:00:01:2",
270
             "port_number": 2,
271
             "speed": "350 Mbps"}
272
273
        Returns:
274
            string: Json filled with interface attributes.
275
276
        """
277
        return json.dumps(self.as_dict())
278
279
280 1
class Switch:  # pylint: disable=too-many-instance-attributes
281
    """Switch class is a abstraction from switches.
282
283
    A new Switch will be created every time the handshake process is done
284
    (after receiving the first FeaturesReply). Considering this, the
285
    :attr:`socket`, :attr:`connection_id`, :attr:`of_version` and
286
    :attr:`features` need to be passed on init. But when the connection to the
287
    switch is lost, then this attributes can be set to None (actually some of
288
    them must be).
289
290
    The :attr:`dpid` attribute will be the unique identifier of a Switch.
291
    It is the :attr:`pyof.*.controller2switch.SwitchFeatures.datapath-id` that
292
    defined by the OpenFlow Specification, it is a 64 bit field that should be
293
    thought of as analogous to a Ethernet Switches bridge MAC, its a unique
294
    identifier for the specific packet processing pipeline being managed. One
295
    physical switch may have more than one datapath-id (think virtualization of
296
    the switch).
297
298
    :attr:`socket` is the request from a TCP connection, it represents the
299
    effective connection between the switch and the controller.
300
301
    :attr:`connection_id` is a tuple, composed by the ip and port of the
302
    stabilished connection (if any). It will be used to help map the connection
303
    to the Switch and vice-versa.
304
305
    :attr:`ofp_version` is a string representing the accorded version of
306
    python-openflow that will be used on the communication between the
307
    Controller and the Switch.
308
309
    :attr:`features` is an instance of
310
    :class:`pyof.*.controller2switch.FeaturesReply` representing the current
311
    featues of the switch.
312
    """
313
314 1
    def __init__(self, dpid, connection=None, features=None):
315
        """Contructor of switches have the below parameters.
316
317
        Args:
318
          dpid (|DPID|): datapath_id of the switch
319
          connection (:class:`~.Connection`): Connection used by switch.
320
          features (|features_reply|): FeaturesReply instance.
321
322
        """
323 1
        self.dpid = dpid
324 1
        self.connection = connection
325 1
        self.features = features
326 1
        self.firstseen = now()
327 1
        self.lastseen = now()
328 1
        self.sent_xid = None
329 1
        self.waiting_for_reply = False
330 1
        self.request_timestamp = 0
331
        #: Dict associating mac addresses to switch ports.
332
        #:      the key of this dict is a mac_address, and the value is a set
333
        #:      containing the ports of this switch in which that mac can be
334
        #:      found.
335 1
        self.mac2port = {}
336
        #: This flood_table will keep track of flood packets to avoid over
337
        #:     flooding on the network. Its key is a hash composed by
338
        #:     (eth_type, mac_src, mac_dst) and the value is the timestamp of
339
        #:     the last flood.
340 1
        self.flood_table = {}
341 1
        self.interfaces = {}
342 1
        self.flows = []
343 1
        self.description = {}
344
345 1
        if connection:
346
            connection.switch = self
347
348 1
    def update_description(self, desc):
349
        """Update switch'descriptions from Switch instance.
350
351
        Args:
352
            desc (|desc_stats|):
353
                Description Class with new values of switch's descriptions.
354
        """
355
        self.description['manufacturer'] = desc.mfr_desc.value
356
        self.description['hardware'] = desc.hw_desc.value
357
        self.description['software'] = desc.sw_desc.value
358
        self.description['serial'] = desc.serial_num.value
359
        self.description['data_path'] = desc.dp_desc.value
360
361 1
    @property
362
    def id(self):  # pylint: disable=invalid-name
363
        """Return id from Switch instance.
364
365
        Returns:
366
            string: the switch id is the data_path_id from switch.
367
368
        """
369 1
        return "{}".format(self.dpid)
370
371 1
    @property
372
    def ofp_version(self):
373
        """Return the OFP version of this switch."""
374
        if self.connection:
375
            return '0x0' + str(self.connection.protocol.version)
376
        return None
377
378 1
    def disconnect(self):
379
        """Disconnect the switch instance."""
380
        self.connection.close()
381
        self.connection = None
382
        LOG.info("Switch %s is disconnected", self.dpid)
383
384 1
    def get_interface_by_port_no(self, port_no):
385
        """Get interface by port number from Switch instance.
386
387
        Returns:
388
            :class:`~.core.switch.Interface`: Interface from specific port.
389
390
        """
391
        return self.interfaces.get(port_no)
392
393 1
    def get_flow_by_id(self, flow_id):
394
        """Return a Flow using the flow_id given. None if not found in flows.
395
396
        Args:
397
            flow_id (int): identifier from specific flow stored.
398
        """
399
        for flow in self.flows:
400
            if flow_id == flow.id:
401
                return flow
402
        return None
403
404 1
    def is_active(self):
405
        """Return true if the switch connection is alive."""
406 1
        return (now() - self.lastseen).seconds <= CONNECTION_TIMEOUT
407
408 1
    def is_connected(self):
409
        """Verify if the switch is connected to a socket."""
410 1
        return (self.connection is not None and
411
                self.connection.is_alive() and
412
                self.connection.is_established() and self.is_active())
413
414 1
    def update_connection(self, connection):
415
        """Update switch connection.
416
417
        Args:
418
            connection (:class:`~.core.switch.Connection`):
419
                New connection to this instance of switch.
420
        """
421
        self.connection = connection
422
        self.connection.switch = self
423
424 1
    def update_features(self, features):
425
        """Update :attr:`features` attribute."""
426
        self.features = features
427
428 1
    def send(self, buffer):
429
        """Send a buffer data to the real switch.
430
431
        Args:
432
          buffer (bytes): bytes to be sent to the switch throught its
433
                            connection.
434
        """
435
        if self.connection:
436
            self.connection.send(buffer)
437
438 1
    def update_lastseen(self):
439
        """Update the lastseen attribute."""
440
        self.lastseen = now()
441
442 1
    def update_interface(self, interface):
443
        """Update a interface from switch instance.
444
445
        Args:
446
            interface (:class:`~kytos.core.switch.Interface`):
447
                Interface object to be storeged.
448
        """
449
        if interface.port_number not in self.interfaces:
450
            self.interfaces[interface.port_number] = interface
451
452 1
    def update_mac_table(self, mac, port_number):
453
        """Link the mac address with a port number.
454
455
        Args:
456
            mac (|hw_address|): mac address from switch.
457
            port (int): port linked in mac address.
458
        """
459
        if mac.value in self.mac2port:
460
            self.mac2port[mac.value].add(port_number)
461
        else:
462
            self.mac2port[mac.value] = set([port_number])
463
464 1
    def last_flood(self, ethernet_frame):
465
        """Return the timestamp when the ethernet_frame was flooded.
466
467
        This method is usefull to check if a frame was flooded before or not.
468
469
        Args:
470
            ethernet_frame (|ethernet|): Ethernet instance to be verified.
471
472
        Returns:
473
            datetime.datetime.now:
474
                Last time when the ethernet_frame was flooded.
475
476
        """
477
        try:
478
            return self.flood_table[ethernet_frame.get_hash()]
479
        except KeyError:
480
            return None
481
482 1
    def should_flood(self, ethernet_frame):
483
        """Verify if the ethernet frame should flood.
484
485
        Args:
486
            ethernet_frame (|ethernet|): Ethernet instance to be verified.
487
488
        Returns:
489
            bool: True if the ethernet_frame should flood.
490
491
        """
492
        last_flood = self.last_flood(ethernet_frame)
493
        diff = (now() - last_flood).microseconds
494
495
        return last_flood is None or diff > FLOOD_TIMEOUT
496
497 1
    def update_flood_table(self, ethernet_frame):
498
        """Update a flood table using the given ethernet frame.
499
500
        Args:
501
            ethernet_frame (|ethernet|): Ethernet frame to be updated.
502
        """
503
        self.flood_table[ethernet_frame.get_hash()] = now()
504
505 1
    def where_is_mac(self, mac):
506
        """Return all ports from specific mac address.
507
508
        Args:
509
            mac (|hw_address|): Mac address from switch.
510
511
        Returns:
512
            :class:`list`: A list of ports. None otherswise.
513
514
        """
515
        try:
516
            return list(self.mac2port[mac.value])
517
        except KeyError:
518
            return None
519
520 1
    def as_dict(self):
521
        """Return a dictionary with switch attributes.
522
523
        Example of output:
524
525
        .. code-block:: python3
526
527
               {'id': '00:00:00:00:00:00:00:03:2',
528
                'name': '00:00:00:00:00:00:00:03:2',
529
                'dpid': '00:00:00:00:03',
530
                'connection':  connection,
531
                'ofp_version': '0x01',
532
                'type': 'switch',
533
                'manufacturer': "",
534
                'serial': "",
535
                'hardware': "Open vSwitch",
536
                'software': 2.5,
537
                'data_path': ""
538
                }
539
540
        Returns:
541
            dict: Dictionary filled with interface attributes.
542
543
        """
544
        connection = ""
545
        if self.connection is not None:
546
            address = self.connection.address
547
            port = self.connection.port
548
            connection = "{}:{}".format(address, port)
549
550
        return {'id': self.id,
551
                'name': self.id,
552
                'dpid': self.dpid,
553
                'connection': connection,
554
                'ofp_version': self.ofp_version,
555
                'type': 'switch',
556
                'manufacturer': self.description.get('manufacturer', ''),
557
                'serial': self.description.get('serial', ''),
558
                'hardware': self.description.get('hardware', ''),
559
                'software': self.description.get('software'),
560
                'data_path': self.description.get('data_path', ''),
561
                'interfaces': {i.id: i.as_dict()
562
                               for i in self.interfaces.values()}}
563
564 1
    def as_json(self):
565
        """Return a json with switch'attributes.
566
567
        Example of output:
568
569
        .. code-block:: json
570
571
            {"data_path": "",
572
             "hardware": "Open vSwitch",
573
             "dpid": "00:00:00:00:03",
574
             "name": "00:00:00:00:00:00:00:03:2",
575
             "manufacturer": "",
576
             "serial": "",
577
             "software": 2.5,
578
             "id": "00:00:00:00:00:00:00:03:2",
579
             "ofp_version": "0x01",
580
             "type": "switch",
581
             "connection": ""}
582
583
        Returns:
584
            string: Json filled with switch'attributes.
585
586
        """
587
        return json.dumps(self.as_dict())
588