Implement ping command wrapper.

Change-Id: Ica9ccfe0bd38bd05e5c6f9c18069f885fdc0e925
This commit is contained in:
Federico Ressi 2019-04-08 12:41:36 +02:00
parent 04c3e72748
commit 0c9d9e9760
8 changed files with 991 additions and 1 deletions

View File

@ -27,7 +27,8 @@ LOG = log.getLogger(__name__)
CONFIG_MODULES = ['tobiko.openstack.keystone.config',
'tobiko.openstack.neutron.config',
'tobiko.openstack.nova.config',
'tobiko.shell.sh.config']
'tobiko.shell.sh.config',
'tobiko.shell.ping.config']
CONFIG_DIRS = [os.getcwd(),
os.path.expanduser("~/.tobiko"),

View File

@ -0,0 +1,41 @@
# 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 tobiko.shell.ping import _exception
from tobiko.shell.ping import _parameters
from tobiko.shell.ping import _ping
from tobiko.shell.ping import _statistics
PingException = _exception.PingException
PingError = _exception.PingError
LocalPingError = _exception.LocalPingError
BadAddressPingError = _exception.BadAddressPingError
UnknowHostError = _exception.UnknowHostError
PingFailed = _exception.PingFailed
ping_parameters = _parameters.ping_parameters
get_ping_parameters = _parameters.get_ping_parameters
default_ping_parameters = _parameters.default_ping_parameters
ping = _ping.ping
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
PingStatistics = _statistics.PingStatistics

View File

@ -0,0 +1,53 @@
# 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
import tobiko
class PingException(tobiko.TobikoException):
"""Base ping command exception"""
class PingError(PingException):
"""Base ping error"""
message = ("%(details)s")
class LocalPingError(PingError):
"""Raised when local error happens"""
class SendToPingError(PingError):
"""Raised when sendto error happens"""
class UnknowHostError(PingError):
"""Raised when unable to resolve host name"""
class BadAddressPingError(PingError):
"""Raised when passing wrong address to ping command"""
message = ("Bad address: %(address)r")
class PingFailed(PingError, tobiko.FailureException):
"""Raised when ping timeout expires before reaching expected message count
"""
message = ("Timeout of %(timeout)d seconds expired after counting only "
"%(count)d out of expected %(expected_count)d ICMP messages of "
"type %(message_type)r.")

View File

@ -0,0 +1,201 @@
# 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
class PingParameters(object):
"""Recollect parameters to be used to format ping command line
PingParameters class is a data model recollecting parameters used to
create a ping command line. It provides the feature of copying default
values from another instance of PingParameters passed using constructor
parameter 'default'.
"""
def __init__(self, host=None, count=None, deadline=None,
fragmentation=None, interval=None, is_cirros_image=None,
ip_version=None, payload_size=None, packet_size=None,
source=None, timeout=None):
self.count = count
self.deadline = deadline
self.host = host
self.fragmentation = fragmentation
self.interval = interval
self.ip_version = ip_version
self.is_cirros_image = is_cirros_image
self.packet_size = packet_size
self.payload_size = payload_size
self.source = source
self.timeout = timeout
def __repr__(self):
return "PingParameters({!s})".format(
", ".join("{!s}={!r}".format(k, v)
for k, v in self.__dict__.items()))
def get_ping_parameters(default=None, **ping_params):
"""Get ping parameters eventually merging them with given extra parameters
The only difference with init_parameters function is that in case
default parameter is not None not any extra parameters is given, then
it simply return given default instance, without performing any validation.
"""
if default and not ping_params:
return default
else:
return ping_parameters(default=default, **ping_params)
def ping_parameters(default=None, count=None, deadline=None,
fragmentation=None, host=None, interval=None,
is_cirros_image=None, ip_version=None, packet_size=None,
payload_size=None, source=None, timeout=None):
"""Validate parameters and initialize a new PingParameters instance
:param default: (PingParameters or None) instance from where to take
default values when other value is not provided. If None (that is the
default value) it will copy default parameters from
DEFAULT_PING_PARAMETERS
:param count: (int or None) number of ping ICMP message expecting to be
received before having a success. Default value can be configured using
'count' option in [ping] config section.
:param host: (str or None) IP address or host name to send ICMP
messages to. It is required to format a valid ping command, therefore
no default value exists for this parameter.
:param deadline: (int or None) positive number representing the maximum
number of seconds ping command can send ICMP messages before stop
executing. Default value can be configured using 'deadline' option
in [ping] config section.
:param fragmentation: (bool or None) when False this would tell ping
to forbid ICMP messages fragmentation. Default value can be configured
using 'fragmentation' option in [ping] config section.
:param interval: (int or None) interval of time before sending following
ICMP message. Default value can be configured using 'interval' option
in [ping] config section.
:param is_cirros_image: (bool or None) when True means that ping command
has to be formated for being executed on a CirrOS based guess instance.
:param ip_version: (4, 6 or None) If not None it makes sure it will
use specified IP version for sending ICMP packages.
:param packet_size: ICMP message size. Default value can be configured
using 'package_size' option in [ping] config section.
:param payload_size: (int or None) if not None, it specifies ICMP message
size minus ICMP and IP header size.
:param source: (str or None) IP address or interface name from where
to send ICMP message.
:param timeout: (int or None) time in seconds after which ping operation
would raise PingFailed exception.
:raises TypeError: in case some parameter cannot be converted to right
expected type
:raises ValueError: in case some parameter has an unexpected value
"""
if packet_size:
if payload_size:
msg = ("Can't set 'package_size' and 'payload_size' parameters "
"at the same time: package_size={!r}, payload_size={!r}"
).format(packet_size, payload_size)
raise ValueError(msg)
count = count or 1
if count < 1:
msg = ("Count is not positive: count={!r}").format(count)
raise ValueError(msg)
if default is not False:
default = default or default_ping_parameters()
# Copy default parameters
count = count or default.count
if deadline is None:
deadline = default.deadline
host = host or default.host
if fragmentation is None:
fragmentation = default.fragmentation
interval = interval or default.interval
ip_version = ip_version or default.ip_version
packet_size = packet_size or default.packet_size
payload_size = payload_size or default.payload_size
source = source or default.source
timeout = timeout or default.timeout
count = int(count)
if count < 1:
msg = "'count' parameter cannot be smaller than 1"
raise ValueError(msg)
deadline = int(deadline)
if deadline < 0:
msg = ("'deadline' parameter cannot be smaller than 0 "
"(deadline={!r})").format(deadline)
raise ValueError(msg)
interval = int(interval)
if interval < 1:
msg = ("'interval' parameter cannot be smaller than 1 "
"(interval={!r})").format(interval)
raise ValueError(msg)
timeout = int(timeout)
if timeout < 1:
msg = ("'timeout' parameter cannot be smaller than 1 "
"(timeout={!r})").format(timeout)
raise ValueError(msg)
if is_cirros_image:
if fragmentation is False:
msg = ("'fragmentation' parameter cannot be False when "
"pinging from a CirrOS image (is_cirros_image={!r})"
).format(is_cirros_image)
raise ValueError(msg)
if interval != 1:
msg = ("Cannot specify 'interval' parameter when pinging from a "
"CirrOS image (interval={!r}, is_cirros_image={!r})"
).format(interval, is_cirros_image)
raise ValueError(msg)
return PingParameters(count=count, host=host,
deadline=deadline, fragmentation=fragmentation,
interval=interval, is_cirros_image=is_cirros_image,
ip_version=ip_version, packet_size=packet_size,
payload_size=payload_size, source=source,
timeout=timeout)
def default_ping_parameters():
from tobiko import config
CONF = config.CONF
return ping_parameters(
default=False,
count=CONF.tobiko.ping.count,
deadline=CONF.tobiko.ping.deadline,
fragmentation=CONF.tobiko.ping.fragmentation,
interval=CONF.tobiko.ping.interval,
packet_size=CONF.tobiko.ping.packet_size,
timeout=CONF.tobiko.ping.timeout)

361
tobiko/shell/ping/_ping.py Normal file
View File

@ -0,0 +1,361 @@
# 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
import time
import netaddr
from neutron_lib import constants
from oslo_log import log
from tobiko.shell import sh
from tobiko.shell.ping import _exception
from tobiko.shell.ping import _parameters
from tobiko.shell.ping import _statistics
LOG = log.getLogger(__name__)
TRANSMITTED = 'transmitted'
DELIVERED = 'delivered'
UNDELIVERED = 'undelivered'
RECEIVED = 'received'
UNRECEIVED = 'unreceived'
def ping(host, until=TRANSMITTED, **ping_params):
"""Send ICMP messages to host address until timeout
:param host: destination host address
:param **ping_params: parameters to be forwarded to get_statistics()
function
:returns: PingStatistics
"""
return get_statistics(host=host, until=until, **ping_params)
def ping_until_delivered(host, **ping_params):
"""Send 'count' ICMP messages
Send 'count' ICMP messages
ICMP messages are considered delivered when they have been
transmitted without being counted as errors.
:param host: destination host address
:param **ping_params: parameters to be forwarded to get_statistics()
function
:returns: PingStatistics
:raises: PingFailed in case timeout expires before delivering all
expected count messages
"""
return ping(host=host, until=DELIVERED, **ping_params)
def ping_until_undelivered(host, **ping_params):
"""Send ICMP messages until it fails to deliver messages
Send ICMP messages until it fails to deliver 'count' messages
ICMP messages are considered undelivered when they have been
transmitted and they have been counted as error in ping statistics (for
example because of errors into the route to remote address).
:param host: destination host address
:param **ping_params: parameters to be forwarded to get_statistics()
function
:returns: PingStatistics
:raises: PingFailed in case timeout expires before failing delivering
expected 'count' of messages
"""
return ping(host=host, until=UNDELIVERED, **ping_params)
def ping_until_received(host, **ping_params):
"""Send ICMP messages until it receives messages back
Send ICMP messages until it receives 'count' messages back
ICMP messages are considered received when they have been
transmitted without any routing errors and they are received back
:param host: destination host address
:param **ping_params: parameters to be forwarded to get_statistics()
function
:returns: PingStatistics
:raises: PingFailed in case timeout expires before receiving all
expected 'count' of messages
"""
return ping(host=host, until=RECEIVED, **ping_params)
def ping_until_unreceived(host, **ping_params):
"""Send ICMP messages until it fails to receive messages
Send ICMP messages until it fails to receive 'count' messages back.
ICMP messages are considered unreceived when they have been
transmitted without any routing error but they failed to be received
back (for example because of network filtering).
:param host: destination host address
:param **ping_params: parameters to be forwarded to get_statistics()
function
:returns: PingStatistics
:raises: PingFailed in case timeout expires before failed receiving
expected 'count' of messages
"""
return ping(host=host, until=UNRECEIVED, **ping_params)
def get_statistics(parameters=None, ssh_client=None, until=None,
**ping_params):
parameters = _parameters.get_ping_parameters(default=parameters,
**ping_params)
statistics = _statistics.PingStatistics()
for partial_statistics in iter_statistics(parameters=parameters,
ssh_client=ssh_client,
until=until):
statistics += partial_statistics
LOG.debug("%r", statistics)
return statistics
def iter_statistics(parameters=None, ssh_client=None, until=None,
**ping_params):
parameters = _parameters.get_ping_parameters(default=parameters,
**ping_params)
now = time.time()
end_of_time = now + parameters.timeout
deadline = parameters.deadline
transmitted = 0
received = 0
undelivered = 0
count = 0
while deadline > 0 and count < parameters.count:
# splitting total timeout interval into smaller deadline intervals will
# cause ping command to be executed more times allowing to handle
# temporary packets routing problems
if until == RECEIVED:
execute_parameters = _parameters.get_ping_parameters(
default=parameters,
deadline=deadline,
count=(parameters.count - count))
else:
# Deadline ping parameter cause ping to be executed until count
# messages are received or deadline is expired
# Therefore to count messages not of received type we have to
# simulate deadline parameter limiting the maximum number of
# transmitted messages
execute_parameters = _parameters.get_ping_parameters(
default=parameters,
deadline=deadline,
count=min(parameters.count - count,
parameters.interval * deadline))
# Command timeout would typically give ping command additional seconds
# to safely reach deadline before shell command timeout expires, while
# in the same time adding an extra verification to forbid using more
# time than expected considering the time required to make SSH
# connection and running a remote shell
try:
result = execute_ping(parameters=execute_parameters,
ssh_client=ssh_client,
timeout=end_of_time - now)
except sh.ShellTimeoutExpired:
pass
else:
if result.exit_status is not None:
statistics = _statistics.parse_ping_statistics(
output=result.stdout, begin_interval=now,
end_interval=time.time())
yield statistics
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]
now = time.time()
deadline = min(int(end_of_time - now), parameters.deadline)
if until and count < parameters.count:
raise _exception.PingFailed(count=count,
expected_count=parameters.count,
timeout=parameters.timeout,
message_type=until)
def execute_ping(parameters, ssh_client=None, **params):
if not ssh_client:
is_cirros_image = params.setdefault('is_cirros_image', False)
if is_cirros_image:
raise ValueError("'ssh_client' parameter is required when "
"to execute ping on a CirrOS image.")
command = get_ping_command(parameters)
result = sh.execute(command=command, ssh_client=ssh_client,
timeout=parameters.timeout, check=False)
if result.exit_status:
handle_ping_command_error(error=result.stderr)
return result
def get_ping_command(parameters):
options = []
ip_version = parameters.ip_version
host = parameters.host
if host:
try:
host = netaddr.IPAddress(host)
except netaddr.core.AddrFormatError:
# NOTE: host could be an host name not an IP address so
# this is fine
LOG.debug("Unable to obtain IP version from host address: %r",
host)
else:
if ip_version != host.version:
if ip_version:
raise ValueError("Mismatching destination IP version.")
else:
ip_version = host.version
else:
raise ValueError("Ping host destination hasn't been specified")
source = parameters.source
if source:
try:
source = netaddr.IPAddress(source)
except netaddr.core.AddrFormatError:
# NOTE: source could be a device name and not an IP address
# so this is fine
LOG.debug("Unable to obtain IP version from source address: "
"%r", source)
else:
if ip_version != source.version:
if ip_version:
raise ValueError("Mismatching source IP version.")
else:
ip_version = source.version
options += ['-I', source]
is_cirros_image = parameters.is_cirros_image
ping_command = 'ping'
if not ip_version:
LOG.warning("Unable to obtain IP version from neither source "
"or destination IP addresses: assuming IPv4")
ip_version = constants.IP_VERSION_4
elif ip_version == constants.IP_VERSION_6:
if is_cirros_image:
options += ['-6']
else:
ping_command = 'ping6'
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.payload_size
packet_size = parameters.packet_size
if not payload_size and packet_size:
payload_size = get_icmp_payload_size(package_size=int(packet_size),
ip_version=ip_version)
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:
if is_cirros_image:
msg = ("'is_cirros_image' parameter must be set to False"
" when 'fragmention' parameter is False "
"(is_cirros_image={!r})").format(is_cirros_image)
raise ValueError(msg)
options += ['-M', 'do']
return [ping_command] + options + [host]
def handle_ping_command_error(error):
for error in error.splitlines():
error = error.strip()
if error:
prefix = 'ping: '
if error.startswith('ping: '):
text = error[len(prefix):]
prefix = 'bad address '
if text.startswith(prefix):
address = text[len(prefix):].replace("'", '').strip()
raise _exception.BadAddressPingError(address=address)
prefix = 'local error: '
if text.startswith(prefix):
details = text[len(prefix):].strip()
raise _exception.LocalPingError(details=details)
prefix = 'sendto: '
if text.startswith(prefix):
details = text[len(prefix):].strip()
raise _exception.SendToPingError(details=details)
prefix = 'unknown host '
if text.startswith(prefix):
details = text[len(prefix):].strip()
raise _exception.UnknowHostError(details=details)
suffix = ': Name or service not known'
if text.endswith(suffix):
details = text[:-len(suffix)].strip()
raise _exception.UnknowHostError(details=details)
raise _exception.PingError(details=text)
IP_HEADER_SIZE = {4: 20, 6: 40}
ICMP_HEADER_SIZE = {4: 8, 6: 4}
def get_icmp_payload_size(package_size, ip_version):
"""Return the maximum size of ping payload that will fit into MTU."""
header_size = IP_HEADER_SIZE[ip_version] + ICMP_HEADER_SIZE[ip_version]
if package_size < header_size:
message = ("package size {package_size!s} is smaller than package "
"header size {header_size!s}").format(
package_size=package_size,
header_size=header_size)
raise ValueError(message)
return package_size - header_size

View File

@ -0,0 +1,192 @@
# 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 __future__ import division
import re
from oslo_log import log
import netaddr
import tobiko
LOG = log.getLogger(__name__)
def parse_ping_statistics(output, begin_interval=None, end_interval=None):
lines = output.split('\n')
line_it = iter(lines)
try:
source, destination = parse_ping_header(line_it)
except Exception as ex:
LOG.debug('Error parsing ping output header: %s', ex)
source = destination = None
try:
transmitted, received, errors = parse_ping_footer(line_it)
except Exception as ex:
LOG.debug('Error parsing ping output footer: %s', ex)
transmitted = received = errors = 0
return PingStatistics(source=source, destination=destination,
transmitted=transmitted, received=received,
undelivered=errors, end_interval=end_interval,
begin_interval=begin_interval)
def parse_ping_header(line_it):
for line in line_it:
if line.startswith('PING '):
header = line
break
else:
raise ValueError('Ping output header not found')
header_fields = [f.strip() for f in header.split()]
header_fields = [(f[:-1] if f.endswith(':') else f)
for f in header_fields]
destination = header_fields[2]
if destination[0] == '(':
destination = destination[1:]
if destination[-1] == ')':
destination = destination[:-1]
destination = netaddr.IPAddress(destination)
field_it = iter(header_fields[3:])
source = None
for field in field_it:
if field == 'from':
source = next(field_it).strip()
if source.endswith(':'):
source = source[:-1]
source = netaddr.IPAddress(source)
break
return source, destination
def parse_ping_footer(line_it):
for line in line_it:
if line.startswith('--- ') and line.endswith(' ping statistics ---'):
# parse footer
footer = next(line_it).strip()
break
else:
raise ValueError('Ping output footer not found')
transmitted = received = errors = 0
for field in footer.split(','):
try:
if 'transmitted' in field:
transmitted = extract_integer(field)
elif 'received' in field:
received = extract_integer(field)
elif 'error' in field:
errors = extract_integer(field)
except Exception as ex:
LOG.exception('Error parsing ping output footer: %s', ex)
return transmitted, received, errors
def extract_integer(field):
for number in extract_integers(field):
return number
raise ValueError("Integer not found in {!r}".format(field))
MATCH_NUMBERS_RE = re.compile('([0-9]).')
def extract_integers(field):
for match_obj in MATCH_NUMBERS_RE.finditer(field):
yield int(field[match_obj.start():match_obj.end()])
class PingStatistics(object):
"""Ping command statistics
"""
def __init__(self, source=None, destination=None, transmitted=0,
received=0, undelivered=0, begin_interval=None,
end_interval=None):
self.source = source
self.destination = destination
self.transmitted = transmitted
self.received = received
self.undelivered = undelivered
self.begin_interval = begin_interval
self.end_interval = end_interval
@property
def unreceived(self):
return max(0, self.transmitted - self.received)
@property
def delivered(self):
return max(0, self.transmitted - self.undelivered)
@property
def loss(self):
transmitted = max(0, float(self.transmitted))
if transmitted:
return float(self.unreceived) / transmitted
else:
return 0.
def __bool__(self):
return bool(self.received)
def __add__(self, other):
begin_interval = min(i for i in [self.begin_interval,
other.begin_interval] if i)
end_interval = max(i for i in [self.end_interval,
other.end_interval] if i)
return PingStatistics(
source=self.source or other.source,
destination=self.destination or other.destination,
transmitted=self.transmitted + other.transmitted,
received=self.received + other.received,
undelivered=self.undelivered + other.undelivered,
begin_interval=begin_interval,
end_interval=end_interval)
def __repr__(self):
return "PingStatistics({!s})".format(
", ".join("{!s}={!r}".format(k, v)
for k, v in self.__dict__.items()))
def assert_transmitted(self):
if not self.transmitted:
tobiko.fail("Any package has been transmitted to %(destination)r",
destination=self.destination)
def assert_not_transmitted(self):
if self.transmitted:
tobiko.fail("Some packages has been transmitted to "
"%(destination)r", destination=self.destination)
def assert_replied(self):
if not self.received:
tobiko.fail("Any reply package has been received from "
"%(destination)r", destination=self.destination)
def assert_not_replied(self):
if self.received:
tobiko.fail("Some reply packages has been received from "
"%(destination)r", destination=self.destination)

View File

@ -0,0 +1,47 @@
# Copyright 2019 Red Hat
#
# 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_config import cfg
def register_tobiko_options(conf):
conf.register_opts(
group=cfg.OptGroup('ping'),
opts=[cfg.IntOpt('count',
default=1,
help="Number of ICMP messages to wait before ending "
"ping command execution"),
cfg.IntOpt('deadline',
default=5,
help="Max seconds waited from ping command before "
"self terminating himself"),
cfg.StrOpt('fragmentation',
default=True,
help="If disable it will not allow ICMP messages to "
"be delivered in smaller fragments"),
cfg.StrOpt('interval',
default=1,
help="Seconds of time interval between "
"consecutive before ICMP messages"),
cfg.IntOpt('packet_size',
default=None,
help="Size in bytes of ICMP messages (including "
"headers and payload)"),
cfg.IntOpt('timeout',
default=60.,
help="Maximum time in seconds a sequence of ICMP "
"messages is sent to a destination host before "
"reporting as a failure")])

View File

@ -0,0 +1,94 @@
# 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
import netaddr
from tobiko import config
from tobiko.shell import ping
from tobiko.tests import unit
CONF = config.CONF
class PingTest(unit.TobikoUnitTest):
def setUp(self):
super(PingTest, self).setUp()
self.config = self.patch_object(CONF.tobiko, 'ping',
count=1,
deadline=5,
timeout=60,
fragmentation=True,
interval=1,
packet_size=None)
def test_ping_recheable_address(self):
result = ping.ping('127.0.0.1', count=3)
self.assertIsNone(result.source)
self.assertEqual(netaddr.IPAddress('127.0.0.1'), result.destination)
result.assert_transmitted()
result.assert_replied()
def test_ping_reachable_hostname(self):
result = ping.ping('example.org', count=3)
self.assertIsNone(result.source)
# self.assertIsNotNone(result.destination)
result.assert_transmitted()
result.assert_replied()
def test_ping_unreachable_address(self):
result = ping.ping('1.2.3.4', count=3)
self.assertIsNone(result.source)
self.assertEqual(netaddr.IPAddress('1.2.3.4'), result.destination)
result.assert_transmitted()
result.assert_not_replied()
def test_ping_unreachable_hostname(self):
ex = self.assertRaises(ping.UnknowHostError, ping.ping,
'unreachable-host', count=3)
self.assertEqual('unreachable-host', ex.details)
def test_ping_until_received(self):
result = ping.ping_until_received('127.0.0.1', count=3)
self.assertIsNone(result.source)
self.assertEqual(netaddr.IPAddress('127.0.0.1'), result.destination)
result.assert_transmitted()
result.assert_replied()
def test_ping_until_received_unreachable(self):
ex = self.assertRaises(ping.PingFailed, ping.ping_until_received,
'1.2.3.4', count=3, timeout=6)
self.assertEqual(6, ex.timeout)
self.assertEqual(0, ex.count)
self.assertEqual(3, ex.expected_count)
self.assertEqual('received', ex.message_type)
def test_ping_until_unreceived_recheable(self):
ex = self.assertRaises(ping.PingFailed, ping.ping_until_unreceived,
'127.0.0.1', count=3, timeout=6)
self.assertEqual(6, ex.timeout)
self.assertEqual(0, ex.count)
self.assertEqual(3, ex.expected_count)
self.assertEqual('unreceived', ex.message_type)
def test_ping_until_unreceived_unrecheable(self):
result = ping.ping_until_unreceived('1.2.3.4', count=3)
self.assertIsNone(result.source)
self.assertEqual(netaddr.IPAddress('1.2.3.4'), result.destination)
result.assert_transmitted()
result.assert_not_replied()