214 lines
8.0 KiB
Python
214 lines
8.0 KiB
Python
# 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 collections
|
|
|
|
import netaddr
|
|
from oslo_log import log
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
PING_PARAMETER_NAMES = ['host', 'count', 'deadline', 'fragmentation',
|
|
'interval', 'ip_version', 'packet_size', 'source',
|
|
'timeout']
|
|
|
|
|
|
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,
|
|
ip_version=None, packet_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. Fragmentation can't
|
|
be disabled when using ping provided by BusyBox (IE with CirrOS images).
|
|
|
|
: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 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: (int or None) if not None, it specifies the total ICMP
|
|
message size (headers + payload).
|
|
|
|
: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 default is None:
|
|
default = default_ping_parameters()
|
|
|
|
return PingParameters(
|
|
count=get_positive_integer('count', count, default),
|
|
host=get_address('host', host, default),
|
|
deadline=get_positive_integer('deadline', deadline, default),
|
|
fragmentation=get_boolean('fragmentation', fragmentation, default),
|
|
interval=get_positive_integer('interval', interval, default),
|
|
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))
|
|
|
|
|
|
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)
|
|
|
|
|
|
class PingParameters(collections.namedtuple('PingParameters',
|
|
PING_PARAMETER_NAMES)):
|
|
"""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 get_ping_ip_version(parameters):
|
|
ip_version = parameters.ip_version
|
|
if ip_version is not None:
|
|
ip_version = int(ip_version)
|
|
if ip_version not in [4, 6]:
|
|
message = "Invalid IP version: {!r}".format(ip_version)
|
|
raise ValueError(message)
|
|
for address in [parameters.host, parameters.source]:
|
|
if isinstance(address, netaddr.IPAddress):
|
|
if ip_version != address.version:
|
|
if ip_version:
|
|
meassage = ("{!s} address IP version is not {!r}"
|
|
).format(address, ip_version)
|
|
raise ValueError(meassage)
|
|
ip_version = address.version
|
|
return ip_version
|
|
|
|
|
|
def get_ping_payload_size(parameters):
|
|
packet_size = parameters.packet_size
|
|
if packet_size is None:
|
|
return None
|
|
|
|
header_size = get_ping_header_size(parameters)
|
|
if packet_size < header_size:
|
|
message = ("packet size {packet_size!s} can't be smaller than "
|
|
"header size {header_size!s}").format(
|
|
packet_size=packet_size,
|
|
header_size=header_size)
|
|
raise ValueError(message)
|
|
return packet_size - header_size
|
|
|
|
|
|
def get_positive_integer(name, value, default=None):
|
|
if value is None and default:
|
|
return get_positive_integer(name, getattr(default, name))
|
|
if value is not None:
|
|
value = int(value)
|
|
if value <= 0:
|
|
message = "{!r} value must be positive: {!r}".format(
|
|
name, value)
|
|
raise ValueError(message)
|
|
return value
|
|
|
|
|
|
def get_boolean(name, value, default=None):
|
|
if value is None and default:
|
|
return get_boolean(name, getattr(default, name))
|
|
if value is not None:
|
|
value = bool(value)
|
|
return value
|
|
|
|
|
|
def get_address(name, value, default=None):
|
|
if value is None and default:
|
|
return get_address(name, getattr(default, name))
|
|
if value is not None:
|
|
try:
|
|
value = netaddr.IPAddress(value)
|
|
except netaddr.core.AddrFormatError:
|
|
# NOTE: value may be an host name so this is fine
|
|
value = str(value)
|
|
return value
|
|
|
|
|
|
IP_HEADER_SIZE = {4: 20, 6: 40}
|
|
ICMP_HEADER_SIZE = {4: 8, 6: 4}
|
|
|
|
|
|
def get_ping_header_size(parameters):
|
|
ip_version = get_ping_ip_version(parameters)
|
|
if ip_version is None:
|
|
message = "can't get ICMP header size without knowing IP version"
|
|
raise ValueError(message)
|
|
|
|
if ip_version not in IP_HEADER_SIZE or ip_version not in ICMP_HEADER_SIZE:
|
|
message = "Invalid IP version: {!r}".format(ip_version)
|
|
raise ValueError(message)
|
|
|
|
return IP_HEADER_SIZE[ip_version] + ICMP_HEADER_SIZE[ip_version]
|