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

build.main   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 609
Duplicated Lines 0 %

Test Coverage

Coverage 58.07%

Importance

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