Test Failed
Pull Request — master (#392)
by
unknown
01:54
created

GenericStruct   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Test Coverage

Coverage 72.72%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
c 1
b 0
f 0
dl 0
loc 230
ccs 56
cts 77
cp 0.7272
rs 8.3999

16 Methods

Rating   Name   Duplication   Size   Complexity  
A _get_size() 0 3 2
B _validate_attributes_type() 0 11 6
A __eq__() 0 13 1
A is_valid() 0 15 1
A __init__() 0 4 2
B get_class_attributes() 0 26 3
A _unpack_attribute() 0 14 3
A _is_pyof_attribute() 0 11 1
A _pack() 0 11 3
A _get_attributes() 0 15 2
A pack() 0 14 1
A get_size() 0 17 1
A _attr_fits_into_class() 0 8 3
A _work_or_pass() 0 13 4
A unpack() 0 14 2
A _get_instance_attributes() 0 15 3
1
"""Base and fundamental classes used all over the library.
2
3
Besides classes, several constants are defined here. We designed
4
python-openflow in a manner to make it easy to create new messages and OpenFlow
5
structs. You can realize that when you see a message class definition.
6
7
A **struct** here is a group of basic attributes and/or struct attributes (i.e.
8
:class:`~pyof.v0x01.common.header.Header`). A **message** here is like a
9
struct, but all messages have a header attribute (i.e.
10
:class:`~pyof.v0x01.asynchronous.packet_in.PacketIn`).
11
12
13
The main classes of this module are :class:`GenericStruct`,
14
:class:`GenericMessage`, :class:`GenericBitMask` and :class:`GenericType`.
15
These classes are used in all parts of this library.
16
"""
17
18 1
# System imports
19 1
import importlib
20 1
import re
21 1
import struct
22 1
from collections import OrderedDict
23 1
from copy import deepcopy
24
from enum import Enum, IntEnum
25
26 1
# Local source tree imports
27
from pyof.foundation.exceptions import (
28
    BadValueException, PackException, UnpackException, ValidationError)
29
30
# Third-party imports
31
32
33 1
# This will determine the order on sphinx documentation.
34
__all__ = ('GenericStruct', 'GenericMessage', 'GenericType',
35
           'GenericUBIntType', 'GenericBitMask', 'MetaStruct', 'MetaBitMask')
36
37
# Classes
38
39 1
40
class GenericType:
41
    """Foundation class for all custom attributes.
42
43
    Base class for :class:`~.UBInt8`, :class:`~.Char`
44
    and others.
45
    """
46 1
47
    _fmt = None
48 1
49
    def __init__(self, value=None, enum_ref=None):
50
        """The constructor takes the optional parameters below.
51
52
        Args:
53
            value: The type's value.
54
            enum_ref (type): If :attr:`value` is from an Enum, specify
55
                its type.
56 1
        """
57 1
        self._value = value
58
        self.enum_ref = enum_ref
59 1
60
    def __repr__(self):
61
        return "{}({})".format(type(self).__name__, self._value)
62 1
63 1
    def __str__(self):
64
        return '{}'.format(str(self._value))
65 1
66 1
    def __eq__(self, other):
67 1
        if isinstance(other, self.__class__):
68 1
            return self.pack() == other.pack()
69 1
        elif hasattr(other, 'value'):
70 1
            return self.value == other.value
71
        return self.value == other
72 1
73 1
    def __ne__(self, other):
74
        return self._value != other
75 1
76
    def __gt__(self, other):
77
        return self._value > other
78 1
79
    def __ge__(self, other):
80
        return self._value >= other
81 1
82
    def __lt__(self, other):
83
        return self._value <= other
84 1
85
    def __le__(self, other):
86
        return self._value <= other
87 1
88 1
    def __add__(self, other):
89
        return self.value + other
90 1
91 1
    def __radd__(self, other):
92
        return self.value + other
93 1
94 1
    def __sub__(self, other):
95
        return self.value - other
96 1
97 1
    def __rsub__(self, other):
98
        return self.value - other
99 1
100 1
    def __or__(self, other):
101
        return self.value | other
102 1
103 1
    def __ror__(self, other):
104
        return self.value | other
105 1
106 1
    def __and__(self, other):
107
        return self.value & other
108 1
109 1
    def __rand__(self, other):
110
        return self.value & other
111 1
112 1
    def __xor__(self, other):
113
        return self.value ^ other
114 1
115 1
    def __rxor__(self, other):
116
        return self.value ^ other
117 1
118
    def __lshift__(self, other):
119
        return self.value << other
120
121
    def __rshift__(self, other):
122
        return self.value >> other
123 1
124 1
    @property
125 1
    def value(self):
126 1
        """Return this type's value.
127 1
128 1
        Returns: The value of an enum, bitmask, etc.
129
        """
130 1
        if self.isenum():
131
            if isinstance(self._value, self.enum_ref):
132 1
                return self._value.value
133
            return self._value
134
        elif self.is_bitmask():
135
            return self._value.bitmask
136
        else:
137
            return self._value
138
139
    def _get_new_instance(self, value):
140
        return type(self)(value)
141
142
    def _work_or_pass(self, value, work_func):
143
        if value is None:
144
            return getattr(self, work_func)()
145
        elif isinstance(value, type(self)):
146
            return getattr(value, work_func)()
147
        elif 'value' in dir(value):
148
            # if it is enum or bitmask gets only the 'int' value
149
            value = value.value
150
151
        try:
152
            new_item = self._get_new_instance(value)
153
            if hasattr(self, 'enum_ref'):
154
                new_item.enum_ref = self.enum_ref
155
        except Exception as e:  # noqa - there is no generic Initialization Exception...
156
            print(e.args)
157
            msg = "{} is not an instance of {}".format(value,
158
                                                       type(self).__name__)
159
            raise PackException(msg)
160 1
        return getattr(new_item, work_func)()
161 1
162
    def pack(self, value=None):
163 1
        r"""Pack the value as a binary representation.
164 1
165 1
        Considering an example with UBInt8 class, that inherits from
166
        GenericType:
167 1
168
        >>> from pyof.foundation.basic_types import UBInt8
169 1
        >>> objectA = UBInt8(1)
170 1
        >>> objectB = 5
171 1
        >>> objectA.pack()
172 1
        b'\x01'
173
        >>> objectA.pack(objectB)
174
        b'\x05'
175 1
176
        Args:
177 1
            value: If the value is None, then we will pack the value of the
178
                current instance. Otherwise, if value is an instance of the
179
                same type as the current instance, then we call the pack of the
180
                value object. Otherwise, we will use the current instance pack
181
                method on the passed value.
182
183
        Returns:
184
            bytes: The binary representation.
185
186
        Raises:
187
            :exc:`~.exceptions.BadValueException`: If the value does not
188
                fit the binary format.
189
        """
190 1
        return self._work_or_pass(value, '_pack')
191 1
192 1
    def _pack(self):
193 1
        try:
194
            return struct.pack(self._fmt, self.value)
195
        except struct.error:
196
            msg = '{} could not pack {} = {}.'.format(
197
                type(self).__name__,
198
                type(self.value).__name__,
199 1
                self.value)
200
            raise PackException(msg)
201
202
    def unpack(self, buff, offset=0):
203
        """Unpack *buff* into this object.
204
205 1
        This method will convert a binary data into a readable value according
206
        to the attribute format.
207 1
208
        Args:
209
            buff (bytes): Binary buffer.
210
            offset (int): Where to begin unpacking.
211
212
        Raises:
213
            :exc:`~.exceptions.UnpackException`: If unpack fails.
214
        """
215
        try:
216
            self._value = struct.unpack_from(self._fmt, buff, offset)[0]
217
            if self.enum_ref:
218
                self._value = self.enum_ref(self._value)
219
        except (struct.error, TypeError, ValueError) as e:
220
            msg = '{}; fmt = {}, buff = {}, offset = {}.'.format(e, self._fmt,
221 1
                                                                 buff, offset)
222
            raise UnpackException(msg)
223
224
    def _get_size(self):
225
        return struct.calcsize(self._fmt)
226
227 1
    def get_size(self, value=None):
228
        """Return the size in bytes of this type.
229 1
230
        Returns:
231
            int: Size in bytes.
232
        """
233
        return self._work_or_pass(value, '_get_size')
234
235 1
    def is_valid(self):
236
        """Check whether the value fits the binary format.
237
238 1
        Assert that :func:`pack` succeeds.
239
240
        Returns:
241
            bool: Whether the value is valid for this type.
242
        """
243
        try:
244
            self.pack()
245
            return True
246
        except BadValueException:
247
            return False
248
249 1
    def isenum(self):
250
        """Test whether it is an :class:`~enum.Enum`.
251 1
252
        Returns:
253 1
            bool: Whether it is an :class:`~enum.Enum`.
254
        """
255
        return self.enum_ref and issubclass(self.enum_ref, (Enum, IntEnum))
256
257
    def is_bitmask(self):
258
        """Test whether it is a :class:`GenericBitMask`.
259
260
        Returns:
261
            bool: Whether it is a :class:`GenericBitMask`.
262
        """
263
        return self._value and issubclass(type(self._value), GenericBitMask)
264
265
266
class GenericUBIntType(GenericType):
267
268
    _buff_size = 0
269
270
    def _pack(self):
271
        return self.value.to_bytes(self._buff_size, byteorder='big')
272
273
    def unpack(self, buff, offset=0):
274
        """Unpack *buff* into this object.
275 1
276
        This method will convert a binary data into a readable value according
277
        to the attribute format.
278
279 1
        Args:
280 1
            buff (bytes): Binary buffer.
281
            offset (int): Where to begin unpacking.
282 1
283
        Raises:
284
            :exc:`~.exceptions.UnpackException`: If unpack fails.
285
        """
286
        try:
287 1
            self._value = int.from_bytes(buff[offset:offset + self._buff_size],
288
                                         byteorder='big')
289 1
            if self.enum_ref:
290 1
                self._value = self.enum_ref(self._value)
291 1
        except (struct.error, TypeError, ValueError) as e:
292
            msg = '{}; fmt = {}, buff = {}, offset = {}.'.format(
293
                e, 'UBInt' + 8 * self._buff_size, buff, offset)
294
            raise UnpackException(msg)
295 1
296
    def _get_size(self):
297
        return self._buff_size
298
299 1
300
class MetaStruct(type):
301
    """MetaClass that dinamically handles openflow version of class attributes.
302
303 1
    See more about it at:
304 1
        https://github.com/kytos/python-openflow/wiki/Version-Inheritance
305 1
306
    You do not need to use this class. Inherit from :class:`GenericStruct`
307
    instead.
308
    """
309 1
310 1
    # pylint: disable=unused-argument
311 1
    @classmethod
312 1
    def __prepare__(mcs, name, bases, **kwargs):
313 1
        return OrderedDict()
314
315 1
    def __new__(mcs, name, bases, classdict, **kwargs):
316
        """Inherit attributes from parent class and update their versions.
317 1
318
        Here is the moment that the new class is going to be created. During
319
        this process, two things may be done.
320
321
        Firstly, we will look if the type of any parent classes is this
322
        MetaStruct. We will inherit from the first parent class that fits this
323 1
        requirement. If any is found, then we will get all attributes from this
324
        class and place them as class attributes on the class being created.
325 1
326 1
        Secondly, for each class attribute being inherited, we will make sure
327
        that the pyof version of this attribute is the same as the version of
328
        the current class being created. If it is not, then we will find out
329
        which is the class and module of that attribute, look for a version
330 1
        that matches the version of the current class and replace that
331 1
        attribute with the correct version.
332
333 1
        See this link for more information on why this is being done:
334
            - https://github.com/kytos/python-openflow/wiki/Version-Inheritance
335 1
        """
336
        #: Retrieving class attributes management markers
337
        removed_attributes = classdict.pop('_removed_attributes', [])
338
        # renamed_attributes = classdict.pop('_renamed_attributes', [])
339
        # reordered_attributes = classdict.pop('_reordered_attributes', {})
340
341
        curr_module = classdict.get('__module__')
342
        curr_version = MetaStruct.get_pyof_version(curr_module)
343
344
        inherited_attributes = None
345
346
        #: looking for (kytos) class attributes defined on the bases
347 1
        #: classes so we can copy them into the current class being created
348 1
        #: so we can "inherit" them as class attributes
349 1
        for base in bases:
350 1
            #: Check if we are inheriting from one of our classes.
351
            if isinstance(base, MetaStruct):
352 1
                inherited_attributes = OrderedDict()
353 1
                for attr_name, obj in base.get_class_attributes():
354
                    #: Get an updated version of this attribute,
355 1
                    #: considering the version of the current class being
356
                    #: created.
357
                    attr = MetaStruct.get_pyof_obj_new_version(attr_name, obj,
358
                                                               curr_version)
359
360
                    if attr_name == 'header':
361
                        attr = mcs._header_message_type_update(obj, attr)
362
363
                    inherited_attributes.update([attr])
364
                #: We are going to inherit just from the 'closest parent'
365
                break
366
367
        #: If we have inherited something, then first we will remove the
368
        #: attributes marked to be removed on the 'removed_attributes' and
369
        #: after that we will update the inherited 'classdict' with the
370
        #: attributes from the current classdict.
371
        if inherited_attributes is not None:
372
            #: removing attributes set to be removed
373
            for attr_name in removed_attributes:
374 1
                inherited_attributes.pop(attr_name, None)
375 1
376 1
            #: Updating the inherited attributes with those defined on the
377
            #: body of the class being created.
378
            inherited_attributes.update(classdict)
379
            classdict = inherited_attributes
380 1
381
        return super().__new__(mcs, name, bases, classdict, **kwargs)
382
383
    @staticmethod
384
    def _header_message_type_update(obj, attr):
385
        """Update the message type on the header.
386
387
        Set the message_type of the header according to the message_type of
388
        the parent class.
389
        """
390
        old_enum = obj.message_type
391
        new_header = attr[1]
392
        new_enum = new_header.__class__.message_type.enum_ref
393
        #: This 'if' will be removed on the future with an
394
        #: improved version of __init_subclass__ method of the
395
        #: GenericMessage class
396
        if old_enum:
397
            msg_type_name = old_enum.name
398
            new_type = new_enum[msg_type_name]
399
            new_header.message_type = new_type
400
        return (attr[0], new_header)
401
402
    @staticmethod
403
    def get_pyof_version(module_fullname):
404
        """Get the module pyof version based on the module fullname.
405
406
        Args:
407
            module_fullname (str): The fullname of the module
408
                (e.g.: pyof.v0x01.common.header)
409
410
        Returns:
411
            str: openflow version.
412
                 The openflow version, on the format 'v0x0?' if any. Or None
413
                 if there isn't a version on the fullname.
414
        """
415
        ver_module_re = re.compile(r'(pyof\.)(v0x\d+)(\..*)')
416
        matched = ver_module_re.match(module_fullname)
417
        if matched:
418
            version = matched.group(2)
419 1
            return version
420 1
        return None
421
422 1
    @staticmethod
423 1
    def replace_pyof_version(module_fullname, version):
424 1
        """Replace the OF Version of a module fullname.
425
426
        Get's a module name (eg. 'pyof.v0x01.common.header') and returns it on
427
        a new 'version' (eg. 'pyof.v0x02.common.header').
428
429 1
        Args:
430 1
            module_fullname (str): The fullname of the module
431
                                   (e.g.: pyof.v0x01.common.header)
432
            version (str): The version to be 'inserted' on the module fullname.
433
434
        Returns:
435
            str: module fullname
436
                 The new module fullname, with the replaced version,
437
                 on the format "pyof.v0x01.common.header". If the requested
438 1
                 version is the same as the one of the module_fullname or if
439
                 the module_fullname is not a 'OF version' specific module,
440
                 returns None.
441 1
        """
442
        module_version = MetaStruct.get_pyof_version(module_fullname)
443
        if not module_version or module_version == version:
444
            return None
445
446
        return module_fullname.replace(module_version, version)
447
448
    @staticmethod
449
    def get_pyof_obj_new_version(name, obj, new_version):
450
        r"""Return a class attribute on a different pyof version.
451
452 1
        This method receives the name of a class attribute, the class attribute
453
        itself (object) and an openflow version.
454 1
        The attribute will be evaluated and from it we will recover its class
455 1
        and the module where the class was defined.
456
        If the module is a "python-openflow version specific module" (starts
457 1
        with "pyof.v0"), then we will get it's version and if it is different
458
        from the 'new_version', then we will get the module on the
459
        'new_version', look for the 'obj' class on the new module and return
460
        an instance of the new version of the 'obj'.
461
462
        Example:
463
464
        >>> from pyof.foundation.base import MetaStruct as ms
465
        >>> from pyof.v0x01.common.header import Header
466 1
        >>> name = 'header'
467
        >>> obj = Header()
468 1
        >>> new_version = 'v0x04'
469
        >>> header, obj2 = ms.get_pyof_obj_new_version(name, obj, new_version)
470
        >>> header
471
        'header'
472
        >>> obj.version
473
        UBInt8(1)
474
        >>> obj2.version
475
        UBInt8(4)
476
477 1
        Args:
478
            name (str): the name of the class attribute being handled.
479
            obj (object): the class attribute itself
480
            new_version (string): the pyof version in which you want the object
481
                'obj'.
482
483
        Return:
484
            (str, obj): Tuple with class name and object instance.
485
                A tuple in which the first item is the name of the class
486
                attribute (the same that was passed), and the second item is a
487
                instance of the passed class attribute. If the class attribute
488 1
                is not a pyof versioned attribute, then the same passed object
489
                is returned without any changes. Also, if the obj is a pyof
490 1
                versioned attribute, but it is already on the right version
491
                (same as new_version), then the passed obj is return.
492
        """
493
        if new_version is None:
494
            return (name, obj)
495
496
        cls = obj.__class__
497
        cls_name = cls.__name__
498
        cls_mod = cls.__module__
499
500
        #: If the module name does not starts with pyof.v0 then it is not a
501
        #: 'pyof versioned' module (OpenFlow specification defined), so we do
502 1
        #: not have anything to do with it.
503
        new_mod = MetaStruct.replace_pyof_version(cls_mod, new_version)
504
        if new_mod is not None:
505
            # Loads the module
506
            new_mod = importlib.import_module(new_mod)
507
            #: Get the class from the loaded module
508
            new_cls = getattr(new_mod, cls_name)
509
            #: return the tuple with the attribute name and the instance
510
            return (name, new_cls())
511
512
        return (name, obj)
513
514
515
class GenericStruct(object, metaclass=MetaStruct):
516
    """Class inherited by all OpenFlow structs.
517
518
    If you need to insert a method that will be used by all structs, this is
519
    the place to code it.
520
521
    .. note:: A struct on this library's context is like a struct in C. It
522
              has a list of attributes and theses attributes can be structs,
523 1
              too.
524
    """
525
526 1
    def __init__(self):
527 1
        """Contructor takes no argument and stores attributes' deep copies."""
528
        for name, value in self.get_class_attributes():
529 1
            setattr(self, name, deepcopy(value))
530
531
    def __eq__(self, other):
532
        """Check whether two structures have the same structure and values.
533
534
        Compare the binary representation of structs to decide whether they
535
        are equal or not.
536
537
        Args:
538
            other (GenericStruct): The struct to be compared with.
539
540
        Returns:
541 1
            bool: Returns the result of comparation.
542 1
        """
543 1
        return self.pack() == other.pack()
544
545 1
    @staticmethod
546
    def _attr_fits_into_class(attr, cls):
547
        if not isinstance(attr, cls):
548
            try:
549
                struct.pack(cls._fmt, attr)  # pylint: disable=protected-access
550
            except struct.error:
551
                return False
552
        return True
553
554
    @staticmethod
555
    def _is_pyof_attribute(obj):
556
        """Return True if the object is a kytos attribute.
557 1
558
        To be a kytos attribute the item must be an instance of either
559
        GenericType or GenericStruct.
560
561 1
        Returns:
562 1
            bool: Returns TRUE if the obj is a kytos attribute, otherwise False
563 1
        """
564 1
        return isinstance(obj, (GenericType, GenericStruct))
565 1
566
    def _validate_attributes_type(self):
567 1
        """Validate the type of each attribute."""
568 1
        for _attr, _class in self._get_attributes():
569 1
            if isinstance(_attr, _class):
570
                return True
571
            elif issubclass(_class, GenericType):
572
                if GenericStruct._attr_fits_into_class(_attr, _class):
573
                    return True
574 1
            elif not isinstance(_attr, _class):
575
                return False
576 1
        return True
577
578
    @classmethod
579
    def get_class_attributes(cls):
580
        """Return a generator for class attributes' names and value.
581
582
        This method strict relies on the PEP 520 (Preserving Class Attribute
583
        Definition Order), implemented on Python 3.6. So, if this behaviour
584
        changes this whole lib can loose its functionality (since the
585
        attributes order are a strong requirement.) For the same reason, this
586
        lib will not work on python versions earlier than 3.6.
587
588
        .. code-block:: python3
589
590
            for name, value in self.get_class_attributes():
591
                print("attribute name: {}".format(name))
592 1
                print("attribute type: {}".format(value))
593 1
594
        returns:
595 1
            generator: tuples with attribute name and value.
596 1
        """
597
        #: see this method docstring for a important notice about the use of
598
        #: cls.__dict__
599
        for name, value in cls.__dict__.items():
600
            # gets only our (kytos) attributes. this ignores methods, dunder
601
            # methods and attributes, and common python type attributes.
602 1
            if GenericStruct._is_pyof_attribute(value):
603
                yield (name, value)
604
605
    def _get_instance_attributes(self):
606
        """Return a generator for instance attributes' name and value.
607
608
        .. code-block:: python3
609
610
            for _name, _value in self._get_instance_attributes():
611
                print("attribute name: {}".format(_name))
612
                print("attribute value: {}".format(_value))
613
614
        returns:
615 1
            generator: tuples with attribute name and value.
616 1
        """
617
        for name, value in self.__dict__.items():
618
            if name in map((lambda x: x[0]), self.get_class_attributes()):
619
                yield (name, value)
620
621 1
    def _get_attributes(self):
622
        """Return a generator for instance and class attribute.
623 1
624 1
        .. code-block:: python3
625 1
626 1
            for instance_attribute, class_attribute in self._get_attributes():
627 1
                print("Instance Attribute: {}".format(instance_attribute))
628
                print("Class Attribute: {}".format(class_attribute))
629
630
        Returns:
631
            generator: Tuples with instance attribute and class attribute
632
        """
633 1
        return map((lambda i, c: (i[1], c[1])),
634
                   self._get_instance_attributes(),
635
                   self.get_class_attributes())
636
637
    def _unpack_attribute(self, name, obj, buff, begin):
638
        attribute = deepcopy(obj)
639
        setattr(self, name, attribute)
640
        if not buff:
641
            size = 0
642
        else:
643 1
            try:
644 1
                attribute.unpack(buff, begin)
645 1
                size = attribute.get_size()
646 1
            except UnpackException as e:
647
                child_cls = type(self).__name__
648 1
                msg = '{}.{}; {}'.format(child_cls, name, e)
649
                raise UnpackException(msg)
650
        return size
651
652
    def _work_or_pass(self, value, work_func):
653
        if value is None:
654
            return getattr(self, work_func)()
655
        elif isinstance(value, type(self)):
656
            return getattr(value, work_func)()
657
        else:
658
            try:
659 1
                new_item = type(self)(value)
660
            except:  # noqa - there is no generic Initialization Exception...
661
                msg = "{} is not an instance of {}".format(value,
662
                                                           type(self).__name__)
663
                raise PackException(msg)
664 1
            return getattr(new_item, work_func)()
665
666
    def _get_size(self):
667
        return sum(cls_val.get_size(obj_val)
668
                   for obj_val, cls_val in self._get_attributes())
669
670
    def get_size(self, value=None):
671
        """Calculate the total struct size in bytes.
672
673 1
        For each struct attribute, sum the result of each one's ``get_size()``
674
        method.
675 1
676
        Args:
677 1
            value: In structs, the user can assign other value instead of a
678 1
                class' instance.
679 1
680
        Returns:
681 1
            int: Total number of bytes used by the struct.
682 1
683
        Raises:
684
            Exception: If the struct is not valid.
685
        """
686 1
        return self._work_or_pass(value, '_get_size')
687
688 1
    def pack(self, value=None):
689
        """Pack the struct in a binary representation.
690
691 1
        Iterate over the class attributes, according to the
692
        order of definition, and then convert each attribute to its byte
693
        representation using its own ``pack`` method.
694
695
        Returns:
696
            bytes: Binary representation of the struct object.
697
698
        Raises:
699
            :exc:`~.exceptions.ValidationError`: If validation fails.
700
        """
701
        return self._work_or_pass(value, '_pack')
702 1
703
    def _pack(self):
704
        if not self.is_valid():
705
            error_msg = "Error on validation prior to pack() on class "
706 1
            error_msg += "{}.".format(type(self).__name__)
707
            raise ValidationError(error_msg)
708
        else:
709
            message = b''
710
            # pylint: disable=no-member
711
            for instance_attr, class_attr in self._get_attributes():
712
                message += class_attr.pack(instance_attr)
713
            return message
714
715
    def unpack(self, buff, offset=0):
716
        """Unpack a binary struct into this object's attributes.
717
718
        Update this object attributes based on the unpacked values of *buff*.
719
        It is an inplace method and it receives the binary data of the struct.
720
721
        Args:
722
            buff (bytes): Binary data package to be unpacked.
723
            offset (int): Where to begin unpacking.
724
        """
725 1
        begin = offset
726 1
        for name, value in self.get_class_attributes():
727 1
            size = self._unpack_attribute(name, value, buff, begin)
728
            begin += size
729
730
    def is_valid(self):
731
        """Check whether all struct attributes in are valid.
732
733
        This method will check whether all struct attributes have a proper
734
        value according to the OpenFlow specification. For instance, if you
735 1
        have a struct with an attribute of type
736
        :class:`~pyof.foundation.basic_types.UBInt8` and you assign a string
737
        value to it, this method will return False.
738
739
        Returns:
740
            bool: Whether the struct is valid.
741
        """
742
        return True
743
        # pylint: disable=unreachable
744
        return self._validate_attributes_type()
745
746
747 1
class GenericMessage(GenericStruct):
748 1
    """Base class that is the foundation for all OpenFlow messages.
749 1
750 1
    To add a method that will be used by all messages, write it here.
751 1
752
    .. note:: A Message on this library context is like a Struct but has a
753 1
              also a :attr:`header` attribute.
754
    """
755
756
    header = None
757
758
    def __init__(self, xid=None):
759 1
        """Initialize header's xid."""
760
        super().__init__()
761
        if xid is not None:
762 1
            self.header.xid = xid
763
764
    def __init_subclass__(cls, **kwargs):
765
        if cls.header is None or cls.header.__class__.__name__ != 'Header':
766
            msg = "The header attribute must be implemented on the class "
767
            msg += cls.__name__ + "."
768
            raise NotImplementedError(msg)
769
        super().__init_subclass__(**kwargs)
770
771
    def _validate_message_length(self):
772
        return self.header.length == self.get_size()
773
774 1
    def is_valid(self):
775
        """Check whether a message is valid or not.
776 1
777
        This method will validate the Message content. During the validation
778
        process, we check whether the attributes' values are valid according to
779
        the OpenFlow specification. Call this method if you want to verify
780 1
        whether the message is ready to pack.
781 1
782
        Returns:
783
            bool: Whether the message is valid.
784 1
        """
785 1
        return True
786
        # pylint: disable=unreachable
787 1
        return super().is_valid() and self._validate_message_length()
788 1
789
    def _pack(self):
790 1
        self.update_header_length()
791
        return super()._pack()
792
793
    def unpack(self, buff, offset=0):
794
        """Unpack a binary message into this object's attributes.
795
796
        Unpack the binary value *buff* and update this object attributes based
797 1
        on the results. It is an inplace method and it receives the binary data
798
        of the message **without the header**.
799
800 1
        Args:
801
            buff (bytes): Binary data package to be unpacked, without the
802
                header.
803
            offset (int): Where to begin unpacking.
804
        """
805
        begin = offset
806 1
        for name, value in self.get_class_attributes():
807 1
            if type(value).__name__ != "Header":
808
                size = self._unpack_attribute(name, value, buff, begin)
809 1
                begin += size
810
811
    def update_header_length(self):
812 1
        """Update the header length attribute based on current message size.
813
814
        When sending an OpenFlow message we need to inform the message length
815 1
        on the header. This is mandatory.
816
        """
817
        self.header.length = self.get_size()
818
819
820
class MetaBitMask(type):
821
    """MetaClass to create a special BitMaskEnum type.
822
823
    You probably do not need to use this class. Inherit from
824
    :class:`GenericBitMask` instead.
825
826
    This metaclass converts the declared class attributes into elements of an
827
    enum. It also replaces the :meth:`__dir__` and :meth:`__getattr__` methods,
828 1
    so the resulting class will behave as an :class:`~Enum` class (you can
829
    access object.ELEMENT and recover either values or names).
830
    """
831
832
    def __new__(mcs, name, bases, classdict):
833
        """Convert class attributes into enum elements."""
834
        _enum = OrderedDict([(key, value) for key, value in classdict.items()
835
                             if key[0] != '_' and not
836
                             hasattr(value, '__call__') and not
837
                             isinstance(value, property)])
838
        if _enum:
839
            classdict = {key: value for key, value in classdict.items()
840
                         if key[0] == '_' or hasattr(value, '__call__') or
841
                         isinstance(value, property)}
842
            classdict['_enum'] = _enum
843
        return type.__new__(mcs, name, bases, classdict)
844
845
    def __getattr__(cls, name):
846
        return cls._enum[name]
847
848
    def __dir__(cls):
849
        res = dir(type(cls)) + list(cls.__dict__.keys())
850
        if cls is not GenericBitMask:
851
            res.extend(cls._enum)
852
        return res
853
854
855
class GenericBitMask(object, metaclass=MetaBitMask):
856
    """Base class for enums that use bitmask values."""
857
858
    def __init__(self, bitmask=None):
859
        """The constructor has the optional parameter below.
860
861
        Args:
862
            bitmask: Bitmask value.
863
        """
864
        self.bitmask = bitmask
865
        self._enum = {}
866
867
    def __str__(self):
868
        return "{}".format(self.bitmask)
869
870
    def __repr__(self):
871
        return "{}({})".format(type(self).__name__, self.bitmask)
872
873
    @property
874
    def names(self):
875
        """List of selected enum names.
876
877
        Returns:
878
            list: Enum names.
879
        """
880
        result = []
881
        for key, value in self.iteritems():
882
            if value & self.bitmask:
883
                result.append(key)
884
        return result
885
886
    def iteritems(self):
887
        """Generator for attributes' name-value pairs.
888
889
        Returns:
890
            generator: Attributes' (name, value) tuples.
891
        """
892
        for key, value in self._enum.items():
893
            yield (key, value)
894