Passed
Branch master (45645d)
by Humberto
03:09
created

build.main.Main._get_links_dict()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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