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

build.main.Main.handle_interface_link_up()   B

Complexity

Conditions 7

Size

Total Lines 28
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7

Importance

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