Passed
Pull Request — master (#165)
by Antonio
04:32
created

build.main   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 655
Duplicated Lines 0 %

Test Coverage

Coverage 64.13%

Importance

Changes 0
Metric Value
eloc 390
dl 0
loc 655
ccs 236
cts 368
cp 0.6413
rs 2
c 0
b 0
f 0
wmc 98

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 14 2
A Main.get_circuit() 0 15 2
B Main.create_circuit() 0 72 5
C Main._evc_dict_with_instances() 0 40 9
A Main._find_evc_by_schedule_id() 0 21 5
A Main._evc_from_dict() 0 3 1
A Main._link_from_dict() 0 20 4
A Main._uni_from_dict() 0 18 4
A Main._get_circuits_buffer() 0 13 3
C Main.load_evcs() 0 28 9
B Main.update_schedule() 0 65 6
A Main.handle_link_up() 0 7 4
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
A Main.handle_link_down() 0 8 3
B Main.list_schedules() 0 29 5
A Main._is_duplicated_evc() 0 14 4
B Main.update() 0 31 6
C Main.create_schedule() 0 94 10
A Main.delete_circuit() 0 31 4

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