From 3244abd02054f2aced3e18668eb752fa052c23d3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 22 Dec 2021 14:57:19 +0000 Subject: [PATCH] tests: Centralize configuration of default flavor, image Different tests were doing this in different ways. Centralize it all in the base test class for functional tests. For both flavor and image, the order of precedence is: - Environment variables - clouds.yaml configuration - Guesswork (pick a cirros, Ubuntu or CentOS image for images, or the flavor with the lowest RAM for flavors) Change-Id: I90fda8ef48008c7fa634edc295c0e83e5f29387f Signed-off-by: Stephen Finucane --- doc/source/contributor/testing.rst | 5 +- openstack/tests/functional/base.py | 133 +++++++++++++----- .../tests/functional/cloud/test_clustering.py | 76 +++++----- .../tests/functional/cloud/test_compute.py | 6 - .../functional/cloud/test_floating_ip.py | 8 +- .../tests/functional/cloud/test_image.py | 3 - .../tests/functional/cloud/test_inventory.py | 8 +- openstack/tests/functional/cloud/util.py | 43 ------ .../functional/clustering/test_cluster.py | 4 +- .../functional/compute/v2/test_server.py | 41 +++--- 10 files changed, 163 insertions(+), 164 deletions(-) delete mode 100644 openstack/tests/functional/cloud/util.py diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index c3261df16..275a418b2 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -68,8 +68,9 @@ configured for the one cloud. These accounts are: configurable via the ``OPENSTACKSDK_DEMO_CLOUD_ALT`` environment variable In addition, you must indicate the names of the flavor and image that should be -used for tests. These can be configured via ``functional.flavor_name`` and -``functional.image_name`` settings in the ``clouds.yaml`` file. +used for tests. These can be configured via ``OPENSTACKSDK_FLAVOR`` and +``OPENSTACKSDK_IMAGE`` environment variables or ``functional.flavor_name`` and +``functional.image_name`` settings in the ``clouds.yaml`` file, respectively. Finally, you can configure the timeout for tests using the ``OPENSTACKSDK_FUNC_TEST_TIMEOUT`` environment variable (defaults to 300 diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 19dc66d80..1e20f3ee4 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import operator import os from keystoneauth1 import discover @@ -28,9 +29,8 @@ TEST_CLOUD_NAME = os.getenv('OS_CLOUD', 'devstack-admin') TEST_CLOUD_REGION = openstack.config.get_cloud_region(cloud=TEST_CLOUD_NAME) -def _get_resource_value(resource_key, default): - return TEST_CONFIG.get_extra_config( - 'functional').get(resource_key, default) +def _get_resource_value(resource_key): + return TEST_CONFIG.get_extra_config('functional').get(resource_key) def _disable_keep_alive(conn): @@ -38,10 +38,6 @@ def _disable_keep_alive(conn): sess.keep_alive = False -IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.4.0-x86_64-disk') -FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') - - class BaseFunctionalTest(base.TestCase): _wait_for_timeout_key = '' @@ -52,10 +48,12 @@ class BaseFunctionalTest(base.TestCase): _disable_keep_alive(self.conn) self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') - self._demo_name_alt = os.environ.get('OPENSTACKSDK_DEMO_CLOUD_ALT', - 'devstack-alt') + self._demo_name_alt = os.environ.get( + 'OPENSTACKSDK_DEMO_CLOUD_ALT', 'devstack-alt', + ) self._op_name = os.environ.get( - 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin') + 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin', + ) self.config = openstack.config.OpenStackConfig() self._set_user_cloud() @@ -64,6 +62,9 @@ class BaseFunctionalTest(base.TestCase): self.identity_version = \ self.operator_cloud.config.get_api_version('identity') + self.flavor = self._pick_flavor() + self.image = self._pick_image() + # Defines default timeout for wait_for methods used # in the functional tests self._wait_for_timeout = int( @@ -71,8 +72,7 @@ class BaseFunctionalTest(base.TestCase): 'OPENSTACKSDK_FUNC_TEST_TIMEOUT', 300))) def _set_user_cloud(self, **kwargs): - user_config = self.config.get_one( - cloud=self._demo_name, **kwargs) + user_config = self.config.get_one(cloud=self._demo_name, **kwargs) self.user_cloud = connection.Connection(config=user_config) _disable_keep_alive(self.user_cloud) @@ -84,37 +84,95 @@ class BaseFunctionalTest(base.TestCase): _disable_keep_alive(self.user_cloud_alt) def _set_operator_cloud(self, **kwargs): - operator_config = self.config.get_one( - cloud=self._op_name, **kwargs) + operator_config = self.config.get_one(cloud=self._op_name, **kwargs) self.operator_cloud = connection.Connection(config=operator_config) _disable_keep_alive(self.operator_cloud) - def pick_image(self): + def _pick_flavor(self): + """Pick a sensible flavor to run tests with. + + This returns None if the compute service is not present (e.g. + ironic-only deployments). + """ + if not self.user_cloud.has_service('compute'): + return None + + flavors = self.user_cloud.list_flavors(get_extra=False) + self.add_info_on_exception('flavors', flavors) + + flavor_name = os.environ.get('OPENSTACKSDK_FLAVOR') + + if not flavor_name: + flavor_name = _get_resource_value('flavor_name') + + if flavor_name: + for flavor in flavors: + if flavor.name == flavor_name: + return flavor + + raise self.failureException( + "Cloud does not have flavor '%s'", flavor_name, + ) + + # Enable running functional tests against RAX, which requires + # performance flavors be used for boot from volume + + for flavor in sorted(flavors, key=operator.attrgetter('ram')): + if 'performance' in flavor.name: + return flavor + + # Otherwise, pick the smallest flavor with a ephemeral disk configured + + for flavor in sorted(flavors, key=operator.attrgetter('ram')): + if flavor.disk: + return flavor + + raise self.failureException('No sensible flavor found') + + def _pick_image(self): + """Pick a sensible image to run tests with. + + This returns None if the image service is not present. + """ + if not self.user_cloud.has_service('image'): + return None + images = self.user_cloud.list_images() self.add_info_on_exception('images', images) image_name = os.environ.get('OPENSTACKSDK_IMAGE') + + if not image_name: + image_name = _get_resource_value('image_name') + if image_name: for image in images: if image.name == image_name: return image - self.assertFalse( - "Cloud does not have {image}".format(image=image_name)) + + raise self.failureException( + "Cloud does not have image '%s'", image_name, + ) for image in images: if image.name.startswith('cirros') and image.name.endswith('-uec'): return image + for image in images: - if (image.name.startswith('cirros') - and image.disk_format == 'qcow2'): + if ( + image.name.startswith('cirros') + and image.disk_format == 'qcow2' + ): return image + for image in images: if image.name.lower().startswith('ubuntu'): return image for image in images: if image.name.lower().startswith('centos'): return image - self.assertFalse('no sensible image available') + + raise self.failureException('No sensible image found') def addEmptyCleanup(self, func, *args, **kwargs): def cleanup(): @@ -125,12 +183,12 @@ class BaseFunctionalTest(base.TestCase): def require_service(self, service_type, min_microversion=None, **kwargs): """Method to check whether a service exists - Usage: - class TestMeter(base.BaseFunctionalTest): - ... - def setUp(self): - super(TestMeter, self).setUp() - self.require_service('metering') + Usage:: + + class TestMeter(base.BaseFunctionalTest): + def setUp(self): + super(TestMeter, self).setUp() + self.require_service('metering') :returns: True if the service exists, otherwise False. """ @@ -144,16 +202,19 @@ class BaseFunctionalTest(base.TestCase): data = self.conn.session.get_endpoint_data( service_type=service_type, **kwargs) - if not (data.min_microversion - and data.max_microversion - and discover.version_between( - data.min_microversion, - data.max_microversion, - min_microversion)): - self.skipTest('Service {service_type} does not provide ' - 'microversion {ver}'.format( - service_type=service_type, - ver=min_microversion)) + if not ( + data.min_microversion + and data.max_microversion + and discover.version_between( + data.min_microversion, + data.max_microversion, + min_microversion, + ) + ): + self.skipTest( + f'Service {service_type} does not provide microversion ' + f'{min_microversion}' + ) class KeystoneBaseFunctionalTest(BaseFunctionalTest): diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 354f2d965..80fd8a7a7 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -115,8 +115,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -145,8 +145,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -189,8 +189,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -232,8 +232,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -318,8 +318,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -392,8 +392,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -474,8 +474,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -567,8 +567,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -656,8 +656,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -713,8 +713,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -775,8 +775,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -863,8 +863,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -914,8 +914,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1018,8 +1018,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1056,8 +1056,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1094,8 +1094,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1130,8 +1130,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1297,8 +1297,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1356,8 +1356,8 @@ class TestClustering(base.BaseFunctionalTest): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 787f356da..c91fe7e08 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -23,7 +23,6 @@ from fixtures import TimeoutException from openstack.cloud import exc from openstack.tests.functional import base -from openstack.tests.functional.cloud.util import pick_flavor from openstack import utils @@ -34,11 +33,6 @@ class TestCompute(base.BaseFunctionalTest): self.TIMEOUT_SCALING_FACTOR = 1.5 super(TestCompute, self).setUp() - self.flavor = pick_flavor( - self.user_cloud.list_flavors(get_extra=False)) - if self.flavor is None: - self.assertFalse('no sensible flavor available') - self.image = self.pick_image() self.server_name = self.getUniqueString() def _cleanup_servers_and_volumes(self, server_name): diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 3014977bd..24d72ed4b 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -28,7 +28,6 @@ from openstack.cloud.exc import OpenStackCloudException from openstack.cloud import meta from openstack import proxy from openstack.tests.functional import base -from openstack.tests.functional.cloud.util import pick_flavor from openstack import utils @@ -36,12 +35,7 @@ class TestFloatingIP(base.BaseFunctionalTest): timeout = 60 def setUp(self): - super(TestFloatingIP, self).setUp() - self.flavor = pick_flavor( - self.user_cloud.list_flavors(get_extra=False)) - if self.flavor is None: - self.assertFalse('no sensible flavor available') - self.image = self.pick_image() + super().setUp() # Generate a random name for these tests self.new_item_name = self.getUniqueString() diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index 46692866d..b6e43fbb0 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -25,9 +25,6 @@ from openstack.tests.functional import base class TestImage(base.BaseFunctionalTest): - def setUp(self): - super(TestImage, self).setUp() - self.image = self.pick_image() def test_create_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 9b9d52702..92f0a3e84 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -21,21 +21,15 @@ Functional tests for `shade` inventory methods. from openstack.cloud import inventory from openstack.tests.functional import base -from openstack.tests.functional.cloud.util import pick_flavor class TestInventory(base.BaseFunctionalTest): def setUp(self): - super(TestInventory, self).setUp() + super().setUp() # This needs to use an admin account, otherwise a public IP # is not allocated from devstack. self.inventory = inventory.OpenStackInventory(cloud='devstack-admin') self.server_name = self.getUniqueString('inventory') - self.flavor = pick_flavor( - self.user_cloud.list_flavors(get_extra=False)) - if self.flavor is None: - self.assertTrue(False, 'no sensible flavor available') - self.image = self.pick_image() self.addCleanup(self._cleanup_server) server = self.operator_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, diff --git a/openstack/tests/functional/cloud/util.py b/openstack/tests/functional/cloud/util.py deleted file mode 100644 index d16bfd8c1..000000000 --- a/openstack/tests/functional/cloud/util.py +++ /dev/null @@ -1,43 +0,0 @@ -# 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. - -""" -util --------------------------------- - -Util methods for functional tests -""" -import operator -import os - - -def pick_flavor(flavors): - """Given a flavor list pick the smallest one.""" - # Enable running functional tests against rax - which requires - # performance flavors be used for boot from volume - flavor_name = os.environ.get('OPENSTACKSDK_FLAVOR') - if flavor_name: - for flavor in flavors: - if flavor.name == flavor_name: - return flavor - return None - - for flavor in sorted( - flavors, - key=operator.attrgetter('ram')): - if 'performance' in flavor.name: - return flavor - for flavor in sorted( - flavors, - key=operator.attrgetter('ram')): - if flavor.disk: - return flavor diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index 4f03e9f21..9db6e18fa 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -40,8 +40,8 @@ class TestCluster(base.BaseFunctionalTest): 'version': 1.0, 'properties': { 'name': self.getUniqueString(), - 'flavor': base.FLAVOR_NAME, - 'image': base.IMAGE_NAME, + 'flavor': self.flavor.name, + 'image': self.image.name, 'networks': [{'network': self.network.id}] }}} diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index d19b7605b..8803607a4 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -11,7 +11,6 @@ # under the License. from openstack.compute.v2 import server -from openstack.tests.functional import base from openstack.tests.functional.compute import base as ft_base from openstack.tests.functional.network.v2 import test_network @@ -23,21 +22,24 @@ class TestServerAdmin(ft_base.BaseComputeTest): self._set_operator_cloud(interface='admin') self.NAME = 'needstobeshortandlowercase' self.USERDATA = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' - flavor = self.conn.compute.find_flavor(base.FLAVOR_NAME, - ignore_missing=False) - image = self.conn.compute.find_image(base.IMAGE_NAME, - ignore_missing=False) volume = self.conn.create_volume(1) sot = self.conn.compute.create_server( - name=self.NAME, flavor_id=flavor.id, image_id=image.id, - networks='none', user_data=self.USERDATA, - block_device_mapping=[{ - 'uuid': volume.id, - 'source_type': 'volume', - 'boot_index': 0, - 'destination_type': 'volume', - 'delete_on_termination': True, - 'volume_size': 1}]) + name=self.NAME, + flavor_id=self.flavor.id, + image_id=self.image.id, + networks='none', + user_data=self.USERDATA, + block_device_mapping=[ + { + 'uuid': volume.id, + 'source_type': 'volume', + 'boot_index': 0, + 'destination_type': 'volume', + 'delete_on_termination': True, + 'volume_size': 1, + }, + ], + ) self.conn.compute.wait_for_server(sot, wait=self._wait_for_timeout) assert isinstance(sot, server.Server) self.assertEqual(self.NAME, sot.name) @@ -72,10 +74,6 @@ class TestServer(ft_base.BaseComputeTest): self.subnet = None self.cidr = '10.99.99.0/16' - flavor = self.conn.compute.find_flavor(base.FLAVOR_NAME, - ignore_missing=False) - image = self.conn.compute.find_image(base.IMAGE_NAME, - ignore_missing=False) self.network, self.subnet = test_network.create_network( self.conn, self.NAME, @@ -83,8 +81,11 @@ class TestServer(ft_base.BaseComputeTest): self.assertIsNotNone(self.network) sot = self.conn.compute.create_server( - name=self.NAME, flavor_id=flavor.id, image_id=image.id, - networks=[{"uuid": self.network.id}]) + name=self.NAME, + flavor_id=self.flavor.id, + image_id=self.image.id, + networks=[{"uuid": self.network.id}], + ) self.conn.compute.wait_for_server(sot, wait=self._wait_for_timeout) assert isinstance(sot, server.Server) self.assertEqual(self.NAME, sot.name)