Passed
Push — master ( b66520...374a48 )
by Humberto
02:29 queued 11s
created

build.main   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 560
Duplicated Lines 6.07 %

Test Coverage

Coverage 44.51%

Importance

Changes 0
Metric Value
wmc 97
eloc 371
dl 34
loc 560
rs 2
c 0
b 0
f 0
ccs 146
cts 328
cp 0.4451

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_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.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.execute() 0 2 1
A Main.get_switch_metadata() 0 8 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._get_link_or_create() 0 9 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.verify_storehouse() 0 8 1
A Main.load_from_store() 0 8 2
A Main.request_retrieve_entities() 0 18 2
A Main.update_instance() 0 8 2
A Main._get_switches_dict() 0 4 1
A Main.handle_interface_down() 0 9 1
A Main.handle_interface_link_up() 0 13 3
A Main.handle_interface_created() 0 4 1
A Main.handle_new_switch() 0 12 1
A Main.handle_connection_lost() 0 12 2
A Main.handle_interface_link_down() 0 12 3
A Main.handle_interface_up() 0 9 1
A Main.handle_interface_deleted() 0 4 1
A Main.notify_topology_update() 0 6 1
A Main.add_links() 0 14 1
A Main._get_topology() 0 3 1
B Main.update_instance_metadata() 0 19 8
A Main.save_metadata_on_store() 0 25 4
A Main._get_link_from_interface() 0 6 3
A Main.notify_port_created() 0 6 1
A Main.notify_link_status_change() 0 9 2
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
7 1
from kytos.core import KytosEvent, KytosNApp, log, rest
8 1
from kytos.core.helpers import listen_to
9 1
from kytos.core.interface import Interface
10 1
from kytos.core.link import Link
11 1
from kytos.core.switch import Switch
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 1
        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 1
        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 1
        for link in self.links.values():
70
            if interface in (link.endpoint_a, link.endpoint_b):
71
                return link
72 1
        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 1
        switch = event.content['switch']
320 1
        switch.activate()
321 1
        log.debug('Switch %s added to the Topology.', switch.id)
322 1
        self.notify_topology_update()
323 1
        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 1
        switch = event.content['source'].switch
333 1
        if switch:
334 1
            switch.deactivate()
335 1
            log.debug('Switch %s removed from the Topology.', switch.id)
336 1
            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 1
        interface = event.content['interface']
344 1
        interface.activate()
345 1
        self.notify_topology_update()
346 1
        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 1
        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 1
        interface = event.content['interface']
359 1
        interface.deactivate()
360 1
        self.handle_interface_link_down(event)
361 1
        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 1
        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 1
        interface = event.content['interface']
375 1
        link = self._get_link_from_interface(interface)
376 1
        if link and not link.is_active():
377 1
            link.activate()
378 1
            self.notify_topology_update()
379 1
            self.update_instance_metadata(interface.link)
380 1
            self.notify_link_status_change(link)
381
382 1
    @listen_to('.*.switch.interface.link_down')
383
    def handle_interface_link_down(self, event):
384
        """Update the topology based on a Port Modify event.
385
386
        The event notifies that an interface's link was changed to 'down'.
387
        """
388 1
        interface = event.content['interface']
389 1
        link = self._get_link_from_interface(interface)
390 1
        if link and link.is_active():
391 1
            link.deactivate()
392 1
            self.notify_topology_update()
393 1
            self.notify_link_status_change(link)
394
395 1
    @listen_to('.*.interface.is.nni')
396
    def add_links(self, event):
397
        """Update the topology with links related to the NNI interfaces."""
398 1
        interface_a = event.content['interface_a']
399 1
        interface_b = event.content['interface_b']
400
401 1
        link = self._get_link_or_create(interface_a, interface_b)
402 1
        interface_a.update_link(link)
403 1
        interface_b.update_link(link)
404
405 1
        interface_a.nni = True
406 1
        interface_b.nni = True
407
408 1
        self.notify_topology_update()
409
410
    # def add_host(self, event):
411
    #    """Update the topology with a new Host."""
412
413
    #    interface = event.content['port']
414
    #    mac = event.content['reachable_mac']
415
416
    #    host = Host(mac)
417
    #    link = self.topology.get_link(interface.id)
418
    #    if link is not None:
419
    #        return
420
421
    #    self.topology.add_link(interface.id, host.id)
422
    #    self.topology.add_device(host)
423
424
    #    if settings.DISPLAY_FULL_DUPLEX_LINKS:
425
    #        self.topology.add_link(host.id, interface.id)
426
427 1
    def notify_topology_update(self):
428
        """Send an event to notify about updates on the topology."""
429 1
        name = 'kytos/topology.updated'
430 1
        event = KytosEvent(name=name, content={'topology':
431
                                               self._get_topology()})
432 1
        self.controller.buffers.app.put(event)
433
434 1
    def notify_link_status_change(self, link):
435
        """Send an event to notify about a status change on a link."""
436 1
        name = 'kytos/topology.'
437 1
        if link.is_active():
438 1
            status = 'link_up'
439
        else:
440
            status = 'link_down'
441 1
        event = KytosEvent(name=name+status, content={'link': link})
442 1
        self.controller.buffers.app.put(event)
443
444 1
    def notify_metadata_changes(self, obj, action):
445
        """Send an event to notify about metadata changes."""
446 1
        if isinstance(obj, Switch):
447 1
            entity = 'switch'
448 1
            entities = 'switches'
449
        elif isinstance(obj, Interface):
450
            entity = 'interface'
451
            entities = 'interfaces'
452
        elif isinstance(obj, Link):
453
            entity = 'link'
454
            entities = 'links'
455
456 1
        name = f'kytos/topology.{entities}.metadata.{action}'
457 1
        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...
458
                                               'metadata': obj.metadata})
459 1
        self.controller.buffers.app.put(event)
460 1
        log.debug(f'Metadata from {obj.id} was {action}.')
461
462 1
    @listen_to('.*.switch.port.created')
463
    def notify_port_created(self, original_event):
464
        """Notify when a port is created."""
465 1
        name = f'kytos/topology.port.created'
466 1
        event = KytosEvent(name=name, content=original_event.content)
467 1
        self.controller.buffers.app.put(event)
468
469 1
    @listen_to('kytos/topology.*.metadata.*')
470
    def save_metadata_on_store(self, event):
471
        """Send to storehouse the data updated."""
472 1
        name = 'kytos.storehouse.update'
473 1
        if 'switch' in event.content:
474 1
            store = self.store_items.get('switches')
475 1
            obj = event.content.get('switch')
476 1
            namespace = 'kytos.topology.switches.metadata'
477
        elif 'interface' in event.content:
478
            store = self.store_items.get('interfaces')
479
            obj = event.content.get('interface')
480
            namespace = 'kytos.topology.iterfaces.metadata'
481
        elif 'link' in event.content:
482
            store = self.store_items.get('links')
483
            obj = event.content.get('link')
484
            namespace = 'kytos.topology.links.metadata'
485
486 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...
487 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...
488
                   'box_id': store.box_id,
489
                   'data': store.data,
490
                   'callback': self.update_instance}
491
492 1
        event = KytosEvent(name=name, content=content)
493 1
        self.controller.buffers.app.put(event)
494
495 1
    @staticmethod
496
    def update_instance(event, _data, error):
497
        """Display in Kytos console if the data was updated."""
498
        entities = event.content.get('namespace', '').split('.')[-2]
499
        if error:
500
            log.error(f'Error trying to update storehouse {entities}.')
501
        else:
502
            log.debug(f'Storehouse update to entities: {entities}.')
503
504 1
    def verify_storehouse(self, entities):
505
        """Request a list of box saved by specific entity."""
506 1
        name = 'kytos.storehouse.list'
507 1
        content = {'namespace': f'kytos.topology.{entities}.metadata',
508
                   'callback': self.request_retrieve_entities}
509 1
        event = KytosEvent(name=name, content=content)
510 1
        self.controller.buffers.app.put(event)
511 1
        log.info(f'verify data in storehouse for {entities}.')
512
513 1
    def request_retrieve_entities(self, event, data, _error):
514
        """Create a box or retrieve an existent box from storehouse."""
515
        msg = ''
516
        content = {'namespace': event.content.get('namespace'),
517
                   'callback': self.load_from_store,
518
                   'data': {}}
519
520
        if not data:
521
            name = 'kytos.storehouse.create'
522
            msg = 'Create new box in storehouse'
523
        else:
524
            name = 'kytos.storehouse.retrieve'
525
            content['box_id'] = data[0]
526
            msg = 'Retrieve data from storeohouse.'
527
528
        event = KytosEvent(name=name, content=content)
529
        self.controller.buffers.app.put(event)
530
        log.debug(msg)
531
532 1
    def load_from_store(self, event, box, error):
533
        """Save the data retrived from storehouse."""
534
        entities = event.content.get('namespace', '').split('.')[-2]
535
        if error:
536
            log.error('Error while get a box from storehouse.')
537
        else:
538
            self.store_items[entities] = box
539
            log.debug('Data updated')
540
541 1
    def update_instance_metadata(self, obj):
542
        """Update object instance with saved metadata."""
543 1
        metadata = None
544 1
        if isinstance(obj, Interface):
545 1
            all_metadata = self.store_items.get('interfaces', None)
546 1
            if all_metadata:
547
                metadata = all_metadata.data.get(obj.id)
548 1
        elif isinstance(obj, Switch):
549 1
            all_metadata = self.store_items.get('switches', None)
550 1
            if all_metadata:
551 1
                metadata = all_metadata.data.get(obj.id)
552
        elif isinstance(obj, Link):
553
            all_metadata = self.store_items.get('links', None)
554
            if all_metadata:
555
                metadata = all_metadata.data.get(obj.id)
556
557 1
        if metadata:
558
            obj.extend_metadata(metadata)
559
            log.debug(f'Metadata to {obj.id} was updated')
560