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

build.main   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 462
Duplicated Lines 0 %

Test Coverage

Coverage 58.33%

Importance

Changes 0
Metric Value
eloc 256
dl 0
loc 462
ccs 140
cts 240
cp 0.5833
rs 2.64
c 0
b 0
f 0
wmc 72

19 Methods

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