build.main.Main.shutdown()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 3
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
from werkzeug.exceptions import BadRequest, UnsupportedMediaType
9
10 1
from kytos.core import KytosEvent, KytosNApp, log, rest
11 1
from kytos.core.exceptions import KytosLinkCreationError
12 1
from kytos.core.helpers import listen_to
13 1
from kytos.core.interface import Interface
14 1
from kytos.core.link import Link
15 1
from kytos.core.switch import Switch
16 1
from napps.kytos.topology import settings
17 1
from napps.kytos.topology.exceptions import RestoreError
18 1
from napps.kytos.topology.models import Topology
19 1
from napps.kytos.topology.storehouse import StoreHouse
20
21 1
DEFAULT_LINK_UP_TIMER = 10
22 1
DEFAULT_INTERFACE_RESTORE_TIMER = 2
23 1
RESTORE_INTERFACE_ATTEMPTS = 20
24
25
26 1
class Main(KytosNApp):  # pylint: disable=too-many-public-methods
27
    """Main class of kytos/topology NApp.
28
29
    This class is the entry point for this napp.
30
    """
31
32 1
    def setup(self):
33
        """Initialize the NApp's links list."""
34 1
        self.links = {}
35 1
        self.store_items = {}
36 1
        self.switches_state = {}
37 1
        self.interfaces_state = {}
38 1
        self.links_state = {}
39 1
        self._verified_links = []
40 1
        self.link_up_timer = getattr(settings, 'LINK_UP_TIMER',
41
                                     DEFAULT_LINK_UP_TIMER)
42 1
        self.interface_restore = getattr(settings, 'INTERFACE_RESTORE_TIMER',
43
                                         DEFAULT_INTERFACE_RESTORE_TIMER)
44
45 1
        self.verify_storehouse('switches')
46 1
        self.verify_storehouse('interfaces')
47 1
        self.verify_storehouse('links')
48
49 1
        self.storehouse = StoreHouse(self.controller)
50
51 1
    def execute(self):
52
        """Execute once when the napp is running."""
53
        self._load_network_status()
54
55 1
    def shutdown(self):
56
        """Do nothing."""
57
        log.info('NApp kytos/topology shutting down.')
58
59 1
    @staticmethod
60
    def _get_metadata():
61
        """Return a JSON with metadata."""
62 1
        try:
63 1
            metadata = request.get_json()
64 1
            content_type = request.content_type
65 1
        except BadRequest:
66 1
            result = 'The request body is not a well-formed JSON.'
67 1
            raise BadRequest(result)
68 1
        if content_type is None:
69
            result = 'The request body is empty.'
70
            raise BadRequest(result)
71 1
        if metadata is None:
72 1
            if content_type != 'application/json':
73
                result = ('The content type must be application/json '
74
                          f'(received {content_type}).')
75
            else:
76 1
                result = 'Metadata is empty.'
77 1
            raise UnsupportedMediaType(result)
78 1
        return metadata
79
80 1
    def _get_link_or_create(self, endpoint_a, endpoint_b):
81 1
        new_link = Link(endpoint_a, endpoint_b)
82
83 1
        for link in self.links.values():
84
            if new_link == link:
85
                return link
86
87 1
        self.links[new_link.id] = new_link
88 1
        return new_link
89
90 1
    def _get_switches_dict(self):
91
        """Return a dictionary with the known switches."""
92 1
        switches = {'switches': {}}
93 1
        for idx, switch in enumerate(self.controller.switches.values()):
94 1
            switch_data = switch.as_dict()
95 1
            if not all(key in switch_data['metadata']
96
                       for key in ('lat', 'lng')):
97
                # Switches are initialized somewhere in the ocean
98
                switch_data['metadata']['lat'] = str(0.0)
99
                switch_data['metadata']['lng'] = str(-30.0+idx*10.0)
100 1
            switches['switches'][switch.id] = switch_data
101 1
        return switches
102
103 1
    def _get_links_dict(self):
104
        """Return a dictionary with the known links."""
105 1
        return {'links': {l.id: l.as_dict() for l in
106
                          self.links.values()}}
107
108 1
    def _get_topology_dict(self):
109
        """Return a dictionary with the known topology."""
110 1
        return {'topology': {**self._get_switches_dict(),
111
                             **self._get_links_dict()}}
112
113 1
    def _get_topology(self):
114
        """Return an object representing the topology."""
115 1
        return Topology(self.controller.switches, self.links)
116
117 1
    def _get_link_from_interface(self, interface):
118
        """Return the link of the interface, or None if it does not exist."""
119 1
        for link in self.links.values():
120 1
            if interface in (link.endpoint_a, link.endpoint_b):
121 1
                return link
122 1
        return None
123
124 1
    def _restore_link(self, link_id):
125
        """Restore link's administrative state from storehouse."""
126 1
        try:
127 1
            state = self.links_state[link_id]
128 1
        except KeyError:
129 1
            error = (f'The link {link_id} has no stored '
130
                     'administrative state to be restored.')
131 1
            raise RestoreError(error)
132
133 1
        try:
134 1
            link = self.links[link_id]
135 1
            if state['enabled']:
136 1
                link.enable()
137
            else:
138 1
                link.disable()
139 1
        except KeyError:
140 1
            error = ('Error restoring link status.'
141
                     f'The link {link_id} does not exist.')
142 1
            raise RestoreError(error)
143 1
        log.info(f'The state of link {link.id} has been restored.')
144 1
        self.notify_topology_update()
145 1
        self.update_instance_metadata(link)
146 1
        self.notify_link_status_change(link)
147
148 1
    def _restore_switch(self, switch_id):
149
        """Restore switch's administrative state from storehouse."""
150 1
        try:
151 1
            state = self.switches_state[switch_id]
152 1
        except KeyError:
153 1
            error = (f'The switch {switch_id} has no stored'
154
                     ' administrative state to be restored.')
155 1
            raise RestoreError(error)
156
157 1
        try:
158 1
            switch = self.controller.switches[switch_id]
159 1
        except KeyError:
160
            # Maybe we should remove the switch from switches_state here
161 1
            error = ('Error while restoring switches status. The '
162
                     f'switch {switch_id} does not exist.')
163 1
            raise RestoreError(error)
164
165 1
        if state:
166 1
            switch.enable()
167 1
            self.notify_switch_enabled(switch_id)
168
        else:
169 1
            switch.disable()
170 1
            self.notify_switch_disabled(switch_id)
171
172 1
        log.debug('Waiting to restore administrative state of switch '
173
                  f'{switch_id} interfaces.')
174 1
        i = 0
175
        # wait to restore interfaces
176 1
        while not switch.interfaces and i < RESTORE_INTERFACE_ATTEMPTS:
177
            time.sleep(self.interface_restore)
178
            i += 1
179 1
        if not switch.interfaces:
180
            error = ('Error restoring administrative state of switch '
181
                     f'{switch_id} interfaces.')
182
            raise RestoreError(error)
183
184
        # restore interfaces
185 1
        for interface_id in switch.interfaces:
186 1
            iface_id = ":".join([switch_id, str(interface_id)])
187
            # restore only the administrative state of saved interfaces
188 1
            if iface_id not in self.interfaces_state:
189
                error = ("The stored topology is different from the current "
190
                         f"topology. The interface {iface_id} hasn't been "
191
                         "stored.")
192
                log.info(error)
193
                continue
194 1
            state = self.interfaces_state[iface_id]
195 1
            iface_number = int(interface_id)
196 1
            iface_status, lldp_status = state
197 1
            try:
198 1
                interface = switch.interfaces[iface_number]
199
            except KeyError:
200
                log.error('Error restoring interface status: '
201
                          '%s does not exist.', iface_id)
202
                continue
203
204 1
            if iface_status:
205 1
                interface.enable()
206
            else:
207 1
                interface.disable()
208 1
            interface.lldp = lldp_status
209 1
            self.update_instance_metadata(interface)
210
211 1
        log.info(f'The state of switch {switch_id} has been restored.')
212
213
    # pylint: disable=attribute-defined-outside-init
214 1
    def _load_network_status(self):
215
        """Load network status saved in storehouse."""
216 1
        try:
217 1
            status = self.storehouse.get_data()
218
        except FileNotFoundError as error:
219
            log.info(error)
220
            return
221 1
        if status:
222 1
            switches = status['network_status']['switches']
223 1
            self.links_state = status['network_status']['links']
224
225 1
            for switch_id, switch_att in switches.items():
226
                # get switches status
227 1
                self.switches_state[switch_id] = switch_att['enabled']
228 1
                iface = switch_att['interfaces']
229
                # get interface status
230 1
                for iface_id, iface_att in iface.items():
231 1
                    enabled_value = iface_att['enabled']
232 1
                    lldp_value = iface_att['lldp']
233 1
                    self.interfaces_state[iface_id] = (enabled_value,
234
                                                       lldp_value)
235
236
        else:
237
            error = 'There is no status saved to restore.'
238
            log.info(error)
239
240 1
    @rest('v3/')
241
    def get_topology(self):
242
        """Return the latest known topology.
243
244
        This topology is updated when there are network events.
245
        """
246 1
        return jsonify(self._get_topology_dict())
247
248 1
    def restore_network_status(self, obj):
249
        """Restore the network administrative status saved in storehouse."""
250 1
        try:
251 1
            if isinstance(obj, Switch):
252 1
                self._restore_switch(obj.id)
253 1
            elif isinstance(obj, Link):
254 1
                if obj.id not in self._verified_links:
255 1
                    self._verified_links.append(obj.id)
256 1
                    self._restore_link(obj.id)
257
        except RestoreError as exc:
258
            log.debug(exc)
259
260
    # Switch related methods
261 1
    @rest('v3/switches')
262
    def get_switches(self):
263
        """Return a json with all the switches in the topology."""
264
        return jsonify(self._get_switches_dict())
265
266 1
    @rest('v3/switches/<dpid>/enable', methods=['POST'])
267
    def enable_switch(self, dpid):
268
        """Administratively enable a switch in the topology."""
269 1
        try:
270 1
            self.controller.switches[dpid].enable()
271 1
        except KeyError:
272 1
            return jsonify("Switch not found"), 404
273
274 1
        log.info(f"Storing administrative state from switch {dpid}"
275
                 " to enabled.")
276 1
        self.save_status_on_storehouse()
277 1
        self.notify_switch_enabled(dpid)
278 1
        return jsonify("Operation successful"), 201
279
280 1
    @rest('v3/switches/<dpid>/disable', methods=['POST'])
281
    def disable_switch(self, dpid):
282
        """Administratively disable a switch in the topology."""
283 1
        try:
284 1
            self.controller.switches[dpid].disable()
285 1
        except KeyError:
286 1
            return jsonify("Switch not found"), 404
287
288 1
        log.info(f"Storing administrative state from switch {dpid}"
289
                 " to disabled.")
290 1
        self.save_status_on_storehouse()
291 1
        self.notify_switch_disabled(dpid)
292 1
        return jsonify("Operation successful"), 201
293
294 1
    @rest('v3/switches/<dpid>/metadata')
295
    def get_switch_metadata(self, dpid):
296
        """Get metadata from a switch."""
297 1
        try:
298 1
            return jsonify({"metadata":
299
                            self.controller.switches[dpid].metadata}), 200
300 1
        except KeyError:
301 1
            return jsonify("Switch not found"), 404
302
303 1
    @rest('v3/switches/<dpid>/metadata', methods=['POST'])
304
    def add_switch_metadata(self, dpid):
305
        """Add metadata to a switch."""
306 1
        metadata = self._get_metadata()
307
308 1
        try:
309 1
            switch = self.controller.switches[dpid]
310 1
        except KeyError:
311 1
            return jsonify("Switch not found"), 404
312
313 1
        switch.extend_metadata(metadata)
314 1
        self.notify_metadata_changes(switch, 'added')
315 1
        return jsonify("Operation successful"), 201
316
317 1
    @rest('v3/switches/<dpid>/metadata/<key>', methods=['DELETE'])
318
    def delete_switch_metadata(self, dpid, key):
319
        """Delete metadata from a switch."""
320 1
        try:
321 1
            switch = self.controller.switches[dpid]
322 1
        except KeyError:
323 1
            return jsonify("Switch not found"), 404
324
325 1
        switch.remove_metadata(key)
326 1
        self.notify_metadata_changes(switch, 'removed')
327 1
        return jsonify("Operation successful"), 200
328
329
    # Interface related methods
330 1
    @rest('v3/interfaces')
331
    def get_interfaces(self):
332
        """Return a json with all the interfaces in the topology."""
333
        interfaces = {}
334
        switches = self._get_switches_dict()
335
        for switch in switches['switches'].values():
336
            for interface_id, interface in switch['interfaces'].items():
337
                interfaces[interface_id] = interface
338
339
        return jsonify({'interfaces': interfaces})
340
341 1 View Code Duplication
    @rest('v3/interfaces/switch/<dpid>/enable', methods=['POST'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
342 1
    @rest('v3/interfaces/<interface_enable_id>/enable', methods=['POST'])
343 1
    def enable_interface(self, interface_enable_id=None, dpid=None):
344
        """Administratively enable interfaces in the topology."""
345 1
        error_list = []  # List of interfaces that were not activated.
346 1
        msg_error = "Some interfaces couldn't be found and activated: "
347 1
        if dpid is None:
348 1
            dpid = ":".join(interface_enable_id.split(":")[:-1])
349 1
        try:
350 1
            switch = self.controller.switches[dpid]
351 1
        except KeyError as exc:
352 1
            return jsonify(f"Switch not found: {exc}"), 404
353
354 1
        if interface_enable_id:
355 1
            interface_number = int(interface_enable_id.split(":")[-1])
356
357 1
            try:
358 1
                switch.interfaces[interface_number].enable()
359 1
            except KeyError as exc:
360 1
                error_list.append(f"Switch {dpid} Interface {exc}")
361
        else:
362 1
            for interface in switch.interfaces.values():
363 1
                interface.enable()
364 1
        if not error_list:
365 1
            log.info(f"Storing administrative state for enabled interfaces.")
366 1
            self.save_status_on_storehouse()
367 1
            return jsonify("Operation successful"), 200
368 1
        return jsonify({msg_error:
369
                        error_list}), 409
370
371 1 View Code Duplication
    @rest('v3/interfaces/switch/<dpid>/disable', methods=['POST'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
372 1
    @rest('v3/interfaces/<interface_disable_id>/disable', methods=['POST'])
373 1
    def disable_interface(self, interface_disable_id=None, dpid=None):
374
        """Administratively disable interfaces in the topology."""
375 1
        error_list = []  # List of interfaces that were not deactivated.
376 1
        msg_error = "Some interfaces couldn't be found and deactivated: "
377 1
        if dpid is None:
378 1
            dpid = ":".join(interface_disable_id.split(":")[:-1])
379 1
        try:
380 1
            switch = self.controller.switches[dpid]
381 1
        except KeyError as exc:
382 1
            return jsonify(f"Switch not found: {exc}"), 404
383
384 1
        if interface_disable_id:
385 1
            interface_number = int(interface_disable_id.split(":")[-1])
386
387 1
            try:
388 1
                switch.interfaces[interface_number].disable()
389 1
            except KeyError as exc:
390 1
                error_list.append(f"Switch {dpid} Interface {exc}")
391
        else:
392 1
            for interface in switch.interfaces.values():
393 1
                interface.disable()
394 1
        if not error_list:
395 1
            log.info(f"Storing administrative state for disabled interfaces.")
396 1
            self.save_status_on_storehouse()
397 1
            return jsonify("Operation successful"), 200
398 1
        return jsonify({msg_error:
399
                        error_list}), 409
400
401 1
    @rest('v3/interfaces/<interface_id>/metadata')
402
    def get_interface_metadata(self, interface_id):
403
        """Get metadata from an interface."""
404 1
        switch_id = ":".join(interface_id.split(":")[:-1])
405 1
        interface_number = int(interface_id.split(":")[-1])
406 1
        try:
407 1
            switch = self.controller.switches[switch_id]
408 1
        except KeyError:
409 1
            return jsonify("Switch not found"), 404
410
411 1
        try:
412 1
            interface = switch.interfaces[interface_number]
413 1
        except KeyError:
414 1
            return jsonify("Interface not found"), 404
415
416 1
        return jsonify({"metadata": interface.metadata}), 200
417
418 1 View Code Duplication
    @rest('v3/interfaces/<interface_id>/metadata', methods=['POST'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
419
    def add_interface_metadata(self, interface_id):
420
        """Add metadata to an interface."""
421 1
        metadata = self._get_metadata()
422 1
        switch_id = ":".join(interface_id.split(":")[:-1])
423 1
        interface_number = int(interface_id.split(":")[-1])
424 1
        try:
425 1
            switch = self.controller.switches[switch_id]
426 1
        except KeyError:
427 1
            return jsonify("Switch not found"), 404
428
429 1
        try:
430 1
            interface = switch.interfaces[interface_number]
431 1
        except KeyError:
432 1
            return jsonify("Interface not found"), 404
433
434 1
        interface.extend_metadata(metadata)
435 1
        self.notify_metadata_changes(interface, 'added')
436 1
        return jsonify("Operation successful"), 201
437
438 1 View Code Duplication
    @rest('v3/interfaces/<interface_id>/metadata/<key>', methods=['DELETE'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
439
    def delete_interface_metadata(self, interface_id, key):
440
        """Delete metadata from an interface."""
441 1
        switch_id = ":".join(interface_id.split(":")[:-1])
442 1
        interface_number = int(interface_id.split(":")[-1])
443
444 1
        try:
445 1
            switch = self.controller.switches[switch_id]
446 1
        except KeyError:
447 1
            return jsonify("Switch not found"), 404
448
449 1
        try:
450 1
            interface = switch.interfaces[interface_number]
451 1
        except KeyError:
452 1
            return jsonify("Interface not found"), 404
453
454 1
        if interface.remove_metadata(key) is False:
455 1
            return jsonify("Metadata not found"), 404
456
457 1
        self.notify_metadata_changes(interface, 'removed')
458 1
        return jsonify("Operation successful"), 200
459
460
    # Link related methods
461 1
    @rest('v3/links')
462
    def get_links(self):
463
        """Return a json with all the links in the topology.
464
465
        Links are connections between interfaces.
466
        """
467
        return jsonify(self._get_links_dict()), 200
468
469 1
    @rest('v3/links/<link_id>/enable', methods=['POST'])
470
    def enable_link(self, link_id):
471
        """Administratively enable a link in the topology."""
472 1
        try:
473 1
            self.links[link_id].enable()
474 1
        except KeyError:
475 1
            return jsonify("Link not found"), 404
476 1
        self.save_status_on_storehouse()
477 1
        self.notify_link_status_change(self.links[link_id])
478 1
        return jsonify("Operation successful"), 201
479
480 1
    @rest('v3/links/<link_id>/disable', methods=['POST'])
481
    def disable_link(self, link_id):
482
        """Administratively disable a link in the topology."""
483 1
        try:
484 1
            self.links[link_id].disable()
485 1
        except KeyError:
486 1
            return jsonify("Link not found"), 404
487 1
        self.save_status_on_storehouse()
488 1
        self.notify_link_status_change(self.links[link_id])
489 1
        return jsonify("Operation successful"), 201
490
491 1
    @rest('v3/links/<link_id>/metadata')
492
    def get_link_metadata(self, link_id):
493
        """Get metadata from a link."""
494 1
        try:
495 1
            return jsonify({"metadata": self.links[link_id].metadata}), 200
496 1
        except KeyError:
497 1
            return jsonify("Link not found"), 404
498
499 1
    @rest('v3/links/<link_id>/metadata', methods=['POST'])
500
    def add_link_metadata(self, link_id):
501
        """Add metadata to a link."""
502 1
        metadata = self._get_metadata()
503 1
        try:
504 1
            link = self.links[link_id]
505 1
        except KeyError:
506 1
            return jsonify("Link not found"), 404
507
508 1
        link.extend_metadata(metadata)
509 1
        self.notify_metadata_changes(link, 'added')
510 1
        return jsonify("Operation successful"), 201
511
512 1
    @rest('v3/links/<link_id>/metadata/<key>', methods=['DELETE'])
513
    def delete_link_metadata(self, link_id, key):
514
        """Delete metadata from a link."""
515 1
        try:
516 1
            link = self.links[link_id]
517 1
        except KeyError:
518 1
            return jsonify("Link not found"), 404
519
520 1
        if link.remove_metadata(key) is False:
521 1
            return jsonify("Metadata not found"), 404
522
523 1
        self.notify_metadata_changes(link, 'removed')
524 1
        return jsonify("Operation successful"), 200
525
526 1
    @listen_to('.*.switch.(new|reconnected)')
527
    def handle_new_switch(self, event):
528
        """Create a new Device on the Topology.
529
530
        Handle the event of a new created switch and update the topology with
531
        this new device. Also notify if the switch is enabled.
532
        """
533 1
        switch = event.content['switch']
534 1
        switch.activate()
535 1
        log.debug('Switch %s added to the Topology.', switch.id)
536 1
        self.notify_topology_update()
537 1
        self.update_instance_metadata(switch)
538 1
        self.restore_network_status(switch)
539 1
        if switch.is_enabled():
540 1
            self.notify_switch_enabled(switch.id)
541
542 1
    @listen_to('.*.connection.lost')
543
    def handle_connection_lost(self, event):
544
        """Remove a Device from the topology.
545
546
        Remove the disconnected Device and every link that has one of its
547
        interfaces.
548
        """
549 1
        switch = event.content['source'].switch
550 1
        if switch:
551 1
            switch.deactivate()
552 1
            log.debug('Switch %s removed from the Topology.', switch.id)
553 1
            self.notify_topology_update()
554
555 1
    def handle_interface_up(self, event):
556
        """Update the topology based on a Port Modify event.
557
558
        The event notifies that an interface was changed to 'up'.
559
        """
560 1
        interface = event.content['interface']
561 1
        interface.activate()
562 1
        self.notify_topology_update()
563 1
        self.update_instance_metadata(interface)
564
565 1
    @listen_to('.*.switch.interface.created')
566
    def handle_interface_created(self, event):
567
        """Update the topology based on a Port Create event."""
568 1
        self.handle_interface_up(event)
569
570 1
    def handle_interface_down(self, event):
571
        """Update the topology based on a Port Modify event.
572
573
        The event notifies that an interface was changed to 'down'.
574
        """
575 1
        interface = event.content['interface']
576 1
        interface.deactivate()
577 1
        self.handle_interface_link_down(event)
578 1
        self.notify_topology_update()
579
580 1
    @listen_to('.*.switch.interface.deleted')
581
    def handle_interface_deleted(self, event):
582
        """Update the topology based on a Port Delete event."""
583 1
        self.handle_interface_down(event)
584
585 1
    @listen_to('.*.switch.interface.link_up')
586
    def handle_interface_link_up(self, event):
587
        """Update the topology based on a Port Modify event.
588
589
        The event notifies that an interface's link was changed to 'up'.
590
        """
591 1
        interface = event.content['interface']
592 1
        self.handle_link_up(interface)
593
594 1
    @listen_to('kytos/maintenance.end_switch')
595
    def handle_switch_maintenance_end(self, event):
596
        """Handle the end of the maintenance of a switch."""
597 1
        switches = event.content['switches']
598 1
        for switch in switches:
599 1
            switch.enable()
600 1
            switch.activate()
601 1
            for interface in switch.interfaces.values():
602 1
                interface.enable()
603 1
                self.handle_link_up(interface)
604
605 1
    def handle_link_up(self, interface):
606
        """Notify a link is up."""
607 1
        link = self._get_link_from_interface(interface)
608 1
        if not link:
609
            return
610 1
        if link.endpoint_a == interface:
611 1
            other_interface = link.endpoint_b
612
        else:
613
            other_interface = link.endpoint_a
614 1
        interface.activate()
615 1
        if other_interface.is_active() is False:
616
            return
617 1
        if link.is_active() is False:
618 1
            link.update_metadata('last_status_change', time.time())
619 1
            link.activate()
620
621
            # As each run of this method uses a different thread,
622
            # there is no risk this sleep will lock the NApp.
623 1
            time.sleep(self.link_up_timer)
624
625 1
            last_status_change = link.get_metadata('last_status_change')
626 1
            now = time.time()
627 1
            if link.is_active() and \
628
                    now - last_status_change >= self.link_up_timer:
629 1
                self.notify_topology_update()
630 1
                self.update_instance_metadata(link)
631 1
                self.notify_link_status_change(link)
632
633 1
    @listen_to('.*.switch.interface.link_down')
634
    def handle_interface_link_down(self, event):
635
        """Update the topology based on a Port Modify event.
636
637
        The event notifies that an interface's link was changed to 'down'.
638
        """
639 1
        interface = event.content['interface']
640 1
        self.handle_link_down(interface)
641
642 1
    @listen_to('kytos/maintenance.start_switch')
643
    def handle_switch_maintenance_start(self, event):
644
        """Handle the start of the maintenance of a switch."""
645 1
        switches = event.content['switches']
646 1
        for switch in switches:
647 1
            switch.disable()
648 1
            switch.deactivate()
649 1
            for interface in switch.interfaces.values():
650 1
                interface.disable()
651 1
                if interface.is_active():
652 1
                    self.handle_link_down(interface)
653
654 1
    def handle_link_down(self, interface):
655
        """Notify a link is down."""
656 1
        link = self._get_link_from_interface(interface)
657 1
        if link and link.is_active():
658 1
            link.deactivate()
659 1
            link.update_metadata('last_status_change', time.time())
660 1
            self.notify_topology_update()
661 1
            self.notify_link_status_change(link)
662
663 1
    @listen_to('.*.interface.is.nni')
664
    def add_links(self, event):
665
        """Update the topology with links related to the NNI interfaces."""
666 1
        interface_a = event.content['interface_a']
667 1
        interface_b = event.content['interface_b']
668
669 1
        try:
670 1
            link = self._get_link_or_create(interface_a, interface_b)
671
        except KytosLinkCreationError as err:
672
            log.error(f'Error creating link: {err}.')
673
            return
674
675 1
        interface_a.update_link(link)
676 1
        interface_b.update_link(link)
677
678 1
        interface_a.nni = True
679 1
        interface_b.nni = True
680
681 1
        self.notify_topology_update()
682 1
        self.restore_network_status(link)
683
684
    # def add_host(self, event):
685
    #    """Update the topology with a new Host."""
686
687
    #    interface = event.content['port']
688
    #    mac = event.content['reachable_mac']
689
690
    #    host = Host(mac)
691
    #    link = self.topology.get_link(interface.id)
692
    #    if link is not None:
693
    #        return
694
695
    #    self.topology.add_link(interface.id, host.id)
696
    #    self.topology.add_device(host)
697
698
    #    if settings.DISPLAY_FULL_DUPLEX_LINKS:
699
    #        self.topology.add_link(host.id, interface.id)
700
701
    # pylint: disable=unused-argument
702 1
    @listen_to('.*.network_status.updated')
703 1
    def save_status_on_storehouse(self, event=None):
704
        """Save the network administrative status using storehouse."""
705 1
        status = self._get_switches_dict()
706 1
        status['id'] = 'network_status'
707 1
        if event:
708
            content = event.content
709
            log.info(f"Storing the administrative state of the"
710
                     f" {content['attribute']} attribute to"
711
                     f" {content['state']} in the interfaces"
712
                     f" {content['interface_ids']}")
713 1
        status.update(self._get_links_dict())
714 1
        self.storehouse.save_status(status)
715
716 1
    def notify_switch_enabled(self, dpid):
717
        """Send an event to notify that a switch is enabled."""
718 1
        name = 'kytos/topology.switch.enabled'
719 1
        event = KytosEvent(name=name, content={'dpid': dpid})
720 1
        self.controller.buffers.app.put(event)
721
722 1
    def notify_switch_disabled(self, dpid):
723
        """Send an event to notify that a switch is disabled."""
724 1
        name = 'kytos/topology.switch.disabled'
725 1
        event = KytosEvent(name=name, content={'dpid': dpid})
726 1
        self.controller.buffers.app.put(event)
727
728 1
    def notify_topology_update(self):
729
        """Send an event to notify about updates on the topology."""
730 1
        name = 'kytos/topology.updated'
731 1
        event = KytosEvent(name=name, content={'topology':
732
                                               self._get_topology()})
733 1
        self.controller.buffers.app.put(event)
734
735 1
    def notify_link_status_change(self, link):
736
        """Send an event to notify about a status change on a link."""
737 1
        name = 'kytos/topology.'
738 1
        if link.is_active() and link.is_enabled():
739 1
            status = 'link_up'
740
        else:
741
            status = 'link_down'
742 1
        event = KytosEvent(name=name+status, content={'link': link})
743 1
        self.controller.buffers.app.put(event)
744
745 1
    def notify_metadata_changes(self, obj, action):
746
        """Send an event to notify about metadata changes."""
747 1
        if isinstance(obj, Switch):
748 1
            entity = 'switch'
749 1
            entities = 'switches'
750 1
        elif isinstance(obj, Interface):
751 1
            entity = 'interface'
752 1
            entities = 'interfaces'
753
        elif isinstance(obj, Link):
754
            entity = 'link'
755
            entities = 'links'
756
757 1
        name = f'kytos/topology.{entities}.metadata.{action}'
758 1
        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...
759
                                               'metadata': obj.metadata})
760 1
        self.controller.buffers.app.put(event)
761 1
        log.debug(f'Metadata from {obj.id} was {action}.')
762
763 1
    @listen_to('.*.switch.port.created')
764
    def notify_port_created(self, original_event):
765
        """Notify when a port is created."""
766 1
        name = 'kytos/topology.port.created'
767 1
        event = KytosEvent(name=name, content=original_event.content)
768 1
        self.controller.buffers.app.put(event)
769
770 1
    @listen_to('kytos/topology.*.metadata.*')
771
    def save_metadata_on_store(self, event):
772
        """Send to storehouse the data updated."""
773 1
        name = 'kytos.storehouse.update'
774 1
        if 'switch' in event.content:
775 1
            store = self.store_items.get('switches')
776 1
            obj = event.content.get('switch')
777 1
            namespace = 'kytos.topology.switches.metadata'
778 1
        elif 'interface' in event.content:
779 1
            store = self.store_items.get('interfaces')
780 1
            obj = event.content.get('interface')
781 1
            namespace = 'kytos.topology.interfaces.metadata'
782 1
        elif 'link' in event.content:
783 1
            store = self.store_items.get('links')
784 1
            obj = event.content.get('link')
785 1
            namespace = 'kytos.topology.links.metadata'
786
787 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...
788 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...
789
                   'box_id': store.box_id,
790
                   'data': store.data,
791
                   'callback': self.update_instance}
792
793 1
        event = KytosEvent(name=name, content=content)
794 1
        self.controller.buffers.app.put(event)
795
796 1
    @staticmethod
797
    def update_instance(event, _data, error):
798
        """Display in Kytos console if the data was updated."""
799
        entities = event.content.get('namespace', '').split('.')[-2]
800
        if error:
801
            log.error(f'Error trying to update storehouse {entities}.')
802
        else:
803
            log.debug(f'Storehouse update to entities: {entities}.')
804
805 1
    def verify_storehouse(self, entities):
806
        """Request a list of box saved by specific entity."""
807 1
        name = 'kytos.storehouse.list'
808 1
        content = {'namespace': f'kytos.topology.{entities}.metadata',
809
                   'callback': self.request_retrieve_entities}
810 1
        event = KytosEvent(name=name, content=content)
811 1
        self.controller.buffers.app.put(event)
812 1
        log.info(f'verify data in storehouse for {entities}.')
813
814 1
    def request_retrieve_entities(self, event, data, _error):
815
        """Create a box or retrieve an existent box from storehouse."""
816 1
        msg = ''
817 1
        content = {'namespace': event.content.get('namespace'),
818
                   'callback': self.load_from_store,
819
                   'data': {}}
820
821 1
        if not data:
822 1
            name = 'kytos.storehouse.create'
823 1
            msg = 'Create new box in storehouse'
824
        else:
825 1
            name = 'kytos.storehouse.retrieve'
826 1
            content['box_id'] = data[0]
827 1
            msg = 'Retrieve data from storehouse.'
828
829 1
        event = KytosEvent(name=name, content=content)
830 1
        self.controller.buffers.app.put(event)
831 1
        log.debug(msg)
832
833 1
    def load_from_store(self, event, box, error):
834
        """Save the data retrived from storehouse."""
835
        entities = event.content.get('namespace', '').split('.')[-2]
836
        if error:
837
            log.error('Error while get a box from storehouse.')
838
        else:
839
            self.store_items[entities] = box
840
            log.debug('Data updated')
841
842 1
    def update_instance_metadata(self, obj):
843
        """Update object instance with saved metadata."""
844 1
        metadata = None
845 1
        if isinstance(obj, Interface):
846 1
            all_metadata = self.store_items.get('interfaces', None)
847 1
            if all_metadata:
848
                metadata = all_metadata.data.get(obj.id)
849 1
        elif isinstance(obj, Switch):
850
            all_metadata = self.store_items.get('switches', None)
851
            if all_metadata:
852
                metadata = all_metadata.data.get(obj.id)
853 1
        elif isinstance(obj, Link):
854 1
            all_metadata = self.store_items.get('links', None)
855 1
            if all_metadata:
856
                metadata = all_metadata.data.get(obj.id)
857 1
        if metadata:
858
            obj.extend_metadata(metadata)
859
            log.debug(f'Metadata to {obj.id} was updated')
860
861 1
    @listen_to('kytos/maintenance.start_link')
862
    def handle_link_maintenance_start(self, event):
863
        """Deals with the start of links maintenance."""
864 1
        notify_links = []
865 1
        maintenance_links = event.content['links']
866 1
        for maintenance_link in maintenance_links:
867 1
            try:
868 1
                link = self.links[maintenance_link.id]
869 1
            except KeyError:
870 1
                continue
871 1
            notify_links.append(link)
872 1
        for link in notify_links:
873 1
            link.disable()
874 1
            link.deactivate()
875 1
            link.endpoint_a.deactivate()
876 1
            link.endpoint_b.deactivate()
877 1
            link.endpoint_a.disable()
878 1
            link.endpoint_b.disable()
879 1
            self.notify_link_status_change(link)
880
881 1
    @listen_to('kytos/maintenance.end_link')
882
    def handle_link_maintenance_end(self, event):
883
        """Deals with the end of links maintenance."""
884 1
        notify_links = []
885 1
        maintenance_links = event.content['links']
886 1
        for maintenance_link in maintenance_links:
887 1
            try:
888 1
                link = self.links[maintenance_link.id]
889 1
            except KeyError:
890 1
                continue
891 1
            notify_links.append(link)
892 1
        for link in notify_links:
893 1
            link.enable()
894 1
            link.activate()
895 1
            link.endpoint_a.activate()
896 1
            link.endpoint_b.activate()
897 1
            link.endpoint_a.enable()
898 1
            link.endpoint_b.enable()
899
            self.notify_link_status_change(link)
900