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