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