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]
|
||||
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
|
||||
# under the License.
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import libvirt
|
||||
from oslo_log import log as logging
|
||||
from tempest.api.compute import base
|
||||
from tempest.common import waiters
|
||||
from tempest import config
|
||||
|
||||
from whitebox_tempest_plugin.common import utils as whitebox_utils
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseTest(base.BaseV2ComputeTest):
|
||||
|
||||
credentials = ['primary', 'admin']
|
||||
class BaseTest(base.BaseV2ComputeAdminTest):
|
||||
|
||||
@classmethod
|
||||
def setup_clients(cls):
|
||||
super(BaseTest, cls).setup_clients()
|
||||
# TODO(stephenfin): Rewrite tests to use 'admin_servers_client' etc.
|
||||
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):
|
||||
super(PointerDeviceTypeFromImages, cls).setup_clients()
|
||||
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):
|
||||
req_metadata = {'hw_pointer_model': 'usbtablet'}
|
||||
@ -52,7 +51,6 @@ class PointerDeviceTypeFromImages(base.BaseTest):
|
||||
# Retrieve the server's hypervizor hostname
|
||||
compute_node_address = whitebox_utils.get_hypervisor_ip(
|
||||
self.servers_client, server_id)
|
||||
self.assertIsNotNone(compute_node_address)
|
||||
|
||||
# Retrieve input device from virsh dumpxml
|
||||
virshxml_client = clients.VirshXMLClient(compute_node_address)
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
from oslo_log import log as logging
|
||||
from tempest import config
|
||||
from whitebox_tempest_plugin import exceptions
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
@ -27,5 +28,5 @@ def get_hypervisor_ip(admin_servers_client, server_id):
|
||||
try:
|
||||
return CONF.whitebox.hypervisors[host]
|
||||
except KeyError:
|
||||
LOG.error('Unable to find IP in conf. Server: %s, host: %s.',
|
||||
(server_id, host))
|
||||
raise exceptions.MissingHypervisorException(server=server_id,
|
||||
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
|
||||
|
||||
from whitebox_tempest_plugin.common import utils
|
||||
from whitebox_tempest_plugin import exceptions
|
||||
from whitebox_tempest_plugin.tests import base
|
||||
|
||||
|
||||
@ -39,5 +40,5 @@ class UtilsTestCase(base.WhiteboxPluginTestCase):
|
||||
|
||||
@mock.patch.object(utils.LOG, 'error')
|
||||
def test_get_hypervisor_ip_keyerror(self, mock_log):
|
||||
self.assertIsNone(utils.get_hypervisor_ip(self.client, 'missing-id'))
|
||||
self.assertIn('Unable', mock_log.call_args_list[0][0][0])
|
||||
self.assertRaises(exceptions.MissingHypervisorException,
|
||||
utils.get_hypervisor_ip, self.client, 'missing-id')
|
||||
|
Loading…
x
Reference in New Issue
Block a user