Test Failed
Pull Request — master (#80)
by
unknown
02:23
created

build.main.Main._get_data()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2.5

Importance

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