Test Failed
Pull Request — master (#195)
by Antonio
02:29
created

build.main.Main.update()   C

Complexity

Conditions 10

Size

Total Lines 53
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 10.4632

Importance

Changes 0
Metric Value
cc 10
eloc 43
nop 2
dl 0
loc 53
rs 5.9999
c 0
b 0
f 0
ccs 30
cts 36
cp 0.8333
crap 10.4632

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

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