Passed
Pull Request — master (#106)
by Vinicius
02:46
created

build.main.Main._is_duplicated_evc()   A

Complexity

Conditions 4

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

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