Passed
Pull Request — master (#148)
by Carlos
02:22
created

build.main   F

Complexity

Total Complexity 162

Size/Duplication

Total Lines 892
Duplicated Lines 10.99 %

Test Coverage

Coverage 89.46%

Importance

Changes 0
Metric Value
eloc 632
dl 98
loc 892
ccs 518
cts 579
cp 0.8946
rs 1.968
c 0
b 0
f 0
wmc 162

61 Methods

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