From dcd17a896c38455b7c303ebcb05f300573dd84b2 Mon Sep 17 00:00:00 2001 From: Adrian Moreno Date: Fri, 8 Jul 2022 20:03:01 +0200 Subject: python: Add mask, ip and eth decoders. Add more decoders that can be used by KVParser. For IPv4 and IPv6 addresses, create a new class that wraps netaddr.IPAddress. For Ethernet addresses, create a new class that wraps netaddr.EUI. For Integers, create a new class that performs basic bitwise mask comparisons netaddr is added as a new shoft dependency: - extras_require in setup.py - Suggests in deb and rpm packages Acked-by: Eelco Chaudron Signed-off-by: Adrian Moreno Signed-off-by: Ilya Maximets --- .ci/linux-prepare.sh | 2 +- .cirrus.yml | 2 +- Documentation/topics/language-bindings.rst | 9 + debian/control.in | 2 + python/ovs/flow/__init__.py | 13 + python/ovs/flow/decoders.py | 398 +++++++++++++++++++++++++++++ python/setup.py | 3 +- rhel/openvswitch-fedora.spec.in | 1 + 8 files changed, 427 insertions(+), 3 deletions(-) diff --git a/.ci/linux-prepare.sh b/.ci/linux-prepare.sh index 1fe890846..831ce46ba 100755 --- a/.ci/linux-prepare.sh +++ b/.ci/linux-prepare.sh @@ -26,7 +26,7 @@ cd .. # https://github.com/pypa/pip/issues/10655 pip3 install --disable-pip-version-check --user wheel pip3 install --disable-pip-version-check --user \ - flake8 'hacking>=3.0' sphinx setuptools pyelftools + flake8 'hacking>=3.0' netaddr sphinx setuptools pyelftools pip3 install --user 'meson==0.49.2' if [ "$M32" ]; then diff --git a/.cirrus.yml b/.cirrus.yml index a4d2a5bbc..4caa03705 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -9,7 +9,7 @@ freebsd_build_task: env: DEPENDENCIES: automake libtool gmake gcc wget openssl python3 - PY_DEPS: sphinx + PY_DEPS: sphinx|netaddr matrix: COMPILER: gcc COMPILER: clang diff --git a/Documentation/topics/language-bindings.rst b/Documentation/topics/language-bindings.rst index 43c64349a..5025f069f 100644 --- a/Documentation/topics/language-bindings.rst +++ b/Documentation/topics/language-bindings.rst @@ -40,6 +40,15 @@ the bindings using ``pip``: $ pip install ovs +The Python bindings include an optional flow parsing library. To install it's +required dependencies, run: + +:: + + $ pip install ovs[flow] + +or install `python3-netaddr`. + __ https://github.com/openvswitch/ovs/tree/master/python/ovs Third-Party Bindings diff --git a/debian/control.in b/debian/control.in index fa7ee7932..f97124e07 100644 --- a/debian/control.in +++ b/debian/control.in @@ -284,6 +284,8 @@ Depends: ${misc:Depends}, ${python3:Depends}, ${shlibs:Depends}, +Suggests: + python3-netaddr Description: Python 3 bindings for Open vSwitch Open vSwitch is a production quality, multilayer, software-based, Ethernet virtual switch. It is designed to enable massive network diff --git a/python/ovs/flow/__init__.py b/python/ovs/flow/__init__.py index e69de29bb..68453cc8e 100644 --- a/python/ovs/flow/__init__.py +++ b/python/ovs/flow/__init__.py @@ -0,0 +1,13 @@ +""" Global flow library entrypoint. +""" +for libname in ["netaddr"]: + try: + lib = __import__(libname) + except ModuleNotFoundError as e: + raise ImportError( + f"OVS Flow library requires {libname} to be installed." + " To install all the dependencies needed for the Flow library, run" + " 'pip install -e ovs[flow]' (or 'pip install -e .[flow]' locally)" + ) from e + else: + globals()[libname] = lib diff --git a/python/ovs/flow/decoders.py b/python/ovs/flow/decoders.py index 0c2259c76..883e61acf 100644 --- a/python/ovs/flow/decoders.py +++ b/python/ovs/flow/decoders.py @@ -5,6 +5,15 @@ A decoder is generally a callable that accepts a string and returns the value object. """ +import netaddr + + +class Decoder(object): + """Base class for all decoder classes.""" + + def to_json(self): + raise NotImplementedError() + def decode_default(value): """Default decoder. @@ -16,3 +25,392 @@ def decode_default(value): return int(value, 0) except ValueError: return value + + +def decode_flag(value): + """Decode a flag. It's existence is just flagged by returning True.""" + return True + + +def decode_int(value): + """Integer decoder. + + Both base10 and base16 integers are supported. + + Used for fields such as: + n_bytes=34 + metadata=0x4 + """ + return int(value, 0) + + +def decode_time(value): + """Time decoder. + + Used for fields such as: + duration=1234.123s + """ + if value == "never": + return value + + time_str = value.rstrip("s") + return float(time_str) + + +class IntMask(Decoder): + """Base class for Integer Mask decoder classes. + + It supports decoding a value/mask pair. The class has to be derived, + and the size attribute must be set. + """ + + size = None # Size in bits. + + def __init__(self, string): + if not self.size: + raise NotImplementedError( + "IntMask should be derived and size should be fixed" + ) + + parts = string.split("/") + if len(parts) > 1: + self._value = int(parts[0], 0) + self._mask = int(parts[1], 0) + if self._mask.bit_length() > self.size: + raise ValueError( + "Integer mask {} is bigger than size {}".format( + self._mask, self.size + ) + ) + else: + self._value = int(parts[0], 0) + self._mask = self.max_mask() + + if self._value.bit_length() > self.size: + raise ValueError( + "Integer value {} is bigger than size {}".format( + self._value, self.size + ) + ) + + @property + def value(self): + return self._value + + @property + def mask(self): + return self._mask + + def max_mask(self): + return 2 ** self.size - 1 + + def fully(self): + """Returns True if it's fully masked.""" + return self._mask == self.max_mask() + + def __str__(self): + if self.fully(): + return str(self._value) + else: + return "{}/{}".format(hex(self._value), hex(self._mask)) + + def __repr__(self): + return "%s('%s')" % (self.__class__.__name__, self) + + def __eq__(self, other): + """Equality operator. + + Both value and mask must be the same for the comparison to result True. + This can be used to implement filters that expect a specific mask, + e.g: ct.state = 0x1/0xff. + + Args: + other (IntMask): Another IntMask to compare against. + + Returns: + True if the other IntMask is the same as this one. + """ + if isinstance(other, IntMask): + return self.value == other.value and self.mask == other.mask + elif isinstance(other, int): + return self.value == other and self.mask == self.max_mask() + else: + raise ValueError("Cannot compare against ", other) + + def __contains__(self, other): + """Contains operator. + + Args: + other (int or IntMask): Another integer or fully-masked IntMask + to compare against. + + Returns: + True if the other integer or fully-masked IntMask is + contained in this IntMask. + + Example: + 0x1 in IntMask("0xf1/0xff"): True + 0x1 in IntMask("0xf1/0x0f"): True + 0x1 in IntMask("0xf1/0xf0"): False + """ + if isinstance(other, IntMask): + if other.fully(): + return other.value in self + else: + raise ValueError( + "Comparing non fully-masked IntMasks is not supported" + ) + else: + return other & self._mask == self._value & self._mask + + def dict(self): + return {"value": self._value, "mask": self._mask} + + def to_json(self): + return self.dict() + + +class Mask8(IntMask): + size = 8 + + +class Mask16(IntMask): + size = 16 + + +class Mask32(IntMask): + size = 32 + + +class Mask64(IntMask): + size = 64 + + +class Mask128(IntMask): + size = 128 + + +class Mask992(IntMask): + size = 992 + + +def decode_mask(mask_size): + """Value/Mask decoder for values of specific size (bits). + + Used for fields such as: + reg0=0x248/0xff + """ + + class Mask(IntMask): + size = mask_size + __name__ = "Mask{}".format(size) + + return Mask + + +class EthMask(Decoder): + """EthMask represents an Ethernet address with optional mask. + + It uses netaddr.EUI. + + Attributes: + eth (netaddr.EUI): The Ethernet address. + mask (netaddr.EUI): Optional, the Ethernet address mask. + + Args: + string (str): A string representing the masked Ethernet address + e.g: 00.11:22:33:44:55 or 01:00:22:00:33:00/01:00:00:00:00:00 + """ + + def __init__(self, string): + mask_parts = string.split("/") + self._eth = netaddr.EUI(mask_parts[0]) + if len(mask_parts) == 2: + self._mask = netaddr.EUI(mask_parts[1]) + else: + self._mask = None + + @property + def eth(self): + """The Ethernet address.""" + return self._eth + + @property + def mask(self): + """The Ethernet address mask.""" + return self._mask + + def __eq__(self, other): + """Equality operator. + + Both the Ethernet address and the mask are compared. This can be used + to implement filters where we expect a specific mask to be present, + e.g: dl_dst=01:00:00:00:00:00/01:00:00:00:00:00. + + Args: + other (EthMask): Another EthMask to compare against. + + Returns: + True if this EthMask is the same as the other. + """ + return self._mask == other._mask and self._eth == other._eth + + def __contains__(self, other): + """Contains operator. + + Args: + other (netaddr.EUI or EthMask): An Ethernet address. + + Returns: + True if the other netaddr.EUI or fully-masked EthMask is + contained in this EthMask's address range. + """ + if isinstance(other, EthMask): + if other._mask: + raise ValueError( + "Comparing non fully-masked EthMask is not supported" + ) + return other._eth in self + + if self._mask: + return (other.value & self._mask.value) == ( + self._eth.value & self._mask.value + ) + else: + return other == self._eth + + def __str__(self): + if self._mask: + return "/".join( + [ + self._eth.format(netaddr.mac_unix), + self._mask.format(netaddr.mac_unix), + ] + ) + else: + return self._eth.format(netaddr.mac_unix) + + def __repr__(self): + return "%s('%s')" % (self.__class__.__name__, self) + + def to_json(self): + return str(self) + + +class IPMask(Decoder): + """IPMask stores an IPv6 or IPv4 and a mask. + + It uses netaddr.IPAddress. + + IPMasks can represent valid CIDRs or randomly masked IP Addresses. + + Args: + string (str): A string representing the ip/mask. + """ + + def __init__(self, string): + self._ipnet = None + self._ip = None + self._mask = None + try: + self._ipnet = netaddr.IPNetwork(string) + except netaddr.AddrFormatError: + pass + + if not self._ipnet: + # It's not a valid CIDR. Store ip and mask independently. + parts = string.split("/") + if len(parts) != 2: + raise ValueError( + "value {}: is not an ipv4 or ipv6 address".format(string) + ) + try: + self._ip = netaddr.IPAddress(parts[0]) + self._mask = netaddr.IPAddress(parts[1]) + except netaddr.AddrFormatError as exc: + raise ValueError( + "value {}: is not an ipv4 or ipv6 address".format(string) + ) from exc + + def __eq__(self, other): + """Equality operator. + + Both the IPAddress and the mask are compared. This can be used + to implement filters where a specific mask is expected, e.g: + nw_src=192.168.1.0/24. + + Args: + other (IPMask or netaddr.IPNetwork or netaddr.IPAddress): + Another IPAddress or IPNetwork to compare against. + + Returns: + True if this IPMask is the same as the other. + """ + if isinstance(other, netaddr.IPNetwork): + return self._ipnet and self._ipnet == other + if isinstance(other, netaddr.IPAddress): + return self._ipnet and self._ipnet.ip == other + elif isinstance(other, IPMask): + if self._ipnet: + return self._ipnet == other._ipnet + + return self._ip == other._ip and self._mask == other._mask + else: + return False + + def __contains__(self, other): + """Contains operator. + + Only comparing valid CIDRs is supported. + + Args: + other (netaddr.IPAddress or IPMask): An IP address. + + Returns: + True if the other IPAddress is contained in this IPMask's address + range. + """ + if isinstance(other, IPMask): + if not other._ipnet: + raise ValueError("Only comparing valid CIDRs is supported") + + return ( + netaddr.IPAddress(other._ipnet.first) in self + and netaddr.IPAddress(other._ipnet.last) in self + ) + + elif isinstance(other, netaddr.IPAddress): + if self._ipnet: + return other in self._ipnet + return (other & self._mask) == (self._ip & self._mask) + + def cidr(self): + """ + Returns True if the IPMask is a valid CIDR. + """ + return self._ipnet is not None + + @property + def ip(self): + """The IP address.""" + if self._ipnet: + return self._ipnet.ip + return self._ip + + @property + def mask(self): + """The IP mask.""" + if self._ipnet: + return self._ipnet.netmask + return self._mask + + def __str__(self): + if self._ipnet: + return str(self._ipnet) + return "/".join([str(self._ip), str(self._mask)]) + + def __repr__(self): + return "%s('%s')" % (self.__class__.__name__, self) + + def to_json(self): + return str(self) diff --git a/python/setup.py b/python/setup.py index 174befceb..e3021cb6b 100644 --- a/python/setup.py +++ b/python/setup.py @@ -98,7 +98,8 @@ setup_args = dict( libraries=json_libraries)], cmdclass={'build_ext': try_build_ext}, install_requires=['sortedcontainers'], - extras_require={':sys_platform == "win32"': ['pywin32 >= 1.0']}, + extras_require={':sys_platform == "win32"': ['pywin32 >= 1.0'], + 'flow': ['netaddr']}, ) try: diff --git a/rhel/openvswitch-fedora.spec.in b/rhel/openvswitch-fedora.spec.in index cb7a02cab..7f0fad918 100644 --- a/rhel/openvswitch-fedora.spec.in +++ b/rhel/openvswitch-fedora.spec.in @@ -113,6 +113,7 @@ Summary: Open vSwitch python3 bindings License: ASL 2.0 BuildArch: noarch Requires: python3 +Suggests: python3-netaddr %{?python_provide:%python_provide python3-openvswitch = %{version}-%{release}} %description -n python3-openvswitch -- cgit v1.2.1