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