Merge "Create bw limit qos test"

This commit is contained in:
Zuul 2021-04-16 11:59:42 +00:00 committed by Gerrit Code Review
commit 9e63f9d1c7
10 changed files with 469 additions and 4 deletions

View File

@ -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']

View File

@ -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'])

View 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

View 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)

View 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]

View 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)

View 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'.
"""

View 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))]

View File

@ -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

View File

@ -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)