Passed
Pull Request — master (#90)
by Antonio
05:03
created

build.main   F

Complexity

Total Complexity 133

Size/Duplication

Total Lines 837
Duplicated Lines 0 %

Test Coverage

Coverage 74.19%

Importance

Changes 0
Metric Value
eloc 522
dl 0
loc 837
rs 2
c 0
b 0
f 0
ccs 342
cts 461
cp 0.7419
wmc 133

30 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.list_circuits() 0 23 3
B Main.add_metadata() 0 30 6
C Main._evc_dict_with_instances() 0 42 9
A Main.update_schedule() 0 50 3
A Main._find_evc_by_schedule_id() 0 21 5
A Main._evc_from_dict() 0 3 1
A Main.load_all_evcs() 0 5 3
A Main.get_metadata() 0 10 2
A Main.handle_link_up() 0 8 5
A Main.delete_metadata() 0 11 2
A Main.delete_schedule() 0 35 3
A Main.get_circuit() 0 15 2
A Main.shutdown() 0 2 1
A Main.handle_link_down() 0 19 5
A Main.setup() 0 26 1
A Main.execute() 0 8 3
A Main._json_from_request() 0 23 4
D Main.create_circuit() 0 98 12
B Main.list_schedules() 0 31 5
A Main._link_from_dict() 0 20 4
A Main._uni_from_dict() 0 22 4
A Main.redeploy() 0 20 4
A Main.handle_flow_mod_error() 0 10 3
A Main._is_duplicated_evc() 0 14 4
C Main.execute_consistency() 0 24 10
D Main.update() 0 57 13
A Main._load_evc() 0 18 3
B Main.create_schedule() 0 76 7
A Main._get_circuits_buffer() 0 13 3
A Main.delete_circuit() 0 34 3

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
# pylint: disable=protected-access
2 1
"""Main module of kytos/mef_eline Kytos Network Application.
3
4
NApp to provision circuits from user request.
5
"""
6 1
from threading import Lock
7
8 1
from flask import jsonify, request
9 1
from werkzeug.exceptions import (BadRequest, Conflict, Forbidden,
10
                                 MethodNotAllowed, NotFound,
11
                                 UnsupportedMediaType)
12
13 1
from kytos.core import KytosNApp, log, rest
14 1
from kytos.core.events import KytosEvent
15 1
from kytos.core.helpers import listen_to
16 1
from kytos.core.interface import TAG, UNI
17 1
from kytos.core.link import Link
18 1
from napps.kytos.mef_eline import settings
19 1
from napps.kytos.mef_eline.exceptions import InvalidPath
20 1
from napps.kytos.mef_eline.models import EVC, DynamicPathManager, Path
21 1
from napps.kytos.mef_eline.scheduler import CircuitSchedule, Scheduler
22 1
from napps.kytos.mef_eline.storehouse import StoreHouse
23 1
from napps.kytos.mef_eline.utils import emit_event, load_spec, validate
24
25
26
# pylint: disable=too-many-public-methods
27 1
class Main(KytosNApp):
28
    """Main class of amlight/mef_eline NApp.
29
30
    This class is the entry point for this napp.
31
    """
32
33 1
    spec = load_spec()
34
35 1
    def setup(self):
36
        """Replace the '__init__' method for the KytosNApp subclass.
37
38
        The setup method is automatically called by the controller when your
39
        application is loaded.
40
41
        So, if you have any setup routine, insert it here.
42
        """
43
        # object used to scheduler circuit events
44 1
        self.sched = Scheduler()
45
46
        # object to save and load circuits
47 1
        self.storehouse = StoreHouse(self.controller)
48
49
        # set the controller that will manager the dynamic paths
50 1
        DynamicPathManager.set_controller(self.controller)
51
52
        # dictionary of EVCs created. It acts as a circuit buffer.
53
        # Every create/update/delete must be synced to storehouse.
54 1
        self.circuits = {}
55
56 1
        self._lock = Lock()
57
58 1
        self.execute_as_loop(settings.DEPLOY_EVCS_INTERVAL)
59 1
        self.execution_rounds = 0
60 1
        self.load_all_evcs()
61
62 1
    def execute(self):
63
        """Execute once when the napp is running."""
64 1
        if self._lock.locked():
65 1
            return
66 1
        log.debug("Starting consistency routine")
67 1
        with self._lock:
68 1
            self.execute_consistency()
69 1
        log.debug("Finished consistency routine")
70
71 1
    def execute_consistency(self):
72
        """Execute consistency routine."""
73 1
        self.execution_rounds += 1
74 1
        stored_circuits = self.storehouse.get_data().copy()
75 1
        for circuit in tuple(self.circuits.values()):
76 1
            stored_circuits.pop(circuit.id, None)
77 1
            if (
78
                circuit.is_enabled()
79
                and not circuit.is_active()
80
                and not circuit.lock.locked()
81
            ):
82 1
                if circuit.check_traces():
83 1
                    log.info(f"{circuit} enabled but inactive - activating")
84 1
                    with circuit.lock:
85 1
                        circuit.activate()
86 1
                        circuit.sync()
87
                else:
88 1
                    if self.execution_rounds > settings.WAIT_FOR_OLD_PATH:
89 1
                        log.info(f"{circuit} enabled but inactive - redeploy")
90 1
                        with circuit.lock:
91 1
                            circuit.deploy()
92 1
        for circuit_id in stored_circuits:
93 1
            log.info(f"EVC found in storehouse but unloaded {circuit_id}")
94 1
            self._load_evc(stored_circuits[circuit_id])
95
96 1
    def shutdown(self):
97
        """Execute when your napp is unloaded.
98
99
        If you have some cleanup procedure, insert it here.
100
        """
101
102 1
    @rest("/v2/evc/", methods=["GET"])
103
    def list_circuits(self):
104
        """Endpoint to return circuits stored.
105
106
        If archived is set to True return all circuits, else only the ones
107
        not archived.
108
        """
109 1
        log.debug("list_circuits /v2/evc")
110 1
        archived = request.args.get("archived", False)
111 1
        circuits = self.storehouse.get_data()
112 1
        if not circuits:
113 1
            return jsonify({}), 200
114 1
        if archived:
115 1
            return jsonify(circuits), 200
116 1
        return (
117
            jsonify(
118
                {
119
                    circuit_id: circuit
120
                    for circuit_id, circuit in circuits.items()
121
                    if not circuit.get("archived", False)
122
                }
123
            ),
124
            200,
125
        )
126
127 1
    @rest("/v2/evc/<circuit_id>", methods=["GET"])
128
    def get_circuit(self, circuit_id):
129
        """Endpoint to return a circuit based on id."""
130 1
        log.debug("get_circuit /v2/evc/%s", circuit_id)
131 1
        circuits = self.storehouse.get_data()
132
133 1
        try:
134 1
            result = circuits[circuit_id]
135 1
        except KeyError:
136 1
            result = f"circuit_id {circuit_id} not found"
137 1
            log.debug("get_circuit result %s %s", result, 404)
138 1
            raise BadRequest(result) from KeyError
139 1
        status = 200
140 1
        log.debug("get_circuit result %s %s", result, status)
141 1
        return jsonify(result), status
142
143 1
    @rest("/v2/evc/", methods=["POST"])
144 1
    @validate(spec)
145
    def create_circuit(self, data):
146
        """Try to create a new circuit.
147
148
        Firstly, for EVPL: E-Line NApp verifies if UNI_A's requested C-VID and
149
        UNI_Z's requested C-VID are available from the interfaces' pools. This
150
        is checked when creating the UNI object.
151
152
        Then, E-Line NApp requests a primary and a backup path to the
153
        Pathfinder NApp using the attributes primary_links and backup_links
154
        submitted via REST
155
156
        # For each link composing paths in #3:
157
        #  - E-Line NApp requests a S-VID available from the link VLAN pool.
158
        #  - Using the S-VID obtained, generate abstract flow entries to be
159
        #    sent to FlowManager
160
161
        Push abstract flow entries to FlowManager and FlowManager pushes
162
        OpenFlow entries to datapaths
163
164
        E-Line NApp generates an event to notify all Kytos NApps of a new EVC
165
        creation
166
167
        Finnaly, notify user of the status of its request.
168
        """
169
        # Try to create the circuit object
170 1
        log.debug("create_circuit /v2/evc/")
171
172 1
        try:
173 1
            evc = self._evc_from_dict(data)
174 1
        except ValueError as exception:
175
            log.debug("create_circuit result %s %s", exception, 400)
176
            raise BadRequest(str(exception)) from BadRequest
177
178 1
        if evc.primary_path:
179
            try:
180
                evc.primary_path.is_valid(
181
                    evc.uni_a.interface.switch,
182
                    evc.uni_z.interface.switch,
183
                    bool(evc.circuit_scheduler),
184
                )
185
            except InvalidPath as exception:
186
                raise BadRequest(
187
                    f"primary_path is not valid: {exception}"
188
                ) from exception
189 1
        if evc.backup_path:
190
            try:
191
                evc.backup_path.is_valid(
192
                    evc.uni_a.interface.switch,
193
                    evc.uni_z.interface.switch,
194
                    bool(evc.circuit_scheduler),
195
                )
196
            except InvalidPath as exception:
197
                raise BadRequest(
198
                    f"backup_path is not valid: {exception}"
199
                ) from exception
200
201
        # verify duplicated evc
202 1
        if self._is_duplicated_evc(evc):
203 1
            result = "The EVC already exists."
204 1
            log.debug("create_circuit result %s %s", result, 409)
205 1
            raise Conflict(result)
206
207 1
        if (
208
            not evc.primary_path
209
            and evc.dynamic_backup_path is False
210
            and evc.uni_a.interface.switch != evc.uni_z.interface.switch
211
        ):
212 1
            result = "The EVC must have a primary path or allow dynamic paths."
213 1
            log.debug("create_circuit result %s %s", result, 400)
214 1
            raise BadRequest(result)
215
216
        # store circuit in dictionary
217 1
        self.circuits[evc.id] = evc
218
219
        # save circuit
220 1
        self.storehouse.save_evc(evc)
221
222
        # Schedule the circuit deploy
223 1
        self.sched.add(evc)
224
225
        # Circuit has no schedule, deploy now
226 1
        if not evc.circuit_scheduler:
227 1
            with evc.lock:
228 1
                evc.deploy()
229
230
        # Notify users
231 1
        event = KytosEvent(
232
            name="kytos.mef_eline.created", content=evc.as_dict()
233
        )
234 1
        self.controller.buffers.app.put(event)
235
236 1
        result = {"circuit_id": evc.id}
237 1
        status = 201
238 1
        log.debug("create_circuit result %s %s", result, status)
239 1
        emit_event(self.controller, "created", evc_id=evc.id)
240 1
        return jsonify(result), status
241
242 1
    @rest("/v2/evc/<circuit_id>", methods=["PATCH"])
243
    def update(self, circuit_id):
244
        """Update a circuit based on payload.
245
246
        The EVC required attributes (name, uni_a, uni_z) can't be updated.
247
        """
248 1
        log.debug("update /v2/evc/%s", circuit_id)
249 1
        try:
250 1
            evc = self.circuits[circuit_id]
251 1
        except KeyError:
252 1
            result = f"circuit_id {circuit_id} not found"
253 1
            log.debug("update result %s %s", result, 404)
254 1
            raise NotFound(result) from NotFound
255
256 1
        if evc.archived:
257 1
            result = "Can't update archived EVC"
258 1
            log.debug("update result %s %s", result, 405)
259 1
            raise MethodNotAllowed(["GET"], result)
260
261 1
        try:
262 1
            data = request.get_json()
263 1
        except BadRequest:
264 1
            result = "The request body is not a well-formed JSON."
265 1
            log.debug("update result %s %s", result, 400)
266 1
            raise BadRequest(result) from BadRequest
267 1
        if data is None:
268 1
            result = "The request body mimetype is not application/json."
269 1
            log.debug("update result %s %s", result, 415)
270 1
            raise UnsupportedMediaType(result) from UnsupportedMediaType
271
272 1
        try:
273 1
            enable, redeploy = evc.update(
274
                **self._evc_dict_with_instances(data)
275
            )
276 1
        except ValueError as exception:
277 1
            log.error(exception)
278 1
            log.debug("update result %s %s", exception, 400)
279 1
            raise BadRequest(str(exception)) from BadRequest
280
281 1
        if evc.is_active():
282
            if enable is False:  # disable if active
283
                with evc.lock:
284
                    evc.remove()
285
            elif redeploy is not None:  # redeploy if active
286
                with evc.lock:
287
                    evc.remove()
288
                    evc.deploy()
289
        else:
290 1
            if enable is True:  # enable if inactive
291 1
                with evc.lock:
292 1
                    evc.deploy()
293 1
        result = {evc.id: evc.as_dict()}
294 1
        status = 200
295
296 1
        log.debug("update result %s %s", result, status)
297 1
        emit_event(self.controller, "updated", evc_id=evc.id, data=data)
298 1
        return jsonify(result), status
299
300 1
    @rest("/v2/evc/<circuit_id>", methods=["DELETE"])
301
    def delete_circuit(self, circuit_id):
302
        """Remove a circuit.
303
304
        First, the flows are removed from the switches, and then the EVC is
305
        disabled.
306
        """
307 1
        log.debug("delete_circuit /v2/evc/%s", circuit_id)
308 1
        try:
309 1
            evc = self.circuits[circuit_id]
310 1
        except KeyError:
311 1
            result = f"circuit_id {circuit_id} not found"
312 1
            log.debug("delete_circuit result %s %s", result, 404)
313 1
            raise NotFound(result) from NotFound
314
315 1
        if evc.archived:
316
            result = f"Circuit {circuit_id} already removed"
317
            log.debug("delete_circuit result %s %s", result, 404)
318
            raise NotFound(result) from NotFound
319
320 1
        log.info("Removing %s", evc)
321 1
        evc.remove_current_flows()
322 1
        evc.deactivate()
323 1
        evc.disable()
324 1
        self.sched.remove(evc)
325 1
        evc.archive()
326 1
        evc.sync()
327 1
        log.info("EVC removed. %s", evc)
328 1
        result = {"response": f"Circuit {circuit_id} removed"}
329 1
        status = 200
330
331 1
        log.debug("delete_circuit result %s %s", result, status)
332 1
        emit_event(self.controller, "deleted", evc_id=evc.id)
333 1
        return jsonify(result), status
334
335 1
    @rest("v2/evc/<circuit_id>/metadata", methods=["GET"])
336
    def get_metadata(self, circuit_id):
337
        """Get metadata from an EVC."""
338
        try:
339
            return (
340
                jsonify({"metadata": self.circuits[circuit_id].metadata}),
341
                200,
342
            )
343
        except KeyError as error:
344
            raise NotFound(f"circuit_id {circuit_id} not found.") from error
345
346 1
    @rest("v2/evc/<circuit_id>/metadata", methods=["POST"])
347
    def add_metadata(self, circuit_id):
348
        """Add metadata to an EVC."""
349 1
        try:
350 1
            metadata = request.get_json()
351 1
            content_type = request.content_type
352
        except BadRequest as error:
353
            result = "The request body is not a well-formed JSON."
354
            raise BadRequest(result) from error
355 1
        if content_type is None:
356
            result = "The request body is empty."
357
            raise BadRequest(result)
358 1
        if metadata is None:
359
            if content_type != "application/json":
360
                result = (
361
                    "The content type must be application/json "
362
                    f"(received {content_type})."
363
                )
364
            else:
365
                result = "Metadata is empty."
366
            raise UnsupportedMediaType(result)
367
368 1
        try:
369 1
            evc = self.circuits[circuit_id]
370
        except KeyError as error:
371
            raise NotFound(f"circuit_id {circuit_id} not found.") from error
372
373 1
        evc.extend_metadata(metadata)
374 1
        evc.sync()
375 1
        return jsonify("Operation successful"), 201
376
377 1
    @rest("v2/evc/<circuit_id>/metadata/<key>", methods=["DELETE"])
378
    def delete_metadata(self, circuit_id, key):
379
        """Delete metadata from an EVC."""
380
        try:
381
            evc = self.circuits[circuit_id]
382
        except KeyError as error:
383
            raise NotFound(f"circuit_id {circuit_id} not found.") from error
384
385
        evc.remove_metadata(key)
386
        evc.sync()
387
        return jsonify("Operation successful"), 200
388
389 1
    @rest("/v2/evc/<circuit_id>/redeploy", methods=["PATCH"])
390
    def redeploy(self, circuit_id):
391
        """Endpoint to force the redeployment of an EVC."""
392 1
        log.debug("redeploy /v2/evc/%s/redeploy", circuit_id)
393 1
        try:
394 1
            evc = self.circuits[circuit_id]
395 1
        except KeyError:
396 1
            result = f"circuit_id {circuit_id} not found"
397 1
            raise NotFound(result) from NotFound
398 1
        if evc.is_enabled():
399 1
            with evc.lock:
400 1
                evc.remove_current_flows()
401 1
                evc.deploy()
402 1
            result = {"response": f"Circuit {circuit_id} redeploy received."}
403 1
            status = 202
404
        else:
405 1
            result = {"response": f"Circuit {circuit_id} is disabled."}
406 1
            status = 409
407
408 1
        return jsonify(result), status
409
410 1
    @rest("/v2/evc/schedule", methods=["GET"])
411
    def list_schedules(self):
412
        """Endpoint to return all schedules stored for all circuits.
413
414
        Return a JSON with the following template:
415
        [{"schedule_id": <schedule_id>,
416
         "circuit_id": <circuit_id>,
417
         "schedule": <schedule object>}]
418
        """
419 1
        log.debug("list_schedules /v2/evc/schedule")
420 1
        circuits = self.storehouse.get_data().values()
421 1
        if not circuits:
422 1
            result = {}
423 1
            status = 200
424 1
            return jsonify(result), status
425
426 1
        result = []
427 1
        status = 200
428 1
        for circuit in circuits:
429 1
            circuit_scheduler = circuit.get("circuit_scheduler")
430 1
            if circuit_scheduler:
431 1
                for scheduler in circuit_scheduler:
432 1
                    value = {
433
                        "schedule_id": scheduler.get("id"),
434
                        "circuit_id": circuit.get("id"),
435
                        "schedule": scheduler,
436
                    }
437 1
                    result.append(value)
438
439 1
        log.debug("list_schedules result %s %s", result, status)
440 1
        return jsonify(result), status
441
442 1
    @rest("/v2/evc/schedule/", methods=["POST"])
443
    def create_schedule(self):
444
        """
445
        Create a new schedule for a given circuit.
446
447
        This service do no check if there are conflicts with another schedule.
448
        Payload example:
449
            {
450
              "circuit_id":"aa:bb:cc",
451
              "schedule": {
452
                "date": "2019-08-07T14:52:10.967Z",
453
                "interval": "string",
454
                "frequency": "1 * * * *",
455
                "action": "create"
456
              }
457
            }
458
        """
459 1
        log.debug("create_schedule /v2/evc/schedule/")
460
461 1
        json_data = self._json_from_request("create_schedule")
462 1
        try:
463 1
            circuit_id = json_data["circuit_id"]
464
        except TypeError:
465
            result = "The payload should have a dictionary."
466
            log.debug("create_schedule result %s %s", result, 400)
467
            raise BadRequest(result) from BadRequest
468
        except KeyError:
469
            result = "Missing circuit_id."
470
            log.debug("create_schedule result %s %s", result, 400)
471
            raise BadRequest(result) from BadRequest
472
473 1
        try:
474 1
            schedule_data = json_data["schedule"]
475
        except KeyError:
476
            result = "Missing schedule data."
477
            log.debug("create_schedule result %s %s", result, 400)
478
            raise BadRequest(result) from BadRequest
479
480
        # Get EVC from circuits buffer
481 1
        circuits = self._get_circuits_buffer()
482
483
        # get the circuit
484 1
        evc = circuits.get(circuit_id)
485
486
        # get the circuit
487 1
        if not evc:
488
            result = f"circuit_id {circuit_id} not found"
489
            log.debug("create_schedule result %s %s", result, 404)
490
            raise NotFound(result) from NotFound
491
        # Can not modify circuits deleted and archived
492 1
        if evc.archived:
493
            result = f"Circuit {circuit_id} is archived. Update is forbidden."
494
            log.debug("create_schedule result %s %s", result, 403)
495
            raise Forbidden(result) from Forbidden
496
497
        # new schedule from dict
498 1
        new_schedule = CircuitSchedule.from_dict(schedule_data)
499
500
        # If there is no schedule, create the list
501 1
        if not evc.circuit_scheduler:
502
            evc.circuit_scheduler = []
503
504
        # Add the new schedule
505 1
        evc.circuit_scheduler.append(new_schedule)
506
507
        # Add schedule job
508 1
        self.sched.add_circuit_job(evc, new_schedule)
509
510
        # save circuit to storehouse
511 1
        evc.sync()
512
513 1
        result = new_schedule.as_dict()
514 1
        status = 201
515
516 1
        log.debug("create_schedule result %s %s", result, status)
517 1
        return jsonify(result), status
518
519 1
    @rest("/v2/evc/schedule/<schedule_id>", methods=["PATCH"])
520
    def update_schedule(self, schedule_id):
521
        """Update a schedule.
522
523
        Change all attributes from the given schedule from a EVC circuit.
524
        The schedule ID is preserved as default.
525
        Payload example:
526
            {
527
              "date": "2019-08-07T14:52:10.967Z",
528
              "interval": "string",
529
              "frequency": "1 * * *",
530
              "action": "create"
531
            }
532
        """
533 1
        log.debug("update_schedule /v2/evc/schedule/%s", schedule_id)
534
535
        # Try to find a circuit schedule
536 1
        evc, found_schedule = self._find_evc_by_schedule_id(schedule_id)
537
538
        # Can not modify circuits deleted and archived
539 1
        if not found_schedule:
540
            result = f"schedule_id {schedule_id} not found"
541
            log.debug("update_schedule result %s %s", result, 404)
542
            raise NotFound(result) from NotFound
543 1
        if evc.archived:
544 1
            result = f"Circuit {evc.id} is archived. Update is forbidden."
545 1
            log.debug("update_schedule result %s %s", result, 403)
546 1
            raise Forbidden(result) from Forbidden
547
548 1
        data = self._json_from_request("update_schedule")
549
550 1
        new_schedule = CircuitSchedule.from_dict(data)
551 1
        new_schedule.id = found_schedule.id
552
        # Remove the old schedule
553 1
        evc.circuit_scheduler.remove(found_schedule)
554
        # Append the modified schedule
555 1
        evc.circuit_scheduler.append(new_schedule)
556
557
        # Cancel all schedule jobs
558 1
        self.sched.cancel_job(found_schedule.id)
559
        # Add the new circuit schedule
560 1
        self.sched.add_circuit_job(evc, new_schedule)
561
        # Save EVC to the storehouse
562 1
        evc.sync()
563
564 1
        result = new_schedule.as_dict()
565 1
        status = 200
566
567 1
        log.debug("update_schedule result %s %s", result, status)
568 1
        return jsonify(result), status
569
570 1
    @rest("/v2/evc/schedule/<schedule_id>", methods=["DELETE"])
571
    def delete_schedule(self, schedule_id):
572
        """Remove a circuit schedule.
573
574
        Remove the Schedule from EVC.
575
        Remove the Schedule from cron job.
576
        Save the EVC to the Storehouse.
577
        """
578 1
        log.debug("delete_schedule /v2/evc/schedule/%s", schedule_id)
579 1
        evc, found_schedule = self._find_evc_by_schedule_id(schedule_id)
580
581
        # Can not modify circuits deleted and archived
582 1
        if not found_schedule:
583
            result = f"schedule_id {schedule_id} not found"
584
            log.debug("delete_schedule result %s %s", result, 404)
585
            raise NotFound(result)
586
587 1
        if evc.archived:
588 1
            result = f"Circuit {evc.id} is archived. Update is forbidden."
589 1
            log.debug("delete_schedule result %s %s", result, 403)
590 1
            raise Forbidden(result)
591
592
        # Remove the old schedule
593 1
        evc.circuit_scheduler.remove(found_schedule)
594
595
        # Cancel all schedule jobs
596 1
        self.sched.cancel_job(found_schedule.id)
597
        # Save EVC to the storehouse
598 1
        evc.sync()
599
600 1
        result = "Schedule removed"
601 1
        status = 200
602
603 1
        log.debug("delete_schedule result %s %s", result, status)
604 1
        return jsonify(result), status
605
606 1
    def _is_duplicated_evc(self, evc):
607
        """Verify if the circuit given is duplicated with the stored evcs.
608
609
        Args:
610
            evc (EVC): circuit to be analysed.
611
612
        Returns:
613
            boolean: True if the circuit is duplicated, otherwise False.
614
615
        """
616 1
        for circuit in tuple(self.circuits.values()):
617 1
            if not circuit.archived and circuit.shares_uni(evc):
618 1
                return True
619 1
        return False
620
621 1
    @listen_to("kytos/topology.link_up")
622
    def handle_link_up(self, event):
623
        """Change circuit when link is up or end_maintenance."""
624 1
        log.debug("Event handle_link_up %s", event)
625 1
        for evc in self.circuits.values():
626 1
            if evc.is_enabled() and not evc.archived:
627 1
                with evc.lock:
628 1
                    evc.handle_link_up(event.content["link"])
629
630 1
    @listen_to("kytos/topology.link_down")
631
    def handle_link_down(self, event):
632
        """Change circuit when link is down or under_mantenance."""
633 1
        log.debug("Event handle_link_down %s", event)
634 1
        for evc in self.circuits.values():
635 1
            with evc.lock:
636 1
                if evc.is_affected_by_link(event.content["link"]):
637 1
                    log.debug(f"Handling evc {evc.id} on link down")
638 1
                    if evc.handle_link_down():
639 1
                        emit_event(
640
                            self.controller,
641
                            "redeployed_link_down",
642
                            evc_id=evc.id,
643
                        )
644
                    else:
645
                        emit_event(
646
                            self.controller,
647
                            "error_redeploy_link_down",
648
                            evc_id=evc.id,
649
                        )
650
651 1
    def load_all_evcs(self):
652
        """Try to load all EVCs on startup."""
653 1
        for circuit_id, circuit in self.storehouse.get_data().items():
654
            if circuit_id not in self.circuits:
655
                self._load_evc(circuit)
656
657 1
    def _load_evc(self, circuit_dict):
658
        """Load one EVC from storehouse to memory."""
659
        try:
660
            evc = self._evc_from_dict(circuit_dict)
661
        except ValueError as exception:
662
            log.error(
663
                f'Could not load EVC {circuit_dict["id"]} '
664
                f"because {exception}"
665
            )
666
            return None
667
668
        if evc.archived:
669
            return None
670
        evc.deactivate()
671
        evc.sync()
672
        self.circuits.setdefault(evc.id, evc)
673
        self.sched.add(evc)
674
        return evc
675
676 1
    @listen_to("kytos/flow_manager.flow.error")
677
    def handle_flow_mod_error(self, event):
678
        """Handle flow mod errors related to an EVC."""
679
        flow = event.content["flow"]
680
        command = event.content.get("error_command")
681
        if command != "add":
682
            return
683
        evc = self.circuits.get(EVC.get_id_from_cookie(flow.cookie))
684
        if evc:
685
            evc.remove_current_flows()
686
687 1
    def _evc_dict_with_instances(self, evc_dict):
688
        """Convert some dict values to instance of EVC classes.
689
690
        This method will convert: [UNI, Link]
691
        """
692 1
        data = evc_dict.copy()  # Do not modify the original dict
693
694 1
        for attribute, value in data.items():
695
            # Get multiple attributes.
696
            # Ex: uni_a, uni_z
697 1
            if "uni" in attribute:
698 1
                try:
699 1
                    data[attribute] = self._uni_from_dict(value)
700 1
                except ValueError:
701 1
                    result = "Error creating UNI: Invalid value"
702 1
                    raise BadRequest(result) from BadRequest
703
704 1
            if attribute == "circuit_scheduler":
705 1
                data[attribute] = []
706 1
                for schedule in value:
707 1
                    data[attribute].append(CircuitSchedule.from_dict(schedule))
708
709
            # Get multiple attributes.
710
            # Ex: primary_links,
711
            #     backup_links,
712
            #     current_links_cache,
713
            #     primary_links_cache,
714
            #     backup_links_cache
715 1
            if "links" in attribute:
716 1
                data[attribute] = [
717
                    self._link_from_dict(link) for link in value
718
                ]
719
720
            # Ex: current_path,
721
            #     primary_path,
722
            #     backup_path
723 1
            if "path" in attribute and attribute != "dynamic_backup_path":
724 1
                data[attribute] = Path(
725
                    [self._link_from_dict(link) for link in value]
726
                )
727
728 1
        return data
729
730 1
    def _evc_from_dict(self, evc_dict):
731 1
        data = self._evc_dict_with_instances(evc_dict)
732 1
        return EVC(self.controller, **data)
733
734 1
    def _uni_from_dict(self, uni_dict):
735
        """Return a UNI object from python dict."""
736
        if uni_dict is None:
737
            return False
738
739
        interface_id = uni_dict.get("interface_id")
740
        interface = self.controller.get_interface_by_id(interface_id)
741
        if interface is None:
742
            result = (
743
                "Error creating UNI:"
744
                + f"Could not instantiate interface {interface_id}"
745
            )
746
            raise ValueError(result) from ValueError
747
748
        tag_dict = uni_dict.get("tag", None)
749
        if tag_dict:
750
            tag = TAG.from_dict(tag_dict)
751
        else:
752
            tag = None
753
        uni = UNI(interface, tag)
754
755
        return uni
756
757 1
    def _link_from_dict(self, link_dict):
758
        """Return a Link object from python dict."""
759 1
        id_a = link_dict.get("endpoint_a").get("id")
760 1
        id_b = link_dict.get("endpoint_b").get("id")
761
762 1
        endpoint_a = self.controller.get_interface_by_id(id_a)
763 1
        endpoint_b = self.controller.get_interface_by_id(id_b)
764
765 1
        link = Link(endpoint_a, endpoint_b)
766 1
        if "metadata" in link_dict:
767
            link.extend_metadata(link_dict.get("metadata"))
768
769 1
        s_vlan = link.get_metadata("s_vlan")
770 1
        if s_vlan:
771
            tag = TAG.from_dict(s_vlan)
772
            if tag is False:
773
                error_msg = f"Could not instantiate tag from dict {s_vlan}"
774
                raise ValueError(error_msg)
775
            link.update_metadata("s_vlan", tag)
776 1
        return link
777
778 1
    def _find_evc_by_schedule_id(self, schedule_id):
779
        """
780
        Find an EVC and CircuitSchedule based on schedule_id.
781
782
        :param schedule_id: Schedule ID
783
        :return: EVC and Schedule
784
        """
785 1
        circuits = self._get_circuits_buffer()
786 1
        found_schedule = None
787 1
        evc = None
788
789
        # pylint: disable=unused-variable
790 1
        for c_id, circuit in circuits.items():
791 1
            for schedule in circuit.circuit_scheduler:
792 1
                if schedule.id == schedule_id:
793 1
                    found_schedule = schedule
794 1
                    evc = circuit
795 1
                    break
796 1
            if found_schedule:
797 1
                break
798 1
        return evc, found_schedule
799
800 1
    def _get_circuits_buffer(self):
801
        """
802
        Return the circuit buffer.
803
804
        If the buffer is empty, try to load data from storehouse.
805
        """
806 1
        if not self.circuits:
807
            # Load storehouse circuits to buffer
808 1
            circuits = self.storehouse.get_data()
809 1
            for c_id, circuit in circuits.items():
810 1
                evc = self._evc_from_dict(circuit)
811 1
                self.circuits[c_id] = evc
812 1
        return self.circuits
813
814 1
    @staticmethod
815
    def _json_from_request(caller):
816
        """Return a json from request.
817
818
        If it was not possible to get a json from the request, log, for debug,
819
        who was the caller and the error that ocurred, and raise an
820
        Exception.
821
        """
822 1
        try:
823 1
            json_data = request.get_json()
824
        except ValueError as exception:
825
            log.error(exception)
826
            log.debug(f"{caller} result {exception} 400")
827
            raise BadRequest(str(exception)) from BadRequest
828
        except BadRequest:
829
            result = "The request is not a valid JSON."
830
            log.debug(f"{caller} result {result} 400")
831
            raise BadRequest(result) from BadRequest
832 1
        if json_data is None:
833
            result = "Content-Type must be application/json"
834
            log.debug(f"{caller} result {result} 415")
835
            raise UnsupportedMediaType(result)
836
        return json_data
837