Passed
Pull Request — master (#140)
by Rogerio
03:21
created

build.main   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 539
Duplicated Lines 0 %

Test Coverage

Coverage 60.89%

Importance

Changes 0
Metric Value
eloc 289
dl 0
loc 539
ccs 165
cts 271
cp 0.6089
rs 2
c 0
b 0
f 0
wmc 81

20 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.get_circuit() 0 13 2
A Main.setup() 0 20 1
A Main.delete_circuit() 0 28 4
A Main.list_circuits() 0 8 2
B Main.update_schedule() 0 61 6
A Main.shutdown() 0 2 1
A Main.execute() 0 2 1
A Main.list_schedules() 0 14 4
B Main.create_circuit() 0 60 5
B Main.update() 0 30 6
C Main.create_schedule() 0 84 10
A Main.get_circuits_buffer() 0 13 3
A Main.uni_from_dict() 0 18 4
A Main.is_duplicated_evc() 0 14 4
A Main.find_evc_by_schedule_id() 0 21 5
A Main.handle_link_up() 0 6 4
A Main.link_from_dict() 0 20 4
A Main.delete_schedule() 0 32 3
A Main.handle_link_down() 0 7 3
C Main.evc_from_dict() 0 40 9

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