Browse Source

Merge "Create bw limit qos test"

changes/97/786197/6
Zuul 1 month ago
committed by Gerrit Code Review
parent
commit
9e63f9d1c7
10 changed files with 469 additions and 4 deletions
  1. +1
    -0
      tobiko/config.py
  2. +7
    -1
      tobiko/openstack/stacks/_centos.py
  3. +21
    -0
      tobiko/shell/iperf/__init__.py
  4. +55
    -0
      tobiko/shell/iperf/_assert.py
  5. +185
    -0
      tobiko/shell/iperf/_interface.py
  6. +75
    -0
      tobiko/shell/iperf/_iperf.py
  7. +58
    -0
      tobiko/shell/iperf/_parameters.py
  8. +49
    -0
      tobiko/shell/iperf/config.py
  9. +1
    -0
      tobiko/shell/sh/__init__.py
  10. +17
    -3
      tobiko/tests/scenario/neutron/test_qos.py

+ 1
- 0
tobiko/config.py 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']


+ 7
- 1
tobiko/openstack/stacks/_centos.py 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'])

+ 21
- 0
tobiko/shell/iperf/__init__.py 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

+ 55
- 0
tobiko/shell/iperf/_assert.py 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)

+ 185
- 0
tobiko/shell/iperf/_interface.py 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]

+ 75
- 0
tobiko/shell/iperf/_iperf.py 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)

+ 58
- 0
tobiko/shell/iperf/_parameters.py 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'.
"""

+ 49
- 0
tobiko/shell/iperf/config.py 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))]

+ 1
- 0
tobiko/shell/sh/__init__.py 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


+ 17
- 3
tobiko/tests/scenario/neutron/test_qos.py 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)

Loading…
Cancel
Save