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