Test Failed
Push — master ( 4aff4f...dba1ae )
by Antonio
05:19 queued 12s
created

build.main.Main._json_from_request()   A

Complexity

Conditions 4

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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