Test Failed
Pull Request — master (#76)
by Antonio
02:58
created

build.main.Main.handle_interface_link_up()   B

Complexity

Conditions 7

Size

Total Lines 29
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 40.6659

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 29
rs 7.904
c 0
b 0
f 0
ccs 2
cts 17
cp 0.1176
cc 7
nop 2
crap 40.6659
1
"""Main module of kytos/topology Kytos Network Application.
2
3
Manage the network topology
4
"""
5 1
import time
6 1
7 1
from flask import jsonify, request
8 1
from kytos.core import KytosEvent, KytosNApp, log, rest
9 1
from kytos.core.helpers import listen_to
10 1
from kytos.core.interface import Interface
11
from kytos.core.link import Link
12
from kytos.core.switch import Switch
13 1
14
from napps.kytos.topology import settings
15
from napps.kytos.topology.models import Topology
16 1
17
18
class Main(KytosNApp):  # pylint: disable=too-many-public-methods
19
    """Main class of kytos/topology NApp.
20
21
    This class is the entry point for this napp.
22 1
    """
23
24 1
    def setup(self):
25 1
        """Initialize the NApp's links list."""
26
        self.links = {}
27 1
        self.store_items = {}
28 1
        self.link_up_timer = settings.LINK_UP_TIMER
29 1
30
        self.verify_storehouse('switches')
31 1
        self.verify_storehouse('interfaces')
32
        self.verify_storehouse('links')
33
34 1
    def execute(self):
35
        """Do nothing."""
36
37
    def shutdown(self):
38 1
        """Do nothing."""
39
        log.info('NApp kytos/topology shutting down.')
40
41
    def _get_link_or_create(self, endpoint_a, endpoint_b):
42
        new_link = Link(endpoint_a, endpoint_b)
43
44
        for link in self.links.values():
45
            if new_link == link:
46
                return link
47
48 1
        self.links[new_link.id] = new_link
49
        return new_link
50
51
    def _get_switches_dict(self):
52
        """Return a dictionary with the known switches."""
53 1
        return {'switches': {s.id: s.as_dict() for s in
54
                             self.controller.switches.values()}}
55
56
    def _get_links_dict(self):
57
        """Return a dictionary with the known links."""
58 1
        return {'links': {l.id: l.as_dict() for l in
59
                          self.links.values()}}
60
61
    def _get_topology_dict(self):
62
        """Return a dictionary with the known topology."""
63 1
        return {'topology': {**self._get_switches_dict(),
64
                             **self._get_links_dict()}}
65
66
    def _get_topology(self):
67 1
        """Return an object representing the topology."""
68
        return Topology(self.controller.switches, self.links)
69
70
    def _get_link_from_interface(self, interface):
71
        """Return the link of the interface, or None if it does not exist."""
72
        for link in self.links.values():
73
            if interface in (link.endpoint_a, link.endpoint_b):
74 1
                return link
75
        return None
76
77
    @rest('v3/')
78
    def get_topology(self):
79
        """Return the latest known topology.
80
81
        This topology is updated when there are network events.
82
        """
83 1
        return jsonify(self._get_topology_dict())
84
85
    # Switch related methods
86
    @rest('v3/switches')
87
    def get_switches(self):
88 1
        """Return a json with all the switches in the topology."""
89
        return jsonify(self._get_switches_dict())
90
91
    @rest('v3/switches/<dpid>/enable', methods=['POST'])
92
    def enable_switch(self, dpid):
93
        """Administratively enable a switch in the topology."""
94
        try:
95
            self.controller.switches[dpid].enable()
96
            return jsonify("Operation successful"), 201
97 1
        except KeyError:
98
            return jsonify("Switch not found"), 404
99
100
    @rest('v3/switches/<dpid>/disable', methods=['POST'])
101
    def disable_switch(self, dpid):
102
        """Administratively disable a switch in the topology."""
103
        try:
104
            self.controller.switches[dpid].disable()
105
            return jsonify("Operation successful"), 201
106 1
        except KeyError:
107
            return jsonify("Switch not found"), 404
108
109
    @rest('v3/switches/<dpid>/metadata')
110
    def get_switch_metadata(self, dpid):
111
        """Get metadata from a switch."""
112
        try:
113
            return jsonify({"metadata":
114
                            self.controller.switches[dpid].metadata}), 200
115 1
        except KeyError:
116
            return jsonify("Switch not found"), 404
117
118
    @rest('v3/switches/<dpid>/metadata', methods=['POST'])
119
    def add_switch_metadata(self, dpid):
120
        """Add metadata to a switch."""
121
        metadata = request.get_json()
122
        try:
123
            switch = self.controller.switches[dpid]
124
        except KeyError:
125
            return jsonify("Switch not found"), 404
126
127
        switch.extend_metadata(metadata)
128 1
        self.notify_metadata_changes(switch, 'added')
129
        return jsonify("Operation successful"), 201
130
131
    @rest('v3/switches/<dpid>/metadata/<key>', methods=['DELETE'])
132
    def delete_switch_metadata(self, dpid, key):
133
        """Delete metadata from a switch."""
134
        try:
135
            switch = self.controller.switches[dpid]
136
        except KeyError:
137
            return jsonify("Switch not found"), 404
138
139
        switch.remove_metadata(key)
140
        self.notify_metadata_changes(switch, 'removed')
141 1
        return jsonify("Operation successful"), 200
142
143
    # Interface related methods
144
    @rest('v3/interfaces')
145
    def get_interfaces(self):
146
        """Return a json with all the interfaces in the topology."""
147
        interfaces = {}
148
        switches = self._get_switches_dict()
149
        for switch in switches['switches'].values():
150
            for interface_id, interface in switch['interfaces'].items():
151
                interfaces[interface_id] = interface
152 1
153
        return jsonify({'interfaces': interfaces})
154
155 View Code Duplication
    @rest('v3/interfaces/<interface_id>/enable', methods=['POST'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
156
    def enable_interface(self, interface_id):
157
        """Administratively enable an interface in the topology."""
158
        switch_id = ":".join(interface_id.split(":")[:-1])
159
        interface_number = int(interface_id.split(":")[-1])
160
161
        try:
162
            switch = self.controller.switches[switch_id]
163
        except KeyError:
164
            return jsonify("Switch not found"), 404
165
166
        try:
167
            switch.interfaces[interface_number].enable()
168
        except KeyError:
169
            return jsonify("Interface not found"), 404
170 1
171
        return jsonify("Operation successful"), 201
172
173 View Code Duplication
    @rest('v3/interfaces/<interface_id>/disable', methods=['POST'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
174
    def disable_interface(self, interface_id):
175
        """Administratively disable an interface in the topology."""
176
        switch_id = ":".join(interface_id.split(":")[:-1])
177
        interface_number = int(interface_id.split(":")[-1])
178
179
        try:
180
            switch = self.controller.switches[switch_id]
181
        except KeyError:
182
            return jsonify("Switch not found"), 404
183
184
        try:
185
            switch.interfaces[interface_number].disable()
186
        except KeyError:
187
            return jsonify("Interface not found"), 404
188 1
189
        return jsonify("Operation successful"), 201
190
191
    @rest('v3/interfaces/<interface_id>/metadata')
192
    def get_interface_metadata(self, interface_id):
193
        """Get metadata from an interface."""
194
        switch_id = ":".join(interface_id.split(":")[:-1])
195
        interface_number = int(interface_id.split(":")[-1])
196
        try:
197
            switch = self.controller.switches[switch_id]
198
        except KeyError:
199
            return jsonify("Switch not found"), 404
200
201
        try:
202
            interface = switch.interfaces[interface_number]
203
        except KeyError:
204
            return jsonify("Interface not found"), 404
205 1
206
        return jsonify({"metadata": interface.metadata}), 200
207
208
    @rest('v3/interfaces/<interface_id>/metadata', methods=['POST'])
209
    def add_interface_metadata(self, interface_id):
210
        """Add metadata to an interface."""
211
        metadata = request.get_json()
212
213
        switch_id = ":".join(interface_id.split(":")[:-1])
214
        interface_number = int(interface_id.split(":")[-1])
215
        try:
216
            switch = self.controller.switches[switch_id]
217
        except KeyError:
218
            return jsonify("Switch not found"), 404
219
220
        try:
221
            interface = switch.interfaces[interface_number]
222
        except KeyError:
223
            return jsonify("Interface not found"), 404
224
225
        interface.extend_metadata(metadata)
226 1
        self.notify_metadata_changes(interface, 'added')
227
        return jsonify("Operation successful"), 201
228
229
    @rest('v3/interfaces/<interface_id>/metadata/<key>', methods=['DELETE'])
230
    def delete_interface_metadata(self, interface_id, key):
231
        """Delete metadata from an interface."""
232
        switch_id = ":".join(interface_id.split(":")[:-1])
233
        interface_number = int(interface_id.split(":")[-1])
234
235
        try:
236
            switch = self.controller.switches[switch_id]
237
        except KeyError:
238
            return jsonify("Switch not found"), 404
239
240
        try:
241
            interface = switch.interfaces[interface_number]
242
        except KeyError:
243
            return jsonify("Interface not found"), 404
244
245
        if interface.remove_metadata(key) is False:
246
            return jsonify("Metadata not found"), 404
247
248
        self.notify_metadata_changes(interface, 'removed')
249 1
        return jsonify("Operation successful"), 200
250
251
    # Link related methods
252
    @rest('v3/links')
253
    def get_links(self):
254
        """Return a json with all the links in the topology.
255
256
        Links are connections between interfaces.
257 1
        """
258
        return jsonify(self._get_links_dict()), 200
259
260
    @rest('v3/links/<link_id>/enable', methods=['POST'])
261
    def enable_link(self, link_id):
262
        """Administratively enable a link in the topology."""
263
        try:
264
            self.links[link_id].enable()
265
        except KeyError:
266
            return jsonify("Link not found"), 404
267 1
268
        return jsonify("Operation successful"), 201
269
270
    @rest('v3/links/<link_id>/disable', methods=['POST'])
271
    def disable_link(self, link_id):
272
        """Administratively disable a link in the topology."""
273
        try:
274
            self.links[link_id].disable()
275
        except KeyError:
276
            return jsonify("Link not found"), 404
277 1
278
        return jsonify("Operation successful"), 201
279
280
    @rest('v3/links/<link_id>/metadata')
281
    def get_link_metadata(self, link_id):
282
        """Get metadata from a link."""
283
        try:
284
            return jsonify({"metadata": self.links[link_id].metadata}), 200
285 1
        except KeyError:
286
            return jsonify("Link not found"), 404
287
288
    @rest('v3/links/<link_id>/metadata', methods=['POST'])
289
    def add_link_metadata(self, link_id):
290
        """Add metadata to a link."""
291
        metadata = request.get_json()
292
        try:
293
            link = self.links[link_id]
294
        except KeyError:
295
            return jsonify("Link not found"), 404
296
297
        link.extend_metadata(metadata)
298 1
        self.notify_metadata_changes(link, 'added')
299
        return jsonify("Operation successful"), 201
300
301
    @rest('v3/links/<link_id>/metadata/<key>', methods=['DELETE'])
302
    def delete_link_metadata(self, link_id, key):
303
        """Delete metadata from a link."""
304
        try:
305
            link = self.links[link_id]
306
        except KeyError:
307
            return jsonify("Link not found"), 404
308
309
        if link.remove_metadata(key) is False:
310
            return jsonify("Metadata not found"), 404
311
312 1
        self.notify_metadata_changes(link, 'removed')
313
        return jsonify("Operation successful"), 200
314
315
    @listen_to('.*.switch.(new|reconnected)')
316
    def handle_new_switch(self, event):
317
        """Create a new Device on the Topology.
318
319
        Handle the event of a new created switch and update the topology with
320
        this new device.
321
        """
322
        switch = event.content['switch']
323
        switch.activate()
324
        log.debug('Switch %s added to the Topology.', switch.id)
325 1
        self.notify_topology_update()
326
        self.update_instance_metadata(switch)
327
328
    @listen_to('.*.connection.lost')
329
    def handle_connection_lost(self, event):
330
        """Remove a Device from the topology.
331
332
        Remove the disconnected Device and every link that has one of its
333
        interfaces.
334
        """
335
        switch = event.content['source'].switch
336
        if switch:
337
            switch.deactivate()
338 1
            log.debug('Switch %s removed from the Topology.', switch.id)
339
            self.notify_topology_update()
340
341
    def handle_interface_up(self, event):
342
        """Update the topology based on a Port Modify event.
343
344
        The event notifies that an interface was changed to 'up'.
345
        """
346
        interface = event.content['interface']
347
        interface.activate()
348 1
        self.notify_topology_update()
349
        self.update_instance_metadata(interface)
350
351
    @listen_to('.*.switch.interface.created')
352
    def handle_interface_created(self, event):
353 1
        """Update the topology based on a Port Create event."""
354
        self.handle_interface_up(event)
355
356
    def handle_interface_down(self, event):
357
        """Update the topology based on a Port Modify event.
358
359
        The event notifies that an interface was changed to 'down'.
360
        """
361
        interface = event.content['interface']
362
        interface.deactivate()
363 1
        self.handle_interface_link_down(event)
364
        self.notify_topology_update()
365
366
    @listen_to('.*.switch.interface.deleted')
367
    def handle_interface_deleted(self, event):
368 1
        """Update the topology based on a Port Delete event."""
369
        self.handle_interface_down(event)
370
371
    @listen_to('.*.switch.interface.link_up')
372
    def handle_interface_link_up(self, event):
373
        """Update the topology based on a Port Modify event.
374
375
        The event notifies that an interface's link was changed to 'up'.
376
        """
377
        interface = event.content['interface']
378
        link = self._get_link_from_interface(interface)
379
        if not link:
380
            return
381
        if link.endpoint_a == interface:
382 1
            other_interface = link.endpoint_b
383
        else:
384
            other_interface = link.endpoint_a
385
        interface.activate()
386
        if other_interface.is_active() is False:
387
            return
388
        if link.is_active() is False:
389
            link.update_metadata('last_status_change', time.time())
390
            link.activate()
391
            time.sleep(self.link_up_timer)
392
            last_status_change = link.get_metadata('last_status_change')
393
            now = time.time()
394
            if link.is_active() and \
395 1
                    now - last_status_change >= self.link_up_timer:
396
                log.info('Notificando link up')
397
                self.notify_topology_update()
398
                self.update_instance_metadata(interface.link)
399
                self.notify_link_status_change(link)
400
401
    @listen_to('.*.switch.interface.link_down')
402
    def handle_interface_link_down(self, event):
403
        """Update the topology based on a Port Modify event.
404
405
        The event notifies that an interface's link was changed to 'down'.
406
        """
407
        interface = event.content['interface']
408
        link = self._get_link_from_interface(interface)
409
        if link and link.is_active():
410
            link.deactivate()
411
            link.update_metadata('last_status_change', time.time())
412
            self.notify_topology_update()
413
            self.notify_link_status_change(link)
414
415
    @listen_to('.*.interface.is.nni')
416
    def add_links(self, event):
417
        """Update the topology with links related to the NNI interfaces."""
418
        interface_a = event.content['interface_a']
419
        interface_b = event.content['interface_b']
420
421
        link = self._get_link_or_create(interface_a, interface_b)
422
        interface_a.update_link(link)
423
        interface_b.update_link(link)
424
425
        interface_a.nni = True
426
        interface_b.nni = True
427 1
428
        self.notify_topology_update()
429
430
    # def add_host(self, event):
431
    #    """Update the topology with a new Host."""
432
433
    #    interface = event.content['port']
434 1
    #    mac = event.content['reachable_mac']
435
436
    #    host = Host(mac)
437
    #    link = self.topology.get_link(interface.id)
438
    #    if link is not None:
439
    #        return
440
441
    #    self.topology.add_link(interface.id, host.id)
442
    #    self.topology.add_device(host)
443
444 1
    #    if settings.DISPLAY_FULL_DUPLEX_LINKS:
445
    #        self.topology.add_link(host.id, interface.id)
446
447
    def notify_topology_update(self):
448
        """Send an event to notify about updates on the topology."""
449
        name = 'kytos/topology.updated'
450
        event = KytosEvent(name=name, content={'topology':
451
                                               self._get_topology()})
452
        self.controller.buffers.app.put(event)
453
454
    def notify_link_status_change(self, link):
455
        """Send an event to notify about a status change on a link."""
456
        name = 'kytos/topology.'
457
        if link.is_active():
458
            status = 'link_up'
459
        else:
460
            status = 'link_down'
461
        event = KytosEvent(name=name+status, content={'link': link})
462 1
        self.controller.buffers.app.put(event)
463
464
    def notify_metadata_changes(self, obj, action):
465
        """Send an event to notify about metadata changes."""
466
        if isinstance(obj, Switch):
467
            entity = 'switch'
468
            entities = 'switches'
469 1
        elif isinstance(obj, Interface):
470
            entity = 'interface'
471
            entities = 'interfaces'
472
        elif isinstance(obj, Link):
473
            entity = 'link'
474
            entities = 'links'
475
476
        name = f'kytos/topology.{entities}.metadata.{action}'
477
        event = KytosEvent(name=name, content={entity: obj,
0 ignored issues
show
introduced by
The variable entity does not seem to be defined for all execution paths.
Loading history...
478
                                               'metadata': obj.metadata})
479
        self.controller.buffers.app.put(event)
480
        log.debug(f'Metadata from {obj.id} was {action}.')
481
482
    @listen_to('.*.switch.port.created')
483
    def notify_port_created(self, original_event):
484
        """Notify when a port is created."""
485
        name = f'kytos/topology.port.created'
486
        event = KytosEvent(name=name, content=original_event.content)
487
        self.controller.buffers.app.put(event)
488
489
    @listen_to('kytos/topology.*.metadata.*')
490
    def save_metadata_on_store(self, event):
491
        """Send to storehouse the data updated."""
492
        name = 'kytos.storehouse.update'
493
        if 'switch' in event.content:
494
            store = self.store_items.get('switches')
495 1
            obj = event.content.get('switch')
496
            namespace = 'kytos.topology.switches.metadata'
497
        elif 'interface' in event.content:
498
            store = self.store_items.get('interfaces')
499
            obj = event.content.get('interface')
500
            namespace = 'kytos.topology.iterfaces.metadata'
501
        elif 'link' in event.content:
502
            store = self.store_items.get('links')
503
            obj = event.content.get('link')
504 1
            namespace = 'kytos.topology.links.metadata'
505
506 1
        store.data[obj.id] = obj.metadata
0 ignored issues
show
introduced by
The variable store does not seem to be defined for all execution paths.
Loading history...
introduced by
The variable obj does not seem to be defined for all execution paths.
Loading history...
507 1
        content = {'namespace': namespace,
0 ignored issues
show
introduced by
The variable namespace does not seem to be defined for all execution paths.
Loading history...
508
                   'box_id': store.box_id,
509 1
                   'data': store.data,
510 1
                   'callback': self.update_instance}
511 1
512
        event = KytosEvent(name=name, content=content)
513 1
        self.controller.buffers.app.put(event)
514
515
    @staticmethod
516
    def update_instance(event, _data, error):
517
        """Display in Kytos console if the data was updated."""
518
        entities = event.content.get('namespace', '').split('.')[-2]
519
        if error:
520
            log.error(f'Error trying to update storehouse {entities}.')
521
        else:
522
            log.debug(f'Storehouse update to entities: {entities}.')
523
524
    def verify_storehouse(self, entities):
525
        """Request a list of box saved by specific entity."""
526
        name = 'kytos.storehouse.list'
527
        content = {'namespace': f'kytos.topology.{entities}.metadata',
528
                   'callback': self.request_retrieve_entities}
529
        event = KytosEvent(name=name, content=content)
530
        self.controller.buffers.app.put(event)
531
        log.info(f'verify data in storehouse for {entities}.')
532 1
533
    def request_retrieve_entities(self, event, data, _error):
534
        """Create a box or retrieve an existent box from storehouse."""
535
        msg = ''
536
        content = {'namespace': event.content.get('namespace'),
537
                   'callback': self.load_from_store,
538
                   'data': {}}
539
540
        if not data:
541 1
            name = 'kytos.storehouse.create'
542
            msg = 'Create new box in storehouse'
543
        else:
544
            name = 'kytos.storehouse.retrieve'
545
            content['box_id'] = data[0]
546
            msg = 'Retrieve data from storeohouse.'
547
548
        event = KytosEvent(name=name, content=content)
549
        self.controller.buffers.app.put(event)
550
        log.debug(msg)
551
552
    def load_from_store(self, event, box, error):
553
        """Save the data retrived from storehouse."""
554
        entities = event.content.get('namespace', '').split('.')[-2]
555
        if error:
556
            log.error('Error while get a box from storehouse.')
557
        else:
558
            self.store_items[entities] = box
559
            log.debug('Data updated')
560
561
    def update_instance_metadata(self, obj):
562
        """Update object instance with saved metadata."""
563
        metadata = None
564
        if isinstance(obj, Interface):
565
            all_metadata = self.store_items.get('interfaces', None)
566
            if all_metadata:
567
                metadata = all_metadata.data.get(obj.id)
568
        elif isinstance(obj, Switch):
569
            all_metadata = self.store_items.get('switches', None)
570
            if all_metadata:
571
                metadata = all_metadata.data.get(obj.id)
572
        elif isinstance(obj, Link):
573
            all_metadata = self.store_items.get('links', None)
574
            if all_metadata:
575
                metadata = all_metadata.data.get(obj.id)
576
577
        if metadata:
578
            obj.extend_metadata(metadata)
579
            log.debug(f'Metadata to {obj.id} was updated')
580