Test Failed
Pull Request — master (#140)
by Rogerio
04:09
created

build.main   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 488
Duplicated Lines 0 %

Test Coverage

Coverage 45.1%

Importance

Changes 0
Metric Value
eloc 266
dl 0
loc 488
ccs 69
cts 153
cp 0.451
rs 2.32
c 0
b 0
f 0
wmc 76

20 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.get_circuits_buffer() 0 8 3
A Main.list_circuits() 0 8 2
A Main.uni_from_dict() 0 18 4
A Main.update_schedule() 0 47 3
A Main.is_duplicated_evc() 0 14 4
A Main.find_evc_by_schedule_id() 0 20 5
A Main.handle_link_up() 0 6 4
A Main.link_from_dict() 0 20 4
A Main.delete_schedule() 0 26 2
A Main.get_circuit() 0 13 2
A Main.shutdown() 0 2 1
A Main.handle_link_down() 0 7 3
A Main.setup() 0 20 1
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
B Main.create_schedule() 0 69 7
A Main.delete_circuit() 0 28 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 2
        # Every create/update/delete must be synced to storehouse.
43
        self.circuits = {}
44 2
45
    def execute(self):
46
        """Execute once when the napp is running."""
47 2
48
    def shutdown(self):
49
        """Execute when your napp is unloaded.
50
51
        If you have some cleanup procedure, insert it here.
52
        """
53 2
54
    @rest('/v2/evc/', methods=['GET'])
55
    def list_circuits(self):
56 2
        """Endpoint to return all circuits stored."""
57 2
        circuits = self.storehouse.get_data()
58 2
        if not circuits:
59
            return jsonify({}), 200
60 2
61
        return jsonify(circuits), 200
62 2
63
    @rest('/v2/evc/<circuit_id>', methods=['GET'])
64
    def get_circuit(self, circuit_id):
65 2
        """Endpoint to return a circuit based on id."""
66 2
        circuits = self.storehouse.get_data()
67 2
68 2
        try:
69 2
            result = circuits[circuit_id]
70 2
            status = 200
71 2
        except KeyError:
72
            result = {'response': f'circuit_id {circuit_id} not found'}
73 2
            status = 404
74
75 2
        return jsonify(result), status
76
77
    @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 2
        """
102
        # Try to create the circuit object
103 2
        data = request.get_json()
104 2
105
        if not data:
106 2
            return jsonify("Bad request: The request do not have a json."), 400
107 2
108
        try:
109
            evc = self.evc_from_dict(data)
110
        except ValueError as exception:
111
            log.error(exception)
112 2
            return jsonify("Bad request: {}".format(exception)), 400
113 2
114
        # verify duplicated evc
115
        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 2
127
        # Circuit has no schedule, deploy now
128
        if not evc.circuit_scheduler:
129 2
            evc.deploy()
130
131 2
        # Notify users
132
        event = KytosEvent(name='kytos.mef_eline.created',
133 2
                           content=evc.as_dict())
134
        self.controller.buffers.app.put(event)
135 2
136
        return jsonify({"circuit_id": evc.id}), 201
137
138
    @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 2
            status = 200
166
167
        return jsonify(result), status
168
169
    @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 2
                status = 200
195
196
        return jsonify(result), status
197
198
    @rest('/v2/evc/schedule', methods=['GET'])
199
    def list_schedules(self):
200
        """Endpoint to return all circuits stored."""
201
        circuits = self.storehouse.get_data().values()
202
        if not circuits:
203
            return jsonify({}), 200
204 2
205 2
        result = []
206 2
        for circuit in circuits:
207 2
            if circuit["circuit_scheduler"]:
208
                schedule = {circuit["id"]: circuit["circuit_scheduler"]}
209 2
                result.append(schedule)
210
211
        return jsonify(result), 200
212
213
    @rest('/v2/evc/schedule/', methods=['POST'])
214
    def create_schedule(self):
215
        """
216 2
        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 2
                "interval": "string",
225
                "frequency": "1 * * * *",
226
                "action": "create"
227
              }
228
            }
229 2
        """
230
        # Try to create the circuit object
231 2
        json_data = request.get_json()
232
        result = ""
233 2
        status = 200
234 2
        if not json_data:
235 2
            result = "Bad request: The request does not have a json."
236
            status = 400
237
        elif "circuit_id" not in json_data:
238
            result = result = "Bad request: Missing circuit_id."
239 2
            status = 400
240
        elif "schedule" not in json_data:
241
            result = "Bad request: Missing schedule data."
242
            status = 400
243
        else:
244 2
            try:
245
                circuit_id = json_data["circuit_id"]
246
                schedule_data = json_data["schedule"]
247
248 2
                # new schedule from dict
249
                new_schedule = CircuitSchedule.from_dict(schedule_data)
250
251
                circuits = self.get_circuits_buffer()
252
253 2
                # get the circuit
254
                if circuit_id not in circuits:
255 2
                    result = {'response': f'circuit_id {circuit_id} not found'}
256
                    status = 404
257
                else:
258
                    # Get EVC from circuits buffer
259
                    evc = circuits[circuit_id]
260
261
                    # If there is no schedule, create the list
262
                    if not evc.circuit_scheduler:
263
                        evc.circuit_scheduler = []
264
265
                    # Add the new schedule
266
                    evc.circuit_scheduler.append(new_schedule)
267
268
                    # Add schedule job
269
                    self.sched.add_circuit_job(evc, new_schedule)
270
271
                    # save circuit to storehouse
272
                    evc.sync()
273
274 2
                    result = new_schedule.as_dict()
275
                    status = 201
276
            except ValueError as exception:
277
                log.exception(exception)
278
                result = "Bad request: {}".format(exception)
279
                status = 400
280
281
        return jsonify(result), status
282
283
    @rest('/v2/evc/schedule/<schedule_id>', methods=['PATCH'])
284
    def update_schedule(self, schedule_id):
285
        """Update a schedule.
286
287
        Change all attributes from the given schedule from a EVC circuit.
288
        The schedule ID is preserved as default.
289
        Payload example:
290
            {
291
              "date": "2019-08-07T14:52:10.967Z",
292
              "interval": "string",
293
              "frequency": "1 * * *",
294
              "action": "create"
295
            }
296
        """
297
        data = request.get_json()
298
299
        try:
300
            # Try to find a circuit schedule
301
            evc, found_schedule = self.find_evc_by_schedule_id(schedule_id)
302
303
            if found_schedule:
304
                new_schedule = CircuitSchedule.from_dict(data)
305
                new_schedule.id = found_schedule.id
306
                # Remove the old schedule
307
                evc.circuit_scheduler.remove(found_schedule)
308
                # Append the modified schedule
309
                evc.circuit_scheduler.append(new_schedule)
310
311
                # Cancel all schedule jobs
312
                self.sched.cancel_job(found_schedule.id)
313
                # Add the new circuit schedule
314
                self.sched.add_circuit_job(evc, new_schedule)
315
                # Save EVC to the storehouse
316
                evc.sync()
317
318
                result = new_schedule.as_dict()
319
                status = 200
320
            else:
321
                result = {'response': f'schedule_id {schedule_id} not found'}
322
                status = 404
323
324
        except ValueError as exception:
325
            log.error(exception)
326
            result = "Bad request: {}".format(exception)
327
            status = 400
328
329
        return jsonify(result), status
330
331
    @rest('/v2/evc/schedule/<schedule_id>', methods=['DELETE'])
332
    def delete_schedule(self, schedule_id):
333
        """Remove a circuit schedule.
334
335
        Remove the Schedule from EVC.
336
        Remove the Schedule from cron job.
337
        Save the EVC to the Storehouse.
338
        """
339
        evc, found_schedule = self.find_evc_by_schedule_id(schedule_id)
340
341
        if found_schedule:
342
            # Remove the old schedule
343
            evc.circuit_scheduler.remove(found_schedule)
344
345
            # Cancel all schedule jobs
346
            self.sched.cancel_job(found_schedule.id)
347
            # Save EVC to the storehouse
348
            evc.sync()
349
350
            result = "Schedule removed"
351
            status = 200
352
        else:
353
            result = {'response': f'schedule_id {schedule_id} not found'}
354
            status = 404
355
356
        return jsonify(result), status
357
358
    def is_duplicated_evc(self, evc):
359
        """Verify if the circuit given is duplicated with the stored evcs.
360
361
        Args:
362
            evc (EVC): circuit to be analysed.
363
364
        Returns:
365
            boolean: True if the circuit is duplicated, otherwise False.
366
367
        """
368
        for circuit in self.circuits.values():
369
            if not circuit.archived and circuit == evc:
370
                return True
371
        return False
372
373
    @listen_to('kytos/topology.link_up')
374
    def handle_link_up(self, event):
375
        """Change circuit when link is up or end_maintenance."""
376
        for evc in self.circuits.values():
377
            if evc.is_enabled() and not evc.archived:
378
                evc.handle_link_up(event.content['link'])
379
380
    @listen_to('kytos/topology.link_down')
381
    def handle_link_down(self, event):
382
        """Change circuit when link is down or under_mantenance."""
383
        for evc in self.circuits.values():
384
            if evc.is_affected_by_link(event.content['link']):
385
                log.info('handling evc %s' % evc)
386
                evc.handle_link_down()
387
388
    def evc_from_dict(self, evc_dict):
389
        """Convert some dict values to instance of EVC classes.
390
391
        This method will convert: [UNI, Link]
392
        """
393
        data = evc_dict.copy()  # Do not modify the original dict
394
395
        for attribute, value in data.items():
396
397
            if 'uni' in attribute:
398
                try:
399
                    data[attribute] = self.uni_from_dict(value)
400
                except ValueError as exc:
401
                    raise ValueError(f'Error creating UNI: {exc}')
402
403
            if attribute == 'circuit_scheduler':
404
                data[attribute] = []
405
                for schedule in value:
406
                    data[attribute].append(CircuitSchedule.from_dict(schedule))
407
408
            if 'link' in attribute:
409
                if value:
410
                    data[attribute] = self.link_from_dict(value)
411
412
            if 'path' in attribute and attribute != 'dynamic_backup_path':
413
                if value:
414
                    data[attribute] = [self.link_from_dict(link)
415
                                       for link in value]
416
417
        return EVC(self.controller, **data)
418
419
    def uni_from_dict(self, uni_dict):
420
        """Return a UNI object from python dict."""
421
        if uni_dict is None:
422
            return False
423
424
        interface_id = uni_dict.get("interface_id")
425
        interface = self.controller.get_interface_by_id(interface_id)
426
        if interface is None:
427
            raise ValueError(f'Could not instantiate interface {interface_id}')
428
429
        tag_dict = uni_dict.get("tag")
430
        tag = TAG.from_dict(tag_dict)
431
        if tag is False:
432
            raise ValueError(f'Could not instantiate tag from dict {tag_dict}')
433
434
        uni = UNI(interface, tag)
435
436
        return uni
437
438
    def link_from_dict(self, link_dict):
439
        """Return a Link object from python dict."""
440
        id_a = link_dict.get('endpoint_a').get('id')
441
        id_b = link_dict.get('endpoint_b').get('id')
442
443
        endpoint_a = self.controller.get_interface_by_id(id_a)
444
        endpoint_b = self.controller.get_interface_by_id(id_b)
445
446
        link = Link(endpoint_a, endpoint_b)
447
        if 'metadata' in link_dict:
448
            link.extend_metadata(link_dict.get('metadata'))
449
450
        s_vlan = link.get_metadata('s_vlan')
451
        if s_vlan:
452
            tag = TAG.from_dict(s_vlan)
453
            if tag is False:
454
                error_msg = f'Could not instantiate tag from dict {s_vlan}'
455
                raise ValueError(error_msg)
456
            link.update_metadata('s_vlan', tag)
457
        return link
458
459
    def find_evc_by_schedule_id(self, schedule_id):
460
        """
461
        Find an EVC and CircuitSchedule based on schedule_id.
462
463
        :param schedule_id: Schedule ID
464
        :return: EVC and Schedule
465
        """
466
        circuits = self.get_circuits_buffer()
467
        found_schedule = None
468
        evc = None
469
470
        for c_id, circuit in circuits.items():
471
            for schedule in circuit.circuit_scheduler:
472
                if schedule.id == schedule_id:
473
                    found_schedule = schedule
474
                    evc = circuit
475
                    break
476
            if found_schedule:
477
                break
478
        return evc, found_schedule
479
480
    def get_circuits_buffer(self):
481
        if not self.circuits:
482
            # Load storehouse circuits to buffer
483
            circuits = self.storehouse.get_data()
484
            for c_id, circuit in circuits.items():
485
                evc = self.evc_from_dict(circuit)
486
                self.circuits[evc.id] = evc
487
        return self.circuits