Passed
Push — master ( 9ba056...d82f2c )
by Beraldo
04:53 queued 11s
created

build.main   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 605
Duplicated Lines 0 %

Test Coverage

Coverage 58.62%

Importance

Changes 0
Metric Value
eloc 341
dl 0
loc 605
rs 2
c 0
b 0
f 0
ccs 187
cts 319
cp 0.5862
wmc 96

23 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.get_circuit() 0 13 2
A Main.setup() 0 23 1
A Main.delete_circuit() 0 28 4
A Main.list_circuits() 0 8 2
B Main.load_evcs() 0 26 8
B Main.update_schedule() 0 61 6
C Main._evc_from_dict() 0 40 9
A Main._find_evc_by_schedule_id() 0 21 5
A Main.handle_link_up() 0 6 4
A Main.load_circuits_by_interface() 0 13 4
A Main.delete_schedule() 0 33 3
A Main.shutdown() 0 2 1
A Main.add_to_dict_of_sets() 0 5 2
A Main.handle_link_down() 0 7 3
A Main.execute() 0 2 1
A Main.list_schedules() 0 24 5
B Main.create_circuit() 0 60 5
A Main._link_from_dict() 0 20 4
A Main._uni_from_dict() 0 18 4
A Main._is_duplicated_evc() 0 14 4
B Main.update() 0 30 6
A Main._get_circuits_buffer() 0 13 3
C Main.create_schedule() 0 89 10

How to fix   Complexity   

Complexity

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/mef_eline Kytos Network Application.
2
3
NApp to provision circuits from user request.
4
"""
5 2
from flask import jsonify, request
6 2
from werkzeug.exceptions import BadRequest
7
8 2
from kytos.core import KytosNApp, log, rest
9 2
from kytos.core.events import KytosEvent
10 2
from kytos.core.helpers import listen_to
11 2
from kytos.core.interface import TAG, UNI
12 2
from kytos.core.link import Link
13 2
from napps.kytos.mef_eline.models import EVC, DynamicPathManager
14 2
from napps.kytos.mef_eline.scheduler import CircuitSchedule, Scheduler
15 2
from napps.kytos.mef_eline.storehouse import StoreHouse
16
17
18 2
class Main(KytosNApp):
19
    """Main class of amlight/mef_eline NApp.
20
21
    This class is the entry point for this napp.
22
    """
23
24 2
    def setup(self):
25
        """Replace the '__init__' method for the KytosNApp subclass.
26
27
        The setup method is automatically called by the controller when your
28
        application is loaded.
29
30
        So, if you have any setup routine, insert it here.
31
        """
32
        # object used to scheduler circuit events
33 2
        self.sched = Scheduler()
34
35
        # object to save and load circuits
36 2
        self.storehouse = StoreHouse(self.controller)
37
38
        # set the controller that will manager the dynamic paths
39 2
        DynamicPathManager.set_controller(self.controller)
40
41
        # dictionary of EVCs created. It acts as a circuit buffer.
42
        # Every create/update/delete must be synced to storehouse.
43 2
        self.circuits = {}
44
45
        # dictionary of EVCs by interface
46 2
        self._circuits_by_interface = {}
47
48 2
    def execute(self):
49
        """Execute once when the napp is running."""
50
51 2
    def shutdown(self):
52
        """Execute when your napp is unloaded.
53
54
        If you have some cleanup procedure, insert it here.
55
        """
56
57 2
    @rest('/v2/evc/', methods=['GET'])
58
    def list_circuits(self):
59
        """Endpoint to return all circuits stored."""
60 2
        circuits = self.storehouse.get_data()
61 2
        if not circuits:
62 2
            return jsonify({}), 200
63
64 2
        return jsonify(circuits), 200
65
66 2
    @rest('/v2/evc/<circuit_id>', methods=['GET'])
67
    def get_circuit(self, circuit_id):
68
        """Endpoint to return a circuit based on id."""
69 2
        circuits = self.storehouse.get_data()
70
71 2
        try:
72 2
            result = circuits[circuit_id]
73 2
            status = 200
74 2
        except KeyError:
75 2
            result = {'response': f'circuit_id {circuit_id} not found'}
76 2
            status = 404
77
78 2
        return jsonify(result), status
79
80 2
    @rest('/v2/evc/', methods=['POST'])
81
    def create_circuit(self):
82
        """Try to create a new circuit.
83
84
        Firstly, for EVPL: E-Line NApp verifies if UNI_A's requested C-VID and
85
        UNI_Z's requested C-VID are available from the interfaces' pools. This
86
        is checked when creating the UNI object.
87
88
        Then, E-Line NApp requests a primary and a backup path to the
89
        Pathfinder NApp using the attributes primary_links and backup_links
90
        submitted via REST
91
92
        # For each link composing paths in #3:
93
        #  - E-Line NApp requests a S-VID available from the link VLAN pool.
94
        #  - Using the S-VID obtained, generate abstract flow entries to be
95
        #    sent to FlowManager
96
97
        Push abstract flow entries to FlowManager and FlowManager pushes
98
        OpenFlow entries to datapaths
99
100
        E-Line NApp generates an event to notify all Kytos NApps of a new EVC
101
        creation
102
103
        Finnaly, notify user of the status of its request.
104
        """
105
        # Try to create the circuit object
106 2
        data = request.get_json()
107
108 2
        if not data:
109 2
            return jsonify("Bad request: The request do not have a json."), 400
110
111 2
        try:
112 2
            evc = self._evc_from_dict(data)
113
        except ValueError as exception:
114
            log.error(exception)
115
            return jsonify("Bad request: {}".format(exception)), 400
116
117
        # verify duplicated evc
118 2
        if self._is_duplicated_evc(evc):
119 2
            return jsonify("Not Acceptable: This evc already exists."), 409
120
121
        # store circuit in dictionary
122 2
        self.circuits[evc.id] = evc
123
124
        # save circuit
125 2
        self.storehouse.save_evc(evc)
126
127
        # Schedule the circuit deploy
128 2
        self.sched.add(evc)
129
130
        # Circuit has no schedule, deploy now
131 2
        if not evc.circuit_scheduler:
132 2
            evc.deploy()
133
134
        # Notify users
135 2
        event = KytosEvent(name='kytos.mef_eline.created',
136
                           content=evc.as_dict())
137 2
        self.controller.buffers.app.put(event)
138
139 2
        return jsonify({"circuit_id": evc.id}), 201
140
141 2
    @rest('/v2/evc/<circuit_id>', methods=['PATCH'])
142
    def update(self, circuit_id):
143
        """Update a circuit based on payload.
144
145
        The EVC required attributes (name, uni_a, uni_z) can't be updated.
146
        """
147
        try:
148
            evc = self.circuits[circuit_id]
149
            data = request.get_json()
150
            evc.update(**data)
151
        except ValueError as exception:
152
            log.error(exception)
153
            result = {'response': 'Bad Request: {}'.format(exception)}
154
            status = 400
155
        except TypeError:
156
            result = {'response': 'Content-Type must be application/json'}
157
            status = 415
158
        except BadRequest:
159
            response = 'Bad Request: The request is not a valid JSON.'
160
            result = {'response': response}
161
            status = 400
162
        except KeyError:
163
            result = {'response': f'circuit_id {circuit_id} not found'}
164
            status = 404
165
        else:
166
            evc.sync()
167
            result = {evc.id: evc.as_dict()}
168
            status = 200
169
170
        return jsonify(result), status
171
172 2
    @rest('/v2/evc/<circuit_id>', methods=['DELETE'])
173
    def delete_circuit(self, circuit_id):
174
        """Remove a circuit.
175
176
        First, the flows are removed from the switches, and then the EVC is
177
        disabled.
178
        """
179
        try:
180
            evc = self.circuits[circuit_id]
181
        except KeyError:
182
            result = {'response': f'circuit_id {circuit_id} not found'}
183
            status = 404
184
        else:
185
            log.info(f'Removing {circuit_id}')
186
            if evc.archived:
187
                result = {'response': f'Circuit {circuit_id} already removed'}
188
                status = 404
189
            else:
190
                evc.remove_current_flows()
191
                evc.deactivate()
192
                evc.disable()
193
                self.sched.remove(evc)
194
                evc.archive()
195
                evc.sync()
196
                result = {'response': f'Circuit {circuit_id} removed'}
197
                status = 200
198
199
        return jsonify(result), status
200
201 2
    @rest('/v2/evc/schedule', methods=['GET'])
202
    def list_schedules(self):
203
        """Endpoint to return all schedules stored for all circuits.
204
205
        Return a JSON with the following template:
206
        [{"schedule_id": <schedule_id>,
207
         "circuit_id": <circuit_id>,
208
         "schedule": <schedule object>}]
209
        """
210 2
        circuits = self.storehouse.get_data().values()
211 2
        if not circuits:
212 2
            return jsonify({}), 200
213
214 2
        result = []
215 2
        for circuit in circuits:
216 2
            circuit_scheduler = circuit.get("circuit_scheduler")
217 2
            if circuit_scheduler:
218 2
                for scheduler in circuit_scheduler:
219 2
                    value = {"schedule_id": scheduler.get("id"),
220
                             "circuit_id": circuit.get("id"),
221
                             "schedule": scheduler}
222 2
                    result.append(value)
223
224 2
        return jsonify(result), 200
225
226 2
    @rest('/v2/evc/schedule/', methods=['POST'])
227
    def create_schedule(self):
228
        """
229
        Create a new schedule for a given circuit.
230
231
        This service do no check if there are conflicts with another schedule.
232
        Payload example:
233
            {
234
              "circuit_id":"aa:bb:cc",
235
              "schedule": {
236
                "date": "2019-08-07T14:52:10.967Z",
237
                "interval": "string",
238
                "frequency": "1 * * * *",
239
                "action": "create"
240
              }
241
            }
242
        """
243 2
        try:
244
            # Try to create the circuit object
245 2
            json_data = request.get_json()
246 2
            result = ""
247 2
            status = 200
248
249 2
            circuit_id = json_data.get("circuit_id")
250 2
            schedule_data = json_data.get("schedule")
251
252 2
            if not json_data:
253
                result = "Bad request: The request does not have a json."
254
                status = 400
255
                return jsonify(result), status
256 2
            if not circuit_id:
257
                result = result = "Bad request: Missing circuit_id."
258
                status = 400
259
                return jsonify(result), status
260 2
            if not schedule_data:
261
                result = "Bad request: Missing schedule data."
262
                status = 400
263
                return jsonify(result), status
264
265
            # Get EVC from circuits buffer
266 2
            circuits = self._get_circuits_buffer()
267
268
            # get the circuit
269 2
            evc = circuits.get(circuit_id)
270
271
            # get the circuit
272 2
            if not evc:
273
                result = {'response': f'circuit_id {circuit_id} not found'}
274
                status = 404
275
                return jsonify(result), status
276
            # Can not modify circuits deleted and archived
277 2
            if evc.archived:
278
                result = {'response': f'Circuit is archived.'
279
                                      f'Update is forbidden.'}
280
                status = 403
281
                return jsonify(result), status
282
283
            # new schedule from dict
284 2
            new_schedule = CircuitSchedule.from_dict(schedule_data)
285
286
            # If there is no schedule, create the list
287 2
            if not evc.circuit_scheduler:
288
                evc.circuit_scheduler = []
289
290
            # Add the new schedule
291 2
            evc.circuit_scheduler.append(new_schedule)
292
293
            # Add schedule job
294 2
            self.sched.add_circuit_job(evc, new_schedule)
295
296
            # save circuit to storehouse
297 2
            evc.sync()
298
299 2
            result = new_schedule.as_dict()
300 2
            status = 201
301
302
        except ValueError as exception:
303
            log.error(exception)
304
            result = {'response': 'Bad Request: {}'.format(exception)}
305
            status = 400
306
        except TypeError:
307
            result = {'response': 'Content-Type must be application/json'}
308
            status = 415
309
        except BadRequest:
310
            response = 'Bad Request: The request is not a valid JSON.'
311
            result = {'response': response}
312
            status = 400
313
314 2
        return jsonify(result), status
315
316 2
    @rest('/v2/evc/schedule/<schedule_id>', methods=['PATCH'])
317
    def update_schedule(self, schedule_id):
318
        """Update a schedule.
319
320
        Change all attributes from the given schedule from a EVC circuit.
321
        The schedule ID is preserved as default.
322
        Payload example:
323
            {
324
              "date": "2019-08-07T14:52:10.967Z",
325
              "interval": "string",
326
              "frequency": "1 * * *",
327
              "action": "create"
328
            }
329
        """
330 2
        try:
331
            # Try to find a circuit schedule
332 2
            evc, found_schedule = self._find_evc_by_schedule_id(schedule_id)
333
334
            # Can not modify circuits deleted and archived
335 2
            if not found_schedule:
336
                result = {'response': f'schedule_id {schedule_id} not found'}
337
                status = 404
338
                return jsonify(result), status
339 2
            if evc.archived:
340 2
                result = {'response': f'Circuit is archived.'
341
                                      f'Update is forbidden.'}
342 2
                status = 403
343 2
                return jsonify(result), status
344
345 2
            data = request.get_json()
346
347 2
            new_schedule = CircuitSchedule.from_dict(data)
348 2
            new_schedule.id = found_schedule.id
349
            # Remove the old schedule
350 2
            evc.circuit_scheduler.remove(found_schedule)
351
            # Append the modified schedule
352 2
            evc.circuit_scheduler.append(new_schedule)
353
354
            # Cancel all schedule jobs
355 2
            self.sched.cancel_job(found_schedule.id)
356
            # Add the new circuit schedule
357 2
            self.sched.add_circuit_job(evc, new_schedule)
358
            # Save EVC to the storehouse
359 2
            evc.sync()
360
361 2
            result = new_schedule.as_dict()
362 2
            status = 200
363
364
        except ValueError as exception:
365
            log.error(exception)
366
            result = {'response': 'Bad Request: {}'.format(exception)}
367
            status = 400
368
        except TypeError:
369
            result = {'response': 'Content-Type must be application/json'}
370
            status = 415
371
        except BadRequest:
372
            result = {'response':
373
                      'Bad Request: The request is not a valid JSON.'}
374
            status = 400
375
376 2
        return jsonify(result), status
377
378 2
    @rest('/v2/evc/schedule/<schedule_id>', methods=['DELETE'])
379
    def delete_schedule(self, schedule_id):
380
        """Remove a circuit schedule.
381
382
        Remove the Schedule from EVC.
383
        Remove the Schedule from cron job.
384
        Save the EVC to the Storehouse.
385
        """
386 2
        evc, found_schedule = self._find_evc_by_schedule_id(schedule_id)
387
388
        # Can not modify circuits deleted and archived
389 2
        if not found_schedule:
390
            result = {'response': f'schedule_id {schedule_id} not found'}
391
            status = 404
392
            return jsonify(result), status
393
394 2
        if evc.archived:
395 2
            result = {'response': f'Circuit is archived. Update is forbidden.'}
396 2
            status = 403
397 2
            return jsonify(result), status
398
399
        # Remove the old schedule
400 2
        evc.circuit_scheduler.remove(found_schedule)
401
402
        # Cancel all schedule jobs
403 2
        self.sched.cancel_job(found_schedule.id)
404
        # Save EVC to the storehouse
405 2
        evc.sync()
406
407 2
        result = "Schedule removed"
408 2
        status = 200
409
410 2
        return jsonify(result), status
411
412 2
    def _is_duplicated_evc(self, evc):
413
        """Verify if the circuit given is duplicated with the stored evcs.
414
415
        Args:
416
            evc (EVC): circuit to be analysed.
417
418
        Returns:
419
            boolean: True if the circuit is duplicated, otherwise False.
420
421
        """
422 2
        for circuit in self.circuits.values():
423 2
            if not circuit.archived and circuit == evc:
424 2
                return True
425 2
        return False
426
427 2
    @listen_to('kytos/topology.link_up')
428
    def handle_link_up(self, event):
429
        """Change circuit when link is up or end_maintenance."""
430
        for evc in self.circuits.values():
431
            if evc.is_enabled() and not evc.archived:
432
                evc.handle_link_up(event.content['link'])
433
434 2
    @listen_to('kytos/topology.link_down')
435
    def handle_link_down(self, event):
436
        """Change circuit when link is down or under_mantenance."""
437
        for evc in self.circuits.values():
438
            if evc.is_affected_by_link(event.content['link']):
439
                log.info('handling evc %s' % evc)
440
                evc.handle_link_down()
441
442 2
    def load_circuits_by_interface(self, circuits):
443
        """Load circuits in storehouse for in-memory dictionary."""
444 2
        for circuit_id, circuit in circuits.items():
445 2
            intf_a = circuit['uni_a']['interface_id']
446 2
            self.add_to_dict_of_sets(intf_a, circuit_id)
447 2
            intf_z = circuit['uni_z']['interface_id']
448 2
            self.add_to_dict_of_sets(intf_z, circuit_id)
449 2
            for path in ('current_path', 'primary_path', 'backup_path'):
450 2
                for link in circuit[path]:
451 2
                    intf_a = link['endpoint_a']['id']
452 2
                    self.add_to_dict_of_sets(intf_a, circuit_id)
453 2
                    intf_b = link['endpoint_b']['id']
454 2
                    self.add_to_dict_of_sets(intf_b, circuit_id)
455
456 2
    def add_to_dict_of_sets(self, intf, circuit_id):
457
        """Add a single item to the dictionary of circuits by interface."""
458 2
        if intf not in self._circuits_by_interface:
459 2
            self._circuits_by_interface[intf] = set()
460 2
        self._circuits_by_interface[intf].add(circuit_id)
461
462 2
    @listen_to('kytos/topology.port.created')
463
    def load_evcs(self, event):
464
        """Try to load the unloaded EVCs from storehouse."""
465
        circuits = self.storehouse.get_data()
466
        if not self._circuits_by_interface:
467
            self.load_circuits_by_interface(circuits)
468
469
        interface_id = '{}:{}'.format(event.content['switch'],
470
                                      event.content['port'])
471
472
        for circuit_id in self._circuits_by_interface.get(interface_id, []):
473
            if circuit_id in circuits and circuit_id not in self.circuits:
474
                try:
475
                    evc = self._evc_from_dict(circuits[circuit_id])
476
                except ValueError as exception:
477
                    log.info(
478
                        f'Could not load EVC {circuit_id} because {exception}')
479
                    continue
480
                log.info(f'Loading EVC {circuit_id}')
481
                if evc.archived:
482
                    continue
483
                if evc.is_enabled():
484
                    log.info(f'Trying to deploy EVC {circuit_id}')
485
                    evc.deploy()
486
                self.circuits[circuit_id] = evc
487
                self.sched.add(evc)
488
489 2
    def _evc_from_dict(self, evc_dict):
490
        """Convert some dict values to instance of EVC classes.
491
492
        This method will convert: [UNI, Link]
493
        """
494 2
        data = evc_dict.copy()  # Do not modify the original dict
495
496 2
        for attribute, value in data.items():
497
            # Get multiple attributes.
498
            # Ex: uni_a, uni_z
499 2
            if 'uni' in attribute:
500 2
                try:
501 2
                    data[attribute] = self._uni_from_dict(value)
502
                except ValueError as exc:
503
                    raise ValueError(f'Error creating UNI: {exc}')
504
505 2
            if attribute == 'circuit_scheduler':
506 2
                data[attribute] = []
507 2
                for schedule in value:
508 2
                    data[attribute].append(CircuitSchedule.from_dict(schedule))
509
510
            # Get multiple attributes.
511
            # Ex: primary_links,
512
            #     backup_links,
513
            #     current_links_cache,
514
            #     primary_links_cache,
515
            #     backup_links_cache
516 2
            if 'links' in attribute:
517 2
                data[attribute] = [self._link_from_dict(link)
518
                                   for link in value]
519
520
            # Get multiple attributes.
521
            # Ex: current_path,
522
            #     primary_path,
523
            #     backup_path
524 2
            if 'path' in attribute and attribute != 'dynamic_backup_path':
525 2
                data[attribute] = [self._link_from_dict(link)
526
                                   for link in value]
527
528 2
        return EVC(self.controller, **data)
529
530 2
    def _uni_from_dict(self, uni_dict):
531
        """Return a UNI object from python dict."""
532
        if uni_dict is None:
533
            return False
534
535
        interface_id = uni_dict.get("interface_id")
536
        interface = self.controller.get_interface_by_id(interface_id)
537
        if interface is None:
538
            raise ValueError(f'Could not instantiate interface {interface_id}')
539
540
        tag_dict = uni_dict.get("tag")
541
        tag = TAG.from_dict(tag_dict)
542
        if tag is False:
543
            raise ValueError(f'Could not instantiate tag from dict {tag_dict}')
544
545
        uni = UNI(interface, tag)
546
547
        return uni
548
549 2
    def _link_from_dict(self, link_dict):
550
        """Return a Link object from python dict."""
551 2
        id_a = link_dict.get('endpoint_a').get('id')
552 2
        id_b = link_dict.get('endpoint_b').get('id')
553
554 2
        endpoint_a = self.controller.get_interface_by_id(id_a)
555 2
        endpoint_b = self.controller.get_interface_by_id(id_b)
556
557 2
        link = Link(endpoint_a, endpoint_b)
558 2
        if 'metadata' in link_dict:
559
            link.extend_metadata(link_dict.get('metadata'))
560
561 2
        s_vlan = link.get_metadata('s_vlan')
562 2
        if s_vlan:
563
            tag = TAG.from_dict(s_vlan)
564
            if tag is False:
565
                error_msg = f'Could not instantiate tag from dict {s_vlan}'
566
                raise ValueError(error_msg)
567
            link.update_metadata('s_vlan', tag)
568 2
        return link
569
570 2
    def _find_evc_by_schedule_id(self, schedule_id):
571
        """
572
        Find an EVC and CircuitSchedule based on schedule_id.
573
574
        :param schedule_id: Schedule ID
575
        :return: EVC and Schedule
576
        """
577 2
        circuits = self._get_circuits_buffer()
578 2
        found_schedule = None
579 2
        evc = None
580
581
        # pylint: disable=unused-variable
582 2
        for c_id, circuit in circuits.items():
583 2
            for schedule in circuit.circuit_scheduler:
584 2
                if schedule.id == schedule_id:
585 2
                    found_schedule = schedule
586 2
                    evc = circuit
587 2
                    break
588 2
            if found_schedule:
589 2
                break
590 2
        return evc, found_schedule
591
592 2
    def _get_circuits_buffer(self):
593
        """
594
        Return the circuit buffer.
595
596
        If the buffer is empty, try to load data from storehouse.
597
        """
598 2
        if not self.circuits:
599
            # Load storehouse circuits to buffer
600 2
            circuits = self.storehouse.get_data()
601 2
            for c_id, circuit in circuits.items():
602 2
                evc = self._evc_from_dict(circuit)
603 2
                self.circuits[c_id] = evc
604
        return self.circuits
605