Passed
Push — master ( 0ead5b...e254e3 )
by Humberto
05:15 queued 58s
created

build.main   F

Complexity

Total Complexity 99

Size/Duplication

Total Lines 661
Duplicated Lines 0 %

Test Coverage

Coverage 58.15%

Importance

Changes 0
Metric Value
wmc 99
eloc 392
dl 0
loc 661
ccs 214
cts 368
cp 0.5815
rs 2
c 0
b 0
f 0

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