2
0
mirror of https://github.com/openvswitch/ovs synced 2025-09-02 23:35:27 +00:00

python: Add async DNS support.

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 <twilson@redhat.com>
Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
This commit is contained in:
Terry Wilson
2023-07-11 22:55:52 -05:00
committed by Ilya Maximets
parent 501f665a5a
commit 4d55a364ff
16 changed files with 615 additions and 17 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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`_.

View File

@@ -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.

3
NEWS
View File

@@ -50,6 +50,9 @@ Post-v3.1.0
table to check the status.
- Linux TC offload:
* Add support for offloading VXLAN tunnels with the GBP extensions.
- Python
* Added async DNS support.
* Dropped support for Python < 3.6.
v3.1.0 - 16 Feb 2023

1
debian/control.in vendored
View File

@@ -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

View File

@@ -375,16 +375,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 python3.6 python3.7 python3.8 python3.9 python3.10 python3.11 python3.12; do
ovs_save_IFS=$IFS; IFS=$PATH_SEPARATOR
for dir in $PATH; do
IFS=$ovs_save_IFS
@@ -401,7 +401,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])

View File

@@ -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.

View File

@@ -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 \

286
python/ovs/dns_resolve.py Normal file
View File

@@ -0,0 +1,286 @@
# 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 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
_global_resolver: typing.Optional[DNSResolver] = None
def init(is_daemon: bool = False) -> DNSResolver:
"""Initialize a global DNSResolver
See DNSResolver.__init__ for more details
"""
global _global_resolver
_global_resolver = DNSResolver(is_daemon)
return _global_resolver
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
"""
if _global_resolver is None:
init()
# mypy doesn't understand that init() sets _global_resolver, so ignore type
return _global_resolver.resolve(name) # type: ignore
def destroy():
"""Destroy the global DNSResolver
This destroys the global DNSResolver instance and any outstanding
asynchronouse requests.
"""
global _global_resolver
del _global_resolver
_global_resolver = None # noqa: F841

View File

@@ -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

View File

@@ -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:

View File

@@ -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, hosts_file, request):
# Allow delaying the instantiation of the DNSResolver
def resolver_factory():
with monkeypatch.context() as m:
m.setenv("OVS_HOSTS_FILE", str(hosts_file))
# Test with both is_daemon False and True
resolver = dns_resolve.init(request.param)
assert resolver._is_daemon == request.param
return resolver
return resolver_factory
@contextlib.contextmanager
def DNSResolver(*args, **kwargs):
"""Clean up after returning a dns_resolver.DNSResolver"""
resolver = dns_resolve.init(*args, **kwargs)
try:
yield resolver
finally:
dns_resolve.destroy()
assert dns_resolve._global_resolver is 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 = "\n".join(f"{ip}\t{host}" for ip, host, _ in HOSTS)
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 _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 and 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)
@pytest.mark.parametrize("ip,host,expected", HOSTS)
def test_resolve_without_init(monkeypatch, missing_unbound, ip, host, expected,
hosts_file):
# make sure we don't have a global resolver
dns_resolve.destroy()
with monkeypatch.context() as m:
m.setenv("OVS_HOSTS_FILE", str(hosts_file))
if missing_unbound:
with pytest.raises(LookupError):
_resolve(None, host)
else:
res = _resolve(None, host)
assert dns_resolve._global_resolver is not None
assert dns_resolve._global_resolver._is_daemon is False
assert ipaddress.ip_address(expected) == ipaddress.ip_address(res)
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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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