Test Failed
Pull Request — master (#76)
by Antonio
02:02
created

build.main.Main._get_switches_dict()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

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