201 lines
6.5 KiB
Python
201 lines
6.5 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
|
|
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]
|
|
while destination and destination[0] == '(':
|
|
destination = destination[1:]
|
|
while destination and 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("{transmitted!r} package(s) has been transmitted "
|
|
"to {destination!r}",
|
|
transmitted=self.transmitted,
|
|
destination=self.destination)
|
|
|
|
def assert_not_transmitted(self):
|
|
if self.transmitted:
|
|
tobiko.fail("{transmitted!r} package(s) has been transmitted to "
|
|
"{destination!r}",
|
|
transmitted=self.transmitted,
|
|
destination=self.destination)
|
|
|
|
def assert_replied(self):
|
|
if not self.received:
|
|
tobiko.fail("{received!r} reply package(s) has been received from "
|
|
"{destination!r}",
|
|
received=self.received,
|
|
destination=self.destination)
|
|
|
|
def assert_not_replied(self):
|
|
if self.received:
|
|
tobiko.fail("{received!r} reply package(s) has been received from "
|
|
"{destination!r}",
|
|
received=self.received,
|
|
destination=self.destination)
|