build.main.Main._find_evc_by_schedule_id()   A
last analyzed

Complexity

Conditions 5

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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