Test Failed
Pull Request — master (#70)
by macartur
05:46 queued 03:30
created

build.models.EVCBase.as_dict()   B

Complexity

Conditions 5

Size

Total Lines 44
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 34
nop 1
dl 0
loc 44
rs 8.5973
c 0
b 0
f 0
ccs 0
cts 25
cp 0
crap 30
1
"""Classes used in the main application."""
2
from datetime import datetime
3
from uuid import uuid4
4
5
import requests
6
7
from kytos.core import log
8
from kytos.core.common import EntityStatus, GenericEntity
9
from kytos.core.helpers import get_time, now
10
from kytos.core.interface import UNI
11
from napps.kytos.mef_eline import settings
12
13
14
class Path(list, GenericEntity):
15
    """Class to represent a Path."""
16
17
    def __init__(self, *args, **kwargs):
18
        """Create a path instance using links."""
19
        super().__init__(*args, **kwargs)
20
        self.links_cache = set(self)
21
22
    def __eq__(self, other=None):
23
        """Compare paths."""
24
        if not other or not isinstance(other, Path):
25
            return False
26
        return super().__eq__(other)
27
28
    def is_affected_by_link(self, link=None):
29
        """Verify if the current path is affected by link."""
30
        if not link:
31
            return False
32
        return link in self.links_cache
33
34
    @property
35
    def status(self):
36
        """Check for the  status of a path.
37
38
        If any link in this path is down, the path is considered down.
39
        """
40
        if not self:
41
            return EntityStatus.DISABLED
42
43
        for link in self:
44
            if link.status is not EntityStatus.UP:
45
                return link.status
46
        return EntityStatus.UP
47
48
    def as_dict(self):
49
        """Return list comprehension of links as_dict."""
50
        return [link.as_dict() for link in self if link]
51
52
53
class EVCBase(GenericEntity):
54
    """"Class to represent a circuit."""
55
56
    unique_attributes = ['name', 'uni_a', 'uni_z']
57
58
    def __init__(self, **kwargs):
59
        """Create an EVC instance with the provided parameters.
60
61
        Args:
62
            id(str): EVC identifier. Whether it's None an ID will be genereted.
63
            name: represents an EVC name.(Required)
64
            uni_a (UNI): Endpoint A for User Network Interface.(Required)
65
            uni_z (UNI): Endpoint Z for User Network Interface.(Required)
66
            start_date(datetime|str): Date when the EVC was registred.
67
                                      Default is now().
68
            end_date(datetime|str): Final date that the EVC will be fineshed.
69
                                    Default is None.
70
            bandwidth(int): Bandwidth used by EVC instance. Default is 0.
71
            primary_links(list): Primary links used by evc. Default is []
72
            backup_links(list): Backups links used by evc. Default is []
73
            current_path(list): Circuit being used at the moment if this is an
74
                                active circuit. Default is [].
75
            primary_path(list): primary circuit offered to user IF one or more
76
                                links were provided. Default is [].
77
            backup_path(list): backup circuit offered to the user IF one or
78
                               more links were provided. Default is [].
79
            dynamic_backup_path(bool): Enable computer backup path dynamically.
80
                                       Dafault is False.
81
            creation_time(datetime|str): datetime when the circuit should be
82
                                         activated. default is now().
83
            enabled(Boolean): attribute to indicate the operational state.
84
                              default is False.
85
            active(Boolean): attribute to Administrative state;
86
                             default is False.
87
            owner(str): The EVC owner. Default is None.
88
            priority(int): Service level provided in the request. Default is 0.
89
90
        Raises:
91
            ValueError: raised when object attributes are invalid.
92
93
        """
94
        self._validate(**kwargs)
95
        super().__init__()
96
97
        # required attributes
98
        self._id = kwargs.get('id', uuid4().hex)
99
        self.uni_a = kwargs.get('uni_a')
100
        self.uni_z = kwargs.get('uni_z')
101
        self.name = kwargs.get('name')
102
103
        # optional attributes
104
        self.start_date = get_time(kwargs.get('start_date')) or now()
105
        self.end_date = get_time(kwargs.get('end_date')) or None
106
107
        self.bandwidth = kwargs.get('bandwidth', 0)
108
        self.primary_links = Path(kwargs.get('primary_links', []))
109
        self.backup_links = Path(kwargs.get('backup_links', []))
110
        self.current_path = Path(kwargs.get('current_path', []))
111
        self.primary_path = Path(kwargs.get('primary_path', []))
112
        self.backup_path = Path(kwargs.get('backup_path', []))
113
        self.dynamic_backup_path = kwargs.get('dynamic_backup_path', False)
114
        self.creation_time = get_time(kwargs.get('creation_time')) or now()
115
        self.owner = kwargs.get('owner', None)
116
        self.priority = kwargs.get('priority', 0)
117
        self.circuit_scheduler = kwargs.get('circuit_scheduler', [])
118
119
        if kwargs.get('active', False):
120
            self.activate()
121
        else:
122
            self.deactivate()
123
124
        if kwargs.get('enabled', False):
125
            self.enable()
126
        else:
127
            self.disable()
128
129
        # datetime of user request for a EVC (or datetime when object was
130
        # created)
131
        self.request_time = kwargs.get('request_time', now())
132
        # dict with the user original request (input)
133
        self._requested = kwargs
134
135
    def update(self, **kwargs):
136
        """Update evc attributes.
137
138
        This method will raises an error trying to change the following
139
        attributes: [name, uni_a and uni_z]
140
141
        Raises:
142
            ValueError: message with error detail.
143
144
        """
145
        for attribute, value in kwargs.items():
146
            if attribute in self.unique_attributes:
147
                raise ValueError(f'{attribute} can\'t be be updated.')
148
            if hasattr(self, attribute):
149
                setattr(self, attribute, value)
150
            else:
151
                raise ValueError(f'The attribute "{attribute}" is invalid.')
152
153
    def __repr__(self):
154
        """Repr method."""
155
        return f"EVC({self._id}, {self.name})"
156
157
    def _validate(self, **kwargs):
158
        """Do Basic validations.
159
160
        Verify required attributes: name, uni_a, uni_z
161
        Verify if the attributes uni_a and uni_z are valid.
162
163
        Raises:
164
            ValueError: message with error detail.
165
166
        """
167
        for attribute in self.unique_attributes:
168
169
            if attribute not in kwargs:
170
                raise ValueError(f'{attribute} is required.')
171
172
            if 'uni' in attribute:
173
                uni = kwargs.get(attribute)
174
                if not isinstance(uni, UNI):
175
                    raise ValueError(f'{attribute} is an invalid UNI.')
176
177
                elif not uni.is_valid():
178
                    tag = uni.user_tag.value
179
                    message = f'VLAN tag {tag} is not available in {attribute}'
180
                    raise ValueError(message)
181
182
    def __eq__(self, other):
183
        """Override the default implementation."""
184
        if not isinstance(other, EVC):
185
            return False
186
187
        attrs_to_compare = ['name', 'uni_a', 'uni_z', 'owner', 'bandwidth']
188
        for attribute in attrs_to_compare:
189
            if getattr(other, attribute) != getattr(self, attribute):
190
                return False
191
        return True
192
193
    def as_dict(self):
194
        """Return a dictionary representing an EVC object."""
195
        evc_dict = {"id": self.id, "name": self.name,
196
                    "uni_a": self.uni_a.as_dict(),
197
                    "uni_z": self.uni_z.as_dict()}
198
199
        time_fmt = "%Y-%m-%dT%H:%M:%S"
200
201
        evc_dict["start_date"] = self.start_date
202
        if isinstance(self.start_date, datetime):
203
            evc_dict["start_date"] = self.start_date.strftime(time_fmt)
204
205
        evc_dict["end_date"] = self.end_date
206
        if isinstance(self.end_date, datetime):
207
            evc_dict["end_date"] = self.end_date.strftime(time_fmt)
208
209
        evc_dict['bandwidth'] = self.bandwidth
210
        evc_dict['primary_links'] = self.primary_links.as_dict()
211
        evc_dict['backup_links'] = self.backup_links.as_dict()
212
        evc_dict['current_path'] = self.current_path.as_dict()
213
        evc_dict['primary_path'] = self.primary_path.as_dict()
214
        evc_dict['backup_path'] = self.backup_path.as_dict()
215
        evc_dict['dynamic_backup_path'] = self.dynamic_backup_path
216
217
        if self._requested:
218
            request_dict = self._requested.copy()
219
            request_dict['uni_a'] = request_dict['uni_a'].as_dict()
220
            request_dict['uni_z'] = request_dict['uni_z'].as_dict()
221
            evc_dict['_requested'] = request_dict
222
223
        evc_dict["request_time"] = self.request_time
224
        if isinstance(self.request_time, datetime):
225
            evc_dict["request_time"] = self.request_time.strftime(time_fmt)
226
227
        time = self.creation_time.strftime(time_fmt)
228
        evc_dict['creation_time'] = time
229
230
        evc_dict['owner'] = self.owner
231
        evc_dict['circuit_scheduler'] = self.circuit_scheduler
232
        evc_dict['active'] = self.is_active()
233
        evc_dict['enabled'] = self.is_enabled()
234
        evc_dict['priority'] = self.priority
235
236
        return evc_dict
237
238
    @property
239
    def id(self):  # pylint: disable=invalid-name
240
        """Return this EVC's ID."""
241
        return self._id
242
243
244
class EVCDeploy(EVCBase):
245
    """Class to handle the deploy procedures."""
246
247
    def create(self):
248
        """Create a EVC."""
249
        pass
250
251
    def discover_new_path(self, path=None):
252
        """Discover a new path to satisfy this circuit and deploy."""
253
        return []
254
255
    def change_path(self, path):
256
        """Change EVC path."""
257
        pass
258
259
    def reprovision(self):
260
        """Force the EVC (re-)provisioning."""
261
        pass
262
263
    def remove(self):
264
        """Remove EVC path."""
265
        pass
266
267
    @staticmethod
268
    def choose_vlans(path=None):
269
        """Choose the VLANs to be used for the circuit."""
270
        for link in path:
271
            tag = link.get_next_available_tag()
272
            link.use_tag(tag)
273
            link.add_metadata('s_vlan', tag)
274
275
    @staticmethod
276
    def links_zipped(path=None):
277
        """Return an iterator which yields pairs of links in order."""
278
        if not path:
279
            return []
280
        return zip(path[:-1], path[1:])
281
282
    def should_deploy(self, path=None):
283
        """Verify if the circuit should be deployed."""
284
        if not path:
285
            log.debug("Path is empty.")
286
            return False
287
288
        if not self.is_enabled():
289
            log.debug(f'{self} is disabled.')
290
            return False
291
292
        if not self.is_active():
293
            log.debug(f'{self} will be deployed.')
294
            return True
295
296
        return False
297
298
    def deploy(self, path=None):
299
        """Install the flows for this circuit.
300
301
        Procedures to deploy:
302
303
        1. Decide if will deploy "path" or discover a new path
304
        2. Choose vlan
305
        3. Install NNI flows
306
        4. Install UNI flows
307
        5. Activate
308
        6. Update current_path
309
        7. Update links caches(primary, current, backup)
310
311
        """
312
        if not self.should_deploy(path):
313
            return False
314
315
        if path is None:
316
            path = self.discover_new_path(path)
317
318
        if not path:
319
            return False
320
321
        self.choose_vlans(path)
322
        self.install_nni_flows(path)
323
        self.install_uni_flows(path)
324
        self.activate()
325
        log.info(f"{self} was deployed.")
326
        return True
327
328
    def install_nni_flows(self, path=None):
329
        """Install NNI flows."""
330
        for incoming, outcoming in self.links_zipped(path):
331
            in_vlan = incoming.get_metadata('s_vlan').value
332
            out_vlan = outcoming.get_metadata('s_vlan').value
333
334
            flows = []
335
            # Flow for one direction
336
            flows.append(self.prepare_nni_flow(incoming.endpoint_b,
337
                                               outcoming.endpoint_a,
338
                                               in_vlan, out_vlan))
339
340
            # Flow for the other direction
341
            flows.append(self.prepare_nni_flow(outcoming.endpoint_a,
342
                                               incoming.endpoint_b,
343
                                               out_vlan, in_vlan))
344
            self.send_flow_mods(incoming.endpoint_b.switch, flows)
345
346
    def install_uni_flows(self, path=None):
347
        """Install UNI flows."""
348
        if not path:
349
            log.info('install uni flows without path.')
350
            return
351
352
        # Determine VLANs
353
        in_vlan_a = self.uni_a.user_tag.value if self.uni_a.user_tag else None
354
        out_vlan_a = path[0].get_metadata('s_vlan').value
355
356
        in_vlan_z = self.uni_z.user_tag.value if self.uni_z.user_tag else None
357
        out_vlan_z = path[-1].get_metadata('s_vlan').value
358
359
        # Flows for the first UNI
360
        flows_a = []
361
362
        # Flow for one direction, pushing the service tag
363
        push_flow = self.prepare_push_flow(self.uni_a.interface,
364
                                           path[0].endpoint_a,
365
                                           in_vlan_a, out_vlan_a, in_vlan_z)
366
        flows_a.append(push_flow)
367
368
        # Flow for the other direction, popping the service tag
369
        pop_flow = self.prepare_pop_flow(path[0].endpoint_a,
370
                                         self.uni_a.interface, out_vlan_a)
371
        flows_a.append(pop_flow)
372
373
        self.send_flow_mods(self.uni_a.interface.switch, flows_a)
374
375
        # Flows for the second UNI
376
        flows_z = []
377
378
        # Flow for one direction, pushing the service tag
379
        push_flow = self.prepare_push_flow(self.uni_z.interface,
380
                                           path[-1].endpoint_b,
381
                                           in_vlan_z, out_vlan_z, in_vlan_a)
382
        flows_z.append(push_flow)
383
384
        # Flow for the other direction, popping the service tag
385
        pop_flow = self.prepare_pop_flow(path[-1].endpoint_b,
386
                                         self.uni_z.interface, out_vlan_z)
387
        flows_z.append(pop_flow)
388
389
        self.send_flow_mods(self.uni_z.interface.switch, flows_z)
390
391
    @staticmethod
392
    def send_flow_mods(switch, flow_mods):
393
        """Send a flow_mod list to a specific switch."""
394
        endpoint = "%s/flows/%s" % (settings.MANAGER_URL, switch.id)
395
396
        data = {"flows": flow_mods}
397
        requests.post(endpoint, json=data)
398
399
    @staticmethod
400
    def prepare_flow_mod(in_interface, out_interface):
401
        """Prepare a common flow mod."""
402
        default_action = {"action_type": "output",
403
                          "port": out_interface.port_number}
404
405
        flow_mod = {"match": {"in_port": in_interface.port_number},
406
                    "actions": [default_action]}
407
408
        return flow_mod
409
410
    def prepare_nni_flow(self, in_interface, out_interface, in_vlan, out_vlan):
411
        """Create NNI flows."""
412
        flow_mod = self.prepare_flow_mod(in_interface, out_interface)
413
        flow_mod['match']['dl_vlan'] = in_vlan
414
415
        new_action = {"action_type": "set_vlan",
416
                      "vlan_id": out_vlan}
417
        flow_mod["actions"].insert(0, new_action)
418
419
        return flow_mod
420
421
    def prepare_push_flow(self, *args):
422
        """Prepare push flow.
423
424
        Arguments:
425
            in_interface(str): Interface input.
426
            out_interface(str): Interface output.
427
            in_vlan(str): Vlan input.
428
            out_vlan(str): Vlan output.
429
            new_in_vlan(str): Interface input.
430
431
        Return:
432
            dict: An python dictionary representing a FlowMod
433
434
        """
435
        # assign all arguments
436
        in_interface, out_interface, in_vlan, out_vlan, new_in_vlan = args
437
438
        flow_mod = self.prepare_flow_mod(in_interface, out_interface)
439
        flow_mod['match']['dl_vlan'] = in_vlan
440
441
        new_action = {"action_type": "set_vlan",
442
                      "vlan_id": out_vlan}
443
        flow_mod["actions"].insert(0, new_action)
444
445
        new_action = {"action_type": "push_vlan",
446
                      "tag_type": "s"}
447
        flow_mod["actions"].insert(0, new_action)
448
449
        new_action = {"action_type": "set_vlan",
450
                      "vlan_id": new_in_vlan}
451
        flow_mod["actions"].insert(0, new_action)
452
453
        return flow_mod
454
455
    def prepare_pop_flow(self, in_interface, out_interface, in_vlan):
456
        """Prepare pop flow."""
457
        flow_mod = self.prepare_flow_mod(in_interface, out_interface)
458
        flow_mod['match']['dl_vlan'] = in_vlan
459
        new_action = {"action_type": "pop_vlan"}
460
        flow_mod["actions"].insert(0, new_action)
461
        return flow_mod
462
463
464
class LinkProtection(EVCDeploy):
465
    """Class to handle link protection."""
466
467
    def is_affected_by_link(self, link=None):
468
        """Verify if the current path is affected by link down event."""
469
        return self.current_path.is_affected_by_link(link)
470
471
    def is_using_primary_path(self):
472
        """Verify if the current deployed path is self.primary_path."""
473
        return self.current_path == self.primary_path
474
475
    def is_using_backup_path(self):
476
        """Verify if the current deployed path is self.backup_path."""
477
        return self.current_path == self.backup_path
478
479
    def is_using_dynamic_path(self):
480
        """Verify if the current deployed path is dynamic."""
481
        if not self.is_using_primary_path() and \
482
           not self.is_using_backup_path() and \
483
           self.current_path.status is EntityStatus.UP:
484
            return True
485
        return False
486
487
    def deploy_to(self, path_name=None, path=None):
488
        """Create a deploy to path."""
489
        if self.current_path == path:
490
            log.debug(f'{path_name} is equal to current_path.')
491
            return True
492
493
        if path.status is EntityStatus.UP:
494
            return self.deploy(path)
495
496
        return False
497
498
    def handle_link_up(self, link):
499
        """Handle circuit when link down.
500
501
        Args:
502
            link(Link): Link affected by link.down event.
503
504
        """
505
        if self.is_using_primary_path():
506
            return True
507
508
        success = False
509
        if self.primary_path.is_affected_by_link(link):
510
            success = self.deploy_to('primary_path', self.primary_path)
511
512
        if success:
513
            return True
514
515
        # We tried to deploy(primary_path) without success.
516
        # And in this case is up by some how. Nothing to do.
517
        if self.is_using_backup_path() or self.is_using_dynamic_path():
518
            return True
519
520
        # In this case, probably the circuit is not being used and
521
        # we can move to backup
522
        if self.backup_path.is_affected_by_link(link):
523
            success = self.deploy_to('backup_path', self.backup_path)
524
525
        if success:
526
            return True
527
528
        # In this case, the circuit is not being used and we should
529
        # try a dynamic path
530
        if self.dynamic_backup_path:
531
            return self.deploy()
532
533
        return True
534
535
    def handle_link_down(self):
536
        """Handle circuit when link down.
537
538
        Returns:
539
            bool: True if the re-deploy was successly otherwise False.
540
541
        """
542
        success = False
543
        if self.is_using_primary_path():
544
            success = self.deploy_to('backup_path', self.backup_path)
545
        elif self.is_using_backup_path():
546
            success = self.deploy_to('primary_path', self.primary_path)
547
548
        if not success and self.dynamic_backup_path:
549
            success = self.deploy()
550
551
        if success:
552
            log.debug(f"{self} deployed after link down.")
553
        else:
554
            log.debug(f'Failed to re-deploy {self} after link down.')
555
556
        return success
557
558
559
class EVC(LinkProtection):
560
    """Class that represents a E-Line Virtual Connection."""
561
562
    pass
563