Test Failed
Pull Request — master (#80)
by
unknown
03:21
created

main.py (2 issues)

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