Just checking in on this. I see a single test failure
https://github.com/ovsrobot/ovs/actions/runs/5272193494 'linux clang
test asan' but it looks like something related to bfd, so definitely
not the python patch here. Anything I need to do?

On Wed, Jun 14, 2023 at 4:07 PM Terry Wilson <twil...@redhat.com> wrote:
>
> This adds a Python version of the async DNS support added in:
>
> 771680d96 DNS: Add basic support for asynchronous DNS resolving
>
> The above version uses the unbound C library, and this
> implimentation uses the SWIG-wrapped Python version of that.
>
> In the event that the Python unbound library is not available,
> a warning will be logged and the resolve() method will just
> return None. For the case where inet_parse_active() is passed
> an IP address, it will not try to resolve it, so existing
> behavior should be preserved in the case that the unbound
> library is unavailable.
>
> Intentional differences from the C version are as follows:
>
>   OVS_HOSTS_FILE environment variable can bet set to override
>   the system 'hosts' file. This is primarily to allow testing to
>   be done without requiring network connectivity.
>
>   Since resolution can still be done via hosts file lookup, DNS
>   lookups are not disabled when resolv.conf cannot be loaded.
>
>   The Python socket_util module has fallen behind its C equivalent.
>   The bare minimum change was done to inet_parse_active() to support
>   sync/async dns, as there is no equivalent to
>   parse_sockaddr_components(), inet_parse_passive(), etc. A TODO
>   was added to bring socket_util.py up to equivalency to the C
>   version.
>
> Signed-off-by: Terry Wilson <twil...@redhat.com>
> ---
>  .github/workflows/build-and-test.yml    |   4 +-
>  Documentation/intro/install/general.rst |   4 +-
>  Documentation/intro/install/rhel.rst    |   2 +-
>  Documentation/intro/install/windows.rst |   2 +-
>  NEWS                                    |   4 +-
>  debian/control.in                       |   1 +
>  m4/openvswitch.m4                       |   8 +-
>  python/TODO.rst                         |   7 +
>  python/automake.mk                      |   2 +
>  python/ovs/dns_resolve.py               | 272 +++++++++++++++++++++++
>  python/ovs/socket_util.py               |  21 +-
>  python/ovs/stream.py                    |   2 +-
>  python/ovs/tests/test_dns_resolve.py    | 280 ++++++++++++++++++++++++
>  python/setup.py                         |   6 +-
>  rhel/openvswitch-fedora.spec.in         |   2 +-
>  tests/vlog.at                           |   2 +
>  16 files changed, 601 insertions(+), 18 deletions(-)
>  create mode 100644 python/ovs/dns_resolve.py
>  create mode 100644 python/ovs/tests/test_dns_resolve.py
>
> diff --git a/.github/workflows/build-and-test.yml 
> b/.github/workflows/build-and-test.yml
> index f66ab43b0..47d239f10 100644
> --- a/.github/workflows/build-and-test.yml
> +++ b/.github/workflows/build-and-test.yml
> @@ -183,10 +183,10 @@ jobs:
>        run:  sudo apt update || true
>      - name: install common dependencies
>        run:  sudo apt install -y ${{ env.dependencies }}
> -    - name: install libunbound libunwind
> +    - name: install libunbound libunwind python3-unbound
>        # GitHub Actions doesn't have 32-bit versions of these libraries.
>        if:   matrix.m32 == ''
> -      run:  sudo apt install -y libunbound-dev libunwind-dev
> +      run:  sudo apt install -y libunbound-dev libunwind-dev python3-unbound
>      - name: install 32-bit libraries
>        if:   matrix.m32 != ''
>        run:  sudo apt install -y gcc-multilib
> diff --git a/Documentation/intro/install/general.rst 
> b/Documentation/intro/install/general.rst
> index 42b5682fd..19e360d47 100644
> --- a/Documentation/intro/install/general.rst
> +++ b/Documentation/intro/install/general.rst
> @@ -90,7 +90,7 @@ need the following software:
>    If libcap-ng is installed, then Open vSwitch will automatically build with
>    support for it.
>
> -- Python 3.4 or later.
> +- Python 3.6 or later.
>
>  - Unbound library, from http://www.unbound.net, is optional but recommended 
> if
>    you want to enable ovs-vswitchd and other utilities to use DNS names when
> @@ -208,7 +208,7 @@ simply install and run Open vSwitch you require the 
> following software:
>    from iproute2 (part of all major distributions and available at
>    https://wiki.linuxfoundation.org/networking/iproute2).
>
> -- Python 3.4 or later.
> +- Python 3.6 or later.
>
>  On Linux you should ensure that ``/dev/urandom`` exists. To support TAP
>  devices, you must also ensure that ``/dev/net/tun`` exists.
> diff --git a/Documentation/intro/install/rhel.rst 
> b/Documentation/intro/install/rhel.rst
> index d1fc42021..f2151d890 100644
> --- a/Documentation/intro/install/rhel.rst
> +++ b/Documentation/intro/install/rhel.rst
> @@ -92,7 +92,7 @@ Once that is completed, remove the file ``/tmp/ovs.spec``.
>  If python3-sphinx package is not available in your version of RHEL, you can
>  install it via pip with 'pip install sphinx'.
>
> -Open vSwitch requires python 3.4 or newer which is not available in older
> +Open vSwitch requires python 3.6 or newer which is not available in older
>  distributions. In the case of RHEL 6.x and its derivatives, one option is
>  to install python34 from `EPEL`_.
>
> diff --git a/Documentation/intro/install/windows.rst 
> b/Documentation/intro/install/windows.rst
> index 78f60f35a..fce099d5d 100644
> --- a/Documentation/intro/install/windows.rst
> +++ b/Documentation/intro/install/windows.rst
> @@ -56,7 +56,7 @@ The following explains the steps in some detail.
>
>        'C:/MinGW /mingw'.
>
> -- Python 3.4 or later.
> +- Python 3.6 or later.
>
>    Install the latest Python 3.x from python.org and verify that its path is
>    part of Windows' PATH environment variable.
> diff --git a/NEWS b/NEWS
> index cfd466663..24c694b8f 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -36,7 +36,9 @@ Post-v3.1.0
>         process extra privileges when mapping physical interconnect memory.
>     - SRv6 Tunnel Protocol
>       * Added support for userspace datapath (only).
> -
> +   - Python
> +     * Added async DNS support
> +     * Dropped support for Python < 3.6
>
>  v3.1.0 - 16 Feb 2023
>  --------------------
> diff --git a/debian/control.in b/debian/control.in
> index 19f590d06..64b0a4ce0 100644
> --- a/debian/control.in
> +++ b/debian/control.in
> @@ -287,6 +287,7 @@ Depends:
>  Suggests:
>   python3-netaddr,
>   python3-pyparsing,
> + python3-unbound,
>  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/m4/openvswitch.m4 b/m4/openvswitch.m4
> index 14d9249b8..373a7e413 100644
> --- a/m4/openvswitch.m4
> +++ b/m4/openvswitch.m4
> @@ -371,16 +371,16 @@ dnl Checks for valgrind/valgrind.h.
>  AC_DEFUN([OVS_CHECK_VALGRIND],
>    [AC_CHECK_HEADERS([valgrind/valgrind.h])])
>
> -dnl Checks for Python 3.4 or later.
> +dnl Checks for Python 3.6 or later.
>  AC_DEFUN([OVS_CHECK_PYTHON3],
>    [AC_CACHE_CHECK(
> -     [for Python 3 (version 3.4 or later)],
> +     [for Python 3 (version 3.6 or later)],
>       [ovs_cv_python3],
>       [if test -n "$PYTHON3"; then
>          ovs_cv_python3=$PYTHON3
>        else
>          ovs_cv_python3=no
> -        for binary in python3 python3.4 python3.5 python3.6 python3.7; do
> +        for binary in python3{,.{6..12}}; do
>            ovs_save_IFS=$IFS; IFS=$PATH_SEPARATOR
>            for dir in $PATH; do
>              IFS=$ovs_save_IFS
> @@ -397,7 +397,7 @@ else:
>          done
>        fi])
>     if test "$ovs_cv_python3" = no; then
> -     AC_MSG_ERROR([Python 3.4 or later is required but not found in $PATH, 
> please install it or set $PYTHON3 to point to it])
> +     AC_MSG_ERROR([Python 3.6 or later is required but not found in $PATH, 
> please install it or set $PYTHON3 to point to it])
>     fi
>     AC_ARG_VAR([PYTHON3])
>     PYTHON3=$ovs_cv_python3])
> diff --git a/python/TODO.rst b/python/TODO.rst
> index 3a53489f1..acc5461e2 100644
> --- a/python/TODO.rst
> +++ b/python/TODO.rst
> @@ -32,3 +32,10 @@ Python Bindings To-do List
>
>    * Support write-only-changed monitor mode (equivalent of
>      OVSDB_IDL_WRITE_CHANGED_ONLY).
> +
> +* socket_util:
> +
> +  * Add equivalent fuctions to inet_parse_passive, parse_sockaddr_components,
> +    et al. to better support using async dns. The reconnect code will
> +    currently log a warning when inet_parse_active() returns w/o yet having
> +    resolved an address, but will continue to connect and eventually succeed.
> diff --git a/python/automake.mk b/python/automake.mk
> index d00911828..82a508787 100644
> --- a/python/automake.mk
> +++ b/python/automake.mk
> @@ -16,6 +16,7 @@ ovs_pyfiles = \
>         python/ovs/compat/sortedcontainers/sorteddict.py \
>         python/ovs/compat/sortedcontainers/sortedset.py \
>         python/ovs/daemon.py \
> +       python/ovs/dns_resolve.py \
>         python/ovs/db/__init__.py \
>         python/ovs/db/custom_index.py \
>         python/ovs/db/data.py \
> @@ -55,6 +56,7 @@ ovs_pyfiles = \
>
>  ovs_pytests = \
>         python/ovs/tests/test_decoders.py \
> +       python/ovs/tests/test_dns_resolve.py \
>         python/ovs/tests/test_filter.py \
>         python/ovs/tests/test_kv.py \
>         python/ovs/tests/test_list.py \
> diff --git a/python/ovs/dns_resolve.py b/python/ovs/dns_resolve.py
> new file mode 100644
> index 000000000..7b0f6c266
> --- /dev/null
> +++ b/python/ovs/dns_resolve.py
> @@ -0,0 +1,272 @@
> +# Copyright (c) 2023 Red Hat, Inc.
> +#
> +# Licensed under the Apache License, Version 2.0 (the "License");
> +# you may not use this file except in compliance with the License.
> +# You may obtain a copy of the License at:
> +#
> +#     http://www.apache.org/licenses/LICENSE-2.0
> +#
> +# Unless required by applicable law or agreed to in writing, software
> +# distributed under the License is distributed on an "AS IS" BASIS,
> +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> +# See the License for the specific language governing permissions and
> +# limitations under the License.
> +
> +import collections
> +import enum
> +import functools
> +import ipaddress
> +import os
> +import time
> +import typing
> +
> +try:
> +    import unbound  # type: ignore
> +except ImportError:
> +    pass
> +
> +import ovs.vlog
> +
> +vlog = ovs.vlog.Vlog("dns_resolve")
> +
> +
> +class ReqState(enum.Enum):
> +    INVALID = 0
> +    PENDING = 1
> +    GOOD = 2
> +    ERROR = 3
> +
> +
> +class DNSRequest:
> +
> +    def __init__(self, name: str):
> +        self.name: str = name
> +        self.state: ReqState = ReqState.INVALID
> +        self.time: typing.Optional[float] = None
> +        # set by DNSResolver._callback
> +        self.result: typing.Optional[str] = None
> +        self.ttl: typing.Optional[float] = None
> +
> +    @property
> +    def expired(self):
> +        return time.time() > self.time + self.ttl
> +
> +    @property
> +    def is_valid(self):
> +        return self.state == ReqState.GOOD and not self.expired
> +
> +    def __str__(self):
> +        return (f"DNSRequest(name={self.name}, state={self.state}, "
> +                f"time={self.time}, result={self.result})")
> +
> +
> +class DefaultReqDict(collections.defaultdict):
> +    def __init__(self):
> +        super().__init__(DNSRequest)
> +
> +    def __missing__(self, key):
> +        ret = self.default_factory(key)
> +        self[key] = ret
> +        return ret
> +
> +
> +class UnboundException(Exception):
> +    def __init__(self, message, errno):
> +        try:
> +            msg = f"{message}: {unbound.ub_strerror(errno)}"
> +        except NameError:
> +            msg = message
> +        super().__init__(msg)
> +
> +
> +def dns_enabled(func):
> +    @functools.wraps(func)
> +    def wrapper(self, *args, **kwargs):
> +        if self.dns_enabled:
> +            return func(self, *args, **kwargs)
> +        vlog.err("DNS support requires the python unbound library")
> +    return wrapper
> +
> +
> +class singleton:
> +    def __init__(self, klass):
> +        self._klass = klass
> +        self._instance = None
> +
> +    def __call__(self, *args, **kwargs):
> +        if self._instance is None:
> +            self._instance = self._klass(*args, **kwargs)
> +        return self._instance
> +
> +
> +@singleton
> +class DNSResolver:
> +    def __init__(self, is_daemon: bool = False):
> +        """Create a resolver instance
> +
> +        If is_daemon is true, set the resolver to handle requests
> +        asynchronously. The following environment variables are processed:
> +
> +        OVS_UNBOUND_CONF: The filename for an unbound.conf file
> +        OVS_RESOLV_CONF: A filename to override the system default 
> resolv.conf
> +        OVS_HOSTS_FILE: A filename to override the system default hosts file
> +
> +        In the event that the unbound library is missing or fails to 
> initialize
> +        DNS lookup support will be disabled and the resolve() method will
> +        return None.
> +        """
> +        self._is_daemon = is_daemon
> +        try:
> +            self._ctx = unbound.ub_ctx()
> +            self.dns_enabled = True
> +        except Exception:
> +            # The unbound docs mention that this could thrown an exception
> +            # but do not specify what exception that is. This can also
> +            # happen with a missing unbound library.
> +            self.dns_enabled = False
> +            vlog.err("Failed to initialize the unbound library")
> +            return
> +
> +        # NOTE(twilson) This cache, like the C version, can grow without 
> bound
> +        # and has no cleanup or aging mechanism. Given our usage patterns, 
> this
> +        # should not be a problem. But this should not be used to resolve an
> +        # unbounded list of addresses in a long-running daemon.
> +        self._requests = DefaultReqDict()
> +
> +        self._ub_call(self._set_unbound_conf)
> +
> +        # NOTE(twilson) The C version disables DNS in this case. I didn't do
> +        # that here since it could still be useful to resolve addresses from
> +        # /etc/hosts even w/o resolv.conf
> +        self._ub_call(self._set_resolv_conf)
> +        self._ub_call(self._set_hosts_file)
> +
> +        self._ctx.set_async(True)  # Sets threaded behavior for 
> resolve_async()
> +
> +    def _ub_call(self, fn, *args, **kwargs):
> +        """Convert UnboundExceptions into vlog warnings"""
> +        try:
> +            return fn(*args, **kwargs)
> +        except UnboundException as e:
> +            vlog.warn(e)
> +
> +    @dns_enabled
> +    def _set_unbound_conf(self):
> +        ub_cfg = os.getenv("OVS_UNBOUND_CONF")
> +        if ub_cfg:
> +            retval = self._ctx.config(ub_cfg)
> +            if retval != 0:
> +                raise UnboundException(
> +                    "Failed to set libunbound context config", retval)
> +
> +    @dns_enabled
> +    def _set_resolv_conf(self):
> +        filename = os.getenv("OVS_RESOLV_CONF")
> +        # The C lib checks that the file exists and also sets filename to
> +        # /etc/resolv.conf on non-Windows, but resolvconf already does this.
> +        retval = self._ctx.resolvconf(filename)
> +        if retval != 0:
> +            location = filename or "system default nameserver"
> +            raise UnboundException(location, retval)
> +
> +    @dns_enabled
> +    def _set_hosts_file(self):
> +        # The C lib doesn't have the ability to set a hosts file, but it is
> +        # useful to have, especially for writing tests that don't rely on
> +        # network connectivity. hosts(None) uses /etc/hosts.
> +        filename = os.getenv("OVS_HOSTS_FILE")
> +        retval = self._ctx.hosts(filename)
> +        if retval != 0:
> +            location = filename or "system default hosts file"
> +            raise UnboundException(location, retval)
> +
> +    @dns_enabled
> +    def _callback(self, req: DNSRequest, err: int, result):
> +        if err != 0 or (result.qtype == unbound.RR_TYPE_AAAA
> +                        and not result.havedata):
> +            req.state = ReqState.ERROR
> +            vlog.warn(f"{req.name}: failed to resolve")
> +            return
> +        if result.qtype == unbound.RR_TYPE_A and not result.havedata:
> +            self._resolve_async(req, unbound.RR_TYPE_AAAA)
> +            return
> +        try:
> +            ip_str = next(iter(result.data.as_raw_data()))
> +            ip = ipaddress.ip_address(ip_str)  # test if IP is valid
> +            # NOTE (twilson) For some reason, accessing result data outside 
> of
> +            # _callback causes a segfault. So just grab and store what we 
> need.
> +            req.result = str(ip)
> +            req.ttl = result.ttl
> +            req.state = ReqState.GOOD
> +            req.time = time.time()
> +        except (ValueError, StopIteration):
> +            req.state = ReqState.ERROR
> +            vlog.err(f"{req.name}: failed to resolve")
> +
> +    @dns_enabled
> +    def _resolve_sync(self, name: str) -> typing.Optional[str]:
> +        for qtype in (unbound.RR_TYPE_A, unbound.RR_TYPE_AAAA):
> +            err, result = self._ctx.resolve(name, qtype)
> +            if err != 0:
> +                return None
> +            if not result.havedata:
> +                continue
> +            try:
> +                ip = ipaddress.ip_address(
> +                    next(iter(result.data.as_raw_data())))
> +            except (ValueError, StopIteration):
> +                return None
> +            return str(ip)
> +
> +        return None
> +
> +    @dns_enabled
> +    def _resolve_async(self, req: DNSRequest, qtype) -> None:
> +        err, _ = self._ctx.resolve_async(req.name, req, self._callback,
> +                                         qtype)
> +        if err != 0:
> +            req.state = ReqState.ERROR
> +            return None
> +
> +        req.state = ReqState.PENDING
> +        return None
> +
> +    @dns_enabled
> +    def resolve(self, name: str) -> typing.Optional[str]:
> +        """Resolve a host name to an IP address
> +
> +        If the resolver is set to handle requests asynchronously, resolve()
> +        should be recalled until it returns a non-None result. Errors will be
> +        logged.
> +
> +        :param name: The host name to resolve
> +        :returns: The IP address or None on error or not (yet) found
> +        """
> +        if not self._is_daemon:
> +            return self._resolve_sync(name)
> +        retval = self._ctx.process()
> +        if retval != 0:
> +            vlog.err(f"dns-resolve error: {unbound.ub_strerror(retval)}")
> +            return None
> +        req = self._requests[name]  # Creates a DNSRequest if not found
> +        if req.is_valid:
> +            return req.result
> +        elif req.state != ReqState.PENDING:
> +            self._resolve_async(req, unbound.RR_TYPE_A)
> +        return None
> +
> +
> +def resolve(name: str) -> typing.Optional[str]:
> +    """Resolve a host name to an IP address
> +
> +    If a DNSResolver instance has not been instantiated, or if it has been
> +    created with is_daemon=False, resolve() will synchronously resolve the
> +    hostname. If DNSResolver has been initialized with is_daemon=True, it
> +    will instead resolve asynchornously and resolve() will return None until
> +    the hostname has been resolved.
> +
> +    :param name: The host name to resolve
> +    :returns: The IP address or None on error or not (yet) found
> +    """
> +
> +    return DNSResolver().resolve(name)
> diff --git a/python/ovs/socket_util.py b/python/ovs/socket_util.py
> index 7b41dc44b..a26298b75 100644
> --- a/python/ovs/socket_util.py
> +++ b/python/ovs/socket_util.py
> @@ -13,12 +13,14 @@
>  # limitations under the License.
>
>  import errno
> +import ipaddress
>  import os
>  import os.path
>  import random
>  import socket
>  import sys
>
> +from ovs import dns_resolve
>  import ovs.fatal_signal
>  import ovs.poller
>  import ovs.vlog
> @@ -216,7 +218,7 @@ def is_valid_ipv4_address(address):
>      return True
>
>
> -def inet_parse_active(target, default_port):
> +def _inet_parse_active(target, default_port):
>      address = target.split(":")
>      if len(address) >= 2:
>          host_name = ":".join(address[0:-1]).lstrip('[').rstrip(']')
> @@ -229,9 +231,24 @@ def inet_parse_active(target, default_port):
>          host_name = address[0]
>      if not host_name:
>          raise ValueError("%s: bad peer name format" % target)
> +    try:
> +        host_name = str(ipaddress.ip_address(host_name))
> +    except ValueError:
> +        host_name = dns_resolve.resolve(host_name)
> +    if not host_name:
> +        raise ValueError("%s: bad peer name format" % target)
>      return (host_name, port)
>
>
> +def inet_parse_active(target, default_port, raises=True):
> +    try:
> +        return _inet_parse_active(target, default_port)
> +    except ValueError:
> +        if raises:
> +            raise
> +        return ("", default_port)
> +
> +
>  def inet_create_socket_active(style, address):
>      try:
>          is_addr_inet = is_valid_ipv4_address(address[0])
> @@ -262,7 +279,7 @@ def inet_connect_active(sock, address, family, dscp):
>
>
>  def inet_open_active(style, target, default_port, dscp):
> -    address = inet_parse_active(target, default_port)
> +    address = inet_parse_active(target, default_port, raises=False)
>      family, sock = inet_create_socket_active(style, address)
>      if sock is None:
>          return family, sock
> diff --git a/python/ovs/stream.py b/python/ovs/stream.py
> index b32341076..82fbb0d68 100644
> --- a/python/ovs/stream.py
> +++ b/python/ovs/stream.py
> @@ -784,7 +784,7 @@ class SSLStream(Stream):
>
>      @staticmethod
>      def _open(suffix, dscp):
> -        address = ovs.socket_util.inet_parse_active(suffix, 0)
> +        address = ovs.socket_util.inet_parse_active(suffix, 0, raises=False)
>          family, sock = ovs.socket_util.inet_create_socket_active(
>                  socket.SOCK_STREAM, address)
>          if sock is None:
> diff --git a/python/ovs/tests/test_dns_resolve.py 
> b/python/ovs/tests/test_dns_resolve.py
> new file mode 100644
> index 000000000..fca8049b6
> --- /dev/null
> +++ b/python/ovs/tests/test_dns_resolve.py
> @@ -0,0 +1,280 @@
> +import contextlib
> +import ipaddress
> +import sys
> +import time
> +from unittest import mock
> +
> +import pytest
> +
> +from ovs import dns_resolve
> +from ovs import socket_util
> +
> +
> +skip_no_unbound = pytest.mark.skipif("unbound" not in dns_resolve.__dict__,
> +                                     reason="Unbound not installed")
> +
> +HOSTS = [("192.0.2.1", "fake.ip4.domain", "192.0.2.1"),
> +         ("2001:db8:2::1", "fake.ip6.domain", "2001:db8:2::1"),
> +         ("192.0.2.2", "fake.both.domain", "192.0.2.2"),
> +         ("2001:db8:2::2", "fake.both.domain", "192.0.2.2")]
> +
> +
> +def _tmp_file(path, content):
> +    path.write_text(content)
> +    assert content == path.read_text()
> +    return path
> +
> +
> +@pytest.fixture(params=[False, True], ids=["not_daemon", "daemon"])
> +def resolver_factory(monkeypatch, tmp_path, request):
> +    # Allow delaying the instantiation of the DNSResolver
> +    def resolver_factory(hosts=HOSTS):
> +        path = tmp_path / "hosts"
> +        content = "\n".join(f"{ip}\t{host}" for ip, host, _ in hosts)
> +        _tmp_file(path, content)
> +
> +        with monkeypatch.context() as m:
> +            m.setenv("OVS_HOSTS_FILE", str(path))
> +            # Test with both is_daemon False and True
> +            resolver = dns_resolve.DNSResolver(request.param)
> +            assert resolver._is_daemon == request.param
> +            return dns_resolve.DNSResolver(request.param)
> +
> +    yield resolver_factory
> +    dns_resolve.DNSResolver._instance = None
> +
> +
> +@contextlib.contextmanager
> +def DNSResolver(*args, **kwargs):
> +    """Clean up after returning a dns_resolver.DNSResolver
> +
> +    Since it is a singleton, and pytest runs all tests in the same process,
> +    we can't use dns_resolver.DNSResolver directly in these tests. This
> +    context manager will reset the singleton at the end of the with block.
> +    """
> +    resolver = dns_resolve.DNSResolver(*args, **kwargs)
> +    try:
> +        yield resolver
> +    finally:
> +        dns_resolve.DNSResolver._instance = None
> +
> +
> +@pytest.fixture
> +def unbound_conf(tmp_path):
> +    path = tmp_path / "unbound.conf"
> +    content = """
> +    server:
> +        verbosity: 1
> +    """
> +    return _tmp_file(path, content)
> +
> +
> +@pytest.fixture
> +def resolv_conf(tmp_path):
> +    path = tmp_path / "resolv.conf"
> +    content = "nameserver 127.0.0.1"
> +    return _tmp_file(path, content)
> +
> +
> +@pytest.fixture
> +def hosts_file(tmp_path):
> +    path = tmp_path / "hosts"
> +    content = "127.0.0.1\tfakelocalhost.localdomain"
> +    return _tmp_file(path, content)
> +
> +
> +@pytest.fixture
> +def missing_file(tmp_path):
> +    f = tmp_path / "missing_file"
> +    assert not f.exists()
> +    return f
> +
> +
> +@pytest.fixture(params=[False, True], ids=["with unbound", "without 
> unbound"])
> +def missing_unbound(monkeypatch, request):
> +    if request.param:
> +        if "unbound" in dns_resolve.__dict__:
> +            monkeypatch.setitem(sys.modules, 'unbound', None)
> +            monkeypatch.delitem(dns_resolve.__dict__, "unbound")
> +    elif "unbound" not in dns_resolve.__dict__:
> +        pytest.skip("Unbound not installed")
> +    return request.param
> +
> +
> +def test_missing_unbound(missing_unbound, resolver_factory):
> +    resolver = resolver_factory()  # Dont fail even w/o unbound
> +    assert resolver.dns_enabled == (not missing_unbound)
> +
> +
> +def test_DNSRequest_defaults():
> +    req = dns_resolve.DNSRequest(HOSTS[0][1])
> +    assert HOSTS[0][1] == req.name
> +    assert req.state == dns_resolve.ReqState.INVALID
> +    assert req.time == req.result == req.ttl is None
> +    assert str(req)
> +
> +
> +def test_DNSResolver_singleton():
> +    with DNSResolver(True) as r1:
> +        assert r1._is_daemon
> +        r2 = dns_resolve.DNSResolver(False)
> +        assert r1 == r2
> +        assert r1._is_daemon
> +
> +
> +def _resolve(resolver, host, fn=dns_resolve.resolve):
> +    """Handle sync/async lookups, giving up if more than 1 second has 
> passed"""
> +
> +    timeout = 1
> +    start = time.time()
> +    name = fn(host)
> +    if resolver._is_daemon:
> +        while name is None:
> +            name = fn(host)
> +            if name:
> +                break
> +            time.sleep(0.01)
> +            end = time.time()
> +            if end - start > timeout:
> +                break
> +    if name:
> +        return name
> +    raise LookupError(f"{host} not found")
> +
> +
> +@pytest.mark.parametrize("ip,host,expected", HOSTS)
> +def test_resolve_addresses(missing_unbound, resolver_factory, ip, host,
> +                           expected):
> +    resolver = resolver_factory()
> +    if missing_unbound:
> +        with pytest.raises(LookupError):
> +            _resolve(resolver, host)
> +    else:
> +        result = _resolve(resolver, host)
> +        assert ipaddress.ip_address(expected) == ipaddress.ip_address(result)
> +
> +
> +def test_resolve_unknown_host(missing_unbound, resolver_factory):
> +    resolver = resolver_factory()
> +    with pytest.raises(LookupError):
> +        _resolve(resolver, "fake.notadomain")
> +
> +
> +@skip_no_unbound
> +def test_resolve_process_error():
> +    with DNSResolver(True) as resolver:
> +        with mock.patch.object(resolver._ctx, "process", return_value=-1):
> +            assert resolver.resolve("fake.domain") is None
> +
> +
> +@skip_no_unbound
> +def test_resolve_resolve_error():
> +    with DNSResolver(False) as resolver:
> +        with mock.patch.object(resolver._ctx, "resolve",
> +                               return_value=(-1, None)):
> +            assert resolver.resolve("fake.domain") is None
> +
> +
> +@skip_no_unbound
> +def test_resolve_resolve_async_error():
> +    with DNSResolver(True) as resolver:
> +        with mock.patch.object(resolver._ctx, "resolve_async",
> +                               return_value=(-1, None)):
> +            with pytest.raises(LookupError):
> +                _resolve(resolver, "fake.domain")
> +
> +
> +@pytest.mark.parametrize("file,raises",
> +                         [(None, False),
> +                          ("missing_file", dns_resolve.UnboundException),
> +                          ("unbound_conf", False)])
> +def test_set_unbound_conf(monkeypatch, missing_unbound, resolver_factory,
> +                          request, file, raises):
> +    if file:
> +        file = str(request.getfixturevalue(file))
> +        monkeypatch.setenv("OVS_UNBOUND_CONF", file)
> +    resolver = resolver_factory()  # Doesn't raise
> +    if missing_unbound:
> +        assert resolver._set_unbound_conf() is None
> +        return
> +    with mock.patch.object(resolver._ctx, "config",
> +                           side_effect=resolver._ctx.config) as c:
> +        if raises:
> +            with pytest.raises(raises):
> +                resolver._set_unbound_conf()
> +        else:
> +            resolver._set_unbound_conf()
> +        if file:
> +            c.assert_called_once_with(file)
> +        else:
> +            c.assert_not_called()
> +
> +
> +@pytest.mark.parametrize("file,raises",
> +                         [(None, False),
> +                          ("missing_file", dns_resolve.UnboundException),
> +                          ("resolv_conf", False)])
> +def test_resolv_conf(monkeypatch, missing_unbound, resolver_factory, request,
> +                     file, raises):
> +    if file:
> +        file = str(request.getfixturevalue(file))
> +        monkeypatch.setenv("OVS_RESOLV_CONF", file)
> +    resolver = resolver_factory()  # Doesn't raise
> +    if missing_unbound:
> +        assert resolver._set_resolv_conf() is None
> +        return
> +    with mock.patch.object(resolver._ctx, "resolvconf",
> +                           side_effect=resolver._ctx.resolvconf) as c:
> +        if raises:
> +            with pytest.raises(raises):
> +                resolver._set_resolv_conf()
> +        else:
> +            resolver._set_resolv_conf()
> +        c.assert_called_once_with(file)
> +
> +
> +@pytest.mark.parametrize("file,raises",
> +                         [(None, False),
> +                          ("missing_file", dns_resolve.UnboundException),
> +                          ("hosts_file", False)])
> +def test_hosts(monkeypatch, missing_unbound, resolver_factory, request, file,
> +               raises):
> +    if file:
> +        file = str(request.getfixturevalue(file))
> +        monkeypatch.setenv("OVS_HOSTS_FILE", file)
> +    resolver = resolver_factory()  # Doesn't raise
> +    if missing_unbound:
> +        assert resolver._set_hosts_file() is None
> +        return
> +    with mock.patch.object(resolver._ctx, "hosts",
> +                           side_effect=resolver._ctx.hosts) as c:
> +        if raises:
> +            with pytest.raises(raises):
> +                resolver._set_hosts_file()
> +        else:
> +            resolver._set_hosts_file()
> +        c.assert_called_once_with(file)
> +
> +
> +def test_UnboundException(missing_unbound):
> +    with pytest.raises(dns_resolve.UnboundException):
> +        raise dns_resolve.UnboundException("Fake exception", -1)
> +
> +
> +@skip_no_unbound
> +@pytest.mark.parametrize("ip,host,expected", HOSTS)
> +def test_inet_parse_active(resolver_factory, ip, host, expected):
> +    resolver = resolver_factory()
> +
> +    def fn(name):
> +        # Return the same thing _resolve() would so we can call
> +        # this multiple times for the is_daemon=True case
> +        return socket_util.inet_parse_active(f"{name}:6640", 6640,
> +                                             raises=False)[0] or None
> +
> +    # parsing IPs still works
> +    IP = _resolve(resolver, ip, fn)
> +    assert ipaddress.ip_address(ip) == ipaddress.ip_address(IP)
> +    # parsing hosts works
> +    IP = _resolve(resolver, host, fn)
> +    assert ipaddress.ip_address(IP) == ipaddress.ip_address(expected)
> diff --git a/python/setup.py b/python/setup.py
> index 27684c404..bcf832ce9 100644
> --- a/python/setup.py
> +++ b/python/setup.py
> @@ -99,8 +99,7 @@ setup_args = dict(
>          'Topic :: System :: Networking',
>          'License :: OSI Approved :: Apache Software License',
>          'Programming Language :: Python :: 3',
> -        'Programming Language :: Python :: 3.4',
> -        'Programming Language :: Python :: 3.5',
> +        'Programming Language :: Python :: 3.6',
>      ],
>      ext_modules=[setuptools.Extension("ovs._json",
>                                        sources=["ovs/_json.c"],
> @@ -110,7 +109,8 @@ setup_args = dict(
>      cmdclass={'build_ext': try_build_ext},
>      install_requires=['sortedcontainers'],
>      extras_require={':sys_platform == "win32"': ['pywin32 >= 1.0'],
> -                    'flow': ['netaddr', 'pyparsing']},
> +                    'flow': ['netaddr', 'pyparsing'],
> +                    'dns': ['unbound']},
>  )
>
>  try:
> diff --git a/rhel/openvswitch-fedora.spec.in b/rhel/openvswitch-fedora.spec.in
> index 44899c1ca..343a5716d 100644
> --- a/rhel/openvswitch-fedora.spec.in
> +++ b/rhel/openvswitch-fedora.spec.in
> @@ -113,7 +113,7 @@ Summary: Open vSwitch python3 bindings
>  License: ASL 2.0
>  BuildArch: noarch
>  Requires: python3
> -Suggests: python3-netaddr python3-pyparsing
> +Suggests: python3-netaddr python3-pyparsing python3-unbound
>  %{?python_provide:%python_provide python3-openvswitch = 
> %{version}-%{release}}
>
>  %description -n python3-openvswitch
> diff --git a/tests/vlog.at b/tests/vlog.at
> index 3e92e70a9..785014956 100644
> --- a/tests/vlog.at
> +++ b/tests/vlog.at
> @@ -385,6 +385,7 @@ AT_CHECK([APPCTL -t test-unixctl.py vlog/list], [0], [dnl
>                   console    syslog    file
>                   -------    ------    ------
>  daemon            info       info       info
> +dns_resolve       info       info       info
>  fatal-signal      info       info       info
>  jsonrpc           info       info       info
>  poller            info       info       info
> @@ -404,6 +405,7 @@ unixctl_server    info       info       info
>                   console    syslog    file
>                   -------    ------    ------
>  daemon            info        err        dbg
> +dns_resolve       info       info        dbg
>  fatal-signal      info       info        dbg
>  jsonrpc           info       info        dbg
>  poller            info       info        dbg
> --
> 2.34.3
>


_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to