Test Failed
Push — master ( a63869...63c1d9 )
by Antonio
04:17 queued 11s
created

build.main.Main.handle_flow_mod_error()   A

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 8
nop 2
dl 0
loc 9
ccs 0
cts 0
cp 0
crap 12
rs 10
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
        evc.remove_current_flows()
322
        evc.deactivate()
323
        evc.disable()
324
        self.sched.remove(evc)
325
        evc.archive()
326
        evc.sync()
327
        log.info("EVC removed. %s", evc)
328
        result = {"response": f"Circuit {circuit_id} removed"}
329
        status = 200
330
331
        log.debug("delete_circuit result %s %s", result, status)
332
        emit_event(self.controller, "deleted", evc_id=evc.id)
333
        return jsonify(result), status
334
335
    @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
    @rest("v2/evc/<circuit_id>/metadata", methods=["POST"])
347
    def add_metadata(self, circuit_id):
348
        """Add metadata to an EVC."""
349
        try:
350
            metadata = request.get_json()
351
            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
        if content_type is None:
356
            result = "The request body is empty."
357
            raise BadRequest(result)
358
        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
        try:
369
            evc = self.circuits[circuit_id]
370
        except KeyError as error:
371
            raise NotFound(f"circuit_id {circuit_id} not found.") from error
372
373
        evc.extend_metadata(metadata)
374
        evc.sync()
375
        return jsonify("Operation successful"), 201
376
377
    @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
    @rest("/v2/evc/<circuit_id>/redeploy", methods=["PATCH"])
390
    def redeploy(self, circuit_id):
391
        """Endpoint to force the redeployment of an EVC."""
392
        log.debug("redeploy /v2/evc/%s/redeploy", circuit_id)
393
        try:
394
            evc = self.circuits[circuit_id]
395
        except KeyError:
396
            result = f"circuit_id {circuit_id} not found"
397
            raise NotFound(result) from NotFound
398
        if evc.is_enabled():
399
            with evc.lock:
400
                evc.remove_current_flows()
401
                evc.deploy()
402
            result = {"response": f"Circuit {circuit_id} redeploy received."}
403
            status = 202
404
        else:
405
            result = {"response": f"Circuit {circuit_id} is disabled."}
406
            status = 409
407
408
        return jsonify(result), status
409
410
    @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
        log.debug("list_schedules /v2/evc/schedule")
420
        circuits = self.storehouse.get_data().values()
421
        if not circuits:
422
            result = {}
423
            status = 200
424
            return jsonify(result), status
425
426
        result = []
427
        status = 200
428
        for circuit in circuits:
429
            circuit_scheduler = circuit.get("circuit_scheduler")
430
            if circuit_scheduler:
431
                for scheduler in circuit_scheduler:
432
                    value = {
433
                        "schedule_id": scheduler.get("id"),
434
                        "circuit_id": circuit.get("id"),
435
                        "schedule": scheduler,
436
                    }
437
                    result.append(value)
438
439
        log.debug("list_schedules result %s %s", result, status)
440
        return jsonify(result), status
441
442
    @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
        log.debug("create_schedule /v2/evc/schedule/")
460
461
        json_data = self._json_from_request("create_schedule")
462
        try:
463
            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
        try:
474
            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
        circuits = self._get_circuits_buffer()
482
483
        # get the circuit
484
        evc = circuits.get(circuit_id)
485
486
        # get the circuit
487
        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
        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
        new_schedule = CircuitSchedule.from_dict(schedule_data)
499
500
        # If there is no schedule, create the list
501
        if not evc.circuit_scheduler:
502
            evc.circuit_scheduler = []
503
504
        # Add the new schedule
505
        evc.circuit_scheduler.append(new_schedule)
506
507
        # Add schedule job
508
        self.sched.add_circuit_job(evc, new_schedule)
509
510
        # save circuit to storehouse
511
        evc.sync()
512
513
        result = new_schedule.as_dict()
514
        status = 201
515
516
        log.debug("create_schedule result %s %s", result, status)
517
        return jsonify(result), status
518
519
    @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
        log.debug("update_schedule /v2/evc/schedule/%s", schedule_id)
534
535
        # Try to find a circuit schedule
536
        evc, found_schedule = self._find_evc_by_schedule_id(schedule_id)
537
538
        # Can not modify circuits deleted and archived
539
        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
        if evc.archived:
544
            result = f"Circuit {evc.id} is archived. Update is forbidden."
545
            log.debug("update_schedule result %s %s", result, 403)
546
            raise Forbidden(result) from Forbidden
547
548
        data = self._json_from_request("update_schedule")
549
550
        new_schedule = CircuitSchedule.from_dict(data)
551
        new_schedule.id = found_schedule.id
552
        # Remove the old schedule
553
        evc.circuit_scheduler.remove(found_schedule)
554
        # Append the modified schedule
555
        evc.circuit_scheduler.append(new_schedule)
556
557
        # Cancel all schedule jobs
558
        self.sched.cancel_job(found_schedule.id)
559
        # Add the new circuit schedule
560
        self.sched.add_circuit_job(evc, new_schedule)
561
        # Save EVC to the storehouse
562
        evc.sync()
563
564
        result = new_schedule.as_dict()
565
        status = 200
566
567
        log.debug("update_schedule result %s %s", result, status)
568
        return jsonify(result), status
569
570
    @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
        log.debug("delete_schedule /v2/evc/schedule/%s", schedule_id)
579
        evc, found_schedule = self._find_evc_by_schedule_id(schedule_id)
580
581
        # Can not modify circuits deleted and archived
582
        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
        if evc.archived:
588
            result = f"Circuit {evc.id} is archived. Update is forbidden."
589
            log.debug("delete_schedule result %s %s", result, 403)
590
            raise Forbidden(result)
591
592
        # Remove the old schedule
593
        evc.circuit_scheduler.remove(found_schedule)
594
595
        # Cancel all schedule jobs
596
        self.sched.cancel_job(found_schedule.id)
597
        # Save EVC to the storehouse
598
        evc.sync()
599
600
        result = "Schedule removed"
601
        status = 200
602
603
        log.debug("delete_schedule result %s %s", result, status)
604
        return jsonify(result), status
605
606
    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
        for circuit in tuple(self.circuits.values()):
617
            if not circuit.archived and circuit.shares_uni(evc):
618
                return True
619
        return False
620
621
    @listen_to("kytos/topology.link_up")
622
    def on_link_up(self, event):
623
        """Change circuit when link is up or end_maintenance."""
624
        self.handle_link_up(event)
625
626
    def handle_link_up(self, event):
627
        """Change circuit when link is up or end_maintenance."""
628
        log.debug("Event handle_link_up %s", event)
629
        for evc in self.circuits.values():
630
            if evc.is_enabled() and not evc.archived:
631
                with evc.lock:
632
                    evc.handle_link_up(event.content["link"])
633
634
    @listen_to("kytos/topology.link_down")
635
    def on_link_down(self, event):
636
        """Change circuit when link is down or under_mantenance."""
637
        self.handle_link_down(event)
638
639
    def handle_link_down(self, event):
640
        """Change circuit when link is down or under_mantenance."""
641
        log.debug("Event handle_link_down %s", event)
642
        for evc in self.circuits.values():
643
            with evc.lock:
644
                if evc.is_affected_by_link(event.content["link"]):
645
                    log.debug(f"Handling evc {evc.id} on link down")
646
                    if evc.handle_link_down():
647
                        emit_event(
648
                            self.controller,
649
                            "redeployed_link_down",
650
                            evc_id=evc.id,
651
                        )
652
                    else:
653
                        emit_event(
654
                            self.controller,
655
                            "error_redeploy_link_down",
656
                            evc_id=evc.id,
657
                        )
658
659
    def load_all_evcs(self):
660
        """Try to load all EVCs on startup."""
661
        for circuit_id, circuit in self.storehouse.get_data().items():
662
            if circuit_id not in self.circuits:
663
                self._load_evc(circuit)
664
665
    def _load_evc(self, circuit_dict):
666
        """Load one EVC from storehouse to memory."""
667
        try:
668
            evc = self._evc_from_dict(circuit_dict)
669
        except ValueError as exception:
670
            log.error(
671
                f'Could not load EVC {circuit_dict["id"]} '
672
                f"because {exception}"
673
            )
674
            return None
675
676
        if evc.archived:
677
            return None
678
        evc.deactivate()
679
        evc.sync()
680
        self.circuits.setdefault(evc.id, evc)
681
        self.sched.add(evc)
682
        return evc
683
684
    @listen_to("kytos/flow_manager.flow.error")
685
    def on_flow_mod_error(self, event):
686
        """Handle flow mod errors related to an EVC."""
687
        self.handle_flow_mod_error(event)
688
689
    def handle_flow_mod_error(self, event):
690
        """Handle flow mod errors related to an EVC."""
691
        flow = event.content["flow"]
692
        command = event.content.get("error_command")
693
        if command != "add":
694
            return
695
        evc = self.circuits.get(EVC.get_id_from_cookie(flow.cookie))
696
        if evc:
697
            evc.remove_current_flows()
698
699
    def _evc_dict_with_instances(self, evc_dict):
700
        """Convert some dict values to instance of EVC classes.
701
702
        This method will convert: [UNI, Link]
703
        """
704
        data = evc_dict.copy()  # Do not modify the original dict
705
706
        for attribute, value in data.items():
707
            # Get multiple attributes.
708
            # Ex: uni_a, uni_z
709
            if "uni" in attribute:
710
                try:
711
                    data[attribute] = self._uni_from_dict(value)
712
                except ValueError:
713
                    result = "Error creating UNI: Invalid value"
714
                    raise BadRequest(result) from BadRequest
715
716
            if attribute == "circuit_scheduler":
717
                data[attribute] = []
718
                for schedule in value:
719
                    data[attribute].append(CircuitSchedule.from_dict(schedule))
720
721
            # Get multiple attributes.
722
            # Ex: primary_links,
723
            #     backup_links,
724
            #     current_links_cache,
725
            #     primary_links_cache,
726
            #     backup_links_cache
727
            if "links" in attribute:
728
                data[attribute] = [
729
                    self._link_from_dict(link) for link in value
730
                ]
731
732
            # Ex: current_path,
733
            #     primary_path,
734
            #     backup_path
735
            if "path" in attribute and attribute != "dynamic_backup_path":
736
                data[attribute] = Path(
737
                    [self._link_from_dict(link) for link in value]
738
                )
739
740
        return data
741
742
    def _evc_from_dict(self, evc_dict):
743
        data = self._evc_dict_with_instances(evc_dict)
744
        return EVC(self.controller, **data)
745
746
    def _uni_from_dict(self, uni_dict):
747
        """Return a UNI object from python dict."""
748
        if uni_dict is None:
749
            return False
750
751
        interface_id = uni_dict.get("interface_id")
752
        interface = self.controller.get_interface_by_id(interface_id)
753
        if interface is None:
754
            result = (
755
                "Error creating UNI:"
756
                + f"Could not instantiate interface {interface_id}"
757
            )
758
            raise ValueError(result) from ValueError
759
760
        tag_dict = uni_dict.get("tag", None)
761
        if tag_dict:
762
            tag = TAG.from_dict(tag_dict)
763
        else:
764
            tag = None
765
        uni = UNI(interface, tag)
766
767
        return uni
768
769
    def _link_from_dict(self, link_dict):
770
        """Return a Link object from python dict."""
771
        id_a = link_dict.get("endpoint_a").get("id")
772
        id_b = link_dict.get("endpoint_b").get("id")
773
774
        endpoint_a = self.controller.get_interface_by_id(id_a)
775
        endpoint_b = self.controller.get_interface_by_id(id_b)
776
777
        link = Link(endpoint_a, endpoint_b)
778
        if "metadata" in link_dict:
779
            link.extend_metadata(link_dict.get("metadata"))
780
781
        s_vlan = link.get_metadata("s_vlan")
782
        if s_vlan:
783
            tag = TAG.from_dict(s_vlan)
784
            if tag is False:
785
                error_msg = f"Could not instantiate tag from dict {s_vlan}"
786
                raise ValueError(error_msg)
787
            link.update_metadata("s_vlan", tag)
788
        return link
789
790
    def _find_evc_by_schedule_id(self, schedule_id):
791
        """
792
        Find an EVC and CircuitSchedule based on schedule_id.
793
794
        :param schedule_id: Schedule ID
795
        :return: EVC and Schedule
796
        """
797
        circuits = self._get_circuits_buffer()
798
        found_schedule = None
799
        evc = None
800
801
        # pylint: disable=unused-variable
802
        for c_id, circuit in circuits.items():
803
            for schedule in circuit.circuit_scheduler:
804
                if schedule.id == schedule_id:
805
                    found_schedule = schedule
806
                    evc = circuit
807
                    break
808
            if found_schedule:
809
                break
810
        return evc, found_schedule
811
812
    def _get_circuits_buffer(self):
813
        """
814
        Return the circuit buffer.
815
816
        If the buffer is empty, try to load data from storehouse.
817
        """
818
        if not self.circuits:
819
            # Load storehouse circuits to buffer
820
            circuits = self.storehouse.get_data()
821
            for c_id, circuit in circuits.items():
822
                evc = self._evc_from_dict(circuit)
823
                self.circuits[c_id] = evc
824
        return self.circuits
825
826
    @staticmethod
827
    def _json_from_request(caller):
828
        """Return a json from request.
829
830
        If it was not possible to get a json from the request, log, for debug,
831
        who was the caller and the error that ocurred, and raise an
832
        Exception.
833
        """
834
        try:
835
            json_data = request.get_json()
836
        except ValueError as exception:
837
            log.error(exception)
838
            log.debug(f"{caller} result {exception} 400")
839
            raise BadRequest(str(exception)) from BadRequest
840
        except BadRequest:
841
            result = "The request is not a valid JSON."
842
            log.debug(f"{caller} result {result} 400")
843
            raise BadRequest(result) from BadRequest
844
        if json_data is None:
845
            result = "Content-Type must be application/json"
846
            log.debug(f"{caller} result {result} 415")
847
            raise UnsupportedMediaType(result)
848
        return json_data
849