Handle multiple ping commands dialects
Change-Id: I05d8e2efe8ef91d836ec509c779435376b0d721a
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
from tobiko.shell.ping import _exception
|
from tobiko.shell.ping import _exception
|
||||||
|
from tobiko.shell.ping import _interface
|
||||||
from tobiko.shell.ping import _parameters
|
from tobiko.shell.ping import _parameters
|
||||||
from tobiko.shell.ping import _ping
|
from tobiko.shell.ping import _ping
|
||||||
from tobiko.shell.ping import _statistics
|
from tobiko.shell.ping import _statistics
|
||||||
@@ -28,6 +29,10 @@ BadAddressPingError = _exception.BadAddressPingError
|
|||||||
UnknowHostError = _exception.UnknowHostError
|
UnknowHostError = _exception.UnknowHostError
|
||||||
PingFailed = _exception.PingFailed
|
PingFailed = _exception.PingFailed
|
||||||
|
|
||||||
|
skip_if_missing_fragment_ping_option = (
|
||||||
|
_interface.skip_if_missing_fragment_ping_option)
|
||||||
|
has_ping_fragment_option = _interface.has_fragment_ping_option
|
||||||
|
|
||||||
ping_parameters = _parameters.ping_parameters
|
ping_parameters = _parameters.ping_parameters
|
||||||
get_ping_parameters = _parameters.get_ping_parameters
|
get_ping_parameters = _parameters.get_ping_parameters
|
||||||
default_ping_parameters = _parameters.default_ping_parameters
|
default_ping_parameters = _parameters.default_ping_parameters
|
||||||
|
@@ -51,3 +51,7 @@ class PingFailed(PingError, tobiko.FailureException):
|
|||||||
message = ("timeout of {timeout} seconds expired after counting only "
|
message = ("timeout of {timeout} seconds expired after counting only "
|
||||||
"{count} out of expected {expected_count} ICMP messages of "
|
"{count} out of expected {expected_count} ICMP messages of "
|
||||||
"type {message_type!r}")
|
"type {message_type!r}")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedPingOption(PingError):
|
||||||
|
pass
|
||||||
|
336
tobiko/shell/ping/_interface.py
Normal file
336
tobiko/shell/ping/_interface.py
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
# Copyright (c) 2019 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
from neutron_lib import constants
|
||||||
|
|
||||||
|
import tobiko
|
||||||
|
from tobiko.shell.ping import _exception
|
||||||
|
from tobiko.shell.ping import _parameters
|
||||||
|
from tobiko.shell import sh
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ping_command(parameters, ssh_client):
|
||||||
|
interface = get_ping_interface(ssh_client=ssh_client)
|
||||||
|
return interface.get_ping_command(parameters)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ping_interface(ssh_client):
|
||||||
|
manager = tobiko.setup_fixture(PingInterfaceManager)
|
||||||
|
interface = manager.get_ping_interface(ssh_client=ssh_client)
|
||||||
|
tobiko.check_valid_type(interface, PingInterface)
|
||||||
|
return interface
|
||||||
|
|
||||||
|
|
||||||
|
def has_fragment_ping_option(ssh_client=None):
|
||||||
|
interface = get_ping_interface(ssh_client=ssh_client)
|
||||||
|
return interface.has_fragment_option
|
||||||
|
|
||||||
|
|
||||||
|
skip_if_missing_fragment_ping_option = tobiko.skip_unless(
|
||||||
|
"requires (don't) fragment Ping option",
|
||||||
|
has_fragment_ping_option)
|
||||||
|
|
||||||
|
|
||||||
|
class PingInterfaceManager(tobiko.SharedFixture):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(PingInterfaceManager, self).__init__()
|
||||||
|
self.client_interfaces = {}
|
||||||
|
self.interfaces = []
|
||||||
|
self.default_interface = PingInterface()
|
||||||
|
|
||||||
|
def add_ping_interface(self, interface):
|
||||||
|
LOG.debug('Register ping interface %r', interface)
|
||||||
|
self.interfaces.append(interface)
|
||||||
|
|
||||||
|
def get_ping_interface(self, ssh_client):
|
||||||
|
try:
|
||||||
|
return self.client_interfaces[ssh_client]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
LOG.debug('Looking for ping interface for SSH client %s', ssh_client)
|
||||||
|
usage = get_ping_usage(ssh_client)
|
||||||
|
interface = find_ping_interface(usage=usage,
|
||||||
|
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)
|
||||||
|
self.client_interfaces[ssh_client] = interface
|
||||||
|
return interface
|
||||||
|
|
||||||
|
|
||||||
|
def get_ping_usage(ssh_client):
|
||||||
|
result = sh.execute('ping --help', expect_exit_status=None,
|
||||||
|
ssh_client=ssh_client)
|
||||||
|
usage = ((result.stdout and str(result.stdout)) or
|
||||||
|
(result.stderr and str(result.stderr)) or "").strip()
|
||||||
|
if usage:
|
||||||
|
LOG.debug('Got ping usage text:\n%s\n', usage)
|
||||||
|
else:
|
||||||
|
LOG.warning("Unable to get usage message from ping command:\n"
|
||||||
|
"%r", result)
|
||||||
|
return usage
|
||||||
|
|
||||||
|
|
||||||
|
def find_ping_interface(usage, interfaces):
|
||||||
|
if usage:
|
||||||
|
for interface in interfaces:
|
||||||
|
if interface.match_ping_usage(usage):
|
||||||
|
return interface
|
||||||
|
|
||||||
|
LOG.warning("No such ping interface class from usage message:\n"
|
||||||
|
"%r", usage)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ping_interface(interface_class):
|
||||||
|
assert issubclass(interface_class, PingInterface)
|
||||||
|
manager = tobiko.setup_fixture(PingInterfaceManager)
|
||||||
|
manager.add_ping_interface(interface=interface_class())
|
||||||
|
return interface_class
|
||||||
|
|
||||||
|
|
||||||
|
class PingInterface(object):
|
||||||
|
|
||||||
|
ping_command = 'ping'
|
||||||
|
ping_interface_name = 'default'
|
||||||
|
ping_usage = None
|
||||||
|
|
||||||
|
def match_ping_usage(self, usage):
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_ping_command(self, parameters):
|
||||||
|
destination = parameters.host
|
||||||
|
if not destination:
|
||||||
|
raise ValueError("Ping host destination hasn't been specified")
|
||||||
|
return ([self.ping_command] +
|
||||||
|
self.get_ping_options(parameters) +
|
||||||
|
[destination])
|
||||||
|
|
||||||
|
def get_ping_options(self, parameters):
|
||||||
|
options = []
|
||||||
|
|
||||||
|
ip_version = _parameters.get_ping_ip_version(parameters)
|
||||||
|
if ip_version == constants.IP_VERSION_4:
|
||||||
|
options += self.get_ipv4_option()
|
||||||
|
elif ip_version == constants.IP_VERSION_6:
|
||||||
|
options += self.get_ipv6_option()
|
||||||
|
elif ip_version is not None:
|
||||||
|
message = 'Invalid IP version: {!r}'.format(ip_version)
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
interface = parameters.source
|
||||||
|
if interface:
|
||||||
|
options += self.get_interface_option(interface)
|
||||||
|
|
||||||
|
deadline = parameters.deadline
|
||||||
|
if deadline > 0:
|
||||||
|
options += self.get_deadline_option(deadline)
|
||||||
|
|
||||||
|
count = parameters.count
|
||||||
|
if count > 0:
|
||||||
|
options += self.get_count_option(count)
|
||||||
|
|
||||||
|
size = _parameters.get_ping_payload_size(parameters)
|
||||||
|
if size:
|
||||||
|
options += self.get_size_option(size)
|
||||||
|
|
||||||
|
interval = parameters.interval
|
||||||
|
if interval > 1:
|
||||||
|
options += self.get_interval_option(interval)
|
||||||
|
|
||||||
|
fragment = parameters.fragmentation
|
||||||
|
if fragment is not None:
|
||||||
|
options += self.get_fragment_option(fragment=fragment)
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
def get_ipv4_option(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_ipv6_option(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_interface_option(self, interface):
|
||||||
|
return ['-I', interface]
|
||||||
|
|
||||||
|
def get_deadline_option(self, deadline):
|
||||||
|
return ['-w', deadline, '-W', deadline]
|
||||||
|
|
||||||
|
def get_count_option(self, count):
|
||||||
|
return ['-c', int(count)]
|
||||||
|
|
||||||
|
def get_size_option(self, size):
|
||||||
|
return ['-s', int(size)]
|
||||||
|
|
||||||
|
def get_interval_option(self, interval):
|
||||||
|
return ['i', int(interval)]
|
||||||
|
|
||||||
|
has_fragment_option = False
|
||||||
|
|
||||||
|
def get_fragment_option(self, fragment):
|
||||||
|
details = ("{!r} ping implementation doesn't support "
|
||||||
|
"'fragment={!r}' option").format(self.ping_interface_name,
|
||||||
|
fragment)
|
||||||
|
raise _exception.UnsupportedPingOption(details=details)
|
||||||
|
|
||||||
|
|
||||||
|
class IpVersionPingInterface(PingInterface):
|
||||||
|
|
||||||
|
def get_ipv4_option(self):
|
||||||
|
return ['-4']
|
||||||
|
|
||||||
|
def get_ipv6_option(self):
|
||||||
|
return ['-6']
|
||||||
|
|
||||||
|
|
||||||
|
IPUTILS_PING_USAGE = """
|
||||||
|
ping: invalid option -- '-'
|
||||||
|
Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
|
||||||
|
[-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
|
||||||
|
[-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
|
||||||
|
[-w deadline] [-W timeout] [hop1 ...] destination
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
@ping_interface
|
||||||
|
class IpUtilsPingInterface(PingInterface):
|
||||||
|
|
||||||
|
ping_interface_name = 'iputils'
|
||||||
|
|
||||||
|
def match_ping_usage(self, usage):
|
||||||
|
return usage.startswith(IPUTILS_PING_USAGE)
|
||||||
|
|
||||||
|
has_fragment_option = True
|
||||||
|
|
||||||
|
def get_fragment_option(self, fragment):
|
||||||
|
if fragment:
|
||||||
|
return ['-M', 'dont']
|
||||||
|
else:
|
||||||
|
return ['-M', 'do']
|
||||||
|
|
||||||
|
|
||||||
|
IP_VERSION_IPUTILS_PING_USAGE = """
|
||||||
|
ping: invalid option -- '-'
|
||||||
|
Usage: ping [-aAbBdDfhLnOqrRUvV64] [-c count] [-i interval] [-I interface]
|
||||||
|
[-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
|
||||||
|
[-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
|
||||||
|
[-w deadline] [-W timeout] [hop1 ...] destination
|
||||||
|
Usage: ping -6 [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
|
||||||
|
[-l preload] [-m mark] [-M pmtudisc_option]
|
||||||
|
[-N nodeinfo_option] [-p pattern] [-Q tclass] [-s packetsize]
|
||||||
|
[-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline]
|
||||||
|
[-W timeout] destination
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
@ping_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)
|
||||||
|
|
||||||
|
|
||||||
|
BUSYBOX_PING_USAGE = """
|
||||||
|
ping: unrecognized option `--usage'
|
||||||
|
BusyBox v1.23.2 (2017-11-20 02:37:12 UTC) multi-call binary.
|
||||||
|
|
||||||
|
Usage: ping [OPTIONS] HOST
|
||||||
|
|
||||||
|
Send ICMP ECHO_REQUEST packets to network hosts
|
||||||
|
|
||||||
|
-4,-6 Force IP or IPv6 name resolution
|
||||||
|
-c CNT Send only CNT pings
|
||||||
|
-s SIZE Send SIZE data bytes in packets (default:56)
|
||||||
|
-t TTL Set TTL
|
||||||
|
-I IFACE/IP Use interface or IP address as source
|
||||||
|
-W SEC Seconds to wait for the first response (default:10)
|
||||||
|
(after all -c CNT packets are sent)
|
||||||
|
-w SEC Seconds until ping exits (default:infinite)
|
||||||
|
(can exit earlier with -c CNT)
|
||||||
|
-q Quiet, only display output at start
|
||||||
|
and when finished
|
||||||
|
-p Pattern to use for payload
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
@ping_interface
|
||||||
|
class BusyBoxPingInterface(IpVersionPingInterface):
|
||||||
|
|
||||||
|
ping_interface_name = 'BusyBox'
|
||||||
|
|
||||||
|
def match_ping_usage(self, usage):
|
||||||
|
return usage.startswith("BusyBox")
|
||||||
|
|
||||||
|
|
||||||
|
INET_TOOLS_PING_USAGE = """
|
||||||
|
Usage: ping [OPTION...] HOST ...
|
||||||
|
Send ICMP ECHO_REQUEST packets to network hosts.
|
||||||
|
|
||||||
|
Options controlling ICMP request types:
|
||||||
|
--address send ICMP_ADDRESS packets (root only)
|
||||||
|
--echo send ICMP_ECHO packets (default)
|
||||||
|
--mask same as --address
|
||||||
|
--timestamp send ICMP_TIMESTAMP packets
|
||||||
|
-t, --type=TYPE send TYPE packets
|
||||||
|
|
||||||
|
Options valid for all request types:
|
||||||
|
|
||||||
|
-c, --count=NUMBER stop after sending NUMBER packets
|
||||||
|
-d, --debug set the SO_DEBUG option
|
||||||
|
-i, --interval=NUMBER wait NUMBER seconds between sending each packet
|
||||||
|
-n, --numeric do not resolve host addresses
|
||||||
|
-r, --ignore-routing send directly to a host on an attached network
|
||||||
|
--ttl=N specify N as time-to-live
|
||||||
|
-T, --tos=NUM set type of service (TOS) to NUM
|
||||||
|
-v, --verbose verbose output
|
||||||
|
-w, --timeout=N stop after N seconds
|
||||||
|
-W, --linger=N number of seconds to wait for response
|
||||||
|
|
||||||
|
Options valid for --echo requests:
|
||||||
|
|
||||||
|
-f, --flood flood ping (root only)
|
||||||
|
--ip-timestamp=FLAG IP timestamp of type FLAG, which is one of
|
||||||
|
"tsonly" and "tsaddr"
|
||||||
|
-l, --preload=NUMBER send NUMBER packets as fast as possible before
|
||||||
|
falling into normal mode of behavior (root only)
|
||||||
|
-p, --pattern=PATTERN fill ICMP packet with given pattern (hex)
|
||||||
|
-q, --quiet quiet output
|
||||||
|
-R, --route record route
|
||||||
|
-s, --size=NUMBER send NUMBER data octets
|
||||||
|
|
||||||
|
-?, --help give this help list
|
||||||
|
--usage give a short usage message
|
||||||
|
-V, --version print program version
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
@ping_interface
|
||||||
|
class InetToolsPingInterface(PingInterface):
|
||||||
|
|
||||||
|
def match_ping_usage(self, usage):
|
||||||
|
return usage.startswith(INET_TOOLS_PING_USAGE)
|
@@ -17,10 +17,11 @@ from __future__ import absolute_import
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from neutron_lib import constants
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
|
|
||||||
from tobiko.shell import sh
|
from tobiko.shell import sh
|
||||||
|
from tobiko.shell.ping import _interface
|
||||||
from tobiko.shell.ping import _exception
|
from tobiko.shell.ping import _exception
|
||||||
from tobiko.shell.ping import _parameters
|
from tobiko.shell.ping import _parameters
|
||||||
from tobiko.shell.ping import _statistics
|
from tobiko.shell.ping import _statistics
|
||||||
@@ -234,8 +235,8 @@ def iter_statistics(parameters=None, ssh_client=None, until=None, check=True,
|
|||||||
|
|
||||||
|
|
||||||
def execute_ping(parameters, ssh_client=None, check=True):
|
def execute_ping(parameters, ssh_client=None, check=True):
|
||||||
command = get_ping_command(parameters)
|
command = _interface.get_ping_command(parameters=parameters,
|
||||||
|
ssh_client=ssh_client)
|
||||||
result = sh.execute(command=command,
|
result = sh.execute(command=command,
|
||||||
ssh_client=ssh_client,
|
ssh_client=ssh_client,
|
||||||
timeout=parameters.deadline + 2.,
|
timeout=parameters.deadline + 2.,
|
||||||
@@ -246,47 +247,6 @@ def execute_ping(parameters, ssh_client=None, check=True):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_ping_command(parameters):
|
|
||||||
options = []
|
|
||||||
|
|
||||||
ip_version = _parameters.get_ping_ip_version(parameters)
|
|
||||||
|
|
||||||
ping_command = 'ping'
|
|
||||||
if ip_version == constants.IP_VERSION_6:
|
|
||||||
ping_command = 'ping6'
|
|
||||||
|
|
||||||
host = parameters.host
|
|
||||||
if not host:
|
|
||||||
raise ValueError("Ping host destination hasn't been specified")
|
|
||||||
|
|
||||||
source = parameters.source
|
|
||||||
if source:
|
|
||||||
options += ['-I', source]
|
|
||||||
|
|
||||||
deadline = parameters.deadline
|
|
||||||
if deadline > 0:
|
|
||||||
options += ['-w', deadline]
|
|
||||||
options += ['-W', deadline]
|
|
||||||
|
|
||||||
count = parameters.count
|
|
||||||
if count > 0:
|
|
||||||
options += ['-c', int(count)]
|
|
||||||
|
|
||||||
payload_size = _parameters.get_ping_payload_size(parameters)
|
|
||||||
if payload_size:
|
|
||||||
options += ['-s', int(payload_size)]
|
|
||||||
|
|
||||||
interval = parameters.interval
|
|
||||||
if interval > 1:
|
|
||||||
options += ['-i', int(interval)]
|
|
||||||
|
|
||||||
fragmentation = parameters.fragmentation
|
|
||||||
if fragmentation is False:
|
|
||||||
options += ['-M', 'do']
|
|
||||||
|
|
||||||
return [ping_command] + options + [host]
|
|
||||||
|
|
||||||
|
|
||||||
def handle_ping_command_error(error):
|
def handle_ping_command_error(error):
|
||||||
for error in error.splitlines():
|
for error in error.splitlines():
|
||||||
error = error.strip()
|
error = error.strip()
|
||||||
|
@@ -28,8 +28,8 @@ OPTIONS = [
|
|||||||
help="Max seconds waited from ping command before "
|
help="Max seconds waited from ping command before "
|
||||||
"self terminating himself"),
|
"self terminating himself"),
|
||||||
cfg.StrOpt('fragmentation',
|
cfg.StrOpt('fragmentation',
|
||||||
default=True,
|
default=None,
|
||||||
help="If disable it will not allow ICMP messages to "
|
help="If False it will not allow ICMP messages to "
|
||||||
"be delivered in smaller fragments"),
|
"be delivered in smaller fragments"),
|
||||||
cfg.StrOpt('interval',
|
cfg.StrOpt('interval',
|
||||||
default=1,
|
default=1,
|
||||||
|
@@ -93,6 +93,7 @@ class FloatingIPTest(testtools.TestCase):
|
|||||||
|
|
||||||
# --- test net-mtu and net-mtu-writable extensions ------------------------
|
# --- test net-mtu and net-mtu-writable extensions ------------------------
|
||||||
|
|
||||||
|
@ping.skip_if_missing_fragment_ping_option
|
||||||
@neutron.skip_if_missing_networking_extensions('net-mtu')
|
@neutron.skip_if_missing_networking_extensions('net-mtu')
|
||||||
def test_ping_with_net_mtu(self):
|
def test_ping_with_net_mtu(self):
|
||||||
"""Test connectivity to floating IP address with MTU sized packets"""
|
"""Test connectivity to floating IP address with MTU sized packets"""
|
||||||
@@ -162,6 +163,7 @@ class FloatingIPWithPortSecurityTest(FloatingIPTest):
|
|||||||
count=5,
|
count=5,
|
||||||
check=False).assert_not_replied()
|
check=False).assert_not_replied()
|
||||||
|
|
||||||
|
@ping.skip_if_missing_fragment_ping_option
|
||||||
@neutron.skip_if_missing_networking_extensions('net-mtu')
|
@neutron.skip_if_missing_networking_extensions('net-mtu')
|
||||||
def test_ping_with_net_mtu(self):
|
def test_ping_with_net_mtu(self):
|
||||||
"""Test connectivity to floating IP address"""
|
"""Test connectivity to floating IP address"""
|
||||||
|
Reference in New Issue
Block a user