Update ping wrapper

- Allow to execute ping from inside network namespaces
- Refactor multi-ip ping api

Change-Id: I5d48ba39de2a4c94e6579b2a07017807b6a6e132
This commit is contained in:
Federico Ressi 2019-10-31 20:41:06 +01:00
parent 98b75fe60b
commit 49f37c6fe5
10 changed files with 153 additions and 71 deletions

View File

@ -23,9 +23,8 @@ from tobiko.shell.ping import _ping
from tobiko.shell.ping import _statistics
assert_reachable_ips = _assert.assert_reachable_ips
get_reachable_ips = _assert.get_reachable_ips
get_unreachable_ips = _assert.get_unreachable_ips
assert_reachable_hosts = _assert.assert_reachable_hosts
assert_unreachable_hosts = _assert.assert_unreachable_hosts
PingException = _exception.PingException
PingError = _exception.PingError
@ -42,11 +41,15 @@ ping_parameters = _parameters.ping_parameters
get_ping_parameters = _parameters.get_ping_parameters
default_ping_parameters = _parameters.default_ping_parameters
list_reachable_hosts = _ping.list_reachable_hosts
list_unreachable_hosts = _ping.list_unreachable_hosts
ping = _ping.ping
ping_hosts = _ping.ping_hosts
ping_until_delivered = _ping.ping_until_delivered
ping_until_undelivered = _ping.ping_until_undelivered
ping_until_received = _ping.ping_until_received
ping_until_unreceived = _ping.ping_until_unreceived
TRANSMITTED = _ping.TRANSMITTED
UNDELIVERED = _ping.UNDELIVERED
DELIVERED = _ping.DELIVERED

View File

@ -15,24 +15,22 @@
# under the License.
from __future__ import absolute_import
from oslo_log import log
import tobiko
from tobiko.shell.ping import _ping
def assert_reachable_ips(target_ips, **params):
unreachable_ips = get_unreachable_ips(target_ips, **params)
if unreachable_ips:
tobiko.fail("Unable to reach IP address(es): {!r}", unreachable_ips)
LOG = log.getLogger(__name__)
def get_reachable_ips(target_ips, **params):
return tobiko.select(address
for address in target_ips
if _ping.ping(address, **params).received)
def assert_reachable_hosts(hosts, **params):
unreachable_hosts = _ping.list_unreachable_hosts(hosts, **params)
if unreachable_hosts:
tobiko.fail("Unable to reach host(s): {!r}", unreachable_hosts)
def get_unreachable_ips(target_ips, **params):
reachable_ips = get_reachable_ips(target_ips, **params)
return tobiko.select(address
for address in target_ips
if address not in reachable_ips)
def assert_unreachable_hosts(hosts, **params):
reachable_hosts = _ping.list_reachable_hosts(hosts, **params)
if reachable_hosts:
tobiko.fail("Reached host(s): {!r}", reachable_hosts)

View File

@ -74,7 +74,7 @@ class PingInterfaceManager(tobiko.SharedFixture):
interfaces=self.interfaces)
interface = interface or self.default_interface
LOG.debug('Assign Ping interface %r to SSH client %r',
interface.ping_interface_name, ssh_client)
interface, ssh_client)
self.client_interfaces[ssh_client] = interface
return interface
@ -112,7 +112,6 @@ def ping_interface(interface_class):
class PingInterface(object):
ping_interface_name = 'default'
ping_usage = None
def match_ping_usage(self, usage):
@ -120,16 +119,23 @@ class PingInterface(object):
return False
def get_ping_command(self, parameters):
destination = parameters.host
if not destination:
host = parameters.host
if not host:
raise ValueError("Ping host destination hasn't been specified")
return ([self.get_ping_executable(parameters)] +
self.get_ping_options(parameters) +
[destination])
command = sh.shell_command([self.get_ping_executable(parameters)] +
self.get_ping_options(parameters) +
[host])
LOG.debug('Got ping command from interface %r for host %r: %r',
self, host, command)
return command
def get_ping_executable(self, parameters):
# pylint: disable=unused-argument
return 'ping'
ip_version = _parameters.get_ping_ip_version(parameters)
if ip_version == constants.IP_VERSION_6:
return 'ping6'
else:
return 'ping'
def get_ping_options(self, parameters):
options = []
@ -195,13 +201,16 @@ class PingInterface(object):
def get_fragment_option(self, fragment):
details = ("{!r} ping implementation doesn't support "
"'fragment={!r}' option").format(self.ping_interface_name,
fragment)
"'fragment={!r}' option").format(self, fragment)
raise _exception.UnsupportedPingOption(details=details)
class IpVersionPingInterface(PingInterface):
def get_ping_executable(self, parameters):
# pylint: disable=unused-argument
return 'ping'
def get_ipv4_option(self):
return ['-4']
@ -221,8 +230,6 @@ Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
@ping_interface
class IpUtilsPingInterface(PingInterface):
ping_interface_name = 'iputils'
def match_ping_usage(self, usage):
return usage.startswith(IPUTILS_PING_USAGE)
@ -253,8 +260,6 @@ Usage: ping -6 [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
class IpUtilsIpVersionPingInterface(IpUtilsPingInterface,
IpVersionPingInterface):
ping_interface_name = 'ip_version_iputils'
def match_ping_usage(self, usage):
return usage.startswith(IP_VERSION_IPUTILS_PING_USAGE)
@ -285,8 +290,6 @@ Send ICMP ECHO_REQUEST packets to network hosts
@ping_interface
class BusyBoxPingInterface(IpVersionPingInterface):
ping_interface_name = 'BusyBox'
def match_ping_usage(self, usage):
return usage.startswith("BusyBox")
@ -339,13 +342,6 @@ class InetToolsPingInterface(PingInterface):
def match_ping_usage(self, usage):
return usage.startswith(INET_TOOLS_PING_USAGE)
def get_ping_executable(self, parameters):
ip_version = _parameters.get_ping_ip_version(parameters)
if ip_version == constants.IP_VERSION_6:
return 'ping6'
else:
return 'ping'
def get_deadline_option(self, parameters):
ip_version = _parameters.get_ping_ip_version(parameters)
if ip_version == constants.IP_VERSION_6:

View File

@ -26,7 +26,7 @@ LOG = log.getLogger(__name__)
PING_PARAMETER_NAMES = ['host', 'count', 'deadline', 'fragmentation',
'interval', 'ip_version', 'packet_size', 'source',
'timeout']
'timeout', 'network_namespace']
def get_ping_parameters(default=None, **ping_params):
@ -45,7 +45,7 @@ def get_ping_parameters(default=None, **ping_params):
def ping_parameters(default=None, count=None, deadline=None,
fragmentation=None, host=None, interval=None,
ip_version=None, packet_size=None, source=None,
timeout=None):
timeout=None, network_namespace=None):
"""Validate parameters and initialize a new PingParameters instance
:param default: (PingParameters or None) instance from where to take
@ -105,7 +105,9 @@ def ping_parameters(default=None, count=None, deadline=None,
ip_version=get_positive_integer('ip_version', ip_version, default),
packet_size=get_positive_integer('packet_size', packet_size, default),
source=get_address('source', source, default),
timeout=get_positive_integer('timeout', timeout, default))
timeout=get_positive_integer('timeout', timeout, default),
network_namespace=get_string('network_namespace', network_namespace,
default))
def default_ping_parameters():
@ -196,6 +198,14 @@ def get_address(name, value, default=None):
return value
def get_string(name, value, default=None):
if value is None and default:
return get_string(name, getattr(default, name))
if value is not None:
value = str(value)
return value
IP_HEADER_SIZE = {4: 20, 6: 40}
ICMP_HEADER_SIZE = {4: 8, 6: 4}

View File

@ -20,6 +20,7 @@ import time
from oslo_log import log
import tobiko
from tobiko.shell import sh
from tobiko.shell.ping import _interface
from tobiko.shell.ping import _exception
@ -37,6 +38,33 @@ RECEIVED = 'received'
UNRECEIVED = 'unreceived'
def list_reachable_hosts(hosts, **params):
reachable_host, _ = ping_hosts(hosts, **params)
return reachable_host
def list_unreachable_hosts(hosts, **params):
_, unreachable_host = ping_hosts(hosts, **params)
return unreachable_host
def ping_hosts(hosts, **params):
reachable = tobiko.Selection()
unreachable = tobiko.Selection()
for host in hosts:
try:
result = ping(host, count=1, **params)
except _exception.PingError:
LOG.exception('Error pinging host: %r', host)
unreachable.append(host)
else:
if result.received:
reachable.append(host)
else:
unreachable.append(host)
return reachable, unreachable
def ping(host, until=TRANSMITTED, check=True, **ping_params):
"""Send ICMP messages to host address until timeout
@ -200,12 +228,18 @@ def iter_statistics(parameters=None, ssh_client=None, until=None, check=True,
transmitted += statistics.transmitted
received += statistics.received
undelivered += statistics.undelivered
count = {None: 0,
TRANSMITTED: transmitted,
DELIVERED: transmitted - undelivered,
UNDELIVERED: undelivered,
RECEIVED: received,
UNRECEIVED: transmitted - received}[until]
else:
# Assume 1 transmitted undelivered package when unable to get
# ping output
transmitted += 1
undelivered += 1
count = {None: 0,
TRANSMITTED: transmitted,
DELIVERED: transmitted - undelivered,
UNDELIVERED: undelivered,
RECEIVED: received,
UNRECEIVED: transmitted - received}[until]
now = time.time()
deadline = min(int(end_of_time - now), parameters.deadline)
@ -228,7 +262,8 @@ def execute_ping(parameters, ssh_client=None, check=True):
result = sh.execute(command=command,
ssh_client=ssh_client,
timeout=parameters.deadline + 2.,
expect_exit_status=None)
expect_exit_status=None,
network_namespace=parameters.network_namespace)
except sh.ShellError as ex:
LOG.exception("Error executing ping command")
stdout = ex.stdout

View File

@ -0,0 +1,19 @@
from __future__ import absolute_import
import tobiko
from tobiko.openstack import topology
from tobiko.shell import ip
class NetworkNamespaceFixture(tobiko.SharedFixture):
network_namespace = None
ssh_client = None
def setup_fixture(self):
for node in topology.list_openstack_nodes():
network_namespaces = ip.list_network_namespaces(
ssh_client=node.ssh_client)
if network_namespaces:
self.network_namespace = network_namespaces.first
self.ssh_client = node.ssh_client

View File

@ -21,9 +21,9 @@ import testtools
import tobiko
from tobiko.openstack import stacks
from tobiko.openstack import topology
from tobiko.shell import ip
from tobiko.shell import ssh
from tobiko.tests.functional.shell import fixtures
class IpTest(testtools.TestCase):
@ -37,6 +37,8 @@ class IpTest(testtools.TestCase):
ubuntu_stack = tobiko.required_setup_fixture(
stacks.UbuntuServerStackFixture)
namespace = tobiko.required_setup_fixture(fixtures.NetworkNamespaceFixture)
def test_list_ip_addresses(self, ip_version=None, scope=None,
**execute_params):
ips = ip.list_ip_addresses(ip_version=ip_version, scope=scope,
@ -100,23 +102,19 @@ class IpTest(testtools.TestCase):
self, **execute_params):
self.test_list_ip_addresses(scope='global', **execute_params)
def test_list_ip_addresses_with_namespace(self):
for node in topology.list_openstack_nodes():
network_namespaces = ip.list_network_namespaces(
ssh_client=node.ssh_client)
if network_namespaces:
network_namespace = network_namespaces.first
ssh_client = node.ssh_client
break
def test_list_ip_addresses_with_namespace(self, **params):
namespace_ips = ip.list_ip_addresses(
ssh_client=ssh_client, scope='global',
network_namespace=network_namespace)
ssh_client=self.namespace.ssh_client, **params,
network_namespace=self.namespace.network_namespace)
self.assertNotEqual([], namespace_ips)
host_ips = ip.list_ip_addresses(ssh_client=ssh_client, scope='global')
host_ips = ip.list_ip_addresses(ssh_client=self.namespace.ssh_client,
**params)
self.assertNotEqual(host_ips, namespace_ips)
def test_list_ip_addresses_with_namespace_and_scope(self):
self.test_list_ip_addresses_with_namespace(scope='global')
def test_list_namespaces(self, **execute_params):
namespaces = ip.list_network_namespaces(**execute_params)
self.assertIsInstance(namespaces, list)

View File

@ -18,8 +18,11 @@ from __future__ import absolute_import
import netaddr
import testtools
import tobiko
from tobiko import config
from tobiko.shell import ip
from tobiko.shell import ping
from tobiko.tests.functional.shell import fixtures
CONF = config.CONF
@ -27,6 +30,8 @@ CONF = config.CONF
class PingTest(testtools.TestCase):
namespace = tobiko.required_setup_fixture(fixtures.NetworkNamespaceFixture)
def test_ping_recheable_address(self):
result = ping.ping('127.0.0.1', count=3)
self.assertIsNone(result.source)
@ -90,3 +95,21 @@ class PingTest(testtools.TestCase):
self.assertEqual(1., ex.timeout)
self.assertEqual(20, ex.expected_count)
self.assertEqual('transmitted', ex.message_type)
def test_ping_hosts(self, ssh_client=None, network_namespace=None,
**params):
ips = ip.list_ip_addresses(ssh_client=ssh_client,
network_namespace=network_namespace)
reachable_ips, unrecheable_ips = ping.ping_hosts(
ips, ssh_client=ssh_client, network_namespace=network_namespace,
**params)
expected_reachable = [i for i in ips if i in reachable_ips]
self.assertEqual(expected_reachable, reachable_ips)
expected_unreachable = [i for i in ips if i not in reachable_ips]
self.assertEqual(expected_unreachable, unrecheable_ips)
def test_ping_hosts_from_network_namespace(self):
self.test_ping_hosts(
ssh_client=self.namespace.ssh_client,
network_namespace=self.namespace.network_namespace)

View File

@ -51,8 +51,8 @@ class PortTest(testtools.TestCase):
subnets = neutron.list_subnets(network_id=network_id)
gateway_ips = [netaddr.IPAddress(subnet['gateway_ip'])
for subnet in subnets]
ping.assert_reachable_ips(gateway_ips,
ssh_client=self.stack.ssh_client)
ping.assert_reachable_hosts(gateway_ips,
ssh_client=self.stack.ssh_client)
def test_ping_port(self, network_id=None, device_id=None):
network_id = network_id or self.stack.network_stack.network_id
@ -64,8 +64,8 @@ class PortTest(testtools.TestCase):
self.assertEqual(network_id, port['network_id'])
self.assertEqual(device_id, port['device_id'])
port_ips.update(neutron.list_port_ip_addresses(port=port))
ping.assert_reachable_ips(port_ips,
ssh_client=self.stack.ssh_client)
ping.assert_reachable_hosts(port_ips,
ssh_client=self.stack.ssh_client)
def test_ping_inner_gateway_ip(self):
if not self.stack.network_stack.has_gateway:

View File

@ -69,12 +69,12 @@ class RouterTest(testtools.TestCase):
self.external_gateway_ips)
def test_internal_router_ipv4_interface_is_reachable(self):
ping.assert_reachable_ips([self.ipv4_subnet_gateway_ip],
ssh_client=self.stack.ssh_client)
ping.assert_reachable_hosts([self.ipv4_subnet_gateway_ip],
ssh_client=self.stack.ssh_client)
def test_internal_router_ipv6_interface_is_reachable(self):
ping.assert_reachable_ips([self.ipv6_subnet_gateway_ip],
ssh_client=self.stack.ssh_client)
ping.assert_reachable_hosts([self.ipv6_subnet_gateway_ip],
ssh_client=self.stack.ssh_client)
def test_ipv4_subnet_gateway_ip(self):
self.assertEqual(4, self.ipv4_subnet_gateway_ip.version)