Passed
Pull Request — master (#148)
by Carlos
03:28
created

build.main   F

Complexity

Total Complexity 164

Size/Duplication

Total Lines 894
Duplicated Lines 6.49 %

Test Coverage

Coverage 89.83%

Importance

Changes 0
Metric Value
eloc 635
dl 58
loc 894
rs 1.965
c 0
b 0
f 0
ccs 521
cts 580
cp 0.8983
wmc 164

60 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.notify_port_created() 0 6 1
A Main.disable_switch() 0 13 2
A Main.verify_storehouse() 0 8 1
A Main.load_from_store() 0 8 2
A Main.request_retrieve_entities() 0 18 2
A Main.disable_link() 0 9 2
A Main.enable_switch() 0 13 2
B Main.enable_interface() 29 29 7
A Main.add_switch_metadata() 0 19 4
A Main.get_interfaces() 0 10 3
A Main._get_switches_dict() 0 12 3
A Main.delete_interface_metadata() 0 21 4
B Main.add_interface_metadata() 0 26 5
B Main.update_instance_metadata() 0 18 8
A Main.get_switches() 0 4 1
A Main.handle_link_maintenance_end() 0 19 4
A Main.notify_link_status_change() 0 9 2
A Main.handle_interface_down() 0 9 1
A Main.handle_interface_link_up() 0 8 1
B Main.handle_link_up() 0 27 7
A Main.notify_switch_enabled() 0 5 1
A Main.handle_interface_created() 0 4 1
A Main.get_interface_metadata() 0 16 3
A Main.delete_link_metadata() 0 13 3
A Main.shutdown() 0 3 1
C Main._restore_switch() 0 64 11
A Main.notify_topology_update() 0 6 1
A Main.enable_link() 0 9 2
A Main.handle_link_down() 0 8 3
A Main.setup() 0 18 1
A Main.handle_new_switch() 0 13 1
A Main.execute() 0 3 1
A Main.save_status_on_storehouse() 0 13 2
A Main.get_switch_metadata() 0 8 2
A Main.update_instance() 0 8 2
A Main.handle_switch_maintenance_end() 0 10 3
A Main.handle_connection_lost() 0 12 2
A Main.get_links() 0 7 1
B Main.disable_interface() 29 29 7
A Main.handle_switch_maintenance_start() 0 11 4
B Main._load_network_status() 0 25 5
A Main.get_topology() 0 7 1
A Main._restore_link() 0 22 4
A Main.save_metadata_on_store() 0 25 4
A Main.handle_interface_link_down() 0 8 1
A Main._get_links_dict() 0 4 1
A Main.handle_link_maintenance_start() 0 19 4
A Main.notify_switch_disabled() 0 5 1
A Main.add_links() 0 20 2
A Main.handle_interface_up() 0 9 1
A Main.handle_interface_deleted() 0 4 1
A Main._get_link_or_create() 0 9 3
A Main.restore_network_status() 0 11 5
A Main._get_link_from_interface() 0 6 3
A Main.add_link_metadata() 0 19 4
A Main.notify_metadata_changes() 0 17 4
A Main.delete_switch_metadata() 0 11 2
A Main._get_topology_dict() 0 4 1
A Main.get_link_metadata() 0 7 2
A Main._get_topology() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like build.main often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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