Passed
Pull Request — master (#159)
by Rogerio
04:10
created

build.main   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 608
Duplicated Lines 0 %

Test Coverage

Coverage 58.07%

Importance

Changes 0
Metric Value
eloc 344
dl 0
loc 608
ccs 187
cts 322
cp 0.5807
rs 2
c 0
b 0
f 0
wmc 96

23 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.get_circuit() 0 13 2
A Main.setup() 0 23 1
A Main.list_circuits() 0 8 2
A Main.shutdown() 0 2 1
A Main.execute() 0 2 1
B Main.create_circuit() 0 60 5
B Main.update() 0 30 6
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.add_to_dict_of_sets() 0 5 2
A Main.handle_link_down() 0 7 3
A Main.list_schedules() 0 24 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
A Main._get_circuits_buffer() 0 13 3
C Main.create_schedule() 0 89 10
A Main.delete_circuit() 0 31 4

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
        log.debug('delete_circuit /v2/evc/%s', circuit_id)
180
        try:
181
            evc = self.circuits[circuit_id]
182
        except KeyError:
183
            result = {'response': f'circuit_id {circuit_id} not found'}
184
            status = 404
185
        else:
186
            log.info('Removing %s', evc)
187
            if evc.archived:
188
                result = {'response': f'Circuit {circuit_id} already removed'}
189
                status = 404
190
            else:
191
                evc.remove_current_flows()
192
                evc.deactivate()
193
                evc.disable()
194
                self.sched.remove(evc)
195
                evc.archive()
196
                evc.sync()
197
                log.info('EVC removed. %s', evc)
198
                result = {'response': f'Circuit {circuit_id} removed'}
199
                status = 200
200
201
        log.debug('delete_circuit result %s %s', result, status)
202
        return jsonify(result), status
203
204 2
    @rest('/v2/evc/schedule', methods=['GET'])
205
    def list_schedules(self):
206
        """Endpoint to return all schedules stored for all circuits.
207
208
        Return a JSON with the following template:
209
        [{"schedule_id": <schedule_id>,
210
         "circuit_id": <circuit_id>,
211
         "schedule": <schedule object>}]
212
        """
213 2
        circuits = self.storehouse.get_data().values()
214 2
        if not circuits:
215 2
            return jsonify({}), 200
216
217 2
        result = []
218 2
        for circuit in circuits:
219 2
            circuit_scheduler = circuit.get("circuit_scheduler")
220 2
            if circuit_scheduler:
221 2
                for scheduler in circuit_scheduler:
222 2
                    value = {"schedule_id": scheduler.get("id"),
223
                             "circuit_id": circuit.get("id"),
224
                             "schedule": scheduler}
225 2
                    result.append(value)
226
227 2
        return jsonify(result), 200
228
229 2
    @rest('/v2/evc/schedule/', methods=['POST'])
230
    def create_schedule(self):
231
        """
232
        Create a new schedule for a given circuit.
233
234
        This service do no check if there are conflicts with another schedule.
235
        Payload example:
236
            {
237
              "circuit_id":"aa:bb:cc",
238
              "schedule": {
239
                "date": "2019-08-07T14:52:10.967Z",
240
                "interval": "string",
241
                "frequency": "1 * * * *",
242
                "action": "create"
243
              }
244
            }
245
        """
246 2
        try:
247
            # Try to create the circuit object
248 2
            json_data = request.get_json()
249 2
            result = ""
250 2
            status = 200
251
252 2
            circuit_id = json_data.get("circuit_id")
253 2
            schedule_data = json_data.get("schedule")
254
255 2
            if not json_data:
256
                result = "Bad request: The request does not have a json."
257
                status = 400
258
                return jsonify(result), status
259 2
            if not circuit_id:
260
                result = result = "Bad request: Missing circuit_id."
261
                status = 400
262
                return jsonify(result), status
263 2
            if not schedule_data:
264
                result = "Bad request: Missing schedule data."
265
                status = 400
266
                return jsonify(result), status
267
268
            # Get EVC from circuits buffer
269 2
            circuits = self._get_circuits_buffer()
270
271
            # get the circuit
272 2
            evc = circuits.get(circuit_id)
273
274
            # get the circuit
275 2
            if not evc:
276
                result = {'response': f'circuit_id {circuit_id} not found'}
277
                status = 404
278
                return jsonify(result), status
279
            # Can not modify circuits deleted and archived
280 2
            if evc.archived:
281
                result = {'response': f'Circuit is archived.'
282
                                      f'Update is forbidden.'}
283
                status = 403
284
                return jsonify(result), status
285
286
            # new schedule from dict
287 2
            new_schedule = CircuitSchedule.from_dict(schedule_data)
288
289
            # If there is no schedule, create the list
290 2
            if not evc.circuit_scheduler:
291
                evc.circuit_scheduler = []
292
293
            # Add the new schedule
294 2
            evc.circuit_scheduler.append(new_schedule)
295
296
            # Add schedule job
297 2
            self.sched.add_circuit_job(evc, new_schedule)
298
299
            # save circuit to storehouse
300 2
            evc.sync()
301
302 2
            result = new_schedule.as_dict()
303 2
            status = 201
304
305
        except ValueError as exception:
306
            log.error(exception)
307
            result = {'response': 'Bad Request: {}'.format(exception)}
308
            status = 400
309
        except TypeError:
310
            result = {'response': 'Content-Type must be application/json'}
311
            status = 415
312
        except BadRequest:
313
            response = 'Bad Request: The request is not a valid JSON.'
314
            result = {'response': response}
315
            status = 400
316
317 2
        return jsonify(result), status
318
319 2
    @rest('/v2/evc/schedule/<schedule_id>', methods=['PATCH'])
320
    def update_schedule(self, schedule_id):
321
        """Update a schedule.
322
323
        Change all attributes from the given schedule from a EVC circuit.
324
        The schedule ID is preserved as default.
325
        Payload example:
326
            {
327
              "date": "2019-08-07T14:52:10.967Z",
328
              "interval": "string",
329
              "frequency": "1 * * *",
330
              "action": "create"
331
            }
332
        """
333 2
        try:
334
            # Try to find a circuit schedule
335 2
            evc, found_schedule = self._find_evc_by_schedule_id(schedule_id)
336
337
            # Can not modify circuits deleted and archived
338 2
            if not found_schedule:
339
                result = {'response': f'schedule_id {schedule_id} not found'}
340
                status = 404
341
                return jsonify(result), status
342 2
            if evc.archived:
343 2
                result = {'response': f'Circuit is archived.'
344
                                      f'Update is forbidden.'}
345 2
                status = 403
346 2
                return jsonify(result), status
347
348 2
            data = request.get_json()
349
350 2
            new_schedule = CircuitSchedule.from_dict(data)
351 2
            new_schedule.id = found_schedule.id
352
            # Remove the old schedule
353 2
            evc.circuit_scheduler.remove(found_schedule)
354
            # Append the modified schedule
355 2
            evc.circuit_scheduler.append(new_schedule)
356
357
            # Cancel all schedule jobs
358 2
            self.sched.cancel_job(found_schedule.id)
359
            # Add the new circuit schedule
360 2
            self.sched.add_circuit_job(evc, new_schedule)
361
            # Save EVC to the storehouse
362 2
            evc.sync()
363
364 2
            result = new_schedule.as_dict()
365 2
            status = 200
366
367
        except ValueError as exception:
368
            log.error(exception)
369
            result = {'response': 'Bad Request: {}'.format(exception)}
370
            status = 400
371
        except TypeError:
372
            result = {'response': 'Content-Type must be application/json'}
373
            status = 415
374
        except BadRequest:
375
            result = {'response':
376
                      'Bad Request: The request is not a valid JSON.'}
377
            status = 400
378
379 2
        return jsonify(result), status
380
381 2
    @rest('/v2/evc/schedule/<schedule_id>', methods=['DELETE'])
382
    def delete_schedule(self, schedule_id):
383
        """Remove a circuit schedule.
384
385
        Remove the Schedule from EVC.
386
        Remove the Schedule from cron job.
387
        Save the EVC to the Storehouse.
388
        """
389 2
        evc, found_schedule = self._find_evc_by_schedule_id(schedule_id)
390
391
        # Can not modify circuits deleted and archived
392 2
        if not found_schedule:
393
            result = {'response': f'schedule_id {schedule_id} not found'}
394
            status = 404
395
            return jsonify(result), status
396
397 2
        if evc.archived:
398 2
            result = {'response': f'Circuit is archived. Update is forbidden.'}
399 2
            status = 403
400 2
            return jsonify(result), status
401
402
        # Remove the old schedule
403 2
        evc.circuit_scheduler.remove(found_schedule)
404
405
        # Cancel all schedule jobs
406 2
        self.sched.cancel_job(found_schedule.id)
407
        # Save EVC to the storehouse
408 2
        evc.sync()
409
410 2
        result = "Schedule removed"
411 2
        status = 200
412
413 2
        return jsonify(result), status
414
415 2
    def _is_duplicated_evc(self, evc):
416
        """Verify if the circuit given is duplicated with the stored evcs.
417
418
        Args:
419
            evc (EVC): circuit to be analysed.
420
421
        Returns:
422
            boolean: True if the circuit is duplicated, otherwise False.
423
424
        """
425 2
        for circuit in self.circuits.values():
426 2
            if not circuit.archived and circuit == evc:
427 2
                return True
428 2
        return False
429
430 2
    @listen_to('kytos/topology.link_up')
431
    def handle_link_up(self, event):
432
        """Change circuit when link is up or end_maintenance."""
433
        for evc in self.circuits.values():
434
            if evc.is_enabled() and not evc.archived:
435
                evc.handle_link_up(event.content['link'])
436
437 2
    @listen_to('kytos/topology.link_down')
438
    def handle_link_down(self, event):
439
        """Change circuit when link is down or under_mantenance."""
440
        for evc in self.circuits.values():
441
            if evc.is_affected_by_link(event.content['link']):
442
                log.info('handling evc %s' % evc)
443
                evc.handle_link_down()
444
445 2
    def load_circuits_by_interface(self, circuits):
446
        """Load circuits in storehouse for in-memory dictionary."""
447 2
        for circuit_id, circuit in circuits.items():
448 2
            intf_a = circuit['uni_a']['interface_id']
449 2
            self.add_to_dict_of_sets(intf_a, circuit_id)
450 2
            intf_z = circuit['uni_z']['interface_id']
451 2
            self.add_to_dict_of_sets(intf_z, circuit_id)
452 2
            for path in ('current_path', 'primary_path', 'backup_path'):
453 2
                for link in circuit[path]:
454 2
                    intf_a = link['endpoint_a']['id']
455 2
                    self.add_to_dict_of_sets(intf_a, circuit_id)
456 2
                    intf_b = link['endpoint_b']['id']
457 2
                    self.add_to_dict_of_sets(intf_b, circuit_id)
458
459 2
    def add_to_dict_of_sets(self, intf, circuit_id):
460
        """Add a single item to the dictionary of circuits by interface."""
461 2
        if intf not in self._circuits_by_interface:
462 2
            self._circuits_by_interface[intf] = set()
463 2
        self._circuits_by_interface[intf].add(circuit_id)
464
465 2
    @listen_to('kytos/topology.port.created')
466
    def load_evcs(self, event):
467
        """Try to load the unloaded EVCs from storehouse."""
468
        circuits = self.storehouse.get_data()
469
        if not self._circuits_by_interface:
470
            self.load_circuits_by_interface(circuits)
471
472
        interface_id = '{}:{}'.format(event.content['switch'],
473
                                      event.content['port'])
474
475
        for circuit_id in self._circuits_by_interface.get(interface_id, []):
476
            if circuit_id in circuits and circuit_id not in self.circuits:
477
                try:
478
                    evc = self._evc_from_dict(circuits[circuit_id])
479
                except ValueError as exception:
480
                    log.info(
481
                        f'Could not load EVC {circuit_id} because {exception}')
482
                    continue
483
                log.info(f'Loading EVC {circuit_id}')
484
                if evc.archived:
485
                    continue
486
                if evc.is_enabled():
487
                    log.info(f'Trying to deploy EVC {circuit_id}')
488
                    evc.deploy()
489
                self.circuits[circuit_id] = evc
490
                self.sched.add(evc)
491
492 2
    def _evc_from_dict(self, evc_dict):
493
        """Convert some dict values to instance of EVC classes.
494
495
        This method will convert: [UNI, Link]
496
        """
497 2
        data = evc_dict.copy()  # Do not modify the original dict
498
499 2
        for attribute, value in data.items():
500
            # Get multiple attributes.
501
            # Ex: uni_a, uni_z
502 2
            if 'uni' in attribute:
503 2
                try:
504 2
                    data[attribute] = self._uni_from_dict(value)
505
                except ValueError as exc:
506
                    raise ValueError(f'Error creating UNI: {exc}')
507
508 2
            if attribute == 'circuit_scheduler':
509 2
                data[attribute] = []
510 2
                for schedule in value:
511 2
                    data[attribute].append(CircuitSchedule.from_dict(schedule))
512
513
            # Get multiple attributes.
514
            # Ex: primary_links,
515
            #     backup_links,
516
            #     current_links_cache,
517
            #     primary_links_cache,
518
            #     backup_links_cache
519 2
            if 'links' in attribute:
520 2
                data[attribute] = [self._link_from_dict(link)
521
                                   for link in value]
522
523
            # Get multiple attributes.
524
            # Ex: current_path,
525
            #     primary_path,
526
            #     backup_path
527 2
            if 'path' in attribute and attribute != 'dynamic_backup_path':
528 2
                data[attribute] = [self._link_from_dict(link)
529
                                   for link in value]
530
531 2
        return EVC(self.controller, **data)
532
533 2
    def _uni_from_dict(self, uni_dict):
534
        """Return a UNI object from python dict."""
535
        if uni_dict is None:
536
            return False
537
538
        interface_id = uni_dict.get("interface_id")
539
        interface = self.controller.get_interface_by_id(interface_id)
540
        if interface is None:
541
            raise ValueError(f'Could not instantiate interface {interface_id}')
542
543
        tag_dict = uni_dict.get("tag")
544
        tag = TAG.from_dict(tag_dict)
545
        if tag is False:
546
            raise ValueError(f'Could not instantiate tag from dict {tag_dict}')
547
548
        uni = UNI(interface, tag)
549
550
        return uni
551
552 2
    def _link_from_dict(self, link_dict):
553
        """Return a Link object from python dict."""
554 2
        id_a = link_dict.get('endpoint_a').get('id')
555 2
        id_b = link_dict.get('endpoint_b').get('id')
556
557 2
        endpoint_a = self.controller.get_interface_by_id(id_a)
558 2
        endpoint_b = self.controller.get_interface_by_id(id_b)
559
560 2
        link = Link(endpoint_a, endpoint_b)
561 2
        if 'metadata' in link_dict:
562
            link.extend_metadata(link_dict.get('metadata'))
563
564 2
        s_vlan = link.get_metadata('s_vlan')
565 2
        if s_vlan:
566
            tag = TAG.from_dict(s_vlan)
567
            if tag is False:
568
                error_msg = f'Could not instantiate tag from dict {s_vlan}'
569
                raise ValueError(error_msg)
570
            link.update_metadata('s_vlan', tag)
571 2
        return link
572
573 2
    def _find_evc_by_schedule_id(self, schedule_id):
574
        """
575
        Find an EVC and CircuitSchedule based on schedule_id.
576
577
        :param schedule_id: Schedule ID
578
        :return: EVC and Schedule
579
        """
580 2
        circuits = self._get_circuits_buffer()
581 2
        found_schedule = None
582 2
        evc = None
583
584
        # pylint: disable=unused-variable
585 2
        for c_id, circuit in circuits.items():
586 2
            for schedule in circuit.circuit_scheduler:
587 2
                if schedule.id == schedule_id:
588 2
                    found_schedule = schedule
589 2
                    evc = circuit
590 2
                    break
591 2
            if found_schedule:
592 2
                break
593 2
        return evc, found_schedule
594
595 2
    def _get_circuits_buffer(self):
596
        """
597
        Return the circuit buffer.
598
599
        If the buffer is empty, try to load data from storehouse.
600
        """
601 2
        if not self.circuits:
602
            # Load storehouse circuits to buffer
603 2
            circuits = self.storehouse.get_data()
604 2
            for c_id, circuit in circuits.items():
605 2
                evc = self._evc_from_dict(circuit)
606 2
                self.circuits[c_id] = evc
607
        return self.circuits
608