Passed
Pull Request — master (#640)
by Carlos Eduardo
01:57
created

Interface.get_speed()   A

Complexity

Conditions 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

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