Refactor QoS test case

- iperf3 is installed on the Ubuntu image used by the QoS tests
- iperf3 server is started as a systemctl service at VM boot and will manage any
- connections initiated from iperf3 clients

Co-Author: Eduardo Olivares <eolivares@redhat.com>
Change-Id: I1bff4953c2fdb47583174781c2578bcc8848f0a2
This commit is contained in:
Federico Ressi 2021-06-30 13:29:13 +02:00
parent de72e5c13f
commit ecddd6c438
13 changed files with 374 additions and 356 deletions

View File

@ -36,7 +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.iperf3.config',
'tobiko.shell.sh.config',
'tobiko.tripleo.config']

View File

@ -44,18 +44,37 @@ class UbuntuMinimalImageFixture(glance.URLGlanceImageFixture):
connection_timeout = CONF.tobiko.ubuntu.connection_timeout or 600.
IPERF3_SERVICE_FILE = """
[Unit]
Description=iperf3 server on port %i
After=syslog.target network.target
[Service]
ExecStart=/usr/bin/iperf3 -s -p %i
Restart=always
RuntimeMaxSec=3600
User=root
[Install]
WantedBy=multi-user.target
DefaultInstance=5201
"""
class UbuntuImageFixture(UbuntuMinimalImageFixture,
glance.CustomizedGlanceImageFixture):
"""Ubuntu server image running an HTTP server
The server has additional commands compared to the minimal one:
iperf3
ping
ncat
nginx
The server has additional installed packages compared to
the minimal one:
- iperf3
- ping
- ncat
- nginx
The image will also have a running HTTPD server listening on
TCP port 80
The image will also have below running services:
- nginx HTTP server listening on TCP port 80
- iperf3 server listening on TCP port 5201
"""
@property
@ -70,8 +89,22 @@ class UbuntuImageFixture(UbuntuMinimalImageFixture,
'ncat',
'nginx']
# port of running HTTP server
http_port = 80
# port of running Iperf3 server
iperf3_port = 5201
@property
def run_commands(self) -> typing.List[str]:
run_commands = super().run_commands
run_commands.append(
f'echo "{IPERF3_SERVICE_FILE}" '
'> /etc/systemd/system/iperf3-server@.service')
run_commands.append(
f"systemctl enable iperf3-server@{self.iperf3_port}")
return run_commands
class UbuntuFlavorStackFixture(_nova.FlavorStackFixture):
ram = 128
@ -103,6 +136,10 @@ class UbuntuServerStackFixture(UbuntuMinimalServerStackFixture):
def http_port(self) -> int:
return self.image_fixture.http_port
@property
def iperf3_port(self) -> int:
return self.image_fixture.iperf3_port
class UbuntuExternalServerStackFixture(UbuntuServerStackFixture,
_nova.ExternalServerStackFixture):

View File

@ -1,55 +0,0 @@
# 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.1)
# an 8% of lower deviation is allowed
testcase.assertGreater(measured_bw, bw_limit * 0.9)

View File

@ -1,147 +0,0 @@
# 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 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
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]

View File

@ -1,74 +0,0 @@
# 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.)
return json.loads(result.stdout)

View File

@ -1,58 +0,0 @@
# 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'.
"""

View File

@ -15,7 +15,7 @@
# under the License.
from __future__ import absolute_import
from tobiko.shell.iperf import _assert
from tobiko.shell.iperf3 import _assert
assert_bw_limit = _assert.assert_bw_limit
assert_has_bandwith_limits = _assert.assert_has_bandwith_limits

View File

@ -0,0 +1,57 @@
# 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 typing
import netaddr
from oslo_log import log
import tobiko
from tobiko import config
from tobiko.shell.iperf3 import _execute
from tobiko.shell import ssh
CONF = config.CONF
LOG = log.getLogger(__name__)
def assert_has_bandwith_limits(
address: typing.Union[str, netaddr.IPAddress],
min_bandwith: float,
max_bandwith: float,
bitrate: int = None,
download: bool = None,
port: int = None,
protocol: str = None,
ssh_client: ssh.SSHClientType = None,
timeout: tobiko.Seconds = None) -> None:
bandwith = _execute.get_bandwidth(address=address,
bitrate=bitrate,
download=download,
port=port,
protocol=protocol,
ssh_client=ssh_client,
timeout=timeout)
testcase = tobiko.get_test_case()
LOG.debug(f'measured bandwith: {bandwith}')
LOG.debug(f'bandwith limits: {min_bandwith} ... {max_bandwith}')
# an 8% of lower deviation is allowed
testcase.assertGreater(bandwith, min_bandwith)
# a 5% of upper deviation is allowed
testcase.assertLess(bandwith, max_bandwith)

View File

@ -0,0 +1,88 @@
# 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 __future__ import division
import json
import typing
import netaddr
from oslo_log import log
import tobiko
from tobiko.shell.iperf3 import _interface
from tobiko.shell.iperf3 import _parameters
from tobiko.shell import sh
from tobiko.shell import ssh
LOG = log.getLogger(__name__)
def get_bandwidth(address: typing.Union[str, netaddr.IPAddress],
bitrate: int = None,
download: bool = None,
port: int = None,
protocol: str = None,
ssh_client: ssh.SSHClientType = None,
timeout: tobiko.Seconds = None) -> float:
iperf_measures = execute_iperf3_client(address=address,
bitrate=bitrate,
download=download,
port=port,
protocol=protocol,
ssh_client=ssh_client,
timeout=timeout)
return calculate_bandwith(iperf_measures)
def calculate_bandwith(iperf_measures) -> float:
# 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])
elapsed_time = sum([interval['sum']['seconds']
for interval in intervals])
# bw in bits per second
return bits_received / elapsed_time
def execute_iperf3_client(address: typing.Union[str, netaddr.IPAddress],
bitrate: int = None,
download: bool = None,
port: int = None,
protocol: str = None,
ssh_client: ssh.SSHClientType = None,
timeout: tobiko.Seconds = None) \
-> typing.Dict:
params_timeout: typing.Optional[int] = None
if timeout is not None:
params_timeout = int(timeout - 0.5)
parameters = _parameters.iperf3_client_parameters(address=address,
bitrate=bitrate,
download=download,
port=port,
protocol=protocol,
timeout=params_timeout)
command = _interface.get_iperf3_client_command(parameters)
# output is a dictionary
output = sh.execute(command,
ssh_client=ssh_client,
timeout=timeout).stdout
return json.loads(output)

View File

@ -0,0 +1,92 @@
# 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
from tobiko.shell.iperf3 import _parameters
from tobiko.shell import sh
LOG = log.getLogger(__name__)
def get_iperf3_client_command(parameters: _parameters.Iperf3ClientParameters):
interface = Iperf3Interface()
return interface.get_iperf3_client_command(parameters)
class Iperf3Interface:
def get_iperf3_client_command(
self,
parameters: _parameters.Iperf3ClientParameters) \
-> sh.ShellCommand:
options = self.get_iperf3_client_options(parameters=parameters)
return sh.shell_command('iperf3') + options
def get_iperf3_client_options(
self,
parameters: _parameters.Iperf3ClientParameters) \
-> sh.ShellCommand:
options = sh.ShellCommand(['-J'])
options += self.get_client_mode_option(parameters.address)
if parameters.port is not None:
options += self.get_port_option(parameters.port)
if parameters.timeout is not None:
options += self.get_timeout_option(parameters.timeout)
if parameters.bitrate is not None:
options += self.get_bitrate_option(parameters.bitrate)
if parameters.download is not None:
options += self.get_download_option(parameters.download)
if parameters.protocol is not None:
options += self.get_protocol_option(parameters.protocol)
return options
@staticmethod
def get_bitrate_option(bitrate: int):
return ['-b', max(0, bitrate)]
@staticmethod
def get_client_mode_option(server_address: str):
return ['-c', server_address]
@staticmethod
def get_download_option(download: bool):
if download:
return ['-R']
else:
return []
@staticmethod
def get_protocol_option(protocol: str):
if protocol == 'tcp':
return []
elif protocol == 'udp':
return ['-u']
else:
raise ValueError('iperf3 protocol values allowed: [tcp|udp]')
@staticmethod
def get_timeout_option(timeout: int):
if timeout > 0:
return ['-t', timeout]
else:
return []
@staticmethod
def get_port_option(port):
return ['-p', port]

View File

@ -0,0 +1,63 @@
# 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 typing
import netaddr
import tobiko
class Iperf3ClientParameters(typing.NamedTuple):
address: str
bitrate: typing.Optional[int] = None
download: typing.Optional[bool] = None
port: typing.Optional[int] = None
protocol: typing.Optional[str] = None
timeout: typing.Optional[int] = None
def iperf3_client_parameters(
address: typing.Union[str, netaddr.IPAddress],
bitrate: int = None,
download: bool = None,
port: int = None,
protocol: str = None,
timeout: int = None):
"""Get iperf3 client parameters
mode allowed values: client or server
ip is only needed for client mode
"""
config = tobiko.tobiko_config().iperf3
if isinstance(address, netaddr.IPAddress):
address = str(address)
if bitrate is None:
bitrate = config.bitrate
if download is None:
download = config.download
if port is None:
port = config.port
if protocol is None:
protocol = config.protocol
if timeout is None:
timeout = config.timeout
return Iperf3ClientParameters(address=address,
bitrate=bitrate,
download=download,
port=port,
protocol=protocol,
timeout=timeout)

View File

@ -17,24 +17,21 @@ import itertools
from oslo_config import cfg
GROUP_NAME = "iperf"
GROUP_NAME = "iperf3"
OPTIONS = [
cfg.IntOpt('port',
default=1234,
default=None,
help="Port number"),
cfg.StrOpt('protocol',
default='tcp',
default=None,
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,
default=20000000, # 20 Mb
help="target bit rate"),
cfg.BoolOpt('download',
default=True,
default=None,
help="direction download (True) or upload (False)"),
cfg.IntOpt('timeout',
default=10,

View File

@ -21,8 +21,9 @@ import tobiko
from tobiko.openstack import keystone
from tobiko.openstack import stacks
from tobiko.openstack import neutron
from tobiko.shell import iperf
from tobiko.shell import iperf3
from tobiko.shell import ping
from tobiko.shell import sh
LOG = log.getLogger(__name__)
@ -52,5 +53,22 @@ class QoSNetworkTest(testtools.TestCase):
def test_qos_bw_limit(self):
"""Verify BW limit using the iperf3 tool"""
iperf.assert_bw_limit(ssh_client=None, # localhost will act as client
ssh_server=self.server.peer_ssh_client)
# localhost will act as client
bandwidth_limit = self.policy.bwlimit_kbps * 1000.
for attempt in tobiko.retry(timeout=100., interval=5.):
try:
iperf3.assert_has_bandwith_limits(
address=self.server.ip_address,
min_bandwith=bandwidth_limit * 0.9,
max_bandwith=bandwidth_limit * 1.1,
port=self.server.iperf3_port,
download=True)
break
except sh.ShellCommandFailed as err:
if ('unable to connect to server: Connection refused'
in str(err)):
attempt.check_limits()
LOG.debug('iperf command failed because the iperf server '
'was not ready yet - retrying...')
else:
raise err