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

build.main   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 606
Duplicated Lines 0 %

Test Coverage

Coverage 58.44%

Importance

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