tobiko/tobiko/shell/ping/_parameters.py

242 lines
9.1 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 typing
import netaddr
from oslo_log import log
LOG = log.getLogger(__name__)
PingAddressType = typing.Union[str, netaddr.IPAddress]
class PingParameters(collections.namedtuple('PingParameters',
['host',
'count',
'deadline',
'fragmentation',
'interval',
'ip_version',
'packet_size',
'source',
'timeout',
'network_namespace'])):
"""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'.
"""
PING_PARAMETERS_NAMES = PingParameters._fields
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: typing.Optional[PingAddressType] = None,
interval=None, ip_version=None, packet_size=None,
source=None, timeout=None, network_namespace=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),
network_namespace=get_string('network_namespace', network_namespace,
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)
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: str, value: typing.Optional[PingAddressType],
default=None) -> typing.Optional[PingAddressType]:
if value is None:
if default:
return get_address(name, getattr(default, name))
else:
return None
if isinstance(value, netaddr.IPAddress):
return value
elif isinstance(value, str):
try:
return netaddr.IPAddress(value)
except netaddr.core.AddrFormatError:
# NOTE: value may be an host name so this is fine
return value
else:
raise TypeError(f"Object '{value}' is not a valid address")
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}
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]