diff --git a/bindep.txt b/bindep.txt new file mode 100644 index 00000000..83b4b563 --- /dev/null +++ b/bindep.txt @@ -0,0 +1,2 @@ +python-libvirt [platform:dpkg] +libvirt-python [platform:rpm] diff --git a/tox.ini b/tox.ini index 631bd7a8..521e0c28 100644 --- a/tox.ini +++ b/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 diff --git a/whitebox_tempest_plugin/api/compute/base.py b/whitebox_tempest_plugin/api/compute/base.py index 8edc8a96..307298ad 100644 --- a/whitebox_tempest_plugin/api/compute/base.py +++ b/whitebox_tempest_plugin/api/compute/base.py @@ -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() diff --git a/whitebox_tempest_plugin/api/compute/test_cpu_pinning.py b/whitebox_tempest_plugin/api/compute/test_cpu_pinning.py new file mode 100644 index 00000000..a565ec5b --- /dev/null +++ b/whitebox_tempest_plugin/api/compute/test_cpu_pinning.py @@ -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") diff --git a/whitebox_tempest_plugin/api/compute/test_pointer_device_type.py b/whitebox_tempest_plugin/api/compute/test_pointer_device_type.py index b84c47a1..952496e9 100644 --- a/whitebox_tempest_plugin/api/compute/test_pointer_device_type.py +++ b/whitebox_tempest_plugin/api/compute/test_pointer_device_type.py @@ -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) diff --git a/whitebox_tempest_plugin/common/utils.py b/whitebox_tempest_plugin/common/utils.py index 90105b83..f9f49d72 100644 --- a/whitebox_tempest_plugin/common/utils.py +++ b/whitebox_tempest_plugin/common/utils.py @@ -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) diff --git a/whitebox_tempest_plugin/exceptions.py b/whitebox_tempest_plugin/exceptions.py new file mode 100644 index 00000000..dee4351e --- /dev/null +++ b/whitebox_tempest_plugin/exceptions.py @@ -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." diff --git a/whitebox_tempest_plugin/tests/test_utils.py b/whitebox_tempest_plugin/tests/test_utils.py index 7e801eae..cae03729 100644 --- a/whitebox_tempest_plugin/tests/test_utils.py +++ b/whitebox_tempest_plugin/tests/test_utils.py @@ -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')