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

build.main   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 528
Duplicated Lines 0 %

Test Coverage

Coverage 56.41%

Importance

Changes 0
Metric Value
eloc 290
dl 0
loc 528
rs 2
c 0
b 0
f 0
ccs 154
cts 273
cp 0.5641
wmc 83

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.get_circuits_buffer() 0 13 3
A Main.list_circuits() 0 8 2
A Main.uni_from_dict() 0 18 4
B Main.update_schedule() 0 61 6
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 31 3
A Main.shutdown() 0 2 1
A Main.handle_link_down() 0 7 3
A Main.execute() 0 2 1
A Main.list_schedules() 0 14 4
B Main.create_circuit() 0 60 5
C Main.evc_from_dict() 0 30 11
B Main.update() 0 30 6
C Main.create_schedule() 0 84 10

How to fix   Complexity   

Complexity

Complex classes like build.main often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Main module of kytos/mef_eline Kytos Network Application.
2
3
NApp to provision circuits from user request.
4
"""
5 2
from flask import jsonify, request
6 2
from werkzeug.exceptions import BadRequest
7
8 2
from kytos.core import KytosNApp, log, rest
9 2
from kytos.core.events import KytosEvent
10 2
from kytos.core.helpers import listen_to
11 2
from kytos.core.interface import TAG, UNI
12 2
from kytos.core.link import Link
13 2
from napps.kytos.mef_eline.models import EVC, DynamicPathManager
14 2
from napps.kytos.mef_eline.scheduler import CircuitSchedule, Scheduler
15 2
from napps.kytos.mef_eline.storehouse import StoreHouse
16
17
18 2
class Main(KytosNApp):
19
    """Main class of amlight/mef_eline NApp.
20
21
    This class is the entry point for this napp.
22
    """
23
24 2
    def setup(self):
25
        """Replace the '__init__' method for the KytosNApp subclass.
26
27
        The setup method is automatically called by the controller when your
28
        application is loaded.
29
30
        So, if you have any setup routine, insert it here.
31
        """
32
        # object used to scheduler circuit events
33 2
        self.sched = Scheduler()
34
35
        # object to save and load circuits
36 2
        self.storehouse = StoreHouse(self.controller)
37
38
        # set the controller that will manager the dynamic paths
39 2
        DynamicPathManager.set_controller(self.controller)
40
41
        # dictionary of EVCs created. It acts as a circuit buffer.
42
        # Every create/update/delete must be synced to storehouse.
43 2
        self.circuits = {}
44
45 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
246 2
                circuit_id = json_data["circuit_id"]
247 2
                schedule_data = json_data["schedule"]
248
249
                # new schedule from dict
250 2
                new_schedule = CircuitSchedule.from_dict(schedule_data)
251
252 2
                circuits = self.get_circuits_buffer()
253
254
                # get the circuit
255 2
                if circuit_id not in circuits:
256
                    result = {'response': f'circuit_id {circuit_id} not found'}
257
                    status = 404
258
                else:
259
                    # Get EVC from circuits buffer
260 2
                    evc = circuits[circuit_id]
261
262
                    # Can not modify circuits deleted and archived
263 2
                    if evc.archived:
264
                        result = {'response': f'Circuit is archived.'
265
                                              f'Update is forbidden.'}
266
                        status = 403
267
                    else:
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 2
            data = request.get_json()
314
315
            # Try to find a circuit schedule
316 2
            evc, found_schedule = self.find_evc_by_schedule_id(schedule_id)
317
318
            # Can not modify circuits deleted and archived
319 2
            if found_schedule:
320 2
                if evc.archived:
321 2
                    result = {'response': f'Circuit is archived.'
322
                                          f'Update is forbidden.'}
323 2
                    status = 403
324
                else:
325 2
                    new_schedule = CircuitSchedule.from_dict(data)
326 2
                    new_schedule.id = found_schedule.id
327
                    # Remove the old schedule
328 2
                    evc.circuit_scheduler.remove(found_schedule)
329
                    # Append the modified schedule
330 2
                    evc.circuit_scheduler.append(new_schedule)
331
332
                    # Cancel all schedule jobs
333 2
                    self.sched.cancel_job(found_schedule.id)
334
                    # Add the new circuit schedule
335 2
                    self.sched.add_circuit_job(evc, new_schedule)
336
                    # Save EVC to the storehouse
337 2
                    evc.sync()
338
339 2
                    result = new_schedule.as_dict()
340 2
                    status = 200
341
            else:
342
                result = {'response':
343
                          f'schedule_id {schedule_id} not found'}
344
                status = 404
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
        # Can not modify circuits deleted and archived
370 2
        if found_schedule:
371 2
            if evc.archived:
372 2
                result = {'response': f'Circuit is archived.'
373
                                      f'Update is forbidden.'}
374 2
                status = 403
375
            else:
376
                # Remove the old schedule
377 2
                evc.circuit_scheduler.remove(found_schedule)
378
379
                # Cancel all schedule jobs
380 2
                self.sched.cancel_job(found_schedule.id)
381
                # Save EVC to the storehouse
382 2
                evc.sync()
383
384 2
                result = "Schedule removed"
385 2
                status = 200
386
        else:
387
            result = {'response': f'schedule_id {schedule_id} not found'}
388
            status = 404
389
390 2
        return jsonify(result), status
391
392 2
    def is_duplicated_evc(self, evc):
393
        """Verify if the circuit given is duplicated with the stored evcs.
394
395
        Args:
396
            evc (EVC): circuit to be analysed.
397
398
        Returns:
399
            boolean: True if the circuit is duplicated, otherwise False.
400
401
        """
402 2
        for circuit in self.circuits.values():
403 2
            if not circuit.archived and circuit == evc:
404 2
                return True
405 2
        return False
406
407 2
    @listen_to('kytos/topology.link_up')
408
    def handle_link_up(self, event):
409
        """Change circuit when link is up or end_maintenance."""
410
        for evc in self.circuits.values():
411
            if evc.is_enabled() and not evc.archived:
412
                evc.handle_link_up(event.content['link'])
413
414 2
    @listen_to('kytos/topology.link_down')
415
    def handle_link_down(self, event):
416
        """Change circuit when link is down or under_mantenance."""
417
        for evc in self.circuits.values():
418
            if evc.is_affected_by_link(event.content['link']):
419
                log.info('handling evc %s' % evc)
420
                evc.handle_link_down()
421
422 2
    def evc_from_dict(self, evc_dict):
423
        """Convert some dict values to instance of EVC classes.
424
425
        This method will convert: [UNI, Link]
426
        """
427 2
        data = evc_dict.copy()  # Do not modify the original dict
428
429 2
        for attribute, value in data.items():
430
431 2
            if 'uni' in attribute:
432 2
                try:
433 2
                    data[attribute] = self.uni_from_dict(value)
434
                except ValueError as exc:
435
                    raise ValueError(f'Error creating UNI: {exc}')
436
437 2
            if attribute == 'circuit_scheduler':
438 2
                data[attribute] = []
439 2
                for schedule in value:
440 2
                    data[attribute].append(CircuitSchedule.from_dict(schedule))
441
442 2
            if 'link' in attribute:
443
                if value:
444
                    data[attribute] = self.link_from_dict(value)
445
446 2
            if 'path' in attribute and attribute != 'dynamic_backup_path':
447
                if value:
448
                    data[attribute] = [self.link_from_dict(link)
449
                                       for link in value]
450
451 2
        return EVC(self.controller, **data)
452
453 2
    def uni_from_dict(self, uni_dict):
454
        """Return a UNI object from python dict."""
455
        if uni_dict is None:
456
            return False
457
458
        interface_id = uni_dict.get("interface_id")
459
        interface = self.controller.get_interface_by_id(interface_id)
460
        if interface is None:
461
            raise ValueError(f'Could not instantiate interface {interface_id}')
462
463
        tag_dict = uni_dict.get("tag")
464
        tag = TAG.from_dict(tag_dict)
465
        if tag is False:
466
            raise ValueError(f'Could not instantiate tag from dict {tag_dict}')
467
468
        uni = UNI(interface, tag)
469
470
        return uni
471
472 2
    def link_from_dict(self, link_dict):
473
        """Return a Link object from python dict."""
474
        id_a = link_dict.get('endpoint_a').get('id')
475
        id_b = link_dict.get('endpoint_b').get('id')
476
477
        endpoint_a = self.controller.get_interface_by_id(id_a)
478
        endpoint_b = self.controller.get_interface_by_id(id_b)
479
480
        link = Link(endpoint_a, endpoint_b)
481
        if 'metadata' in link_dict:
482
            link.extend_metadata(link_dict.get('metadata'))
483
484
        s_vlan = link.get_metadata('s_vlan')
485
        if s_vlan:
486
            tag = TAG.from_dict(s_vlan)
487
            if tag is False:
488
                error_msg = f'Could not instantiate tag from dict {s_vlan}'
489
                raise ValueError(error_msg)
490
            link.update_metadata('s_vlan', tag)
491
        return link
492
493 2
    def find_evc_by_schedule_id(self, schedule_id):
494
        """
495
        Find an EVC and CircuitSchedule based on schedule_id.
496
497
        :param schedule_id: Schedule ID
498
        :return: EVC and Schedule
499
        """
500 2
        circuits = self.get_circuits_buffer()
501 2
        found_schedule = None
502 2
        evc = None
503
504
        # pylint: disable=unused-variable
505 2
        for c_id, circuit in circuits.items():
506 2
            for schedule in circuit.circuit_scheduler:
507 2
                if schedule.id == schedule_id:
508 2
                    found_schedule = schedule
509 2
                    evc = circuit
510 2
                    break
511 2
            if found_schedule:
512 2
                break
513 2
        return evc, found_schedule
514
515 2
    def get_circuits_buffer(self):
516
        """
517
        Return the circuit buffer.
518
519
        If the buffer is empty, try to load data from storehouse.
520
        """
521 2
        if not self.circuits:
522
            # Load storehouse circuits to buffer
523 2
            circuits = self.storehouse.get_data()
524 2
            for c_id, circuit in circuits.items():
525 2
                evc = self.evc_from_dict(circuit)
526 2
                self.circuits[c_id] = evc
527
        return self.circuits
528