Test Failed
Push — master ( dba1ae...723a1d )
by Antonio
04:50 queued 13s
created

build.main.Main.add_metadata()   B

Complexity

Conditions 6

Size

Total Lines 28
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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