Passed
Push — master ( 33580e...df4901 )
by Humberto
02:48 queued 12s
created

build.main   F

Complexity

Total Complexity 106

Size/Duplication

Total Lines 676
Duplicated Lines 0 %

Test Coverage

Coverage 64.47%

Importance

Changes 0
Metric Value
wmc 106
eloc 405
dl 0
loc 676
ccs 245
cts 380
cp 0.6447
rs 2
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.setup() 0 23 1
A Main.shutdown() 0 2 1
A Main.execute() 0 2 1
A Main.list_circuits() 0 17 3
A Main.get_circuit() 0 15 2
B Main.create_circuit() 0 72 5
C Main._evc_dict_with_instances() 0 40 9
C Main.load_evcs() 0 28 9
B Main.update_schedule() 0 65 6
A Main._evc_from_dict() 0 3 1
A Main.load_circuits_by_interface() 0 13 4
A Main.delete_schedule() 0 37 3
A Main.add_to_dict_of_sets() 0 5 2
B Main.list_schedules() 0 29 5
A Main._is_duplicated_evc() 0 14 4
C Main.create_schedule() 0 94 10
A Main.delete_circuit() 0 31 4
A Main.handle_link_up() 0 7 4
A Main.handle_link_down() 0 8 3
D Main.update() 0 48 12
A Main._find_evc_by_schedule_id() 0 21 5
A Main._link_from_dict() 0 20 4
A Main._uni_from_dict() 0 19 5
A Main._get_circuits_buffer() 0 13 3

How to fix   Complexity   

Complexity

Complex classes like build.main often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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