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

build.main   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 474
Duplicated Lines 0 %

Test Coverage

Coverage 57.19%

Importance

Changes 0
Metric Value
eloc 258
dl 0
loc 474
ccs 139
cts 243
cp 0.5719
rs 2.56
c 0
b 0
f 0
wmc 73

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.list_schedules() 0 14 4
B Main.create_circuit() 0 60 5
B Main.update() 0 30 6
A Main.delete_circuit() 0 28 4
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.handle_link_down() 0 7 3
C Main.evc_from_dict() 0 30 11
B Main.create_schedule() 0 66 7

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/schedule/', methods=['POST'])
212
    def create_schedule(self):
213
        """
214
        Create a new schedule for a given circuit.
215
216
        This service do no check if there are conflicts with another schedule.
217
        Payload example:
218
            {
219
              "circuit_id":"aa:bb:cc",
220
              "schedule": {
221
                "date": "2019-08-07T14:52:10.967Z",
222
                "interval": "string",
223
                "frequency": "1 * * * *",
224
                "action": "create"
225
              }
226
            }
227
        """
228
        # Try to create the circuit object
229 2
        json_data = request.get_json()
230 2
        result = ""
231 2
        status = 200
232 2
        if not json_data:
233
            result = "Bad request: The request does not have a json."
234
            status = 400
235 2
        elif "circuit_id" not in json_data:
236
            result = result = "Bad request: Missing circuit_id."
237
            status = 400
238 2
        elif "schedule" not in json_data:
239
            result = "Bad request: Missing schedule data."
240
            status = 400
241
        else:
242 2
            try:
243 2
                circuit_id = json_data["circuit_id"]
244 2
                schedule_data = json_data["schedule"]
245
246
                # new schedule from dict
247 2
                new_schedule = CircuitSchedule.from_dict(schedule_data)
248
                # get the circuit
249 2
                circuits = self.storehouse.get_data()
250 2
                if circuit_id not in circuits:
251
                    result = {'response': f'circuit_id {circuit_id} not found'}
252
                    status = 404
253
                else:
254 2
                    evc = self.evc_from_dict(circuits.get(circuit_id))
255
256
                    # If there is no schedule, create the list
257 2
                    if not evc.circuit_scheduler:
258
                        evc.circuit_scheduler = []
259
260
                    # Add the new schedule
261 2
                    evc.circuit_scheduler.append(new_schedule)
262
263
                    # Add schedule job
264 2
                    self.sched.add_circuit_job(evc, new_schedule)
265
266
                    # save circuit
267 2
                    self.storehouse.save_evc(evc)
268
269 2
                    result = new_schedule.as_dict()
270 2
                    status = 201
271
            except ValueError as exception:
272
                log.exception(exception)
273
                result = "Bad request: {}".format(exception)
274
                status = 400
275
276 2
        return jsonify(result), status
277
278 2
    @rest('/v2/evc/schedule/<schedule_id>', methods=['PATCH'])
279
    def update_schedule(self, schedule_id):
280
        """Update a schedule.
281
282
        Change all attributes from the given schedule from a EVC circuit.
283
        The schedule ID is preserved as default.
284
        Payload example:
285
            {
286
              "date": "2019-08-07T14:52:10.967Z",
287
              "interval": "string",
288
              "frequency": "1 * * *",
289
              "action": "create"
290
            }
291
        """
292 2
        data = request.get_json()
293
294 2
        try:
295
            # Try to find a circuit schedule
296 2
            evc, found_schedule = self.find_evc_by_schedule_id(schedule_id)
297
298 2
            if found_schedule:
299 2
                new_schedule = CircuitSchedule.from_dict(data)
300 2
                new_schedule.id = found_schedule.id
301
                # Remove the old schedule
302 2
                evc.circuit_scheduler.remove(found_schedule)
303
                # Append the modified schedule
304 2
                evc.circuit_scheduler.append(new_schedule)
305
306
                # Cancel all schedule jobs
307 2
                self.sched.cancel_job(found_schedule.id)
308
                # Add the new circuit schedule
309 2
                self.sched.add_circuit_job(evc, new_schedule)
310
                # Save EVC to the storehouse
311 2
                self.storehouse.save_evc(evc)
312
313 2
                result = new_schedule.as_dict()
314 2
                status = 200
315
            else:
316
                result = {'response': f'schedule_id {schedule_id} not found'}
317
                status = 404
318
319
        except ValueError as exception:
320
            log.error(exception)
321
            result = "Bad request: {}".format(exception)
322
            status = 400
323
324 2
        return jsonify(result), status
325
326 2
    @rest('/v2/evc/schedule/<schedule_id>', methods=['DELETE'])
327
    def delete_schedule(self, schedule_id):
328
        """Remove a circuit schedule.
329
330
        Remove the Schedule from EVC.
331
        Remove the Schedule from cron job.
332
        Save the EVC to the Storehouse.
333
        """
334 2
        evc, found_schedule = self.find_evc_by_schedule_id(schedule_id)
335
336 2
        if found_schedule:
337
            # Remove the old schedule
338 2
            evc.circuit_scheduler.remove(found_schedule)
339
340
            # Cancel all schedule jobs
341 2
            self.sched.cancel_job(found_schedule.id)
342
            # Save EVC to the storehouse
343 2
            self.storehouse.save_evc(evc)
344
345 2
            result = "Schedule removed"
346 2
            status = 200
347
        else:
348
            result = {'response': f'schedule_id {schedule_id} not found'}
349
            status = 404
350
351 2
        return jsonify(result), status
352
353 2
    def is_duplicated_evc(self, evc):
354
        """Verify if the circuit given is duplicated with the stored evcs.
355
356
        Args:
357
            evc (EVC): circuit to be analysed.
358
359
        Returns:
360
            boolean: True if the circuit is duplicated, otherwise False.
361
362
        """
363 2
        for circuit in self.circuits.values():
364 2
            if not circuit.archived and circuit == evc:
365 2
                return True
366 2
        return False
367
368 2
    @listen_to('kytos/topology.link_up')
369
    def handle_link_up(self, event):
370
        """Change circuit when link is up or end_maintenance."""
371
        for evc in self.circuits.values():
372
            if evc.is_enabled() and not evc.archived:
373
                evc.handle_link_up(event.content['link'])
374
375 2
    @listen_to('kytos/topology.link_down')
376
    def handle_link_down(self, event):
377
        """Change circuit when link is down or under_mantenance."""
378
        for evc in self.circuits.values():
379
            if evc.is_affected_by_link(event.content['link']):
380
                log.info('handling evc %s' % evc)
381
                evc.handle_link_down()
382
383 2
    def evc_from_dict(self, evc_dict):
384
        """Convert some dict values to instance of EVC classes.
385
386
        This method will convert: [UNI, Link]
387
        """
388 2
        data = evc_dict.copy()  # Do not modify the original dict
389
390 2
        for attribute, value in data.items():
391
392 2
            if 'uni' in attribute:
393 2
                try:
394 2
                    data[attribute] = self.uni_from_dict(value)
395
                except ValueError as exc:
396
                    raise ValueError(f'Error creating UNI: {exc}')
397
398 2
            if attribute == 'circuit_scheduler':
399 2
                data[attribute] = []
400 2
                for schedule in value:
401 2
                    data[attribute].append(CircuitSchedule.from_dict(schedule))
402
403 2
            if 'link' in attribute:
404
                if value:
405
                    data[attribute] = self.link_from_dict(value)
406
407 2
            if 'path' in attribute and attribute != 'dynamic_backup_path':
408
                if value:
409
                    data[attribute] = [self.link_from_dict(link)
410
                                       for link in value]
411
412 2
        return EVC(self.controller, **data)
413
414 2
    def uni_from_dict(self, uni_dict):
415
        """Return a UNI object from python dict."""
416
        if uni_dict is None:
417
            return False
418
419
        interface_id = uni_dict.get("interface_id")
420
        interface = self.controller.get_interface_by_id(interface_id)
421
        if interface is None:
422
            raise ValueError(f'Could not instantiate interface {interface_id}')
423
424
        tag_dict = uni_dict.get("tag")
425
        tag = TAG.from_dict(tag_dict)
426
        if tag is False:
427
            raise ValueError(f'Could not instantiate tag from dict {tag_dict}')
428
429
        uni = UNI(interface, tag)
430
431
        return uni
432
433 2
    def link_from_dict(self, link_dict):
434
        """Return a Link object from python dict."""
435
        id_a = link_dict.get('endpoint_a').get('id')
436
        id_b = link_dict.get('endpoint_b').get('id')
437
438
        endpoint_a = self.controller.get_interface_by_id(id_a)
439
        endpoint_b = self.controller.get_interface_by_id(id_b)
440
441
        link = Link(endpoint_a, endpoint_b)
442
        if 'metadata' in link_dict:
443
            link.extend_metadata(link_dict.get('metadata'))
444
445
        s_vlan = link.get_metadata('s_vlan')
446
        if s_vlan:
447
            tag = TAG.from_dict(s_vlan)
448
            if tag is False:
449
                error_msg = f'Could not instantiate tag from dict {s_vlan}'
450
                raise ValueError(error_msg)
451
            link.update_metadata('s_vlan', tag)
452
        return link
453
454 2
    def find_evc_by_schedule_id(self, schedule_id):
455
        """
456
        Find an EVC and CircuitSchedule based on schedule_id.
457
458
        :param schedule_id: Schedule ID
459
        :return: EVC and Schedule
460
        """
461 2
        found_schedule = None
462 2
        evc = None
463
464 2
        circuits = self.storehouse.get_data()
465
466 2
        for c_id, circuit in circuits.items():
467 2
            if "circuit_scheduler" in circuit:
468 2
                evc = self.evc_from_dict(circuits.get(c_id))
469 2
                for schedule in evc.circuit_scheduler:
470 2
                    if schedule.id == schedule_id:
471 2
                        found_schedule = schedule
472 2
                        break
473
        return evc, found_schedule
474