Create bw limit qos test
iperf is used to measure bw from a CentOS VM instance Change-Id: I4e474bd9fbaefae9e7d356eda2bf9c458ed3356b
This commit is contained in:
parent
1be2747cfd
commit
e91a08df90
@ -36,6 +36,7 @@ CONFIG_MODULES = ['tobiko.openstack.glance.config',
|
||||
'tobiko.openstack.topology.config',
|
||||
'tobiko.shell.ssh.config',
|
||||
'tobiko.shell.ping.config',
|
||||
'tobiko.shell.iperf.config',
|
||||
'tobiko.shell.sh.config',
|
||||
'tobiko.tripleo.config']
|
||||
|
||||
|
@ -16,6 +16,7 @@ from __future__ import absolute_import
|
||||
import tobiko
|
||||
from tobiko import config
|
||||
from tobiko.openstack import glance
|
||||
from tobiko.openstack import nova
|
||||
from tobiko.openstack.stacks import _nova
|
||||
|
||||
|
||||
@ -61,4 +62,9 @@ class CentosExternalServerStackFixture(CentosServerStackFixture,
|
||||
|
||||
class CentosQosServerStackFixture(CentosServerStackFixture,
|
||||
_nova.QosServerStackFixture):
|
||||
pass
|
||||
|
||||
@property
|
||||
def cloud_config(self):
|
||||
return nova.cloud_config(
|
||||
super(CentosQosServerStackFixture, self).cloud_config,
|
||||
packages=['iperf3'])
|
||||
|
21
tobiko/shell/iperf/__init__.py
Normal file
21
tobiko/shell/iperf/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2021 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.iperf import _assert
|
||||
|
||||
|
||||
assert_bw_limit = _assert.assert_bw_limit
|
55
tobiko/shell/iperf/_assert.py
Normal file
55
tobiko/shell/iperf/_assert.py
Normal file
@ -0,0 +1,55 @@
|
||||
# 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
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
import tobiko
|
||||
from tobiko import config
|
||||
from tobiko.shell.iperf import _iperf
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def calculate_bw(iperf_measures):
|
||||
# first interval is removed because BW measured during it is not
|
||||
# limited - it takes ~ 1 second to traffic shaping algorithm to apply
|
||||
# bw limit properly (buffer is empty when traffic starts being sent)
|
||||
intervals = iperf_measures['intervals'][1:]
|
||||
|
||||
bits_received = sum([interval['sum']['bytes'] * 8
|
||||
for interval in intervals])
|
||||
totaltime = sum([interval['sum']['seconds'] for interval in intervals])
|
||||
# bw in bits per second
|
||||
return bits_received / totaltime
|
||||
|
||||
|
||||
def assert_bw_limit(ssh_client, ssh_server, **params):
|
||||
iperf_measures = _iperf.iperf(ssh_client, ssh_server, **params)
|
||||
measured_bw = calculate_bw(iperf_measures)
|
||||
|
||||
testcase = tobiko.get_test_case()
|
||||
bw_limit = float(params.get('bw_limit') or
|
||||
CONF.tobiko.neutron.bwlimit_kbps * 1000.)
|
||||
LOG.debug('measured_bw = %f', measured_bw)
|
||||
LOG.debug('bw_limit = %f', bw_limit)
|
||||
# a 5% of upper deviation is allowed
|
||||
testcase.assertLess(measured_bw, bw_limit * 1.05)
|
||||
# an 8% of lower deviation is allowed
|
||||
testcase.assertGreater(measured_bw, bw_limit * 0.92)
|
185
tobiko/shell/iperf/_interface.py
Normal file
185
tobiko/shell/iperf/_interface.py
Normal file
@ -0,0 +1,185 @@
|
||||
# Copyright (c) 2021 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 oslo_log import log
|
||||
|
||||
import tobiko
|
||||
from tobiko.shell import sh
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def install_iperf(ssh_client):
|
||||
def iperf_help():
|
||||
cmd = 'iperf3 --help'
|
||||
try:
|
||||
return sh.execute(cmd,
|
||||
expect_exit_status=None,
|
||||
ssh_client=ssh_client)
|
||||
except FileNotFoundError:
|
||||
return sh.execute_result(command=cmd,
|
||||
exit_status=127,
|
||||
stdout='command not found')
|
||||
result = iperf_help()
|
||||
usage = ((result.stdout and str(result.stdout)) or
|
||||
(result.stderr and str(result.stderr)) or "").strip()
|
||||
if result.exit_status != 0 and 'command not found' in usage.lower():
|
||||
install_command = '{install_tool} install -y iperf3'
|
||||
install_tools = ('yum', 'apt')
|
||||
for install_tool in install_tools:
|
||||
try:
|
||||
result = sh.execute(
|
||||
command=install_command.format(install_tool=install_tool),
|
||||
ssh_client=ssh_client,
|
||||
sudo=True)
|
||||
except sh.ShellError:
|
||||
LOG.debug(f'Unable to install iperf3 using {install_tool}')
|
||||
else:
|
||||
LOG.debug(f'iperf3 successfully installed with {install_tool}')
|
||||
break
|
||||
|
||||
if iperf_help().exit_status != 0:
|
||||
raise RuntimeError('iperf3 command was not installed successfully')
|
||||
elif result.exit_status != 0:
|
||||
raise RuntimeError('Error executing iperf3 command')
|
||||
else:
|
||||
LOG.debug('iperf3 already installed')
|
||||
|
||||
|
||||
def get_iperf_command(parameters, ssh_client):
|
||||
interface = get_iperf_interface(ssh_client=ssh_client)
|
||||
return interface.get_iperf_command(parameters)
|
||||
|
||||
|
||||
def get_iperf_interface(ssh_client):
|
||||
manager = tobiko.setup_fixture(IperfInterfaceManager)
|
||||
interface = manager.get_iperf_interface(ssh_client=ssh_client)
|
||||
tobiko.check_valid_type(interface, IperfInterface)
|
||||
return interface
|
||||
|
||||
|
||||
class IperfInterfaceManager(tobiko.SharedFixture):
|
||||
def __init__(self):
|
||||
super(IperfInterfaceManager, self).__init__()
|
||||
self.client_interfaces = {}
|
||||
self.interfaces = []
|
||||
self.default_interface = IperfInterface()
|
||||
|
||||
def add_iperf_interface(self, interface):
|
||||
LOG.debug('Register iperf interface %r', interface)
|
||||
self.interfaces.append(interface)
|
||||
|
||||
def get_iperf_interface(self, ssh_client):
|
||||
try:
|
||||
return self.client_interfaces[ssh_client]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
install_iperf(ssh_client)
|
||||
LOG.debug('Assign default iperf interface to SSH client %r',
|
||||
ssh_client)
|
||||
self.client_interfaces[ssh_client] = self.default_interface
|
||||
return self.default_interface
|
||||
|
||||
|
||||
class IperfInterface(object):
|
||||
def get_iperf_command(self, parameters):
|
||||
command = sh.shell_command(['iperf3'] +
|
||||
self.get_iperf_options(parameters))
|
||||
LOG.debug(f'Got iperf command: {command}')
|
||||
return command
|
||||
|
||||
def get_iperf_options(self, parameters):
|
||||
options = []
|
||||
|
||||
port = parameters.port
|
||||
if port:
|
||||
options += self.get_port_option(port)
|
||||
|
||||
timeout = parameters.timeout
|
||||
if timeout and parameters.mode == 'client':
|
||||
options += self.get_timeout_option(timeout)
|
||||
|
||||
output_format = parameters.output_format
|
||||
if output_format:
|
||||
options += self.get_output_format_option(output_format)
|
||||
|
||||
bitrate = parameters.bitrate
|
||||
if bitrate and parameters.mode == 'client':
|
||||
options += self.get_bitrate_option(bitrate)
|
||||
|
||||
download = parameters.download
|
||||
if download and parameters.mode == 'client':
|
||||
options += self.get_download_option(download)
|
||||
|
||||
protocol = parameters.protocol
|
||||
if protocol and parameters.mode == 'client':
|
||||
options += self.get_protocol_option(protocol)
|
||||
|
||||
options += self.get_mode_option(parameters)
|
||||
|
||||
return options
|
||||
|
||||
@staticmethod
|
||||
def get_mode_option(parameters):
|
||||
mode = parameters.mode
|
||||
if not mode or mode not in ('client', 'server'):
|
||||
raise ValueError('iperf mode values allowed: [client|server]')
|
||||
elif mode == 'client' and not parameters.ip:
|
||||
raise ValueError('iperf client mode requires a destination '
|
||||
'IP address')
|
||||
elif mode == 'client':
|
||||
return ['-c', parameters.ip]
|
||||
else: # mode == 'server'
|
||||
return ['-s', '-D'] # server mode is executed with daemon mode
|
||||
|
||||
@staticmethod
|
||||
def get_download_option(download):
|
||||
if download:
|
||||
return ['-R']
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_protocol_option(protocol):
|
||||
if protocol == 'tcp':
|
||||
return []
|
||||
elif protocol == 'udp':
|
||||
return ['-u']
|
||||
else:
|
||||
raise ValueError('iperf protocol values allowed: [tcp|udp]')
|
||||
|
||||
@staticmethod
|
||||
def get_timeout_option(timeout):
|
||||
return ['-t', timeout]
|
||||
|
||||
@staticmethod
|
||||
def get_output_format_option(output_format):
|
||||
if output_format == 'json':
|
||||
return ['-J']
|
||||
else:
|
||||
raise ValueError('iperf output format values allowed: '
|
||||
'[json]')
|
||||
|
||||
@staticmethod
|
||||
def get_port_option(port):
|
||||
return ['-p', port]
|
||||
|
||||
@staticmethod
|
||||
def get_bitrate_option(bitrate):
|
||||
return ['-b', bitrate]
|
75
tobiko/shell/iperf/_iperf.py
Normal file
75
tobiko/shell/iperf/_iperf.py
Normal file
@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2021 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 json
|
||||
import time
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from tobiko.shell.iperf import _interface
|
||||
from tobiko.shell.iperf import _parameters
|
||||
from tobiko.shell import sh
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def iperf(ssh_client, ssh_server, **iperf_params):
|
||||
"""Run iperf on both client and server machines and return obtained
|
||||
statistics
|
||||
|
||||
:param ssh_client: ssh connection to client
|
||||
:param ssh_server: ssh connection to server
|
||||
:param **iperf_params: parameters to be forwarded to get_statistics()
|
||||
function
|
||||
:returns: dict
|
||||
"""
|
||||
parameters_server = _parameters.get_iperf_parameters(
|
||||
mode='server', **iperf_params)
|
||||
# no output expected
|
||||
execute_iperf_server(parameters_server, ssh_server)
|
||||
|
||||
time.sleep(0.1)
|
||||
parameters_client = _parameters.get_iperf_parameters(
|
||||
mode='client', ip=ssh_server.host, **iperf_params)
|
||||
# output is a dictionary
|
||||
output = execute_iperf_client(parameters_client, ssh_client)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def execute_iperf_server(parameters, ssh_client):
|
||||
# kill any iperf3 process running before executing it again
|
||||
sh.execute(command='pkill iperf3',
|
||||
ssh_client=ssh_client,
|
||||
expect_exit_status=None)
|
||||
time.sleep(1)
|
||||
|
||||
# server is executed in background and no output is expected
|
||||
command = _interface.get_iperf_command(parameters=parameters,
|
||||
ssh_client=ssh_client)
|
||||
sh.execute(command=command, ssh_client=ssh_client)
|
||||
|
||||
|
||||
def execute_iperf_client(parameters, ssh_client):
|
||||
command = _interface.get_iperf_command(parameters=parameters,
|
||||
ssh_client=ssh_client)
|
||||
result = sh.execute(command=command,
|
||||
ssh_client=ssh_client,
|
||||
timeout=parameters.timeout + 5.,
|
||||
expect_exit_status=None)
|
||||
return json.loads(result.stdout)
|
58
tobiko/shell/iperf/_parameters.py
Normal file
58
tobiko/shell/iperf/_parameters.py
Normal file
@ -0,0 +1,58 @@
|
||||
# Copyright (c) 2021 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
|
||||
|
||||
from tobiko import config
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
def get_iperf_parameters(mode, ip=None, **iperf_params):
|
||||
"""Get iperf parameters
|
||||
mode allowed values: client or server
|
||||
ip is only needed for client mode
|
||||
"""
|
||||
return IperfParameters(
|
||||
mode=mode,
|
||||
ip=ip,
|
||||
port=iperf_params.get('port', CONF.tobiko.iperf.port),
|
||||
timeout=iperf_params.get('timeout', CONF.tobiko.iperf.timeout),
|
||||
output_format=iperf_params.get('output_format',
|
||||
CONF.tobiko.iperf.output_format),
|
||||
download=iperf_params.get('download', CONF.tobiko.iperf.download),
|
||||
bitrate=iperf_params.get('bitrate', CONF.tobiko.iperf.bitrate),
|
||||
protocol=iperf_params.get('protocol', CONF.tobiko.iperf.protocol))
|
||||
|
||||
|
||||
class IperfParameters(collections.namedtuple('IperfParameters',
|
||||
['mode',
|
||||
'ip',
|
||||
'port',
|
||||
'timeout',
|
||||
'output_format',
|
||||
'download',
|
||||
'bitrate',
|
||||
'protocol'])):
|
||||
"""Recollect parameters to be used to format iperf command line
|
||||
|
||||
IperfParameters class is a data model recollecting parameters used to
|
||||
create an iperf command line. It provides the feature of copying default
|
||||
values from another instance of IperfParameters passed using constructor
|
||||
parameter 'default'.
|
||||
"""
|
49
tobiko/shell/iperf/config.py
Normal file
49
tobiko/shell/iperf/config.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright 2021 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
|
||||
|
||||
import itertools
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
GROUP_NAME = "iperf"
|
||||
OPTIONS = [
|
||||
cfg.IntOpt('port',
|
||||
default=1234,
|
||||
help="Port number"),
|
||||
cfg.StrOpt('protocol',
|
||||
default='tcp',
|
||||
choices=['tcp', 'udp'],
|
||||
help="tcp and udp values are supported"),
|
||||
cfg.StrOpt('output_format',
|
||||
default='json',
|
||||
choices=['', 'json'],
|
||||
help="output format"),
|
||||
cfg.IntOpt('bitrate',
|
||||
default=20000000,
|
||||
help="target bit rate"),
|
||||
cfg.BoolOpt('download',
|
||||
default=True,
|
||||
help="direction download (True) or upload (False)"),
|
||||
cfg.IntOpt('timeout',
|
||||
default=10,
|
||||
help="timeout of the iperf test")]
|
||||
|
||||
|
||||
def register_tobiko_options(conf):
|
||||
conf.register_opts(group=cfg.OptGroup(GROUP_NAME), opts=OPTIONS)
|
||||
|
||||
|
||||
def list_options():
|
||||
return [(GROUP_NAME, itertools.chain(OPTIONS))]
|
@ -42,6 +42,7 @@ ShellStdinClosed = _exception.ShellStdinClosed
|
||||
|
||||
execute = _execute.execute
|
||||
execute_process = _execute.execute_process
|
||||
execute_result = _execute.execute_result
|
||||
ShellExecuteResult = _execute.ShellExecuteResult
|
||||
|
||||
HostNameError = _hostname.HostnameError
|
||||
|
@ -20,6 +20,7 @@ import testtools
|
||||
import tobiko
|
||||
from tobiko.openstack import stacks
|
||||
from tobiko.openstack import topology
|
||||
from tobiko.shell import iperf
|
||||
from tobiko.tripleo import containers
|
||||
from tobiko.tripleo import overcloud
|
||||
|
||||
@ -41,11 +42,24 @@ class QoSBasicTest(testtools.TestCase):
|
||||
if (overcloud.has_overcloud() and
|
||||
topology.verify_osp_version('16.0', lower=True) and
|
||||
containers.ovn_used_on_overcloud()):
|
||||
self.skip("QoS not supported in this setup")
|
||||
self.skipTest("QoS not supported in this setup")
|
||||
|
||||
def test_qos_basic(self):
|
||||
# Verify QoS Policy attached to the network corresponds with the QoS
|
||||
# Policy previously created
|
||||
'''Verify QoS Policy attached to the network corresponds with the QoS
|
||||
Policy previously created'''
|
||||
self.assertEqual(self.stack.network_stack.qos_stack.qos_policy_id,
|
||||
self.stack.network_stack.qos_policy_id)
|
||||
self.assertIsNone(self.stack.port_details['qos_policy_id'])
|
||||
|
||||
def test_qos_bw_limit(self):
|
||||
'''Verify BW limit using the iperf tool
|
||||
The test is executed from the undercloud node (client) to the VM
|
||||
instance (server)'''
|
||||
if not tobiko.tripleo.has_undercloud():
|
||||
# TODO(eolivare): this test does not support devstack environments
|
||||
# yet and that should be fixed
|
||||
tobiko.skip_test('test not supported on devstack environments')
|
||||
|
||||
ssh_client = None # localhost will act as client
|
||||
ssh_server = self.stack.peer_ssh_client
|
||||
iperf.assert_bw_limit(ssh_client, ssh_server)
|
||||
|
Loading…
x
Reference in New Issue
Block a user