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:
Stephen Finucane 2018-01-12 10:58:22 +00:00 committed by Artom Lifshitz
parent a2d308991e
commit d02a9af251
8 changed files with 257 additions and 9 deletions

2
bindep.txt Normal file
View File

@ -0,0 +1,2 @@
python-libvirt [platform:dpkg]
libvirt-python [platform:rpm]

13
tox.ini
View File

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

View File

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

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

View File

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

View File

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

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

View File

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