Use Ubuntu minimal image to run Iperf3 server
- install ipref3 using virt-customize - enables QoS server test on DevStack based setups - remove iperf3 installation from iperf3 shell APIs - raise default QoS BW limit from 1Kbps to 1 Mbps - increase QoS bandwith limit tollerance range (90%-110%) Depends-On: https://review.opendev.org/c/x/devstack-plugin-tobiko/+/788899 Change-Id: Idc7d98b34ce59d14408b15edb6061a36f796a8fc
This commit is contained in:
parent
6f39740e53
commit
ed84b3fcd6
@ -8,6 +8,7 @@ git [platform:redhat]
|
|||||||
iperf3 [platform:redhat]
|
iperf3 [platform:redhat]
|
||||||
iproute [platform:redhat]
|
iproute [platform:redhat]
|
||||||
libffi-devel [platform:redhat]
|
libffi-devel [platform:redhat]
|
||||||
|
libguestfs-tools-c [platform:redhat]
|
||||||
make [platform:redhat]
|
make [platform:redhat]
|
||||||
openssl-devel [platform:redhat]
|
openssl-devel [platform:redhat]
|
||||||
python-docutils [platform:rhel-7]
|
python-docutils [platform:rhel-7]
|
||||||
@ -27,6 +28,7 @@ gcc [platform:ubuntu]
|
|||||||
git [platform:ubuntu]
|
git [platform:ubuntu]
|
||||||
iperf3 [platform:ubuntu]
|
iperf3 [platform:ubuntu]
|
||||||
libffi-dev [platform:ubuntu]
|
libffi-dev [platform:ubuntu]
|
||||||
|
libguestfs-tools [platform:ubuntu]
|
||||||
libssl-dev [platform:ubuntu]
|
libssl-dev [platform:ubuntu]
|
||||||
make [platform:ubuntu]
|
make [platform:ubuntu]
|
||||||
python-docutils [platform:ubuntu]
|
python-docutils [platform:ubuntu]
|
||||||
|
@ -32,6 +32,7 @@ FileGlanceImageFixture = _image.FileGlanceImageFixture
|
|||||||
GlanceImageFixture = _image.GlanceImageFixture
|
GlanceImageFixture = _image.GlanceImageFixture
|
||||||
HasImageMixin = _image.HasImageMixin
|
HasImageMixin = _image.HasImageMixin
|
||||||
URLGlanceImageFixture = _image.URLGlanceImageFixture
|
URLGlanceImageFixture = _image.URLGlanceImageFixture
|
||||||
|
CustomizedGlanceImageFixture = _image.CustomizedGlanceImageFixture
|
||||||
|
|
||||||
open_image_file = _io.open_image_file
|
open_image_file = _io.open_image_file
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ from tobiko.config import get_bool_env
|
|||||||
from tobiko.openstack.glance import _client
|
from tobiko.openstack.glance import _client
|
||||||
from tobiko.openstack.glance import _io
|
from tobiko.openstack.glance import _io
|
||||||
from tobiko.openstack import keystone
|
from tobiko.openstack import keystone
|
||||||
|
from tobiko.shell import sh
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@ -326,7 +327,9 @@ class FileGlanceImageFixture(UploadGranceImageFixture):
|
|||||||
return os.path.join(self.real_image_dir, self.image_file)
|
return os.path.join(self.real_image_dir, self.image_file)
|
||||||
|
|
||||||
def get_image_data(self):
|
def get_image_data(self):
|
||||||
image_file = self.real_image_file
|
return self.get_image_file(image_file=self.real_image_file)
|
||||||
|
|
||||||
|
def get_image_file(self, image_file: str):
|
||||||
image_size = os.path.getsize(image_file)
|
image_size = os.path.getsize(image_file)
|
||||||
LOG.debug('Uploading image %r data from file %r (%d bytes)',
|
LOG.debug('Uploading image %r data from file %r (%d bytes)',
|
||||||
self.image_name, image_file, image_size)
|
self.image_name, image_file, image_size)
|
||||||
@ -338,20 +341,19 @@ class FileGlanceImageFixture(UploadGranceImageFixture):
|
|||||||
|
|
||||||
class URLGlanceImageFixture(FileGlanceImageFixture):
|
class URLGlanceImageFixture(FileGlanceImageFixture):
|
||||||
|
|
||||||
image_url: typing.Optional[str] = None
|
image_url: str
|
||||||
|
|
||||||
def __init__(self, image_url=None, **kwargs):
|
def __init__(self, image_url: typing.Optional[str] = None, **kwargs):
|
||||||
super(URLGlanceImageFixture, self).__init__(**kwargs)
|
super(URLGlanceImageFixture, self).__init__(**kwargs)
|
||||||
if image_url:
|
if image_url is None:
|
||||||
self.image_url = image_url
|
|
||||||
else:
|
|
||||||
image_url = self.image_url
|
image_url = self.image_url
|
||||||
|
else:
|
||||||
|
self.image_url = image_url
|
||||||
tobiko.check_valid_type(image_url, str)
|
tobiko.check_valid_type(image_url, str)
|
||||||
|
|
||||||
def get_image_data(self):
|
def get_image_file(self, image_file: str):
|
||||||
http_request = requests.get(self.image_url, stream=True)
|
http_request = requests.get(self.image_url, stream=True)
|
||||||
expected_size = int(http_request.headers.get('content-length', 0))
|
expected_size = int(http_request.headers.get('content-length', 0))
|
||||||
image_file = self.real_image_file
|
|
||||||
chunks = http_request.iter_content(chunk_size=io.DEFAULT_BUFFER_SIZE)
|
chunks = http_request.iter_content(chunk_size=io.DEFAULT_BUFFER_SIZE)
|
||||||
download_image = True
|
download_image = True
|
||||||
if expected_size:
|
if expected_size:
|
||||||
@ -373,7 +375,9 @@ class URLGlanceImageFixture(FileGlanceImageFixture):
|
|||||||
self._download_image_file(image_file=image_file,
|
self._download_image_file(image_file=image_file,
|
||||||
chunks=chunks,
|
chunks=chunks,
|
||||||
expected_size=expected_size)
|
expected_size=expected_size)
|
||||||
return super(URLGlanceImageFixture, self).get_image_data()
|
image_file = self.customize_image_file(base_file=image_file)
|
||||||
|
return super(URLGlanceImageFixture, self).get_image_file(
|
||||||
|
image_file=image_file)
|
||||||
|
|
||||||
def _download_image_file(self, image_file, chunks, expected_size):
|
def _download_image_file(self, image_file, chunks, expected_size):
|
||||||
image_dir = os.path.dirname(image_file)
|
image_dir = os.path.dirname(image_file)
|
||||||
@ -396,6 +400,42 @@ class URLGlanceImageFixture(FileGlanceImageFixture):
|
|||||||
raise RuntimeError(message)
|
raise RuntimeError(message)
|
||||||
os.rename(temp_file, image_file)
|
os.rename(temp_file, image_file)
|
||||||
|
|
||||||
|
def customize_image_file(self, base_file: str) -> str:
|
||||||
|
return base_file
|
||||||
|
|
||||||
|
|
||||||
|
class CustomizedGlanceImageFixture(URLGlanceImageFixture):
|
||||||
|
|
||||||
|
install_packages: typing.Sequence[str] = tuple()
|
||||||
|
|
||||||
|
def customize_image_file(self, base_file: str) -> str:
|
||||||
|
customized_file = base_file + '.1'
|
||||||
|
if os.path.isfile(customized_file):
|
||||||
|
if (os.stat(base_file).st_mtime_ns <
|
||||||
|
os.stat(customized_file).st_mtime_ns):
|
||||||
|
LOG.debug(f"Image file is up to date '{customized_file}'")
|
||||||
|
return customized_file
|
||||||
|
else:
|
||||||
|
LOG.debug(f"Remove obsolete image file '{customized_file}'")
|
||||||
|
os.remove(customized_file)
|
||||||
|
work_file = sh.execute('mktemp').stdout.strip()
|
||||||
|
try:
|
||||||
|
LOG.debug(f"Copy base image file: '{base_file}' to '{work_file}'")
|
||||||
|
sh.put_file(base_file, work_file)
|
||||||
|
|
||||||
|
command = sh.shell_command(['virt-customize', '-a', work_file])
|
||||||
|
execute = False
|
||||||
|
for package in self.install_packages:
|
||||||
|
execute = True
|
||||||
|
command += ['--install', package]
|
||||||
|
if execute:
|
||||||
|
sh.execute(command, sudo=True)
|
||||||
|
|
||||||
|
sh.get_file(work_file, customized_file)
|
||||||
|
return customized_file
|
||||||
|
finally:
|
||||||
|
sh.execute(['rm', '-f', work_file])
|
||||||
|
|
||||||
|
|
||||||
class InvalidGlanceImageStatus(tobiko.TobikoException):
|
class InvalidGlanceImageStatus(tobiko.TobikoException):
|
||||||
message = ("Invalid image {image_name!r} (id {image_id!r}) status: "
|
message = ("Invalid image {image_name!r} (id {image_id!r}) status: "
|
||||||
|
@ -57,7 +57,7 @@ OPTIONS = [
|
|||||||
default=['/etc/resolv.conf'],
|
default=['/etc/resolv.conf'],
|
||||||
help="File to parse for getting default nameservers list"),
|
help="File to parse for getting default nameservers list"),
|
||||||
cfg.IntOpt('bwlimit_kbps',
|
cfg.IntOpt('bwlimit_kbps',
|
||||||
default=100,
|
default=1000,
|
||||||
help="The BW limit value configured for the QoS Policy Rule"),
|
help="The BW limit value configured for the QoS Policy Rule"),
|
||||||
cfg.StrOpt('direction',
|
cfg.StrOpt('direction',
|
||||||
default='egress',
|
default='egress',
|
||||||
|
@ -81,6 +81,7 @@ UbuntuMinimalImageFixture = _ubuntu.UbuntuMinimalImageFixture
|
|||||||
UbuntuServerStackFixture = _ubuntu.UbuntuServerStackFixture
|
UbuntuServerStackFixture = _ubuntu.UbuntuServerStackFixture
|
||||||
UbuntuMinimalServerStackFixture = _ubuntu.UbuntuMinimalServerStackFixture
|
UbuntuMinimalServerStackFixture = _ubuntu.UbuntuMinimalServerStackFixture
|
||||||
UbuntuExternalServerStackFixture = _ubuntu.UbuntuExternalServerStackFixture
|
UbuntuExternalServerStackFixture = _ubuntu.UbuntuExternalServerStackFixture
|
||||||
|
UbuntuQosServerStackFixture = _ubuntu.UbuntuQosServerStackFixture
|
||||||
|
|
||||||
OctaviaLoadbalancerStackFixture = _octavia.OctaviaLoadbalancerStackFixture
|
OctaviaLoadbalancerStackFixture = _octavia.OctaviaLoadbalancerStackFixture
|
||||||
OctaviaListenerStackFixture = _octavia.OctaviaListenerStackFixture
|
OctaviaListenerStackFixture = _octavia.OctaviaListenerStackFixture
|
||||||
|
@ -85,3 +85,15 @@ class UbuntuMinimalServerStackFixture(UbuntuServerStackFixture):
|
|||||||
class UbuntuExternalServerStackFixture(UbuntuMinimalServerStackFixture,
|
class UbuntuExternalServerStackFixture(UbuntuMinimalServerStackFixture,
|
||||||
_nova.ExternalServerStackFixture):
|
_nova.ExternalServerStackFixture):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UbuntuQosServerImageFixture(UbuntuMinimalImageFixture,
|
||||||
|
glance.CustomizedGlanceImageFixture):
|
||||||
|
install_packages = ['iperf3']
|
||||||
|
|
||||||
|
|
||||||
|
class UbuntuQosServerStackFixture(UbuntuMinimalServerStackFixture,
|
||||||
|
_nova.QosServerStackFixture):
|
||||||
|
|
||||||
|
#: Glance image used to create a Nova server instance
|
||||||
|
image_fixture = tobiko.required_setup_fixture(UbuntuQosServerImageFixture)
|
||||||
|
@ -50,6 +50,6 @@ def assert_bw_limit(ssh_client, ssh_server, **params):
|
|||||||
LOG.debug('measured_bw = %f', measured_bw)
|
LOG.debug('measured_bw = %f', measured_bw)
|
||||||
LOG.debug('bw_limit = %f', bw_limit)
|
LOG.debug('bw_limit = %f', bw_limit)
|
||||||
# a 5% of upper deviation is allowed
|
# a 5% of upper deviation is allowed
|
||||||
testcase.assertLess(measured_bw, bw_limit * 1.05)
|
testcase.assertLess(measured_bw, bw_limit * 1.1)
|
||||||
# an 8% of lower deviation is allowed
|
# an 8% of lower deviation is allowed
|
||||||
testcase.assertGreater(measured_bw, bw_limit * 0.92)
|
testcase.assertGreater(measured_bw, bw_limit * 0.9)
|
||||||
|
@ -24,43 +24,6 @@ from tobiko.shell import sh
|
|||||||
LOG = log.getLogger(__name__)
|
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):
|
def get_iperf_command(parameters, ssh_client):
|
||||||
interface = get_iperf_interface(ssh_client=ssh_client)
|
interface = get_iperf_interface(ssh_client=ssh_client)
|
||||||
return interface.get_iperf_command(parameters)
|
return interface.get_iperf_command(parameters)
|
||||||
@ -90,7 +53,6 @@ class IperfInterfaceManager(tobiko.SharedFixture):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
install_iperf(ssh_client)
|
|
||||||
LOG.debug('Assign default iperf interface to SSH client %r',
|
LOG.debug('Assign default iperf interface to SSH client %r',
|
||||||
ssh_client)
|
ssh_client)
|
||||||
self.client_interfaces[ssh_client] = self.default_interface
|
self.client_interfaces[ssh_client] = self.default_interface
|
||||||
|
@ -70,6 +70,5 @@ def execute_iperf_client(parameters, ssh_client):
|
|||||||
ssh_client=ssh_client)
|
ssh_client=ssh_client)
|
||||||
result = sh.execute(command=command,
|
result = sh.execute(command=command,
|
||||||
ssh_client=ssh_client,
|
ssh_client=ssh_client,
|
||||||
timeout=parameters.timeout + 5.,
|
timeout=parameters.timeout + 5.)
|
||||||
expect_exit_status=None)
|
|
||||||
return json.loads(result.stdout)
|
return json.loads(result.stdout)
|
||||||
|
@ -25,6 +25,7 @@ from tobiko.shell.sh import _nameservers
|
|||||||
from tobiko.shell.sh import _process
|
from tobiko.shell.sh import _process
|
||||||
from tobiko.shell.sh import _ps
|
from tobiko.shell.sh import _ps
|
||||||
from tobiko.shell.sh import _reboot
|
from tobiko.shell.sh import _reboot
|
||||||
|
from tobiko.shell.sh import _sftp
|
||||||
from tobiko.shell.sh import _ssh
|
from tobiko.shell.sh import _ssh
|
||||||
from tobiko.shell.sh import _uptime
|
from tobiko.shell.sh import _uptime
|
||||||
|
|
||||||
@ -80,6 +81,9 @@ crash_method = RebootHostMethod.CRASH
|
|||||||
hard_reset_method = RebootHostMethod.HARD
|
hard_reset_method = RebootHostMethod.HARD
|
||||||
soft_reset_method = RebootHostMethod.SOFT
|
soft_reset_method = RebootHostMethod.SOFT
|
||||||
|
|
||||||
|
put_file = _sftp.put_file
|
||||||
|
get_file = _sftp.get_file
|
||||||
|
|
||||||
ssh_process = _ssh.ssh_process
|
ssh_process = _ssh.ssh_process
|
||||||
ssh_execute = _ssh.ssh_execute
|
ssh_execute = _ssh.ssh_execute
|
||||||
SSHShellProcessFixture = _ssh.SSHShellProcessFixture
|
SSHShellProcessFixture = _ssh.SSHShellProcessFixture
|
||||||
|
65
tobiko/shell/sh/_sftp.py
Normal file
65
tobiko/shell/sh/_sftp.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
from tobiko.shell import ssh
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
SSHClientType = typing.Union[None, ssh.SSHClientFixture, bool]
|
||||||
|
|
||||||
|
|
||||||
|
def sftp_client(ssh_client: SSHClientType) \
|
||||||
|
-> typing.Optional[paramiko.SFTPClient]:
|
||||||
|
if ssh_client is None:
|
||||||
|
ssh_client = ssh.ssh_proxy_client()
|
||||||
|
if isinstance(ssh_client, ssh.SSHClientFixture):
|
||||||
|
return ssh_client.connect().open_sftp()
|
||||||
|
assert ssh_client is None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def put_file(local_file: str,
|
||||||
|
remote_file: str,
|
||||||
|
ssh_client: SSHClientType = None):
|
||||||
|
sftp = sftp_client(ssh_client)
|
||||||
|
if sftp is None:
|
||||||
|
LOG.debug(f"Copy local file: '{local_file}' -> '{remote_file}' ...")
|
||||||
|
shutil.copyfile(local_file, remote_file)
|
||||||
|
else:
|
||||||
|
LOG.debug(f"Put remote file: '{local_file}' -> '{remote_file}' ...")
|
||||||
|
with sftp:
|
||||||
|
sftp.put(local_file, remote_file)
|
||||||
|
|
||||||
|
|
||||||
|
def get_file(remote_file: str,
|
||||||
|
local_file: str,
|
||||||
|
ssh_client: SSHClientType = None):
|
||||||
|
sftp = sftp_client(ssh_client)
|
||||||
|
if sftp is None:
|
||||||
|
LOG.debug(f"Copy local file: '{remote_file}' -> '{local_file}' ...")
|
||||||
|
shutil.copyfile(remote_file, local_file)
|
||||||
|
else:
|
||||||
|
LOG.debug(f"Get remote file: '{remote_file}' -> '{local_file}' ...")
|
||||||
|
with sftp:
|
||||||
|
sftp.get(remote_file, local_file)
|
@ -32,7 +32,7 @@ class QoSBasicTest(testtools.TestCase):
|
|||||||
"""Tests QoS basic functionality"""
|
"""Tests QoS basic functionality"""
|
||||||
|
|
||||||
#: Resources stack with QoS Policy and QoS Rules and Advanced server
|
#: Resources stack with QoS Policy and QoS Rules and Advanced server
|
||||||
stack = tobiko.required_setup_fixture(stacks.CentosQosServerStackFixture)
|
stack = tobiko.required_setup_fixture(stacks.UbuntuQosServerStackFixture)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Skip these tests if OVN is configured and OSP version is lower than
|
"""Skip these tests if OVN is configured and OSP version is lower than
|
||||||
@ -44,22 +44,18 @@ class QoSBasicTest(testtools.TestCase):
|
|||||||
containers.ovn_used_on_overcloud()):
|
containers.ovn_used_on_overcloud()):
|
||||||
self.skipTest("QoS not supported in this setup")
|
self.skipTest("QoS not supported in this setup")
|
||||||
|
|
||||||
def test_qos_basic(self):
|
def test_network_qos_policy_id(self):
|
||||||
'''Verify QoS Policy attached to the network corresponds with the QoS
|
'''Verify QoS Policy attached to the network corresponds with the QoS
|
||||||
Policy previously created'''
|
Policy previously created'''
|
||||||
self.assertEqual(self.stack.network_stack.qos_stack.qos_policy_id,
|
self.assertEqual(self.stack.network_stack.qos_stack.qos_policy_id,
|
||||||
self.stack.network_stack.qos_policy_id)
|
self.stack.network_stack.qos_policy_id)
|
||||||
|
|
||||||
|
def test_server_qos_policy_id(self):
|
||||||
self.assertIsNone(self.stack.port_details['qos_policy_id'])
|
self.assertIsNone(self.stack.port_details['qos_policy_id'])
|
||||||
|
|
||||||
def test_qos_bw_limit(self):
|
def test_qos_bw_limit(self):
|
||||||
'''Verify BW limit using the iperf tool
|
'''Verify BW limit using the iperf tool
|
||||||
The test is executed from the undercloud node (client) to the VM
|
'''
|
||||||
instance (server)'''
|
self.stack.wait_for_cloud_init_done()
|
||||||
if not tobiko.tripleo.has_undercloud():
|
iperf.assert_bw_limit(ssh_client=None, # localhost will act as client
|
||||||
# TODO(eolivare): this test does not support devstack environments
|
ssh_server=self.stack.peer_ssh_client)
|
||||||
# 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