Merge "Add configurable hostname pattern to filter hosts"
diff --git a/requirements.txt b/requirements.txt
index 6e66046..b0df18b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -23,3 +23,4 @@
debtcollector>=1.2.0 # Apache-2.0
defusedxml>=0.7.1 # PSFL
fasteners>=0.16.0 # Apache-2.0
+testscenarios>=0.5.0
diff --git a/tempest/api/image/v2/test_images_formats.py b/tempest/api/image/v2/test_images_formats.py
new file mode 100644
index 0000000..a234fa2
--- /dev/null
+++ b/tempest/api/image/v2/test_images_formats.py
@@ -0,0 +1,188 @@
+# Copyright 2024 Red Hat, Inc.
+#
+# 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 os
+
+import testscenarios
+import yaml
+
+from tempest.api.compute import base as compute_base
+from tempest.api.image import base
+from tempest.common import waiters
+from tempest import config
+from tempest import exceptions
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+def load_tests(loader, suite, pattern):
+ """Generate scenarios from the image manifest."""
+ if CONF.image.images_manifest_file is None:
+ return suite
+ ImagesFormatTest.scenarios = []
+ with open(CONF.image.images_manifest_file) as f:
+ ImagesFormatTest._manifest = yaml.load(f, Loader=yaml.SafeLoader)
+ for imgdef in ImagesFormatTest._manifest['images']:
+ ImagesFormatTest.scenarios.append((imgdef['name'],
+ {'imgdef': imgdef}))
+ result = loader.suiteClass()
+ result.addTests(testscenarios.generate_scenarios(suite))
+ return result
+
+
+class ImagesFormatTest(base.BaseV2ImageTest,
+ compute_base.BaseV2ComputeTest):
+ def setUp(self):
+ super().setUp()
+ if CONF.image.images_manifest_file is None:
+ self.skipTest('Image format testing is not configured')
+ self._image_base = os.path.dirname(os.path.abspath(
+ CONF.image.images_manifest_file))
+
+ self.images = []
+
+ def tearDown(self):
+ for img in self.images:
+ try:
+ self.client.delete_image(img['id'])
+ except lib_exc.NotFound:
+ pass
+ return super().tearDown()
+
+ @classmethod
+ def resource_setup(cls):
+ super().resource_setup()
+ cls.available_import_methods = cls.client.info_import()[
+ 'import-methods']['value']
+
+ def _test_image(self, image_def, override_format=None, asimport=False):
+ image_name = data_utils.rand_name(
+ prefix=CONF.resource_name_prefix,
+ name=image_def['name'])
+ image = self.client.create_image(
+ name=image_name,
+ container_format='bare',
+ disk_format=override_format or image_def['format'])
+ self.images.append(image)
+ image_fn = os.path.join(self._image_base, image_def['filename'])
+ with open(image_fn, 'rb') as f:
+ if asimport:
+ self.client.stage_image_file(image['id'], f)
+ self.client.image_import(image['id'], method='glance-direct')
+ else:
+ self.client.store_image_file(image['id'], f)
+ return image
+
+ @decorators.idempotent_id('a245fcbe-63ce-4dc1-a1d0-c16d76d9e6df')
+ def test_accept_usable_formats(self):
+ if self.imgdef['usable']:
+ if self.imgdef['format'] in CONF.image.disk_formats:
+ # These are expected to work
+ self._test_image(self.imgdef)
+ else:
+ # If this is not configured to be supported, we should get
+ # a BadRequest from glance
+ self.assertRaises(lib_exc.BadRequest,
+ self._test_image, self.imgdef)
+ else:
+ self.skipTest(
+ 'Glance does not currently reject unusable images on upload')
+
+ @decorators.idempotent_id('7c7c2f16-2e97-4dce-8cb4-bc10be031c85')
+ def test_accept_reject_formats_import(self):
+ """Make sure glance rejects invalid images during conversion."""
+ if 'glance-direct' not in self.available_import_methods:
+ self.skipTest('Import via glance-direct is not available')
+ if not CONF.image_feature_enabled.image_conversion:
+ self.skipTest('Import image_conversion not enabled')
+
+ if self.imgdef['format'] == 'iso':
+ # TODO(danms): Glance does not properly handle ISO conversions
+ # today and this is being fixed currently. Remove when this
+ # is stable and able to be tested.
+ self.skipTest('Glance ISO conversion is not testable')
+
+ glance_noconvert = [
+ # Glance does not support vmdk-sparse-with-footer with the
+ # in-tree format_inspector
+ 'vmdk-sparse-with-footer',
+ ]
+ # Any images glance does not support in *conversion* for some
+ # reason will fail, even though the manifest marks them as usable.
+ expect_fail = any(x in self.imgdef['name']
+ for x in glance_noconvert)
+
+ if (self.imgdef['format'] in CONF.image.disk_formats and
+ self.imgdef['usable'] and not expect_fail):
+ # Usable images should end up in active state
+ image = self._test_image(self.imgdef, asimport=True)
+ waiters.wait_for_image_status(self.client, image['id'],
+ 'active')
+ else:
+ # FIXME(danms): Make this better, but gpt will fail before
+ # the import even starts until glance has it in its API
+ # schema as a valid value. Other formats expected to fail
+ # do so during import and return to queued state.
+ if self.imgdef['format'] not in CONF.image.disk_formats:
+ self.assertRaises(lib_exc.BadRequest,
+ self._test_image,
+ self.imgdef, asimport=True)
+ else:
+ image = self._test_image(self.imgdef, asimport=True)
+ waiters.wait_for_image_status(self.client, image['id'],
+ 'queued')
+ self.client.delete_image(image['id'])
+
+ def _create_server_with_image_def(self, image_def, **overrides):
+ image_def = dict(image_def, **overrides)
+ image = self._test_image(image_def)
+ server = self.create_test_server(name='server-%s' % image['name'],
+ image_id=image['id'],
+ wait_until='ACTIVE')
+ return server
+
+ @decorators.idempotent_id('f77394bc-81f4-4d54-9f5b-e48f3d6b5376')
+ def test_compute_rejects_invalid(self):
+ """Make sure compute rejects invalid/insecure images."""
+ if self.imgdef['format'] not in CONF.image.disk_formats:
+ # if this format is not allowed by glance, we can not create
+ # a properly-formatted image for it, so skip it.
+ self.skipTest(
+ 'Format %s not allowed by config' % self.imgdef['format'])
+
+ # VMDK with footer is not supported by anyone yet until fixed:
+ # https://bugs.launchpad.net/glance/+bug/2073262
+ is_broken = 'footer' in self.imgdef['name']
+
+ if self.imgdef['usable'] and not is_broken:
+ server = self._create_server_with_image_def(self.imgdef)
+ self.delete_server(server['id'])
+ else:
+ self.assertRaises(exceptions.BuildErrorException,
+ self._create_server_with_image_def,
+ self.imgdef)
+
+ @decorators.idempotent_id('ffe21610-e801-4992-9b81-e2d646e2e2e9')
+ def test_compute_rejects_format_mismatch(self):
+ """Make sure compute rejects any image with a format mismatch."""
+ # Lying about the disk_format should always fail
+ override_fmt = (
+ self.imgdef['format'] in ('raw', 'gpt') and 'qcow2' or 'raw')
+ self.assertRaises(exceptions.BuildErrorException,
+ self._create_server_with_image_def,
+ self.imgdef,
+ format=override_fmt)
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index b202940..db4407d 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -115,21 +115,32 @@
return [item for item in item_list
if item['tenant_id'] == self.tenant_id]
- def _filter_by_prefix(self, item_list):
- items = [item for item in item_list
- if item['name'].startswith(self.prefix)]
+ def _filter_by_prefix(self, item_list, top_key=None):
+ items = []
+ for item in item_list:
+ name = item[top_key]['name'] if top_key else item['name']
+ if name.startswith(self.prefix):
+ items.append(item)
return items
def _filter_by_resource_list(self, item_list, attr):
if attr not in self.resource_list_json:
return []
- items = [item for item in item_list if item['id']
- in self.resource_list_json[attr].keys()]
+ items = []
+ for item in item_list:
+ item_id = (item['keypair']['name'] if attr == 'keypairs'
+ else item['id'])
+ if item_id in self.resource_list_json[attr].keys():
+ items.append(item)
return items
def _filter_out_ids_from_saved(self, item_list, attr):
- items = [item for item in item_list if item['id']
- not in self.saved_state_json[attr].keys()]
+ items = []
+ for item in item_list:
+ item_id = (item['keypair']['name'] if attr == 'keypairs'
+ else item['id'])
+ if item_id not in self.saved_state_json[attr].keys():
+ items.append(item)
return items
def list(self):
@@ -294,16 +305,11 @@
keypairs = client.list_keypairs()['keypairs']
if self.prefix:
- keypairs = self._filter_by_prefix(keypairs)
+ keypairs = self._filter_by_prefix(keypairs, 'keypair')
elif self.is_resource_list:
- keypairs = [keypair for keypair in keypairs
- if keypair['keypair']['name']
- in self.resource_list_json['keypairs'].keys()]
+ keypairs = self._filter_by_resource_list(keypairs, 'keypairs')
elif not self.is_save_state:
- # recreate list removing saved keypairs
- keypairs = [keypair for keypair in keypairs
- if keypair['keypair']['name']
- not in self.saved_state_json['keypairs'].keys()]
+ keypairs = self._filter_out_ids_from_saved(keypairs, 'keypairs')
LOG.debug("List count, %s Keypairs", len(keypairs))
return keypairs
diff --git a/tempest/config.py b/tempest/config.py
index 9d7526a..b1f736c 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -688,7 +688,11 @@
default=['qcow2', 'raw', 'ami', 'ari', 'aki', 'vhd', 'vmdk',
'vdi', 'iso', 'vhdx'],
help="A list of image's disk formats "
- "users can specify.")
+ "users can specify."),
+ cfg.StrOpt('images_manifest_file',
+ default=None,
+ help="A path to a manifest.yml generated using the "
+ "os-test-images project"),
]
image_feature_group = cfg.OptGroup(name='image-feature-enabled',
@@ -721,6 +725,9 @@
help=('Is show_multiple_locations enabled in glance. '
'Note that at least one http store must be enabled as '
'well, because we use that location scheme to test.')),
+ cfg.BoolOpt('image_conversion',
+ default=False,
+ help=('Is image_conversion enabled in glance.')),
]
network_group = cfg.OptGroup(name='network',
diff --git a/tempest/tests/cmd/test_cleanup_services.py b/tempest/tests/cmd/test_cleanup_services.py
index 2557145..7f8db9f 100644
--- a/tempest/tests/cmd/test_cleanup_services.py
+++ b/tempest/tests/cmd/test_cleanup_services.py
@@ -610,21 +610,14 @@
self._test_prefix_opt_precedence(delete_mock)
def test_resource_list_opt_precedence(self):
- delete_mock = [(self.filter_prefix, [], None),
+ delete_mock = [(self.filter_saved_state, [], None),
+ (self.filter_resource_list, [], None),
+ (self.filter_prefix, [], None),
(self.get_method, self.response, 200),
(self.validate_response, 'validate', None),
(self.delete_method, 'error', None),
(self.log_method, 'exception', None)]
- serv = self._create_cmd_service(
- self.service_class, is_resource_list=True)
-
- _, fixtures = self.run_function_with_mocks(
- serv.delete,
- delete_mock
- )
-
- # Check that prefix was not used for filtering
- fixtures[0].mock.assert_not_called()
+ self._test_resource_list_opt_precedence(delete_mock)
class TestVolumeService(BaseCmdServiceTests):
diff --git a/zuul.d/base.yaml b/zuul.d/base.yaml
index 633f501..4de4111 100644
--- a/zuul.d/base.yaml
+++ b/zuul.d/base.yaml
@@ -12,6 +12,10 @@
timeout: 7200
roles: &base_roles
- zuul: opendev.org/openstack/devstack
+ failure-output:
+ # This matches stestr/tempest output when a test fails
+ # {1} tempest.api.test_blah [5.743446s] ... FAILED
+ - '\{\d+\} (.*?) \[[\d\.]+s\] \.\.\. FAILED'
vars: &base_vars
devstack_localrc:
IMAGE_URLS: http://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img, http://download.cirros-cloud.net/0.6.1/cirros-0.6.1-x86_64-disk.img
@@ -60,6 +64,10 @@
required-projects: *base_required-projects
timeout: 7200
roles: *base_roles
+ failure-output:
+ # This matches stestr/tempest output when a test fails
+ # {1} tempest.api.test_blah [5.743446s] ... FAILED
+ - '\{\d+\} (.*?) \[[\d\.]+s\] \.\.\. FAILED'
vars: *base_vars
run: playbooks/devstack-tempest-ipv6.yaml
post-run: playbooks/post-tempest.yaml