Add CPU pinning tests
Add tests for CPU pinning. - test_cpu_shared - test_cpu_dedicated - test_resize_pinned_server_to_unpinned - test_resize_unpinned_server_to_pinned - test_reboot_pinned_server This adds some helper functions that, curiously, are not included in the base 'BaseV2ComputeAdminTest' class provided by tempest. In addition, it adds a 'bindep.txt' file and appropriate tox target to ensure the libvirt Python libraries, which this uses, are installed. Change-Id: I2eb58c886987e318f21173aac0c8fa4b410fd494 Implements: RHELOSP-12284
This commit is contained in:
parent
a2d308991e
commit
d02a9af251
2
bindep.txt
Normal file
2
bindep.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
python-libvirt [platform:dpkg]
|
||||||
|
libvirt-python [platform:rpm]
|
13
tox.ini
13
tox.ini
@ -28,3 +28,16 @@ exclude = .git,.venv,.tox,dist,doc,*egg
|
|||||||
|
|
||||||
[hacking]
|
[hacking]
|
||||||
local-check-factory = tempest.hacking.checks.factory
|
local-check-factory = tempest.hacking.checks.factory
|
||||||
|
|
||||||
|
[testenv:bindep]
|
||||||
|
# Do not install any requirements. We want this to be fast and work even if
|
||||||
|
# system dependencies are missing, since it's used to tell you what system
|
||||||
|
# dependencies are missing! This also means that bindep must be installed
|
||||||
|
# separately, outside of the requirements files, and develop mode disabled
|
||||||
|
# explicitly to avoid unnecessarily installing the checked-out repo too (this
|
||||||
|
# further relies on "tox.skipsdist = True" above).
|
||||||
|
usedevelop = False
|
||||||
|
deps =
|
||||||
|
bindep
|
||||||
|
commands =
|
||||||
|
bindep test
|
||||||
|
@ -13,20 +13,69 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
import libvirt
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from tempest.api.compute import base
|
from tempest.api.compute import base
|
||||||
|
from tempest.common import waiters
|
||||||
from tempest import config
|
from tempest import config
|
||||||
|
|
||||||
|
from whitebox_tempest_plugin.common import utils as whitebox_utils
|
||||||
|
|
||||||
CONF = config.CONF
|
CONF = config.CONF
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BaseTest(base.BaseV2ComputeTest):
|
class BaseTest(base.BaseV2ComputeAdminTest):
|
||||||
|
|
||||||
credentials = ['primary', 'admin']
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_clients(cls):
|
def setup_clients(cls):
|
||||||
super(BaseTest, cls).setup_clients()
|
super(BaseTest, cls).setup_clients()
|
||||||
|
# TODO(stephenfin): Rewrite tests to use 'admin_servers_client' etc.
|
||||||
cls.servers_client = cls.os_admin.servers_client
|
cls.servers_client = cls.os_admin.servers_client
|
||||||
|
cls.flavors_client = cls.os_admin.flavors_client
|
||||||
|
cls.hypervisor_client = cls.os_admin.hypervisor_client
|
||||||
|
|
||||||
|
def create_test_server(self, *args, **kwargs):
|
||||||
|
# override the function to return the admin view of the created server
|
||||||
|
kwargs['wait_until'] = 'ACTIVE'
|
||||||
|
server = super(BaseTest, self).create_test_server(*args, **kwargs)
|
||||||
|
|
||||||
|
return self.admin_servers_client.show_server(server['id'])['server']
|
||||||
|
|
||||||
|
def create_flavor(self, ram=64, vcpus=2, disk=0, name=None,
|
||||||
|
is_public='True', **kwargs):
|
||||||
|
# override the function to configure sane defaults
|
||||||
|
return super(BaseTest, self).create_flavor(ram, vcpus, disk, name,
|
||||||
|
is_public, **kwargs)
|
||||||
|
|
||||||
|
def resize_server(self, server_id, new_flavor_id, **kwargs):
|
||||||
|
# override the function to return the resized server
|
||||||
|
# TODO(stephenfin): Add this to upstream
|
||||||
|
super(BaseTest, self).resize_server(server_id, new_flavor_id, **kwargs)
|
||||||
|
|
||||||
|
return self.servers_client.show_server(server_id)['server']
|
||||||
|
|
||||||
|
def reboot_server(self, server_id, reboot_type):
|
||||||
|
# TODO(stephenfin): Add this to upstream
|
||||||
|
self.servers_client.reboot_server(server_id, type=reboot_type)
|
||||||
|
waiters.wait_for_server_status(self.servers_client, server_id,
|
||||||
|
'ACTIVE')
|
||||||
|
|
||||||
|
return self.servers_client.show_server(server_id)['server']
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_libvirt_conn(self, hostname):
|
||||||
|
"""Get a read-only connection to a remote libvirt instance.
|
||||||
|
|
||||||
|
:param hostname: The hostname for the remote libvirt instance.
|
||||||
|
"""
|
||||||
|
# Assume we're using QEMU-KVM and that network conectivity is available
|
||||||
|
libvirt_url = 'qemu+ssh://{}@{}/system'.format(
|
||||||
|
CONF.whitebox.target_ssh_user,
|
||||||
|
whitebox_utils.get_hypervisor_ip(self.servers_client, hostname))
|
||||||
|
|
||||||
|
conn = libvirt.openReadOnly(libvirt_url)
|
||||||
|
yield conn
|
||||||
|
conn.close()
|
||||||
|
164
whitebox_tempest_plugin/api/compute/test_cpu_pinning.py
Normal file
164
whitebox_tempest_plugin/api/compute/test_cpu_pinning.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# Copyright 2015 Intel Corporation
|
||||||
|
# Copyright 2018 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.
|
||||||
|
|
||||||
|
"""Tests for CPU pinning and CPU thread pinning policies.
|
||||||
|
|
||||||
|
Based on tests for the Intel NFV CI.
|
||||||
|
|
||||||
|
For more information, refer to:
|
||||||
|
|
||||||
|
- https://wiki.openstack.org/wiki/ThirdPartySystems/Intel_NFV_CI
|
||||||
|
- https://github.com/openstack/intel-nfv-ci-tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import testtools
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from tempest.common import utils
|
||||||
|
from tempest import config
|
||||||
|
|
||||||
|
from whitebox_tempest_plugin.api.compute import base
|
||||||
|
|
||||||
|
CONF = config.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTest(base.BaseTest):
|
||||||
|
|
||||||
|
vcpus = 2
|
||||||
|
|
||||||
|
def get_server_cpu_pinning(self, server):
|
||||||
|
instance_name = server['OS-EXT-SRV-ATTR:instance_name']
|
||||||
|
|
||||||
|
with self.get_libvirt_conn(server['OS-EXT-SRV-ATTR:host']) as conn:
|
||||||
|
dom0 = conn.lookupByName(instance_name)
|
||||||
|
root = ET.fromstring(dom0.XMLDesc())
|
||||||
|
|
||||||
|
vcpupin_nodes = root.findall('./cputune/vcpupin')
|
||||||
|
cpu_pinnings = {int(x.get('vcpu')): int(x.get('cpuset'))
|
||||||
|
for x in vcpupin_nodes if x is not None}
|
||||||
|
|
||||||
|
return cpu_pinnings
|
||||||
|
|
||||||
|
|
||||||
|
class CPUPolicyTest(BaseTest):
|
||||||
|
"""Validate CPU policy support."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def skip_checks(cls):
|
||||||
|
super(CPUPolicyTest, cls).skip_checks()
|
||||||
|
if not utils.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
|
||||||
|
msg = "OS-FLV-EXT-DATA extension not enabled."
|
||||||
|
raise cls.skipException(msg)
|
||||||
|
|
||||||
|
def create_flavor(self, cpu_policy):
|
||||||
|
flavor = super(CPUPolicyTest, self).create_flavor(vcpus=self.vcpus)
|
||||||
|
|
||||||
|
specs = {'hw:cpu_policy': cpu_policy}
|
||||||
|
self.flavors_client.set_flavor_extra_spec(flavor['id'], **specs)
|
||||||
|
|
||||||
|
return flavor
|
||||||
|
|
||||||
|
def test_cpu_shared(self):
|
||||||
|
"""Ensure an instance with an explicit 'shared' policy work."""
|
||||||
|
flavor = self.create_flavor(cpu_policy='shared')
|
||||||
|
self.create_test_server(flavor=flavor['id'])
|
||||||
|
|
||||||
|
def test_cpu_dedicated(self):
|
||||||
|
"""Ensure an instance with 'dedicated' pinning policy work.
|
||||||
|
|
||||||
|
This is implicitly testing the 'prefer' policy, given that that's the
|
||||||
|
default. However, we check specifics of that later and only assert that
|
||||||
|
things aren't overlapping here.
|
||||||
|
"""
|
||||||
|
flavor = self.create_flavor(cpu_policy='dedicated')
|
||||||
|
server_a = self.create_test_server(flavor=flavor['id'])
|
||||||
|
server_b = self.create_test_server(flavor=flavor['id'])
|
||||||
|
cpu_pinnings_a = self.get_server_cpu_pinning(server_a)
|
||||||
|
cpu_pinnings_b = self.get_server_cpu_pinning(server_b)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(cpu_pinnings_a), self.vcpus,
|
||||||
|
"Instance should be pinned but it is unpinned")
|
||||||
|
self.assertEqual(
|
||||||
|
len(cpu_pinnings_b), self.vcpus,
|
||||||
|
"Instance should be pinned but it is unpinned")
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
set(cpu_pinnings_a.values()).isdisjoint(
|
||||||
|
set(cpu_pinnings_b.values())),
|
||||||
|
"Unexpected overlap in CPU pinning: {}; {}".format(
|
||||||
|
cpu_pinnings_a,
|
||||||
|
cpu_pinnings_b))
|
||||||
|
|
||||||
|
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
|
||||||
|
'Resize not available.')
|
||||||
|
def test_resize_pinned_server_to_unpinned(self):
|
||||||
|
"""Ensure resizing an instance to unpinned actually drops pinning."""
|
||||||
|
flavor_a = self.create_flavor(cpu_policy='dedicated')
|
||||||
|
server = self.create_test_server(flavor=flavor_a['id'])
|
||||||
|
cpu_pinnings = self.get_server_cpu_pinning(server)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(cpu_pinnings), self.vcpus,
|
||||||
|
"Instance should be pinned but is unpinned")
|
||||||
|
|
||||||
|
flavor_b = self.create_flavor(cpu_policy='shared')
|
||||||
|
server = self.resize_server(server['id'], flavor_b['id'])
|
||||||
|
cpu_pinnings = self.get_server_cpu_pinning(server)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(cpu_pinnings), 0,
|
||||||
|
"Resized instance should be unpinned but is still pinned")
|
||||||
|
|
||||||
|
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
|
||||||
|
'Resize not available.')
|
||||||
|
def test_resize_unpinned_server_to_pinned(self):
|
||||||
|
"""Ensure resizing an instance to pinned actually applies pinning."""
|
||||||
|
flavor_a = self.create_flavor(cpu_policy='shared')
|
||||||
|
server = self.create_test_server(flavor=flavor_a['id'])
|
||||||
|
cpu_pinnings = self.get_server_cpu_pinning(server)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(cpu_pinnings), 0,
|
||||||
|
"Instance should be unpinned but is pinned")
|
||||||
|
|
||||||
|
flavor_b = self.create_flavor(cpu_policy='dedicated')
|
||||||
|
server = self.resize_server(server['id'], flavor_b['id'])
|
||||||
|
cpu_pinnings = self.get_server_cpu_pinning(server)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(cpu_pinnings), self.vcpus,
|
||||||
|
"Resized instance should be pinned but is still unpinned")
|
||||||
|
|
||||||
|
def test_reboot_pinned_server(self):
|
||||||
|
"""Ensure pinning information is persisted after a reboot."""
|
||||||
|
flavor = self.create_flavor(cpu_policy='dedicated')
|
||||||
|
server = self.create_test_server(flavor=flavor['id'])
|
||||||
|
cpu_pinnings = self.get_server_cpu_pinning(server)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(cpu_pinnings), self.vcpus,
|
||||||
|
"CPU pinning was not applied to new instance.")
|
||||||
|
|
||||||
|
server = self.reboot_server(server['id'], 'HARD')
|
||||||
|
cpu_pinnings = self.get_server_cpu_pinning(server)
|
||||||
|
|
||||||
|
# we don't actually assert that the same pinning information is used
|
||||||
|
# because that's not expected. We just care that _some_ pinning is in
|
||||||
|
# effect
|
||||||
|
self.assertEqual(
|
||||||
|
len(cpu_pinnings), self.vcpus,
|
||||||
|
"Rebooted instance has lost its pinning information")
|
@ -39,7 +39,6 @@ class PointerDeviceTypeFromImages(base.BaseTest):
|
|||||||
def setup_clients(cls):
|
def setup_clients(cls):
|
||||||
super(PointerDeviceTypeFromImages, cls).setup_clients()
|
super(PointerDeviceTypeFromImages, cls).setup_clients()
|
||||||
cls.compute_images_client = cls.os_admin.compute_images_client
|
cls.compute_images_client = cls.os_admin.compute_images_client
|
||||||
cls.hypervisor_client = cls.os_admin.hypervisor_client
|
|
||||||
|
|
||||||
def _set_image_metadata_item(self, image):
|
def _set_image_metadata_item(self, image):
|
||||||
req_metadata = {'hw_pointer_model': 'usbtablet'}
|
req_metadata = {'hw_pointer_model': 'usbtablet'}
|
||||||
@ -52,7 +51,6 @@ class PointerDeviceTypeFromImages(base.BaseTest):
|
|||||||
# Retrieve the server's hypervizor hostname
|
# Retrieve the server's hypervizor hostname
|
||||||
compute_node_address = whitebox_utils.get_hypervisor_ip(
|
compute_node_address = whitebox_utils.get_hypervisor_ip(
|
||||||
self.servers_client, server_id)
|
self.servers_client, server_id)
|
||||||
self.assertIsNotNone(compute_node_address)
|
|
||||||
|
|
||||||
# Retrieve input device from virsh dumpxml
|
# Retrieve input device from virsh dumpxml
|
||||||
virshxml_client = clients.VirshXMLClient(compute_node_address)
|
virshxml_client = clients.VirshXMLClient(compute_node_address)
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from tempest import config
|
from tempest import config
|
||||||
|
from whitebox_tempest_plugin import exceptions
|
||||||
|
|
||||||
|
|
||||||
CONF = config.CONF
|
CONF = config.CONF
|
||||||
@ -27,5 +28,5 @@ def get_hypervisor_ip(admin_servers_client, server_id):
|
|||||||
try:
|
try:
|
||||||
return CONF.whitebox.hypervisors[host]
|
return CONF.whitebox.hypervisors[host]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
LOG.error('Unable to find IP in conf. Server: %s, host: %s.',
|
raise exceptions.MissingHypervisorException(server=server_id,
|
||||||
(server_id, host))
|
host=host)
|
||||||
|
20
whitebox_tempest_plugin/exceptions.py
Normal file
20
whitebox_tempest_plugin/exceptions.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Copyright 2018 Red Hat
|
||||||
|
# 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 tempest.lib import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
class MissingHypervisorException(exceptions.TempestException):
|
||||||
|
message = "Unable to find IP in conf. Server: %(sever)s, host: %(host)s."
|
@ -15,6 +15,7 @@
|
|||||||
import mock
|
import mock
|
||||||
|
|
||||||
from whitebox_tempest_plugin.common import utils
|
from whitebox_tempest_plugin.common import utils
|
||||||
|
from whitebox_tempest_plugin import exceptions
|
||||||
from whitebox_tempest_plugin.tests import base
|
from whitebox_tempest_plugin.tests import base
|
||||||
|
|
||||||
|
|
||||||
@ -39,5 +40,5 @@ class UtilsTestCase(base.WhiteboxPluginTestCase):
|
|||||||
|
|
||||||
@mock.patch.object(utils.LOG, 'error')
|
@mock.patch.object(utils.LOG, 'error')
|
||||||
def test_get_hypervisor_ip_keyerror(self, mock_log):
|
def test_get_hypervisor_ip_keyerror(self, mock_log):
|
||||||
self.assertIsNone(utils.get_hypervisor_ip(self.client, 'missing-id'))
|
self.assertRaises(exceptions.MissingHypervisorException,
|
||||||
self.assertIn('Unable', mock_log.call_args_list[0][0][0])
|
utils.get_hypervisor_ip, self.client, 'missing-id')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user