Passed
Pull Request — master (#87)
by Gleyberson
01:50
created

build.main   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 560
Duplicated Lines 6.07 %

Test Coverage

Coverage 0%

Importance

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

47 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.setup() 0 8 1
A Main.verify_storehouse() 0 8 1
A Main.disable_switch() 0 8 2
A Main.notify_port_created() 0 6 1
A Main.load_from_store() 0 8 2
A Main.request_retrieve_entities() 0 18 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
B Main.update_instance_metadata() 0 19 8
A Main.get_switches() 0 4 1
A Main.handle_interface_down() 0 9 1
A Main.notify_link_status_change() 0 9 2
A Main.handle_interface_link_up() 0 13 3
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.notify_topology_update() 0 6 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.update_instance() 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.save_metadata_on_store() 0 25 4
A Main.handle_interface_link_down() 0 12 3
A Main._get_links_dict() 0 4 1
A Main.add_links() 0 14 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.notify_metadata_changes() 0 17 4
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

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
from flask import jsonify, request
6
7
from kytos.core import KytosEvent, KytosNApp, log, rest
8
from kytos.core.helpers import listen_to
9
from kytos.core.interface import Interface
10
from kytos.core.link import Link
11
from kytos.core.switch import Switch
12
# from napps.kytos.topology import settings
13
from napps.kytos.topology.models import Topology
14
15
16
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
    def setup(self):
23
        """Initialize the NApp's links list."""
24
        self.links = {}
25
        self.store_items = {}
26
27
        self.verify_storehouse('switches')
28
        self.verify_storehouse('interfaces')
29
        self.verify_storehouse('links')
30
31
    def execute(self):
32
        """Do nothing."""
33
34
    def shutdown(self):
35
        """Do nothing."""
36
        log.info('NApp kytos/topology shutting down.')
37
38
    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
    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
    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
    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
    def _get_topology(self):
64
        """Return an object representing the topology."""
65
        return Topology(self.controller.switches, self.links)
66
67
    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
    @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
    @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
    @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
    @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
    @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
    @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
    @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
    @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 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 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
    @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
    @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
    @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
    @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
    @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
    @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
    @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
    @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
    @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
    @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
    @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
    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
    @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
    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
    @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
    @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 link and not link.is_active():
377
            link.activate()
378
            self.notify_topology_update()
379
            self.update_instance_metadata(interface.link)
380
            self.notify_link_status_change(link)
381
382
    @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
        interface = event.content['interface']
389
        link = self._get_link_from_interface(interface)
390
        if link and link.is_active():
391
            link.deactivate()
392
            self.notify_topology_update()
393
            self.notify_link_status_change(link)
394
395
    @listen_to('.*.interface.is.nni')
396
    def add_links(self, event):
397
        """Update the topology with links related to the NNI interfaces."""
398
        interface_a = event.content['interface_a']
399
        interface_b = event.content['interface_b']
400
401
        link = self._get_link_or_create(interface_a, interface_b)
402
        interface_a.update_link(link)
403
        interface_b.update_link(link)
404
405
        interface_a.nni = True
406
        interface_b.nni = True
407
408
        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
    def notify_topology_update(self):
428
        """Send an event to notify about updates on the topology."""
429
        name = 'kytos/topology.updated'
430
        event = KytosEvent(name=name, content={'topology':
431
                                               self._get_topology()})
432
        self.controller.buffers.app.put(event)
433
434
    def notify_link_status_change(self, link):
435
        """Send an event to notify about a status change on a link."""
436
        name = 'kytos/topology.'
437
        if link.is_active():
438
            status = 'link_up'
439
        else:
440
            status = 'link_down'
441
        event = KytosEvent(name=name+status, content={'link': link})
442
        self.controller.buffers.app.put(event)
443
444
    def notify_metadata_changes(self, obj, action):
445
        """Send an event to notify about metadata changes."""
446
        if isinstance(obj, Switch):
447
            entity = 'switch'
448
            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
        name = f'kytos/topology.{entities}.metadata.{action}'
457
        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
        self.controller.buffers.app.put(event)
460
        log.debug(f'Metadata from {obj.id} was {action}.')
461
462
    @listen_to('.*.switch.port.created')
463
    def notify_port_created(self, original_event):
464
        """Notify when a port is created."""
465
        name = f'kytos/topology.port.created'
466
        event = KytosEvent(name=name, content=original_event.content)
467
        self.controller.buffers.app.put(event)
468
469
    @listen_to('kytos/topology.*.metadata.*')
470
    def save_metadata_on_store(self, event):
471
        """Send to storehouse the data updated."""
472
        name = 'kytos.storehouse.update'
473
        if 'switch' in event.content:
474
            store = self.store_items.get('switches')
475
            obj = event.content.get('switch')
476
            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
        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
        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
        event = KytosEvent(name=name, content=content)
493
        self.controller.buffers.app.put(event)
494
495
    @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
    def verify_storehouse(self, entities):
505
        """Request a list of box saved by specific entity."""
506
        name = 'kytos.storehouse.list'
507
        content = {'namespace': f'kytos.topology.{entities}.metadata',
508
                   'callback': self.request_retrieve_entities}
509
        event = KytosEvent(name=name, content=content)
510
        self.controller.buffers.app.put(event)
511
        log.info(f'verify data in storehouse for {entities}.')
512
513
    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
    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
    def update_instance_metadata(self, obj):
542
        """Update object instance with saved metadata."""
543
        metadata = None
544
        if isinstance(obj, Interface):
545
            all_metadata = self.store_items.get('interfaces', None)
546
            if all_metadata:
547
                metadata = all_metadata.data.get(obj.id)
548
        elif isinstance(obj, Switch):
549
            all_metadata = self.store_items.get('switches', None)
550
            if all_metadata:
551
                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
        if metadata:
558
            obj.extend_metadata(metadata)
559
            log.debug(f'Metadata to {obj.id} was updated')
560