411e3dac2e
Change-Id: I4958a119ff9dff661fb04f1973d76c4c3ee645f8
294 lines
12 KiB
Python
294 lines
12 KiB
Python
# Copyright 2014 Cisco Systems, 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.
|
|
#
|
|
|
|
import abc
|
|
import os
|
|
import re
|
|
|
|
# where to copy the tool on the target, must end with slash
|
|
SCP_DEST_DIR = '/tmp/'
|
|
|
|
|
|
|
|
#
|
|
# A base class for all tools that can be associated to an instance
|
|
#
|
|
class PerfTool(object):
|
|
__metaclass__ = abc.ABCMeta
|
|
|
|
def __init__(self, name, perf_tool_path, instance):
|
|
self.name = name
|
|
self.instance = instance
|
|
self.dest_path = SCP_DEST_DIR + name
|
|
self.pid = None
|
|
self.perf_tool_path = perf_tool_path
|
|
|
|
# install the tool to the instance
|
|
# returns False if fail, True if success
|
|
def install(self):
|
|
if self.perf_tool_path:
|
|
local_path = os.path.join(self.perf_tool_path, self.name)
|
|
return self.instance.scp(self.name, local_path, self.dest_path)
|
|
# no install needed
|
|
return True
|
|
|
|
@abc.abstractmethod
|
|
def get_server_launch_cmd(self):
|
|
'''To be implemented by sub-classes.'''
|
|
return None
|
|
|
|
def start_server(self):
|
|
'''Launch the server side of this tool
|
|
:return: True if success, False if error
|
|
'''
|
|
# check if server is already started
|
|
if not self.pid:
|
|
self.pid = self.instance.ssh.pidof(self.name)
|
|
if not self.pid:
|
|
cmd_list = self.get_server_launch_cmd()
|
|
# Start the tool server
|
|
self.instance.buginf('Starting %s server...' % (self.name))
|
|
for launch_cmd in cmd_list:
|
|
launch_out = self.instance.exec_command(launch_cmd)
|
|
self.pid = self.instance.ssh.pidof(self.name)
|
|
else:
|
|
self.instance.buginf('%s server already started pid=%s' % (self.name, self.pid))
|
|
if self.pid:
|
|
return True
|
|
else:
|
|
self.instance.display('Cannot launch server %s: %s' % (self.name, launch_out))
|
|
return False
|
|
|
|
# Terminate pid if started
|
|
def dispose(self):
|
|
if self.pid:
|
|
# Terminate the iperf server
|
|
self.instance.buginf('Terminating %s', self.name)
|
|
self.instance.ssh.kill_proc(self.pid)
|
|
self.pid = None
|
|
|
|
def parse_error(self, msg):
|
|
return {'error': msg, 'tool': self.name}
|
|
|
|
def parse_results(self, protocol, throughput, lossrate=None, retrans=None,
|
|
rtt_ms=None, reverse_dir=False,
|
|
msg_size=None,
|
|
cpu_load=None):
|
|
res = {'throughput_kbps': throughput,
|
|
'protocol': protocol,
|
|
'tool': self.name}
|
|
if self.instance.config.vm_bandwidth:
|
|
res['bandwidth_limit_kbps'] = self.instance.config.vm_bandwidth
|
|
if lossrate is not None:
|
|
res['loss_rate'] = lossrate
|
|
if retrans:
|
|
res['retrans'] = retrans
|
|
if rtt_ms:
|
|
res['rtt_ms'] = rtt_ms
|
|
if reverse_dir:
|
|
res['direction'] = 'reverse'
|
|
if msg_size:
|
|
res['pkt_size'] = msg_size
|
|
if cpu_load:
|
|
res['cpu_load'] = cpu_load
|
|
return res
|
|
|
|
@abc.abstractmethod
|
|
def run_client_dir(self, target_ip,
|
|
mss,
|
|
reverse_dir=False,
|
|
bandwidth_kbps=0,
|
|
udp=False,
|
|
length=0,
|
|
no_cpu_timed=0):
|
|
# must be implemented by sub classes
|
|
return None
|
|
|
|
def find_udp_bdw(self, pkt_size, target_ip):
|
|
'''Find highest UDP bandwidth within max loss rate for given packet size
|
|
:return: a dictionary describing the optimal bandwidth (see parse_results())
|
|
'''
|
|
# we use a binary search to converge to the optimal throughput
|
|
# start with 5Gbps - mid-range between 1 and 10Gbps
|
|
# Convergence can be *very* tricky because UDP throughput behavior
|
|
# can vary dramatically between host runs and guest runs.
|
|
# The packet rate limitation is going to dictate the effective
|
|
# send rate, meaning that small packet sizes will yield the worst
|
|
# throughput.
|
|
# The measured throughput can be vastly smaller than the requested
|
|
# throughput even when the loss rate is zero when the sender cannot
|
|
# send fast enough to fill the network, in that case increasing the
|
|
# requested rate will not make it any better
|
|
# Examples:
|
|
# 1. too much difference between requested/measured bw - regardless of loss rate
|
|
# => retry with bw mid-way between the requested bw and the measured bw
|
|
# /tmp/nuttcp-7.3.2 -T2 -u -l128 -R5000000K -p5001 -P5002 -fparse 192.168.1.2
|
|
# megabytes=36.9785 real_seconds=2.00 rate_Mbps=154.8474 tx_cpu=23 rx_cpu=32
|
|
# drop=78149 pkt=381077 data_loss=20.50746
|
|
# /tmp/nuttcp-7.3.2 -T2 -u -l128 -R2500001K -p5001 -P5002 -fparse 192.168.1.2
|
|
# megabytes=47.8063 real_seconds=2.00 rate_Mbps=200.2801 tx_cpu=24 rx_cpu=34
|
|
# drop=0 pkt=391629 data_loss=0.00000
|
|
# 2. measured and requested bw are very close :
|
|
# if loss_rate is too low
|
|
# increase bw mid-way between requested and last max bw
|
|
# if loss rate is too high
|
|
# decrease bw mid-way between the measured bw and the last min bw
|
|
# else stop iteration (converged)
|
|
# /tmp/nuttcp-7.3.2 -T2 -u -l8192 -R859376K -p5001 -P5002 -fparse 192.168.1.2
|
|
# megabytes=204.8906 real_seconds=2.00 rate_Mbps=859.2992 tx_cpu=99 rx_cpu=10
|
|
# drop=0 pkt=26226 data_loss=0.00000
|
|
|
|
min_kbps = 1
|
|
max_kbps = 10000000
|
|
kbps = 5000000
|
|
min_loss_rate = self.instance.config.udp_loss_rate_range[0]
|
|
max_loss_rate = self.instance.config.udp_loss_rate_range[1]
|
|
# stop if the remaining range to cover is less than 5%
|
|
while (min_kbps * 100 / max_kbps) < 95:
|
|
res_list = self.run_client_dir(target_ip, 0, bandwidth_kbps=kbps,
|
|
udp=True, length=pkt_size,
|
|
no_cpu_timed=1)
|
|
# always pick the first element in the returned list of dict(s)
|
|
# should normally only have 1 element
|
|
res = res_list[0]
|
|
if 'error' in res:
|
|
return res
|
|
loss_rate = res['loss_rate']
|
|
measured_kbps = res['throughput_kbps']
|
|
self.instance.buginf('pkt-size=%d throughput=%d<%d/%d<%d Kbps loss-rate=%d' %
|
|
(pkt_size, min_kbps, measured_kbps, kbps, max_kbps, loss_rate))
|
|
# expected rate must be at least 80% of the requested rate
|
|
if (measured_kbps * 100 / kbps) < 80:
|
|
# the measured bw is too far away from the requested bw
|
|
# take half the distance or 3x the measured bw whichever is lowest
|
|
kbps = measured_kbps + (kbps - measured_kbps) / 2
|
|
if measured_kbps:
|
|
kbps = min(kbps, measured_kbps * 3)
|
|
max_kbps = kbps
|
|
continue
|
|
# The measured bw is within striking distance from the requested bw
|
|
# increase bw if loss rate is too small
|
|
if loss_rate < min_loss_rate:
|
|
# undershot
|
|
if measured_kbps > min_kbps:
|
|
min_kbps = measured_kbps
|
|
else:
|
|
# to make forward progress we need to increase min_kbps
|
|
# and try a higher bw since the loss rate is too low
|
|
min_kbps = int((max_kbps + min_kbps) / 2)
|
|
|
|
kbps = int((max_kbps + min_kbps) / 2)
|
|
# print ' undershot, min=%d kbps=%d max=%d' % (min_kbps, kbps, max_kbps)
|
|
elif loss_rate > max_loss_rate:
|
|
# overshot
|
|
max_kbps = kbps
|
|
if measured_kbps < kbps:
|
|
kbps = measured_kbps
|
|
else:
|
|
kbps = int((max_kbps + min_kbps) / 2)
|
|
# print ' overshot, min=%d kbps=%d max=%d' % (min_kbps, kbps, max_kbps)
|
|
else:
|
|
# converged within loss rate bracket
|
|
break
|
|
return res
|
|
|
|
def get_proto_profile(self):
|
|
'''Return a tuple containing the list of protocols (tcp/udp) and
|
|
list of packet sizes (udp only)
|
|
'''
|
|
# start with TCP (udp=False) then UDP
|
|
proto_list = []
|
|
proto_pkt_sizes = []
|
|
if 'T' in self.instance.config.protocols:
|
|
proto_list.append(False)
|
|
proto_pkt_sizes.append(self.instance.config.tcp_pkt_sizes)
|
|
if 'U' in self.instance.config.protocols:
|
|
proto_list.append(True)
|
|
proto_pkt_sizes.append(self.instance.config.udp_pkt_sizes)
|
|
return (proto_list, proto_pkt_sizes)
|
|
|
|
class PingTool(PerfTool):
|
|
'''
|
|
A class to run ping and get loss rate and round trip time
|
|
'''
|
|
|
|
def __init__(self, instance):
|
|
PerfTool.__init__(self, 'ping', None, instance)
|
|
|
|
def run_client(self, target_ip, ping_count=5):
|
|
'''Perform the ping operation
|
|
:return: a dict containing the results stats
|
|
|
|
Example of output:
|
|
10 packets transmitted, 10 packets received, 0.0% packet loss
|
|
round-trip min/avg/max/stddev = 55.855/66.074/103.915/13.407 ms
|
|
or
|
|
5 packets transmitted, 5 received, 0% packet loss, time 3998ms
|
|
rtt min/avg/max/mdev = 0.455/0.528/0.596/0.057 ms
|
|
'''
|
|
if self.instance.config.ipv6_mode:
|
|
cmd = "ping6 -c " + str(ping_count) + " " + str(target_ip)
|
|
else:
|
|
cmd = "ping -c " + str(ping_count) + " " + str(target_ip)
|
|
cmd_out = self.instance.exec_command(cmd)
|
|
if not cmd_out:
|
|
res = {'protocol': 'ICMP',
|
|
'tool': 'ping',
|
|
'error': 'failed'}
|
|
return res
|
|
match = re.search(r'(\d*) packets transmitted, (\d*) ',
|
|
cmd_out)
|
|
if match:
|
|
tx_packets = match.group(1)
|
|
rx_packets = match.group(2)
|
|
else:
|
|
tx_packets = 0
|
|
rx_packets = 0
|
|
match = re.search(r'min/avg/max/[a-z]* = ([\d\.]*)/([\d\.]*)/([\d\.]*)/([\d\.]*)',
|
|
cmd_out)
|
|
if match:
|
|
rtt_min = match.group(1)
|
|
rtt_avg = match.group(2)
|
|
rtt_max = match.group(3)
|
|
rtt_stddev = match.group(4)
|
|
else:
|
|
rtt_min = 0
|
|
rtt_max = 0
|
|
rtt_avg = 0
|
|
rtt_stddev = 0
|
|
res = {'protocol': 'ICMP',
|
|
'tool': 'ping',
|
|
'tx_packets': tx_packets,
|
|
'rx_packets': rx_packets,
|
|
'rtt_min_ms': rtt_min,
|
|
'rtt_max_ms': rtt_max,
|
|
'rtt_avg_ms': rtt_avg,
|
|
'rtt_stddev': rtt_stddev}
|
|
return res
|
|
|
|
def get_server_launch_cmd(self):
|
|
# not applicable
|
|
return None
|
|
|
|
def run_client_dir(self, target_ip,
|
|
mss,
|
|
reverse_dir=False,
|
|
bandwidth_kbps=0,
|
|
udp=False,
|
|
length=0,
|
|
no_cpu_timed=0):
|
|
# not applicable
|
|
return None
|