From 73ba33773daf1df1be792b616842dd389fd325bc Mon Sep 17 00:00:00 2001 From: melanie witt Date: Tue, 23 Jul 2024 02:20:34 +0000 Subject: [PATCH] Add tests for creating servers with (anti-)affinity Currently there are only two tests for server affinity and anti-affinity and both of them use the /servers multi-create API and so are not covering cases where server(s) in a group are already present on hosts when a new group member is being scheduled. This adds some additional tests for cases of already present server group members and for soft (anti-)affinity. Change-Id: I84a9f9da666e51fad8b5bf4e214e1027b2f56766 --- .../admin/test_servers_on_multinodes.py | 4 + .../api/{admin => compute}/__init__.py | 0 .../api/compute/admin/__init__.py | 0 .../{ => compute}/admin/test_aggregates.py | 0 .../api/compute/admin/test_server_affinity.py | 252 ++++++++++++++++++ 5 files changed, 256 insertions(+) rename tempest/serial_tests/api/{admin => compute}/__init__.py (100%) create mode 100644 tempest/serial_tests/api/compute/admin/__init__.py rename tempest/serial_tests/api/{ => compute}/admin/test_aggregates.py (100%) create mode 100644 tempest/serial_tests/api/compute/admin/test_server_affinity.py diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py index b5ee9b197d..22ff13c675 100644 --- a/tempest/api/compute/admin/test_servers_on_multinodes.py +++ b/tempest/api/compute/admin/test_servers_on_multinodes.py @@ -110,6 +110,8 @@ class ServersOnMultiNodesTest(base.BaseV2ComputeAdminTest): Creates two servers in an anti-affinity server group and asserts the servers are in the group and on different hosts. + + Uses the /servers multi-create API. """ hosts = self._create_servers_with_group('anti-affinity') hostnames = list(hosts.values()) @@ -126,6 +128,8 @@ class ServersOnMultiNodesTest(base.BaseV2ComputeAdminTest): Creates two servers in an affinity server group and asserts the servers are in the group and on same host. + + Uses the /servers multi-create API. """ hosts = self._create_servers_with_group('affinity') hostnames = list(hosts.values()) diff --git a/tempest/serial_tests/api/admin/__init__.py b/tempest/serial_tests/api/compute/__init__.py similarity index 100% rename from tempest/serial_tests/api/admin/__init__.py rename to tempest/serial_tests/api/compute/__init__.py diff --git a/tempest/serial_tests/api/compute/admin/__init__.py b/tempest/serial_tests/api/compute/admin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/serial_tests/api/admin/test_aggregates.py b/tempest/serial_tests/api/compute/admin/test_aggregates.py similarity index 100% rename from tempest/serial_tests/api/admin/test_aggregates.py rename to tempest/serial_tests/api/compute/admin/test_aggregates.py diff --git a/tempest/serial_tests/api/compute/admin/test_server_affinity.py b/tempest/serial_tests/api/compute/admin/test_server_affinity.py new file mode 100644 index 0000000000..4400130ff2 --- /dev/null +++ b/tempest/serial_tests/api/compute/admin/test_server_affinity.py @@ -0,0 +1,252 @@ +# 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. + +import testtools + +from tempest.api.compute import base +from tempest.common import compute +from tempest import config +from tempest import exceptions +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators + +CONF = config.CONF + + +@decorators.serial +class ServersAffinityTest(base.BaseV2ComputeAdminTest): + """Test creating servers without multi-create with scheduler_hints. + + The server affinity tests in ServersOnMultiNodesTest use the /servers + multi-create API and therefore do not test affinity behavior when server + group members already exist on hosts. + + These tests must be run in serial because they will be disabling compute + hosts in order to verify affinity behavior. + """ + # 2.64 added 'policy' and 'rules' fields to POST /os-server-groups + min_microversion = '2.64' + + @classmethod + def resource_setup(cls): + super().resource_setup() + + # Disable all compute hosts except two. + services = cls.os_admin.services_client.list_services( + binary='nova-compute')['services'] + num_extra = len(services) - 2 + for i in range(num_extra): + service = services.pop() + cls.os_admin.services_client.update_service( + service['id'], status='disabled') + cls.services = { + service['host']: service['id'] for service in services} + + @classmethod + def skip_checks(cls): + super().skip_checks() + + if CONF.compute.min_compute_nodes < 2: + raise cls.skipException( + "Less than 2 compute nodes, skipping affinity tests.") + + def _disable_compute_host(self, hostname): + service_id = self.services[hostname] + self.os_admin.services_client.update_service( + service_id, status='disabled') + self.addCleanup( + self.os_admin.services_client.update_service, service_id, + status='enabled') + + def _create_server(self, **kwargs): + body, servers = compute.create_test_server( + self.os_primary, networks='none', **kwargs) + for server in servers: + self.addCleanup(self.servers_client.delete_server, server['id']) + return body + + def _create_server_group(self, **kwargs): + name = data_utils.rand_name( + prefix=CONF.resource_name_prefix, + name=self.__class__.__name__ + "-Server-Group") + group_id = self.server_groups_client.create_server_group( + name=name, **kwargs)['server_group']['id'] + self.addCleanup( + self.server_groups_client.delete_server_group, group_id) + return group_id + + def _create_server_in_group(self, group_id): + hints = {'group': group_id} + server = self._create_server( + scheduler_hints=hints, wait_until='ACTIVE') + # Assert the server is in the group. + server_group = self.server_groups_client.show_server_group( + group_id)['server_group'] + self.assertIn(server['id'], server_group['members']) + return server + + @decorators.attr(type='multinode') + @decorators.idempotent_id('28ef4c29-09db-40a8-aacd-dc5fa321f35e') + @testtools.skipUnless( + compute.is_scheduler_filter_enabled("ServerGroupAffinityFilter"), + 'ServerGroupAffinityFilter is not available.') + def test_create_server_with_affinity(self): + group_id = self._create_server_group(policy='affinity') + + # Create server1 in the group. + server1 = self._create_server_in_group(group_id) + + # Create server2 in the group. + server2 = self._create_server_in_group(group_id) + + # Servers should be on the same host. + self.assertEqual( + self.get_host_for_server(server1['id']), + self.get_host_for_server(server2['id'])) + + @decorators.attr(type='multinode') + @decorators.idempotent_id('3ac1ff4e-0fa2-4069-ae59-695cf829275b') + @testtools.skipUnless( + compute.is_scheduler_filter_enabled("ServerGroupAffinityFilter"), + 'ServerGroupAffinityFilter is not available.') + def test_create_server_with_affinity_negative(self): + group_id = self._create_server_group(policy='affinity') + + # Create server1 in the group. + server1 = self._create_server_in_group(group_id) + + # Disable the compute host server1 is on. + self._disable_compute_host(self.get_host_for_server(server1['id'])) + + # Create server2 in the group. This should fail because affinity policy + # cannot be honored. + self.assertRaises( + exceptions.BuildErrorException, + self._create_server_in_group, group_id) + + @decorators.attr(type='multinode') + @decorators.idempotent_id('99cf4819-479c-4176-a9a6-ad501f6fc4b7') + @testtools.skipUnless( + compute.is_scheduler_filter_enabled("ServerGroupAffinityFilter"), + 'ServerGroupAffinityFilter is not available.') + def test_create_server_with_soft_affinity(self): + group_id = self._create_server_group(policy='soft-affinity') + + # Create server1 in the group. + server1 = self._create_server_in_group(group_id) + + # Disable the compute host server1 is on. + self._disable_compute_host(self.get_host_for_server(server1['id'])) + + # Create server2 in the group. This should succeed because soft + # affinity is best effort and scheduling should go ahead even if the + # policy cannot be honored. + server2 = self._create_server_in_group(group_id) + + # Servers should be on different hosts. + self.assertNotEqual( + self.get_host_for_server(server1['id']), + self.get_host_for_server(server2['id'])) + + @decorators.attr(type='multinode') + @decorators.idempotent_id('475e9db0-5512-41cb-a6b2-4bd6fb3c7603') + @testtools.skipUnless( + compute.is_scheduler_filter_enabled("ServerGroupAntiAffinityFilter"), + 'ServerGroupAntiAffinityFilter is not available.') + def test_create_server_with_anti_affinity(self): + group_id = self._create_server_group(policy='anti-affinity') + + # Create server1 in the group. + server1 = self._create_server_in_group(group_id) + + # Create server2 in the group. + server2 = self._create_server_in_group(group_id) + + # Servers should be on different hosts. + self.assertNotEqual( + self.get_host_for_server(server1['id']), + self.get_host_for_server(server2['id'])) + + @decorators.attr(type='multinode') + @decorators.idempotent_id('c5e43585-0fdd-42a9-a525-2b99465c28df') + @testtools.skipUnless( + compute.is_scheduler_filter_enabled("ServerGroupAntiAffinityFilter"), + 'ServerGroupAntiAffinityFilter is not available.') + def test_create_server_with_anti_affinity_negative(self): + group_id = self._create_server_group(policy='anti-affinity') + + # Create server1 in the group. + server1 = self._create_server_in_group(group_id) + + # Disable the compute host server1 is not on. + self._disable_compute_host(self.get_host_other_than(server1['id'])) + + # Create server2 in the group. This should fail because anti-affinity + # policy cannot be honored. + self.assertRaises( + exceptions.BuildErrorException, + self._create_server_in_group, group_id) + + @decorators.attr(type='multinode') + @decorators.idempotent_id('88a8c3d4-c0e8-4873-ba6f-006004779f29') + @testtools.skipUnless( + compute.is_scheduler_filter_enabled("ServerGroupAntiAffinityFilter"), + 'ServerGroupAntiAffinityFilter is not available.') + def test_create_server_with_anti_affinity_max_server_per_host(self): + group_id = self._create_server_group( + policy='anti-affinity', rules={'max_server_per_host': 2}) + + # Create server1 in the group. + server1 = self._create_server_in_group(group_id) + + # Disable the compute host server1 is not on. + self._disable_compute_host(self.get_host_other_than(server1['id'])) + + # Create server2 in the group. This should succeed because we are + # allowing a maximum of two servers per compute host for anti-affinity. + server2 = self._create_server_in_group(group_id) + + # Servers should be on the same host. + self.assertEqual( + self.get_host_for_server(server1['id']), + self.get_host_for_server(server2['id'])) + + # A attempt to create a third server in the group should fail because + # we have already reached our maximum allowed servers per compute host + # for anti-affinity. + self.assertRaises( + exceptions.BuildErrorException, + self._create_server_in_group, group_id) + + @decorators.attr(type='multinode') + @decorators.idempotent_id('ef2bc189-5ecc-4a23-8c1b-0d70a9138a77') + @testtools.skipUnless( + compute.is_scheduler_filter_enabled("ServerGroupAntiAffinityFilter"), + 'ServerGroupAntiAffinityFilter is not available.') + def test_create_server_with_soft_anti_affinity(self): + group_id = self._create_server_group(policy='soft-anti-affinity') + + # Create server1 in the group. + server1 = self._create_server_in_group(group_id) + + # Disable the compute host server1 is not on. + self._disable_compute_host(self.get_host_other_than(server1['id'])) + + # Create server2 in the group. This should succeed because soft + # anti-affinity is best effort and scheduling should go ahead even if + # the policy cannot be honored. + server2 = self._create_server_in_group(group_id) + + # Servers should be on the same host. + self.assertEqual( + self.get_host_for_server(server1['id']), + self.get_host_for_server(server2['id']))