Passed
Push — master ( 61767c...941289 )
by Italo Valcy
01:54 queued 12s
created

build.main   F

Complexity

Total Complexity 156

Size/Duplication

Total Lines 897
Duplicated Lines 10.93 %

Test Coverage

Coverage 92.62%

Importance

Changes 0
Metric Value
eloc 643
dl 98
loc 897
rs 1.957
c 0
b 0
f 0
ccs 527
cts 569
cp 0.9262
wmc 156

61 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.disable_switch() 0 13 2
A Main.disable_link() 0 13 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
A Main.get_switches() 0 4 1
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.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
A Main.enable_link() 0 13 2
A Main.handle_link_down() 0 8 3
A Main.setup() 0 14 1
A Main.handle_new_switch() 0 14 2
A Main.execute() 0 5 2
A Main._load_switch() 0 38 4
A Main.get_switch_metadata() 0 8 2
A Main._get_metadata() 0 20 5
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 44 7
A Main.get_topology() 0 7 1
A Main.handle_interface_link_down() 0 8 1
A Main._get_links_dict() 0 4 1
A Main.add_links() 0 19 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._get_link_from_interface() 0 6 3
A Main.add_link_metadata() 0 12 2
A Main._load_link() 0 29 3
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
A Main.notify_port_created() 0 6 1
A Main.verify_storehouse() 0 8 1
A Main.load_from_store() 0 8 2
A Main.request_retrieve_entities() 0 18 2
B Main.update_instance_metadata() 0 18 8
A Main.handle_link_maintenance_end() 0 19 4
A Main.notify_link_status_change() 0 14 3
A Main.notify_switch_enabled() 0 5 1
A Main.notify_topology_update() 0 6 1
A Main.save_status_on_storehouse() 0 7 2
A Main.update_instance() 0 8 2
A Main.save_metadata_on_store() 0 25 4
A Main.handle_link_maintenance_start() 0 19 4
A Main.notify_switch_disabled() 0 5 1
A Main.handle_network_status_updated() 0 9 1
A Main.notify_metadata_changes() 0 17 4

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