Passed
Pull Request — master (#76)
by Antonio
02:36
created

build.main   F

Complexity

Total Complexity 99

Size/Duplication

Total Lines 569
Duplicated Lines 5.98 %

Test Coverage

Coverage 19.35%

Importance

Changes 0
Metric Value
wmc 99
eloc 379
dl 34
loc 569
ccs 65
cts 336
cp 0.1935
rs 2
c 0
b 0
f 0

47 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.disable_switch() 0 8 2
A Main.disable_link() 0 9 2
A Main.enable_switch() 0 8 2
A Main.enable_interface() 17 17 3
A Main.add_switch_metadata() 0 12 2
A Main._get_switches_dict() 0 4 1
A Main.get_interfaces() 0 10 3
A Main.delete_interface_metadata() 0 21 4
A Main.add_interface_metadata() 0 20 3
A Main.get_switches() 0 4 1
A Main.handle_interface_down() 0 9 1
A Main.handle_interface_created() 0 4 1
A Main.get_interface_metadata() 0 16 3
A Main.shutdown() 0 3 1
A Main.delete_link_metadata() 0 13 3
A Main.setup() 0 8 1
A Main.enable_link() 0 9 2
A Main.handle_new_switch() 0 12 1
A Main.execute() 0 2 1
A Main.get_switch_metadata() 0 8 2
A Main.handle_connection_lost() 0 12 2
A Main.disable_interface() 17 17 3
A Main.get_links() 0 7 1
A Main.get_topology() 0 7 1
A Main._get_links_dict() 0 4 1
A Main.handle_interface_up() 0 9 1
A Main.handle_interface_deleted() 0 4 1
A Main._get_link_or_create() 0 9 3
A Main._get_link_from_interface() 0 6 3
A Main.add_link_metadata() 0 12 2
A Main.delete_switch_metadata() 0 11 2
A Main._get_topology_dict() 0 4 1
A Main.get_link_metadata() 0 7 2
A Main._get_topology() 0 3 1
A Main.verify_storehouse() 0 8 1
A Main.notify_port_created() 0 6 1
A Main.load_from_store() 0 8 2
A Main.request_retrieve_entities() 0 18 2
B Main.update_instance_metadata() 0 19 8
A Main.notify_link_status_change() 0 9 2
A Main.handle_interface_link_up() 0 22 5
A Main.notify_topology_update() 0 6 1
A Main.update_instance() 0 8 2
A Main.save_metadata_on_store() 0 25 4
A Main.handle_interface_link_down() 0 12 3
A Main.add_links() 0 14 1
A Main.notify_metadata_changes() 0 17 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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