Merge "Validate automatic lessee"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..62b24c9
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,66 @@
+---
+default_language_version:
+ # force all unspecified python hooks to run python3
+ python: python3
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ - id: trailing-whitespace
+ # NOTE(JayF): We shouldn't modify release notes after their
+ # associated release. Instead, ignore these minor lint issues.
+ - id: mixed-line-ending
+ args: ['--fix', 'lf']
+ exclude: |
+ (?x)(
+ .*.svg$|
+ )
+ - id: fix-byte-order-marker
+ - id: check-merge-conflict
+ - id: debug-statements
+ - id: check-json
+ files: .*\.json$
+ - id: check-yaml
+ files: .*\.(yaml|yml)$
+ exclude: releasenotes/.*$
+ - repo: https://github.com/Lucas-C/pre-commit-hooks
+ rev: v1.5.4
+ hooks:
+ - id: remove-tabs
+ exclude: '.*\.(svg)$'
+ - repo: https://opendev.org/openstack/hacking
+ rev: 6.1.0
+ hooks:
+ - id: hacking
+ additional_dependencies: []
+ exclude: '^(doc|releasenotes|tools)/.*$'
+ - repo: https://github.com/codespell-project/codespell
+ rev: v2.4.1
+ hooks:
+ - id: codespell
+ args: [--write-changes]
+ - repo: https://github.com/sphinx-contrib/sphinx-lint
+ rev: v1.0.0
+ hooks:
+ - id: sphinx-lint
+ args: [--enable=default-role]
+ files: ^doc/|releasenotes|api-ref
+ - repo: https://opendev.org/openstack/bashate
+ rev: 2.1.0
+ hooks:
+ - id: bashate
+ args: ["-iE006,E044", "-eE005,E042"]
+ name: bashate
+ description: This hook runs bashate for linting shell scripts
+ entry: bashate
+ language: python
+ types: [shell]
+ - repo: https://github.com/PyCQA/doc8
+ rev: v1.1.2
+ hooks:
+ - id: doc8
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.7.3
+ hooks:
+ - id: ruff
+ args: ['--fix', '--unsafe-fixes']
diff --git a/doc/source/conf.py b/doc/source/conf.py
old mode 100755
new mode 100644
diff --git a/ironic_tempest_plugin/common/waiters.py b/ironic_tempest_plugin/common/waiters.py
index e538cd8..cd13fe7 100644
--- a/ironic_tempest_plugin/common/waiters.py
+++ b/ironic_tempest_plugin/common/waiters.py
@@ -180,7 +180,7 @@
field_value = node[field]
if raise_if_insufficent_access and '** Redacted' in field_value:
msg = ('Unable to see contents of redacted field '
- 'indicating insufficent access to execute this test.')
+ 'indicating insufficient access to execute this test.')
raise lib_exc.InsufficientAPIAccess(msg)
return value in field_value
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index a728bef..1dce427 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -98,7 +98,7 @@
help="Timeout for association of Nova instance and Ironic "
"node"),
cfg.IntOpt('inspect_timeout',
- default=10,
+ default=300,
help="Timeout for inspecting an Ironic node."),
cfg.IntOpt('power_timeout',
default=60,
@@ -203,6 +203,9 @@
cfg.ListOpt('enabled_power_interfaces',
default=['fake', 'ipmitool'],
help="List of Ironic enabled power interfaces."),
+ cfg.ListOpt('enabled_inspect_interfaces',
+ default=['no-inspect'],
+ help="List of Ironic enabled inspect interfaces."),
cfg.StrOpt('default_rescue_interface',
help="Ironic default rescue interface."),
cfg.StrOpt('firmware_image_url',
@@ -279,6 +282,10 @@
"support for embedded network metadata through glean "
"or cloud-init, and thus cannot be executed with "
"most default job configurations."),
+ cfg.BoolOpt('trunks_supported',
+ default=False,
+ help="Define if trunks are supported by networking driver "
+ "with baremetal nodes."),
]
BaremetalIntrospectionGroup = [
diff --git a/ironic_tempest_plugin/exceptions.py b/ironic_tempest_plugin/exceptions.py
index 865ab08..a1a9873 100644
--- a/ironic_tempest_plugin/exceptions.py
+++ b/ironic_tempest_plugin/exceptions.py
@@ -30,5 +30,5 @@
class InsufficientAPIAccess(exceptions.TempestException):
- message = ("Insufficent Access to the API exists. Please use a user "
+ message = ("Insufficient Access to the API exists. Please use a user "
"with an elevated level of access to execute this test.")
diff --git a/ironic_tempest_plugin/services/baremetal/base.py b/ironic_tempest_plugin/services/baremetal/base.py
index f23310f..a4277b2 100644
--- a/ironic_tempest_plugin/services/baremetal/base.py
+++ b/ironic_tempest_plugin/services/baremetal/base.py
@@ -23,6 +23,11 @@
# separate processes so global variables are not shared among them.
BAREMETAL_MICROVERSION = None
+# Interfaces that can be set via the baremetal client and by logic in scenario
+# managers.
+SUPPORTED_INTERFACES = ['bios', 'deploy', 'rescue', 'boot', 'raid',
+ 'management', 'power', 'inspect']
+
def set_baremetal_api_microversion(baremetal_microversion):
global BAREMETAL_MICROVERSION
@@ -74,8 +79,18 @@
def get_min_max_api_microversions(self):
"""Returns a tuple of minimum and remote microversions."""
- _, resp_body = self._show_request(None, uri='/')
- version = resp_body.get('default_version', {})
+ if '/v1' in self.base_url:
+ root_uri = '/'
+ else:
+ # NOTE(dtantsur): we should just use / here but due to a bug in
+ # Ironic, / does not contain the microversion headers. See
+ # https://bugs.launchpad.net/ironic/+bug/2079023
+ root_uri = '/v1'
+ _, resp_body = self._show_request(None, uri=root_uri)
+ try:
+ version = resp_body['default_version']
+ except KeyError:
+ version = resp_body['version']
api_min = version.get('min_version')
api_max = version.get('version')
return (api_min, api_max)
diff --git a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index 5715609..182cacc 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -20,8 +20,28 @@
version = '1'
uri_prefix = 'v1'
- @staticmethod
- def _get_headers(api_version):
+ node_attributes = (
+ 'properties/cpu_arch',
+ 'properties/cpus',
+ 'properties/local_gb',
+ 'properties/memory_mb',
+ 'driver',
+ 'instance_uuid',
+ 'resource_class',
+ 'protected',
+ 'protected_reason',
+ # TODO(dtantsur): maintenance is set differently
+ # in newer API versions.
+ 'maintenance',
+ 'description',
+ 'shard'
+ ) + tuple(
+ f'{iface}_interface'
+ for iface in base.SUPPORTED_INTERFACES
+ )
+
+ @classmethod
+ def _get_headers(cls, api_version):
"""Return headers for a request.
Currently supports a header specifying the API version to use.
@@ -35,7 +55,7 @@
headers = None
if api_version is not None:
extra_headers = True
- headers = {'x-openstack-ironic-api-version': api_version}
+ headers = {cls.api_microversion_header_name: api_version}
return extra_headers, headers
@base.handle_errors
@@ -119,6 +139,11 @@
return self._list_request('deploy_templates', **kwargs)
@base.handle_errors
+ def list_runbooks(self, **kwargs):
+ """List all runbooks."""
+ return self._list_request('runbooks', **kwargs)
+
+ @base.handle_errors
def show_node(self, uuid, api_version=None):
"""Gets a specific node.
@@ -247,6 +272,14 @@
"""
return self._show_request('deploy_templates', deploy_template_ident)
+ def show_runbook(self, runbook_ident):
+ """Gets a specific runbook.
+
+ :param runbook_ident: Name or UUID of runbook.
+ :return: Serialized runbook as a dictionary.
+ """
+ return self._show_request('runbooks', runbook_ident)
+
@base.handle_errors
def create_node(self, chassis_id=None, **kwargs):
"""Create a baremetal node with the specified parameters.
@@ -424,6 +457,23 @@
return self._create_request('deploy_templates', kwargs)
@base.handle_errors
+ def create_runbook(self, name, **kwargs):
+ """Create a runbook with the specified parameters.
+
+ :param name: The name of the runbook.
+ :param kwargs:
+ steps: steps of the runbook.
+ uuid: UUID of the runbook. Optional.
+ public: An optional boolean value indicating whether the runbook
+ is public (accessible to others)
+ or private (restricted to the owner).
+ extra: meta-data of the runbook. Optional.
+ :return: A tuple with the server response and the created runbook.
+ """
+ kwargs['name'] = name
+ return self._create_request('runbooks', kwargs)
+
+ @base.handle_errors
def delete_node(self, uuid):
"""Deletes a node having the specified UUID.
@@ -491,6 +541,15 @@
return self._delete_request('deploy_templates', deploy_template_ident)
@base.handle_errors
+ def delete_runbook(self, runbook_ident):
+ """Deletes a runbook having the specified name or UUID.
+
+ :param runbook_ident: Name or UUID of the runbook.
+ :return: A tuple with the server response and the response body.
+ """
+ return self._delete_request('runbooks', runbook_ident)
+
+ @base.handle_errors
def update_node(self, uuid, patch=None, **kwargs):
"""Update the specified node.
@@ -507,26 +566,8 @@
else:
params = {}
- node_attributes = ('properties/cpu_arch',
- 'properties/cpus',
- 'properties/local_gb',
- 'properties/memory_mb',
- 'driver',
- 'bios_interface',
- 'deploy_interface',
- 'raid_interface',
- 'rescue_interface',
- 'instance_uuid',
- 'resource_class',
- 'protected',
- 'protected_reason',
- # TODO(dtantsur): maintenance is set differently
- # in newer API versions.
- 'maintenance',
- 'description',
- 'shard')
if not patch:
- patch = self._make_patch(node_attributes, **kwargs)
+ patch = self._make_patch(self.node_attributes, **kwargs)
return self._patch_request('nodes', uuid, patch, params=params)
@@ -596,6 +637,18 @@
patch)
@base.handle_errors
+ def update_runbook(self, runbook_ident, patch):
+ """Update the specified runbook.
+
+ :param runbook_ident: Name or UUID of the runbook.
+ :param patch: List of dicts representing json patches. Each dict
+ has keys 'path', 'op' and 'value'; to update a field.
+ :return: A tuple with the server response and the updated runbook.
+ """
+
+ return self._patch_request('runbooks', runbook_ident, patch)
+
+ @base.handle_errors
def set_node_power_state(self, node_uuid, state):
"""Set power state of the specified node.
@@ -622,7 +675,8 @@
@base.handle_errors
def set_node_provision_state(self, node_uuid, state, configdrive=None,
- clean_steps=None, rescue_password=None):
+ clean_steps=None, rescue_password=None,
+ runbook=None):
"""Set provision state of the specified node.
:param node_uuid: The unique identifier of the node.
@@ -632,6 +686,7 @@
configuration drive string.
:param clean_steps: A list with clean steps to execute.
:param rescue_password: user password used to rescue.
+ :param runbook: The unique identifier of a runbook.
"""
data = {'target': state}
# NOTE (vsaienk0): Add both here if specified, do not check anything.
@@ -642,6 +697,8 @@
data['clean_steps'] = clean_steps
if rescue_password is not None:
data['rescue_password'] = rescue_password
+ if runbook is not None:
+ data['runbook'] = runbook
return self._put_request('nodes/%s/states/provision' % node_uuid,
data)
@@ -782,7 +839,7 @@
headers = None
if api_version is not None:
extra_headers = True
- headers = {'x-openstack-ironic-api-version': api_version}
+ headers = {self.api_microversion_header_name: api_version}
return self._list_request('nodes/%s/vifs' % node_uuid,
headers=headers,
extra_headers=extra_headers)
@@ -951,3 +1008,35 @@
}
return self._create_request_no_response_body('heartbeat', kwargs)
+
+ @base.handle_errors
+ def show_inventory(self, uuid, api_version='1.81'):
+ """Gets hardware inventory for the specific node.
+
+ :param uuid: Unique identifier of the node in UUID format.
+ :param api_version: Ironic API version to use.
+ :return: Inventory as a dictionary.
+
+ """
+ extra_headers, headers = self._get_headers(api_version)
+ resp, body = self._show_request(
+ 'inventory', uuid, headers=headers, extra_headers=extra_headers,
+ uri=f'{self.uri_prefix}/nodes/{uuid}/inventory')
+ self.expected_success(http_client.OK, resp.status)
+ return body
+
+ @base.handle_errors
+ def get_shards(self, api_version='1.82'):
+ """Get all shards."""
+
+ extra_headers, headers = self._get_headers(api_version)
+ return self._list_request('shards', headers=headers,
+ extra_headers=extra_headers)
+
+ @base.handle_errors
+ def list_node_firmware(self, node_uuid):
+ """List firmware for a node.
+
+ :param node_uuid: The unique identifier of the node.
+ """
+ return self._list_request('/nodes/%s/firmware' % node_uuid)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_microversion_enforcement.py b/ironic_tempest_plugin/tests/api/admin/test_microversion_enforcement.py
new file mode 100644
index 0000000..ebd7451
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_microversion_enforcement.py
@@ -0,0 +1,158 @@
+# 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 oslo_utils import uuidutils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.services.baremetal.v1.json.baremetal_client import \
+ BaremetalClient
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+class MicroversionTestMixin:
+ """Mixin class containing shared microversion test functionality."""
+
+ def _microversion_test(
+ self, method_name, min_version, expected_error, required_args):
+ """Test methods with invalid API versions"""
+
+ major, minor = map(int, min_version.split('.'))
+ # Set limits, as lowest microversion is 1.1
+ if minor <= 10:
+ minor = 11
+ if minor <= 1:
+ minor = 2
+ invalid_versions = [
+ f"{major}.{minor - 1}",
+ f"{major}.{minor - 10}",
+ ]
+
+ # Get method name from method object
+ method_name = method_name.__name__
+
+ for microversion in invalid_versions:
+ for arg_name, arg_value in required_args.items():
+ msg = (
+ f"Testing {method_name} with version {microversion} "
+ f"and argument {arg_name}={arg_value}"
+ )
+ with self.subTest(
+ msg=msg, method=method_name, version=microversion
+ ):
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture(
+ microversion
+ )
+ )
+ method = getattr(self.client, method_name)
+
+ self.assertRaises(
+ expected_error,
+ method,
+ **{arg_name: arg_value},
+ )
+
+
+class BaseTestMicroversionEnforcement(base.BaseBaremetalTest):
+ """Base class for microversion enforcement tests."""
+
+ def setUp(self):
+ super(BaseTestMicroversionEnforcement, self).setUp()
+ self.resource_class = uuidutils.generate_uuid()
+
+
+class TestShardMicroversions(
+ BaseTestMicroversionEnforcement,
+ MicroversionTestMixin):
+ """Tests for shard-related API microversion enforcement."""
+
+ min_microversion = "1.82"
+
+ @decorators.idempotent_id("e5403a31-e12b-4f97-a776-dcb819e5e9a0")
+ def test_shard(self):
+ self._microversion_test(
+ BaremetalClient.get_shards, "1.82", lib_exc.NotFound, {}
+ )
+
+ @decorators.idempotent_id("5df533c6-7a9c-4639-a47f-1377a2a87e6a")
+ def test_list_node_filter_shard(self):
+ self._microversion_test(
+ BaremetalClient.list_nodes, "1.82",
+ lib_exc.NotAcceptable, {"shard": "testshard"}
+ )
+
+
+class TestAllocationMicroversions(
+ BaseTestMicroversionEnforcement,
+ MicroversionTestMixin):
+ """Tests for allocation-related API microversion enforcement."""
+
+ min_microversion = "1.52"
+
+ @decorators.idempotent_id('8f527b3d-d5f1-4859-920f-8022b5d13621')
+ def test_create_allocations(self):
+ self._microversion_test(
+ BaremetalClient.create_allocation, "1.52",
+ lib_exc.UnexpectedResponseCode, {
+ "resource_class": self.resource_class
+ }
+ )
+
+ @decorators.idempotent_id('511e0c4b-1320-4ac5-9c4a-fb0394d3ff67')
+ def test_list_allocations(self):
+ self._microversion_test(
+ BaremetalClient.list_allocations, "1.52",
+ lib_exc.NotFound, {}
+ )
+
+ @decorators.idempotent_id('a0d17f90-baa0-4518-95f7-a7eab73ff6d1')
+ def test_show_allocations(self):
+ _, allocation = self.create_allocation(self.resource_class)
+
+ self._microversion_test(
+ BaremetalClient.show_allocation, "1.52",
+ lib_exc.NotFound, {
+ "allocation_ident": allocation['uuid']
+ }
+ )
+
+ @decorators.idempotent_id('b05a9b1a-4a12-4b55-93c7-530c3f35c7d9')
+ def test_delete_allocations(self):
+ _, allocation = self.create_allocation(self.resource_class)
+
+ self._microversion_test(
+ BaremetalClient.delete_allocation, "1.52",
+ lib_exc.UnexpectedResponseCode, {
+ "allocation_ident": allocation['uuid']
+ }
+ )
+
+
+class TestNodeFirmwarenMicroversions(
+ BaseTestMicroversionEnforcement,
+ MicroversionTestMixin):
+
+ min_microversion = "1.86"
+
+ def setUp(self):
+ super(TestNodeFirmwarenMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis['uuid'])
+
+ @decorators.idempotent_id('f50e9098-1870-46b1-b05c-660d0f8c534d')
+ def test_list_node_firmware(self):
+ self._microversion_test(
+ BaremetalClient.list_node_firmware, "1.86",
+ lib_exc.NotFound, {"node_uuid": self.node['uuid']}
+ )
diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodes.py b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
index 6d63768..3da3f3e 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
@@ -372,7 +372,7 @@
def test_create_node_resource_class_old_api(self):
"""Try to create a node with resource class using older api version."""
resource_class = data_utils.arbitrary_string()
- self.assertRaises(lib_exc.UnexpectedResponseCode, self.create_node,
+ self.assertRaises(lib_exc.NotAcceptable, self.create_node,
self.chassis['uuid'], resource_class=resource_class)
@decorators.attr(type='negative')
@@ -380,7 +380,7 @@
def test_update_node_resource_class_old_api(self):
"""Try to update a node with resource class using older api version."""
resource_class = data_utils.arbitrary_string()
- self.assertRaises(lib_exc.UnexpectedResponseCode,
+ self.assertRaises(lib_exc.NotAcceptable,
self.client.update_node,
self.node['uuid'], resource_class=resource_class)
@@ -390,10 +390,10 @@
"""Try to list nodes with resource class using older api version."""
resource_class = data_utils.arbitrary_string()
self.assertRaises(
- lib_exc.UnexpectedResponseCode,
+ lib_exc.NotAcceptable,
self.client.list_nodes, resource_class=resource_class)
self.assertRaises(
- lib_exc.UnexpectedResponseCode,
+ lib_exc.NotAcceptable,
self.client.list_nodes_detail, resource_class=resource_class)
@@ -1043,10 +1043,9 @@
@decorators.idempotent_id('5419af7b-4e27-4be4-88f6-e01c598a8102')
def test_list_node_traits_old_api(self):
"""Try to list traits for a node using an older api version."""
- exc = self.assertRaises(
- lib_exc.UnexpectedResponseCode,
+ self.assertRaises(
+ lib_exc.NotAcceptable,
self.client.list_node_traits, self.node['uuid'])
- self.assertEqual(406, exc.resp.status)
@decorators.attr(type='negative')
@decorators.idempotent_id('a4353f3a-bedc-4579-9c7e-4bebcd95903d')
@@ -1096,10 +1095,9 @@
@decorators.idempotent_id('eb75b3c8-ac9c-4399-90a2-c0030bfde7a6')
def test_list_nodes_traits_field(self):
"""Try to list nodes' traits field using older api version."""
- exc = self.assertRaises(
- lib_exc.UnexpectedResponseCode,
+ self.assertRaises(
+ lib_exc.NotAcceptable,
self.client.list_nodes, fields='traits')
- self.assertEqual(406, exc.resp.status)
@decorators.attr(type='negative')
@decorators.idempotent_id('214ae7fc-149b-4657-b6bc-66353d49ade8')
diff --git a/ironic_tempest_plugin/tests/api/admin/test_ports.py b/ironic_tempest_plugin/tests/api/admin/test_ports.py
index dfb371c..9bf2771 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports.py
@@ -25,9 +25,23 @@
super(TestPorts, self).setUp()
_, self.chassis = self.create_chassis()
+ # NOTE(TheJulia): noop is required for the network interface for
+ # these tests, as newer versions of ironic can invoke network
+ # logic, and if is not configured with full integration configuration,
+ # can then result in failed tests.
_, self.node = self.create_node(self.chassis['uuid'])
_, self.port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture('1.31'))
+ # Now with a 1.31 microversion, swap the network interfaces
+ # into place so the test doesn't break depending on
+ # the environment's default state.
+ self.client.update_node(self.node['uuid'],
+ [{'path': '/network_interface',
+ 'op': 'replace',
+ 'value': 'noop'}])
+ self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.1'))
@decorators.idempotent_id('83975898-2e50-42ed-b5f0-e510e36a0b56')
def test_create_port(self):
diff --git a/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py b/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
index bd338f9..fb1913c 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
@@ -26,6 +26,16 @@
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture('1.31'))
+ # Now with a 1.31 microversion, swap the network interfaces
+ # into place so the test doesn't break depending on
+ # the environment's default state.
+ self.client.update_node(self.node['uuid'],
+ [{'path': '/network_interface',
+ 'op': 'replace',
+ 'value': 'noop'}])
+ self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.1'))
@decorators.attr(type=['negative'])
@decorators.idempotent_id('0a6ee1f7-d0d9-4069-8778-37f3aa07303a')
@@ -354,7 +364,7 @@
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
- self.assertRaises((lib_exc.BadRequest, lib_exc.UnexpectedResponseCode),
+ self.assertRaises((lib_exc.BadRequest, lib_exc.NotAcceptable),
self.create_port,
node_id=node_id, address=address,
physical_network='physnet1')
@@ -371,7 +381,7 @@
'op': 'replace',
'value': new_physnet}]
- self.assertRaises((lib_exc.BadRequest, lib_exc.UnexpectedResponseCode),
+ self.assertRaises((lib_exc.BadRequest, lib_exc.NotAcceptable),
self.client.update_port,
port['uuid'], patch)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_runbooks.py b/ironic_tempest_plugin/tests/api/admin/test_runbooks.py
new file mode 100644
index 0000000..af3c37c
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_runbooks.py
@@ -0,0 +1,352 @@
+# 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 copy
+
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+EXAMPLE_STEPS = [{
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 1
+}]
+
+
+def _get_random_trait():
+ return data_utils.rand_name('CUSTOM', '').replace('-', '_')
+
+
+class TestRunbooks(base.BaseBaremetalTest):
+ """Tests for runbooks."""
+
+ min_microversion = '1.92'
+
+ def setUp(self):
+ super(TestRunbooks, self).setUp()
+ self.name = _get_random_trait()
+ self.steps = copy.deepcopy(EXAMPLE_STEPS)
+ _, self.runbook = self.create_runbook(self.name,
+ steps=self.steps)
+
+ _, self.chassis = self.create_chassis()
+ _, self.test_node = self.create_node(self.chassis['uuid'])
+
+ @decorators.idempotent_id('5a0cfe27-b7e8-d4f0-d3c5-e6e1eef63241')
+ def test_create_runbook_specifying_uuid(self):
+ name = _get_random_trait()
+ uuid = data_utils.rand_uuid()
+
+ _, runbook = self.create_runbook(name=name, steps=self.steps,
+ uuid=uuid)
+
+ _, body = self.client.show_runbook(uuid)
+ self._assertExpected(runbook, body)
+
+ @decorators.idempotent_id('b8bfb388-97b0-aa6e-5130-1a1ed34f94db')
+ def test_delete_runbook(self):
+ self.delete_runbook(self.runbook['uuid'])
+ self.assertRaises(lib_exc.NotFound, self.client.show_runbook,
+ self.runbook['uuid'])
+
+ @decorators.idempotent_id('f1cba93a-2894-7296-0f4b-58867359480b')
+ def test_show_runbook(self):
+ _, runbook = self.client.show_runbook(self.runbook['uuid'])
+ self._assertExpected(self.runbook, runbook)
+ self.assertEqual(self.name, runbook['name'])
+ self.assertEqual(self.steps, runbook['steps'])
+ self.assertIn('uuid', runbook)
+ self.assertEqual({}, runbook['extra'])
+
+ @decorators.idempotent_id('c95e2631-24e2-914b-db00-c8dafc35a677')
+ def test_show_runbook_with_links(self):
+ _, runbook = self.client.show_runbook(self.runbook['uuid'])
+ self.assertIn('links', runbook)
+ self.assertEqual(2, len(runbook['links']))
+ self.assertIn(runbook['uuid'], runbook['links'][0]['href'])
+
+ @decorators.idempotent_id('7b7951b3-e177-21d7-933a-1f29891dea52')
+ def test_list_runbooks(self):
+ _, body = self.client.list_runbooks()
+ self.assertIn(self.runbook['uuid'],
+ [i['uuid'] for i in body['runbooks']])
+
+ for runbook in body['runbooks']:
+ self.validate_self_link('runbooks', runbook['uuid'],
+ runbook['links'][0]['href'])
+
+ @decorators.idempotent_id('6aafc619-0d98-5341-7d94-b293e194dcf7')
+ def test_list_with_limit(self):
+ for _ in range(2):
+ name = _get_random_trait()
+ self.create_runbook(name, steps=self.steps)
+
+ _, body = self.client.list_runbooks(limit=3)
+
+ next_marker = body['runbooks'][-1]['uuid']
+ self.assertIn(next_marker, body['next'])
+
+ @decorators.idempotent_id('ebd762c3-c10e-b71f-5efd-af14ac9f6092')
+ def test_list_runbooks_detail(self):
+ uuids = [
+ self.create_runbook(_get_random_trait(), steps=self.steps)
+ [1]['uuid'] for _ in range(0, 5)]
+
+ _, body = self.client.list_runbooks(detail=True)
+
+ runbooks_dict = dict((runbook['uuid'], runbook)
+ for runbook in body['runbooks']
+ if runbook['uuid'] in uuids)
+
+ for uuid in uuids:
+ self.assertIn(uuid, runbooks_dict)
+ runbook = runbooks_dict[uuid]
+ self.assertIn('name', runbook)
+ self.assertEqual(self.steps, runbook['steps'])
+ self.assertIn('uuid', runbook)
+ self.assertEqual({}, runbook['extra'])
+ self.validate_self_link('runbooks', runbook['uuid'],
+ runbook['links'][0]['href'])
+
+ @decorators.idempotent_id('fb192fdd-ea6a-c637-a36e-390c46a7663b')
+ def test_update_runbook_replace(self):
+ new_name = _get_random_trait()
+ new_steps = [{
+ 'interface': 'raid',
+ 'step': 'create_configuration',
+ 'args': {},
+ 'order': 2,
+ }]
+
+ patch = [{'path': '/name', 'op': 'replace', 'value': new_name},
+ {'path': '/steps', 'op': 'replace', 'value': new_steps}]
+
+ self.client.update_runbook(self.runbook['uuid'], patch)
+
+ _, body = self.client.show_runbook(self.runbook['uuid'])
+ self.assertEqual(new_name, body['name'])
+ self.assertEqual(new_steps, body['steps'])
+
+ @decorators.idempotent_id('99f52546-9906-6ded-9186-7262591b99ec')
+ def test_update_runbook_add(self):
+ new_steps = [
+ {
+ 'interface': 'bios',
+ 'step': 'cache_bios_settings',
+ 'args': {},
+ 'order': 2
+ },
+ {
+ 'interface': 'bios',
+ 'step': 'factory_reset',
+ 'args': {},
+ 'order': 3
+ },
+ ]
+
+ patch = [{'path': '/steps/1', 'op': 'add', 'value': new_steps[0]},
+ {'path': '/steps/2', 'op': 'add', 'value': new_steps[1]}]
+
+ self.client.update_runbook(self.runbook['uuid'], patch)
+
+ _, body = self.client.show_runbook(self.runbook['uuid'])
+ self.assertEqual(self.steps + new_steps, body['steps'])
+
+ @decorators.idempotent_id('ec1550c3-264e-fcce-b131-d2815fdb733b')
+ def test_update_runbook_mixed_ops(self):
+ new_name = _get_random_trait()
+ new_steps = [
+ {
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 2
+ },
+ {
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 3
+ },
+ ]
+
+ patch = [{'path': '/name', 'op': 'replace', 'value': new_name},
+ {'path': '/steps/0', 'op': 'replace', 'value': new_steps[0]},
+ {'path': '/steps/0', 'op': 'remove'},
+ {'path': '/steps/0', 'op': 'add', 'value': new_steps[1]}]
+
+ self.client.update_runbook(self.runbook['uuid'], patch)
+
+ _, body = self.client.show_runbook(self.runbook['uuid'])
+ self.assertEqual(new_name, body['name'])
+ self.assertEqual([new_steps[1]], body['steps'])
+
+ @decorators.idempotent_id('5c7f0aca-cee3-d083-ef2a-e33a8dc467c5')
+ def test_combining_runbook_and_explicit_steps(self):
+ explicit_steps = [{'interface': 'deploy', 'step': 'deploy',
+ 'args': {}}]
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.set_node_provision_state,
+ self.test_node['uuid'], 'active',
+ runbook=self.runbook['uuid'],
+ clean_steps=explicit_steps)
+
+ @decorators.idempotent_id('ddf4a9b7-144d-386a-fd3fcf8960d77199')
+ def test_create_runbook_with_invalid_step_format(self):
+ name = _get_random_trait()
+ invalid_steps = [{'invalid_key': 'value'}]
+
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name, steps=invalid_steps)
+
+
+class TestRunbooksOldAPI(base.BaseBaremetalTest):
+ """Negative tests for runbooks using an old API version."""
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e9481a0d-23e0-4757-bc11-c3c9ab9d3839')
+ def test_create_runbook_old_api(self):
+ # With runbooks support, ironic returns 404. Without, 405.
+ self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+ self.create_runbook,
+ name=_get_random_trait(), steps=EXAMPLE_STEPS)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0d3af2aa-ba53-4c8a-92d4-91f9b4179fe7')
+ def test_update_runbook_old_api(self):
+ patch = [{'path': '/name', 'op': 'replace',
+ 'value': _get_random_trait()}]
+
+ # With runbooks support, ironic returns 404. Without, 405.
+ self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+ self.client.update_runbook,
+ _get_random_trait(), patch)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1646b1e5-ab81-45a8-9ea0-30444a4dcaa2')
+ def test_delete_runbook_old_api(self):
+ # With runbooks support, ironic returns 404. Without, 405.
+ self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+ self.client.delete_runbook,
+ _get_random_trait())
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('819480ac-f36a-4402-b1d5-504d7cf55b1f')
+ def test_list_runbooks_old_api(self):
+ self.assertRaises(lib_exc.NotFound,
+ self.client.list_runbooks)
+
+
+class TestRunbooksNegative(base.BaseBaremetalTest):
+ """Negative tests for runbooks."""
+
+ min_microversion = '1.92'
+
+ def setUp(self):
+ super(TestRunbooksNegative, self).setUp()
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture(
+ self.min_microversion)
+ )
+ self.steps = EXAMPLE_STEPS
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a4085c08-e718-4c2f-a796-0e115b659243')
+ def test_create_runbook_invalid_name(self):
+ name = 'invalid-name'
+ self.assertRaises(lib_exc.BadRequest,
+ self.create_runbook, name=name,
+ steps=self.steps)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6390acc4-9490-4b23-8b4c-41888a78c9b7')
+ def test_create_runbook_duplicated_name(self):
+ name = _get_random_trait()
+ self.create_runbook(name=name, steps=self.steps)
+ self.assertRaises(lib_exc.Conflict, self.create_runbook,
+ name=name, steps=self.steps)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ed3f0cec-13e8-4175-9fdb-d129e7b7fe10')
+ def test_create_runbook_no_mandatory_field_name(self):
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name=None, steps=self.steps)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('af5dd0df-d903-463f-9535-9e4e9d6fd576')
+ def test_create_runbook_no_mandatory_field_steps(self):
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name=_get_random_trait())
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('cbd33bc5-7602-40b7-943e-3e92217567a3')
+ def test_create_runbook_malformed_steps(self):
+ steps = {'key': 'value'}
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name=_get_random_trait(), steps=steps)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2a562fca-f377-4a6e-b332-37ee82d3a983')
+ def test_create_runbook_malformed_runbook_uuid(self):
+ uuid = 'malformed:uuid'
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name=_get_random_trait(), steps=self.steps,
+ uuid=uuid)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2c006994-88ca-43b7-b605-897d479229d9')
+ def test_show_runbook_nonexistent(self):
+ self.assertRaises(lib_exc.NotFound, self.client.show_runbook,
+ data_utils.rand_uuid())
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5a815f37-f015-4d68-9b22-099504f74805')
+ def test_update_runbook_remove_mandatory_field_steps(self):
+ name = _get_random_trait()
+ _, runbook = self.create_runbook(name=name, steps=self.steps)
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.update_runbook,
+ runbook['uuid'],
+ [{'path': '/steps/0', 'op': 'remove'}])
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.update_runbook,
+ runbook['uuid'],
+ [{'path': '/steps', 'op': 'remove'}])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ee852ebb-a601-4593-9d59-063fcbc8f964')
+ def test_update_runbook_remove_mandatory_field_name(self):
+ name = _get_random_trait()
+ _, runbook = self.create_runbook(name=name, steps=self.steps)
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.update_runbook,
+ runbook['uuid'],
+ [{'path': '/name', 'op': 'remove'}])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e59bf38d-272f-4490-b21e-9db217f11378')
+ def test_update_runbook_replace_empty_name(self):
+ name = _get_random_trait()
+ _, runbook = self.create_runbook(name=name, steps=self.steps)
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.update_runbook,
+ runbook['uuid'],
+ [{'path': '/name', 'op': 'replace', 'value': ''}])
diff --git a/ironic_tempest_plugin/tests/api/admin/test_shards.py b/ironic_tempest_plugin/tests/api/admin/test_shards.py
index ffe6914..0638702 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_shards.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_shards.py
@@ -15,6 +15,7 @@
from ironic_tempest_plugin.tests.api import base
+
CONF = config.CONF
@@ -126,3 +127,25 @@
self.assertIn(self.none_node_id, fetched_node_ids)
self.assertNotIn(self.bad_node_id, fetched_node_ids)
+
+
+class TestGetAllShards(base.BaseBaremetalTest):
+ """Tests for baremetal shards."""
+
+ min_microversion = '1.82'
+
+ def setUp(self):
+ super(TestGetAllShards, self).setUp()
+ _, self.chassis = self.create_chassis()
+ self.shards = ["shard1", "shard2", "shard3"]
+ self.node_ids = []
+ for shard in self.shards:
+ _, node = self.create_node(self.chassis['uuid'], shard=shard)
+ self.node_ids.append(node['uuid'])
+
+ @decorators.idempotent_id('fc786196-63c7-4e0d-bd14-3e478d4d1e3e')
+ def test_get_all_shards(self):
+ _, fetched_shards = self.client.get_shards()
+ fetched_shards = [shard['name'] for shard in fetched_shards['shards']]
+
+ self.assertItemsEqual(self.shards, fetched_shards)
diff --git a/ironic_tempest_plugin/tests/api/base.py b/ironic_tempest_plugin/tests/api/base.py
index 59f2f94..c07137b 100644
--- a/ironic_tempest_plugin/tests/api/base.py
+++ b/ironic_tempest_plugin/tests/api/base.py
@@ -38,7 +38,7 @@
# NOTE(jroll): resources must be deleted in a specific order, this list
# defines the resource types to clean up, and the correct order.
RESOURCE_TYPES = ['port', 'portgroup', 'node', 'volume_connector',
- 'volume_target', 'chassis', 'deploy_template']
+ 'volume_target', 'chassis', 'deploy_template', 'runbook']
def creates(resource):
@@ -368,6 +368,18 @@
return resp, body
@classmethod
+ @creates('runbook')
+ def create_runbook(cls, name, **kwargs):
+ """Wrapper utility for creating test runbook.
+
+ :param name: The name of the runbook.
+ :return: A tuple with the server response and the created runbook.
+ """
+ resp, body = cls.client.create_runbook(name=name, **kwargs)
+
+ return resp, body
+
+ @classmethod
def delete_chassis(cls, chassis_id):
"""Deletes a chassis having the specified UUID.
@@ -473,6 +485,20 @@
return resp
+ @classmethod
+ def delete_runbook(cls, runbook_ident):
+ """Deletes a runbook having the specified name or UUID.
+
+ :param runbook_ident: Name or UUID of the runbook.
+ :return: Server response.
+ """
+ resp, body = cls.client.delete_runbook(runbook_ident)
+
+ if runbook_ident in cls.created_objects['runbook']:
+ cls.created_objects['runbook'].remove(runbook_ident)
+
+ return resp
+
def validate_self_link(self, resource, uuid, link):
"""Check whether the given self link formatted correctly."""
expected_link = "{base}/{pref}/{res}/{uuid}".format(
@@ -497,8 +523,8 @@
class BaseBaremetalRBACTest(BaseBaremetalTest):
- # Unless otherwise superceeded by a version, RBAC tests generally start at
- # version 1.70 as that is when System scope and the delineation occured.
+ # Unless otherwise superseded by a version, RBAC tests generally start at
+ # version 1.70 as that is when System scope and the delineation occurred.
min_microversion = '1.70'
@classmethod
diff --git a/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py b/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
index cd16fe2..2af624a 100644
--- a/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
@@ -675,7 +675,7 @@
All tests here must always expect *multiple* nodes visible, since
this is a global reader role.
- https://opendev.org/openstack/ironic/src/branch/master/ironic/common/policy.py#L60 # noqa
+ https://opendev.org/openstack/ironic/src/branch/master/ironic/common/policy.py#L60
"""
credentials = ['system_admin', 'system_reader']
diff --git a/ironic_tempest_plugin/tests/api/rbac_defaults/test_runbooks.py b/ironic_tempest_plugin/tests/api/rbac_defaults/test_runbooks.py
new file mode 100644
index 0000000..eb1476c
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/rbac_defaults/test_runbooks.py
@@ -0,0 +1,100 @@
+# 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.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.tests.api import base
+
+
+EXAMPLE_STEPS = [{
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 1
+}]
+
+
+def _get_random_trait():
+ return data_utils.rand_name('CUSTOM', '').replace('-', '_')
+
+
+class TestRunbookRBAC(base.BaseBaremetalRBACTest):
+ min_microversion = '1.92'
+ credentials = ['system_admin',
+ 'system_reader',
+ 'project_admin',
+ 'project_member']
+
+ def setUp(self):
+ super(TestRunbookRBAC, self).setUp()
+ self.system_admin_client = (
+ self.os_system_admin.baremetal.BaremetalClient())
+
+ self.system_reader_client = (
+ self.os_system_reader.baremetal.BaremetalClient())
+
+ self.project_admin_client = (
+ self.os_project_admin.baremetal.BaremetalClient())
+
+ self.project_member_client = (
+ self.os_project_member.baremetal.BaremetalClient())
+
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis['uuid'])
+
+ _, self.public_runbook = self.system_admin_client.create_runbook(
+ _get_random_trait(), steps=EXAMPLE_STEPS, public=True)
+
+ _, self.private_runbook = self.project_admin_client.create_runbook(
+ _get_random_trait(), steps=EXAMPLE_STEPS)
+
+ @decorators.idempotent_id('0410fbed-3454-ded3-6f8b3192abfd3fcd')
+ def test_runbook_visibility_and_access_control(self):
+ # Project-scoped user can see both public and owned runbooks
+ _, project_runbooks = self.project_member_client.list_runbooks()
+ self.assertIn(self.public_runbook['uuid'],
+ [r['uuid'] for r in project_runbooks['runbooks']])
+ self.assertIn(self.private_runbook['uuid'],
+ [r['uuid'] for r in project_runbooks['runbooks']])
+
+ # System-scoped user can see all runbooks
+ _, system_runbooks = self.system_reader_client.list_runbooks()
+ self.assertIn(self.public_runbook['uuid'],
+ [r['uuid'] for r in system_runbooks['runbooks']])
+ self.assertIn(self.private_runbook['uuid'],
+ [r['uuid'] for r in system_runbooks['runbooks']])
+
+ @decorators.idempotent_id('903d0027-9265-9f87-c86d-09867aa24edd')
+ def test_runbook_ownership_and_public_flag(self):
+ # Only system-scoped users can set a runbook as public
+
+ patch_public = [{'path': '/public', 'op': 'replace', 'value': True}]
+ self.assertRaises(lib_exc.Forbidden,
+ self.project_admin_client.update_runbook,
+ self.private_runbook['uuid'],
+ patch=patch_public)
+
+ # Setting a runbook as public nullifies its owner field
+ self.system_admin_client.update_runbook(self.private_runbook['uuid'],
+ patch=patch_public)
+ _, updated_runbook = self.system_admin_client.show_runbook(
+ self.private_runbook['uuid'])
+ self.assertIsNone(updated_runbook['owner'])
+
+ # Project-scoped user cannot change the owner of a runbook
+ patch_owner = [{'path': '/public', 'op': 'replace', 'value': True}]
+ self.assertRaises(lib_exc.Forbidden,
+ self.project_admin_client.update_runbook,
+ self.public_runbook['uuid'],
+ patch=patch_owner)
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
index e9090cb..ad9e4c9 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
@@ -16,10 +16,12 @@
import time
+from oslo_log import log as logging
from tempest.common import waiters
from tempest import config
from tempest.lib.common import api_version_utils
from tempest.lib.common.utils.linux import remote_client
+from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.common import utils
@@ -27,6 +29,7 @@
from ironic_tempest_plugin import manager
CONF = config.CONF
+LOG = logging.getLogger(__name__)
def retry_on_conflict(func):
@@ -307,3 +310,44 @@
instance['id'], 'ACTIVE')
# Verify server connection
self.get_remote_client(server_ip, server=instance)
+
+ def wait_for_ssh(self, ip_address,
+ username=None,
+ private_key=None,
+ server=None,
+ timeout=60,
+ delay=10):
+ def _wait_ssh():
+ try:
+ self.get_remote_client(ip_address, username, private_key,
+ server=server)
+ except Exception:
+ LOG.debug("Failed to get ssh client for %s", ip_address,
+ exc_info=True)
+ return False
+ return True
+
+ res = test_utils.call_until_true(_wait_ssh, timeout, delay)
+ self.assertTrue(res, f"Failed to wait for ssh on {ip_address}")
+
+ def check_vm_connectivity(self,
+ ip_address,
+ username=None,
+ private_key=None,
+ should_connect=True,
+ extra_msg="",
+ server=None,
+ mtu=None):
+ # NOTE(vsaienko): it may take some time to boot VM and initialize
+ # ssh by cloud init. Wait for SSH can pass authentication before
+ # checking connectivity.
+ if should_connect:
+ self.wait_for_ssh(ip_address=ip_address, username=username,
+ private_key=private_key, server=server)
+ super().check_vm_connectivity(ip_address=ip_address,
+ username=username,
+ private_key=private_key,
+ should_connect=should_connect,
+ extra_msg=extra_msg,
+ server=server,
+ mtu=mtu)
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index 01289ce..7fa8cb5 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -343,7 +343,7 @@
:param image_ref: Reference to user image to boot node with.
:param image_checksum: md5sum of image specified in image_ref.
Needed only when direct HTTP link is provided.
- :param boot_option: The defaut boot option to utilize. If not
+ :param boot_option: The default boot option to utilize. If not
specified, the ironic deployment default shall
be utilized.
:param config_drive_networking: If we should load configuration drive
@@ -459,7 +459,7 @@
timeout=CONF.baremetal.unrescue_timeout,
interval=1)
- def manual_cleaning(self, node, clean_steps):
+ def manual_cleaning(self, node, clean_steps=None, runbook=None):
"""Performs manual cleaning.
The following actions are executed:
@@ -470,15 +470,25 @@
:param node: Ironic node to associate instance_uuid with.
:param clean_steps: clean steps for manual cleaning.
+ :param runbook: unique identifier of a runbook.
"""
+ if clean_steps is None and runbook is None:
+ raise ValueError("Either clean_steps or runbook must be provided.")
+
self.set_node_provision_state(node['uuid'], 'manage')
self.wait_provisioning_state(
node['uuid'],
[bm.BaremetalProvisionStates.MANAGEABLE],
timeout=CONF.baremetal.unprovision_timeout,
interval=30)
- self.set_node_provision_state(
- node['uuid'], 'clean', clean_steps=clean_steps)
+
+ if runbook:
+ self.set_node_provision_state(
+ node['uuid'], 'clean', runbook=runbook)
+ else:
+ self.set_node_provision_state(
+ node['uuid'], 'clean', clean_steps=clean_steps)
+
self.wait_provisioning_state(
node['uuid'],
[bm.BaremetalProvisionStates.MANAGEABLE],
@@ -492,6 +502,17 @@
timeout=CONF.baremetal.unprovision_timeout,
interval=30)
+ def manual_cleaning_with_runbook(self, node):
+ steps = [{
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 1
+ }]
+ _, runbook = self.baremetal_client.create_runbook('CUSTOM_AWESOME',
+ steps=steps)
+ self.manual_cleaning(node, runbook=runbook)
+
def check_manual_partition_cleaning(self, node):
"""Tests the cleanup step for erasing devices metadata.
@@ -562,7 +583,7 @@
# If we don't require an explicit driver, then what drivers *can* we
# operate with. In essence, this exists to prevent the test from failing
# on 3rd party drivers, and vendor specific driers which do not support
- # the sort of itnerfaces we may be trying to test by default.
+ # the sort of interfaces we may be trying to test by default.
valid_driver_list = []
# The bios interface to use by the HW type. The bios interface of the
@@ -607,6 +628,12 @@
# set via a different test).
power_interface = None
+ # The inspect interface to use by the HW type. The inspect interface of the
+ # node used in the test will be set to this value. If set to None, the
+ # node will retain its existing inspect_interface value (which may have
+ # been set via a different test).
+ inspect_interface = None
+
# Boolean value specify if image is wholedisk or not.
wholedisk_image = None
@@ -632,55 +659,18 @@
'driver': cls.driver,
'enabled_drivers': CONF.baremetal.enabled_drivers,
'enabled_hw_types': CONF.baremetal.enabled_hardware_types})
- if (cls.bios_interface and cls.bios_interface not in
- CONF.baremetal.enabled_bios_interfaces):
- raise cls.skipException(
- "Bios interface %(iface)s required by the test is not in the "
- "list of enabled bios interfaces %(enabled)s" % {
- 'iface': cls.bios_interface,
- 'enabled': CONF.baremetal.enabled_bios_interfaces})
- if (cls.deploy_interface and cls.deploy_interface not in
- CONF.baremetal.enabled_deploy_interfaces):
- raise cls.skipException(
- "Deploy interface %(iface)s required by test is not "
- "in the list of enabled deploy interfaces %(enabled)s" % {
- 'iface': cls.deploy_interface,
- 'enabled': CONF.baremetal.enabled_deploy_interfaces})
- if (cls.rescue_interface and cls.rescue_interface not in
- CONF.baremetal.enabled_rescue_interfaces):
- raise cls.skipException(
- "Rescue interface %(iface)s required by test is not "
- "in the list of enabled rescue interfaces %(enabled)s" % {
- 'iface': cls.rescue_interface,
- 'enabled': CONF.baremetal.enabled_rescue_interfaces})
- if (cls.boot_interface and cls.boot_interface not in
- CONF.baremetal.enabled_boot_interfaces):
- raise cls.skipException(
- "Boot interface %(iface)s required by test is not "
- "in the list of enabled boot interfaces %(enabled)s" % {
- 'iface': cls.boot_interface,
- 'enabled': CONF.baremetal.enabled_boot_interfaces})
- if (cls.raid_interface and cls.raid_interface not in
- CONF.baremetal.enabled_raid_interfaces):
- raise cls.skipException(
- "RAID interface %(iface)s required by test is not "
- "in the list of enabled RAID interfaces %(enabled)s" % {
- 'iface': cls.raid_interface,
- 'enabled': CONF.baremetal.enabled_raid_interfaces})
- if (cls.management_interface and cls.management_interface not in
- CONF.baremetal.enabled_management_interfaces):
- raise cls.skipException(
- "Management interface %(iface)s required by test is not "
- "in the list of enabled management interfaces %(enabled)s" % {
- 'iface': cls.management_interface,
- 'enabled': CONF.baremetal.enabled_management_interfaces})
- if (cls.power_interface and cls.power_interface not in
- CONF.baremetal.enabled_power_interfaces):
- raise cls.skipException(
- "Power interface %(iface)s required by test is not "
- "in the list of enabled power interfaces %(enabled)s" % {
- 'iface': cls.power_interface,
- 'enabled': CONF.baremetal.enabled_power_interfaces})
+ for iface in base.SUPPORTED_INTERFACES:
+ requested = getattr(cls, f'{iface}_interface')
+ enabled = getattr(CONF.baremetal, f'enabled_{iface}_interfaces')
+ if requested and requested not in enabled:
+ raise cls.skipException(
+ "%(type)s interface %(iface)s required by the test is not "
+ "in the list of enabled %(type)s interfaces "
+ "%(enabled)s" % {
+ 'iface': requested,
+ 'type': iface,
+ 'enabled': ', '.join(enabled),
+ })
if (cls.wholedisk_image is not None
and not cls.wholedisk_image
and CONF.baremetal.use_provision_network):
@@ -720,27 +710,16 @@
if not uuidutils.is_uuid_like(cls.image_ref):
image_checksum = cls.image_checksum
boot_kwargs = {'image_checksum': image_checksum}
- if cls.bios_interface:
- boot_kwargs['bios_interface'] = cls.bios_interface
- if cls.deploy_interface:
- boot_kwargs['deploy_interface'] = cls.deploy_interface
- if cls.rescue_interface:
- boot_kwargs['rescue_interface'] = cls.rescue_interface
- if cls.boot_interface:
- boot_kwargs['boot_interface'] = cls.boot_interface
- if cls.raid_interface:
- boot_kwargs['raid_interface'] = cls.raid_interface
- if cls.management_interface:
- boot_kwargs['management_interface'] = cls.management_interface
- if cls.power_interface:
- boot_kwargs['power_interface'] = cls.power_interface
+ for iface in base.SUPPORTED_INTERFACES:
+ if requested := getattr(cls, f'{iface}_interface'):
+ boot_kwargs[f'{iface}_interface'] = requested
# just get an available node
cls.node = cls.get_and_reserve_node()
if (cls.use_available_driver
and not cls.driver
and cls.node['driver'] in cls.valid_driver_list):
- # If we're attempting to re-use the existing driver, then
+ # If we're attempting to reuse the existing driver, then
# lets save a value for update_node_driver to work with.
cls.driver = cls.node['driver']
cls.update_node_driver(cls.node['uuid'], cls.driver, **boot_kwargs)
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_adoption.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_adoption.py
index ba56917..d6a5a50 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_adoption.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_adoption.py
@@ -34,7 +34,7 @@
driver = 'ipmi'
image_ref = CONF.baremetal.whole_disk_image_ref
wholedisk_image = True
- deploy_interface = 'iscsi'
+ deploy_interface = 'direct'
# 1.37 is required to be able to copy traits
api_microversion = '1.37'
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
index 5901d20..09da6d3 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
@@ -135,6 +135,7 @@
image_ref = CONF.baremetal.whole_disk_image_ref
wholedisk_image = True
+ @decorators.unstable_test(bug='2101021')
@decorators.idempotent_id('cde532cc-81ba-4489-b374-b4a85cc203eb')
@utils.services('image', 'network')
def test_ip_access_to_server(self):
@@ -638,7 +639,7 @@
use_available_driver = True
# List of valid drivers which these tests *can* attempt to utilize.
- # Generally these should be the most commom, stock, upstream drivers.
+ # Generally these should be the most common, stock, upstream drivers.
valid_driver_list = ['ipmi', 'redfish']
# Bypass secondary attribute presence check as these tests don't require
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_inspection_basic.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_inspection_basic.py
index 97f0fa3..7d1d602 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_inspection_basic.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_inspection_basic.py
@@ -21,23 +21,16 @@
CONF = config.CONF
-class BaremetalIdracInspect(bsm.BaremetalStandaloneScenarioTest):
+class BaremetalInspectBase:
- driver = 'idrac'
mandatory_attr = ['driver', 'inspect_interface']
- # The test cases clean up at the end by detaching the VIF.
- # Support for VIFs was introduced by version 1.28
- # (# v1.28: Add vifs subcontroller to node).
- api_microversion = '1.28'
+ # (# v1.31: Support for updating inspect_interface).
+ api_microversion = '1.31'
delete_node = False
+ wait_provisioning_state_interval = 1
- def _verify_node_inspection_data(self):
- _, node = self.baremetal_client.show_node(self.node['uuid'])
-
- self.assertEqual(node['properties']['cpu_arch'], 'x86_64')
- self.assertGreater(int(node['properties']['memory_mb']), 0)
- self.assertGreater(int(node['properties']['cpus']), 0)
- self.assertGreater(int(node['properties']['local_gb']), 0)
+ def _verify_node_inspection_data(self, node):
+ self.assertIn(node['properties']['cpu_arch'], ['x86_64', 'aarch64'])
@decorators.idempotent_id('47ea4487-4720-43e8-a024-53ae82f8c264')
def test_baremetal_inspect(self):
@@ -50,19 +43,100 @@
"""
self.baremetal_client.set_node_provision_state(self.node['uuid'],
'manage')
+ _, node = self.baremetal_client.show_node(self.node['uuid'])
+ if 'cpu_arch' in node['properties']:
+ new_properties = node['properties'].copy()
+ new_properties.pop('cpu_arch')
+ self.baremetal_client.update_node(self.node['uuid'],
+ properties=new_properties)
+
self.baremetal_client.set_node_provision_state(self.node['uuid'],
'inspect')
+ self.wait_provisioning_state(
+ self.node['uuid'], 'manageable',
+ timeout=CONF.baremetal.inspect_timeout,
+ interval=self.wait_provisioning_state_interval)
- self.wait_provisioning_state(self.node['uuid'], 'manageable',
- timeout=CONF.baremetal.inspect_timeout)
-
- self._verify_node_inspection_data()
+ _, node = self.baremetal_client.show_node(self.node['uuid'])
+ self._verify_node_inspection_data(node)
self.baremetal_client.set_node_provision_state(self.node['uuid'],
'provide')
self.wait_provisioning_state(self.node['uuid'], 'available')
+class BaremetalRedfishAgentInspect(BaremetalInspectBase,
+ bsm.BaremetalStandaloneScenarioTest):
+ driver = 'redfish'
+ inspect_interface = 'agent'
+ wait_provisioning_state_interval = 15
+ # 1.81 adds support for inventory API
+ api_microversion = '1.81'
+
+ def _verify_node_inspection_data(self, node):
+ super()._verify_node_inspection_data(node)
+ inspection_data = self.baremetal_client.show_inventory(
+ self.node['uuid'])
+ self.assertEqual({'inventory', 'plugin_data'}, set(inspection_data))
+
+ # Inventory sanity check
+ inventory = inspection_data['inventory']
+ self.assertGreater(inventory['cpu']['count'], 0)
+ self.assertGreater(inventory['memory']['physical_mb'], 256)
+ self.assertGreater(len(inventory['disks']), 0)
+ self.assertGreater(len(inventory['interfaces']), 0)
+
+ @decorators.idempotent_id('82670a85-49b7-4d28-b8a8-6e18b0d4c8d1')
+ def test_inspect_abort(self):
+ _, node = self.baremetal_client.show_node(self.node['uuid'])
+ current_state = node['provision_state']
+
+ def cleanup():
+ nonlocal current_state
+
+ if current_state == 'inspect failed':
+ self.baremetal_client.set_node_provision_state(
+ self.node['uuid'], 'manage')
+ self.wait_provisioning_state(self.node['uuid'], 'manageable')
+ current_state = 'manageable'
+
+ if current_state == 'manageable':
+ self.baremetal_client.set_node_provision_state(
+ self.node['uuid'], 'provide')
+ self.wait_provisioning_state(self.node['uuid'], 'available')
+
+ self.addCleanup(cleanup)
+
+ if current_state != 'manageable':
+ self.baremetal_client.set_node_provision_state(self.node['uuid'],
+ 'manage')
+ current_state = 'manageable'
+
+ self.baremetal_client.set_node_provision_state(self.node['uuid'],
+ 'inspect')
+ self.wait_provisioning_state(
+ self.node['uuid'], 'inspect wait',
+ timeout=300, interval=5)
+
+ self.baremetal_client.set_node_provision_state(self.node['uuid'],
+ 'abort')
+ self.wait_provisioning_state(
+ self.node['uuid'], 'inspect failed',
+ timeout=60, interval=1)
+ current_state = 'inspect failed'
+
+
+class BaremetalIdracInspect(BaremetalInspectBase,
+ bsm.BaremetalStandaloneScenarioTest):
+ driver = 'idrac'
+
+ def _verify_node_inspection_data(self, node):
+ super()._verify_node_inspection_data(node)
+ self.assertGreater(int(node['properties']['memory_mb']), 0)
+ self.assertGreater(int(node['properties']['cpus']), 0)
+ self.assertGreater(int(node['properties']['local_gb']), 0)
+
+
class BaremetalIdracRedfishInspect(BaremetalIdracInspect):
inspect_interface = 'idrac-redfish'
diff --git a/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
index e8e5656..a0bcfc3 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
@@ -227,6 +227,9 @@
self.validate_scheduling()
self.validate_lessee()
ip_address = self.get_server_ip(self.instance)
+ self.check_vm_connectivity(ip_address=ip_address,
+ private_key=self.keypair['private_key'],
+ server=self.instance)
vm_client = self.get_remote_client(ip_address, server=self.instance)
# We expect the ephemeral partition to be mounted on /mnt and to have
@@ -247,12 +250,6 @@
self.rescue_instance(self.instance, self.node, ip_address)
self.unrescue_instance(self.instance, self.node, ip_address)
- # Reboot node
- self.reboot_node(self.instance)
-
- # ensure we can ping the node again
- self.assertTrue(self.ping_ip_address(ip_address))
-
self.terminate_instance(self.instance)
@decorators.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
diff --git a/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
index 086bf21..b347431 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
@@ -13,14 +13,17 @@
# License for the specific language governing permissions and limitations
# under the License.
+from oslo_log import log as logging
from tempest.common import utils
from tempest import config
from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from ironic_tempest_plugin import manager
from ironic_tempest_plugin.tests.scenario import baremetal_manager
+LOG = logging.getLogger(__name__)
CONF = config.CONF
@@ -50,13 +53,16 @@
'a minimum of 2') % CONF.baremetal.available_nodes
raise cls.skipException(msg)
- def create_tenant_network(self, clients, tenant_cidr):
+ def create_tenant_network(self, clients, tenant_cidr, create_router=True):
network = self.create_network(
networks_client=clients.networks_client,
project_id=clients.credentials.project_id)
- router = self.get_router(
- client=clients.routers_client,
- project_id=clients.credentials.tenant_id)
+
+ router = None
+ if create_router:
+ router = self.get_router(
+ client=clients.routers_client,
+ project_id=clients.credentials.tenant_id)
result = clients.subnets_client.create_subnet(
name=data_utils.rand_name('subnet'),
@@ -65,25 +71,39 @@
ip_version=4,
cidr=tenant_cidr)
subnet = result['subnet']
- clients.routers_client.add_router_interface(router['id'],
- subnet_id=subnet['id'])
+ if create_router:
+ clients.routers_client.add_router_interface(router['id'],
+ subnet_id=subnet['id'])
self.addCleanup(clients.subnets_client.delete_subnet, subnet['id'])
- self.addCleanup(clients.routers_client.remove_router_interface,
- router['id'], subnet_id=subnet['id'])
+
+ if create_router:
+ self.addCleanup(clients.routers_client.remove_router_interface,
+ router['id'], subnet_id=subnet['id'])
return network, subnet, router
def verify_l3_connectivity(self, source_ip, private_key,
- destination_ip, conn_expected=True):
+ destination_ip, conn_expected=True, timeout=15):
remote = self.get_remote_client(source_ip, private_key=private_key)
remote.validate_authentication()
+ output = remote.exec_command('ip route')
+ LOG.debug("Routing table on %s is %s", source_ip, output)
+
cmd = 'ping %s -c4 -w4 || exit 0' % destination_ip
success_substring = " bytes from %s" % destination_ip
- output = remote.exec_command(cmd)
- if conn_expected:
- self.assertIn(success_substring, output)
- else:
- self.assertNotIn(success_substring, output)
+
+ def ping_remote():
+ output = remote.exec_command(cmd)
+ LOG.debug("Got output %s while pinging %s", output, destination_ip)
+ if conn_expected:
+ return success_substring in output
+ else:
+ return success_substring not in output
+
+ # NOTE(vsaienko): we may lost couple of pings due to missing ARPs
+ # so do several retries to get stable output.
+ res = test_utils.call_until_true(ping_remote, timeout, 1)
+ self.assertTrue(res)
def multitenancy_check(self, use_vm=False):
tenant_cidr = '10.0.100.0/24'
@@ -165,3 +185,137 @@
self.skipTest('Compute service Nova is disabled,'
' VM is required to run this test')
self.multitenancy_check(use_vm=True)
+
+ @decorators.idempotent_id('6891929f-a254-43b1-bd97-6ea3ec74d6a9')
+ @utils.services('compute', 'image', 'network')
+ def test_baremetal_vm_multitenancy_trunk(self):
+ """Check Trunk scenario for two baremetal servers
+
+
+ fipA -- RouterA -- NetworkA (10.0.100.0/24)
+ |
+ |eth0
+ eth0:instanceA
+ |eth0.vlan_id
+ |
+ NetworkB(10.0.101.0/24)
+ |
+ |eth0
+ instanceB
+
+
+ * Create instanceA within networkA and FIPA with trunk port plugged to
+ networkA as parent(native vlan/untagged) and networkB as
+ vlan subport
+ * Create instanceB within networkB
+ * Verify connectivity to instanceB from instanceA failed
+ * Assign ip address on subport inside instanceA. This step is needed
+ only unless nova configdrive support of trunks is not implemented.
+ * Verify connectivity to instanceB from instanceA success
+ * Remove subport from instanceA
+ * Verify connectivity to instanceB from instanceA failed
+ * Add subport to instanceA
+ * Verify connectivity to instanceB from instanceA Passed
+ """
+
+ if not CONF.baremetal_feature_enabled.trunks_supported:
+ msg = 'Trunks with baremetal are not supported.'
+ raise self.skipException(msg)
+
+ tenant_a_cidr = '10.0.100.0/24'
+ tenant_b_cidr = '10.0.101.0/24'
+
+ keypair = self.create_keypair()
+ networkA, subnetA, routerA = self.create_tenant_network(
+ self.os_primary, tenant_a_cidr)
+ networkB, subnetB, _ = self.create_tenant_network(
+ self.os_primary, tenant_b_cidr, create_router=False)
+ portB = self.create_port(network_id=networkB["id"])
+
+ parent_port = self.create_port(network_id=networkA["id"])
+ subport = self.create_port(network_id=networkB["id"])
+ subports = [{'port_id': subport['id'], 'segmentation_type': 'inherit'}]
+ trunk = self.os_primary.trunks_client.create_trunk(
+ name="test-trunk", port_id=parent_port['id'],
+ sub_ports=subports)['trunk']
+ self.addCleanup(self.os_primary.trunks_client.delete_trunk,
+ trunk['id'])
+
+ # Create instanceB first as we will not check if its booted,
+ # as we don't have FIP, so it has more time than instanceA
+ # to boot.
+ instanceB, nodeB = self.boot_instance(
+ clients=self.os_primary,
+ keypair=keypair,
+ networks=[{'port': portB['id']}]
+ )
+
+ instanceA, nodeA = self.boot_instance(
+ clients=self.os_primary,
+ keypair=keypair,
+ networks=[{'port': parent_port['id']}]
+ )
+
+ floating_ipA = self.create_floating_ip(
+ instanceA,
+ )['floating_ip_address']
+
+ fixed_ipB = instanceB['addresses'][networkB['name']][0]['addr']
+
+ self.check_vm_connectivity(ip_address=floating_ipA,
+ private_key=keypair['private_key'],
+ server=instanceA)
+ ssh_client = self.get_remote_client(floating_ipA,
+ private_key=keypair['private_key'])
+
+ # TODO(vsaienko): add when cloudinit support is implemented
+ # add validation of network_data.json and drop next ip assignment
+
+ self.verify_l3_connectivity(
+ floating_ipA,
+ keypair['private_key'],
+ fixed_ipB,
+ conn_expected=False
+ )
+ vlan_id = trunk['sub_ports'][0]['segmentation_id']
+ subport_ip = subport['fixed_ips'][0]['ip_address']
+
+ interface_name = ssh_client.exec_command(
+ "sudo ip route | awk '/default/ {print $5}'").rstrip()
+ cmds = [
+ f"sudo ip link add link {interface_name} name "
+ f"{interface_name}.{vlan_id} type vlan id {vlan_id}",
+ f"sudo ip addr add {subport_ip}/24 dev {interface_name}.{vlan_id}",
+ f"sudo ip link set dev {interface_name}.{vlan_id} up"]
+
+ for cmd in cmds:
+ ssh_client.exec_command(cmd)
+
+ self.verify_l3_connectivity(
+ floating_ipA,
+ keypair['private_key'],
+ fixed_ipB,
+ conn_expected=True
+ )
+
+ self.os_primary.trunks_client.delete_subports_from_trunk(
+ trunk['id'], trunk['sub_ports'])
+ self.verify_l3_connectivity(
+ floating_ipA,
+ keypair['private_key'],
+ fixed_ipB,
+ conn_expected=False
+ )
+ self.os_primary.trunks_client.add_subports_to_trunk(
+ trunk['id'], trunk['sub_ports'])
+
+ # NOTE(vsaienko): it may take some time for network driver to
+ # setup vlans as this is async operation.
+ self.verify_l3_connectivity(
+ floating_ipA,
+ keypair['private_key'],
+ fixed_ipB,
+ conn_expected=True
+ )
+ self.terminate_instance(instance=instanceA)
+ self.terminate_instance(instance=instanceB)
diff --git a/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py b/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
index f30b5e0..44b8f98 100644
--- a/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
+++ b/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
@@ -61,9 +61,6 @@
* Verifies all properties are inspected
* Verifies introspection data
* Sets node to available state
- * Creates a keypair
- * Boots an instance using the keypair
- * Deletes the instance
"""
# prepare introspection rule
@@ -103,11 +100,6 @@
timeout=CONF.baremetal.active_timeout,
interval=self.wait_provisioning_state_interval)
- self.wait_for_nova_aware_of_bvms()
- self.add_keypair()
- ins, _node = self.boot_instance()
- self.terminate_instance(ins)
-
@decorators.idempotent_id('70ca3070-184b-4b7d-8892-e977d2bc2870')
def test_introspection_abort(self):
"""This smoke test case follows this very basic set of operations:
diff --git a/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py b/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
index 6ebfcc6..9950453 100644
--- a/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
+++ b/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
@@ -136,13 +136,13 @@
* Generate discovery rule;
* Start introspection via ironic-inspector API;
* Delete the node from ironic;
- * Wating for node discovery;
+ * Waiting for node discovery;
* Verify introspected node.
"""
# NOTE(aarefiev): workaround for infra, 'tempest' user doesn't
# have virsh privileges, so lets power on the node via ironic
# and then delete it. Because of node is blacklisted in inspector
- # we can't just power on it, therefor start introspection is used
+ # we can't just power on it, therefore start introspection is used
# to whitelist discovered node first.
self.baremetal_client.set_node_provision_state(
self.node_info['uuid'], 'manage')
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..4fa5ffd
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,18 @@
+[build-system]
+requires = ["pbr>=6.0.0", "setuptools>=64.0.0"]
+build-backend = "pbr.build"
+
+[tool.doc8]
+ignore = ["D001"]
+
+[tool.ruff]
+line-length = 79
+target-version = "py37"
+
+[tool.ruff.lint]
+select = [
+ "E", # pycodestyle (error)
+ "F", # pyflakes
+ "G", # flake8-logging-format
+ "LOG", # flake8-logging
+]
diff --git a/releasenotes/notes/drop-python38-support-8bfac4d85b4b19e2.yaml b/releasenotes/notes/drop-python38-support-8bfac4d85b4b19e2.yaml
new file mode 100644
index 0000000..f0ada81
--- /dev/null
+++ b/releasenotes/notes/drop-python38-support-8bfac4d85b4b19e2.yaml
@@ -0,0 +1,6 @@
+---
+upgrade:
+ - |
+ Support for Python 3.8 has been dropped. Latest release of
+ ironic-tempest-plugin to support python 3.8 is 2.11.0.
+ The minimum version of Python now supported is Python 3.9.
diff --git a/requirements.txt b/requirements.txt
index bfafa58..ff7b704 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-pbr>=2.0.0 # Apache-2.0
+pbr>=6.0.0 # Apache-2.0
oslo.config>=5.2.0 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0
oslo.serialization>=2.18.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index a5cf426..c1cb2cf 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,6 +6,7 @@
author = OpenStack
author_email = openstack-discuss@lists.openstack.org
home_page = https://docs.openstack.org/ironic-tempest-plugin/latest/
+python_requires = >=3.9
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@@ -14,10 +15,10 @@
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 3
- Programming Language :: Python :: 3.6
- Programming Language :: Python :: 3.7
- Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
[files]
packages =
diff --git a/setup.py b/setup.py
index cd35c3c..b997e51 100644
--- a/setup.py
+++ b/setup.py
@@ -16,5 +16,5 @@
import setuptools
setuptools.setup(
- setup_requires=['pbr>=2.0.0'],
+ setup_requires=['pbr>=6.0.0'],
pbr=True)
diff --git a/tox.ini b/tox.ini
index 3a67abc..bf8061c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-minversion = 3.18.0
+minversion = 4.4.0
envlist = pep8
ignore_basepython_conflict=true
@@ -14,11 +14,9 @@
commands = stestr run --slowest {posargs}
[testenv:pep8]
-deps =
- hacking~=6.0.0 # Apache-2.0
- flake8-import-order>=0.17.1 # LGPLv3
- pycodestyle>=2.0.0,<3.0.0 # MIT
-commands = flake8 {posargs}
+deps = pre-commit
+allowlist_externals = pre-commit
+commands = pre-commit run --all-files --show-diff-on-failure {posargs}
[testenv:venv]
commands = {posargs}
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 11eb7c0..4d9da27 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -7,69 +7,63 @@
jobs:
# NOTE(dtantsur): keep N-3 and older non-voting for these jobs.
- ironic-standalone
+ - ironic-standalone-2024.2
- ironic-standalone-2024.1
- - ironic-standalone-2023.2
- - ironic-standalone-2023.1:
+ - ironic-standalone-2023.2:
voting: false
- ironic-tempest-functional-python3
+ - ironic-tempest-functional-python3-2024.2
- ironic-tempest-functional-python3-2024.1
- ironic-tempest-functional-python3-2023.2:
voting: false
- - ironic-tempest-functional-rbac-scope-enforced
+ - ironic-tempest-functional-rbac-scope-enforced-2024.2
- ironic-tempest-functional-rbac-scope-enforced-2024.1
- - ironic-tempest-functional-rbac-scope-enforced-2023.2
- # Enable these *once* we have the policy fix backported
- # for making own node changes.
- # - ironic-tempest-functional-rbac-scope-enforced-2023.1
- - ironic-inspector-tempest
- - ironic-inspector-tempest-2024.1
- - ironic-inspector-tempest-2023.2
- - ironic-inspector-tempest-2023.1:
+ - ironic-tempest-functional-rbac-scope-enforced-2023.2:
voting: false
- ironic-standalone-anaconda
+ - ironic-standalone-anaconda-2024.2
- ironic-standalone-anaconda-2024.1
- - ironic-standalone-anaconda-2023.2
- - ironic-standalone-anaconda-2023.1:
+ - ironic-standalone-anaconda-2023.2:
voting: false
- ironic-standalone-redfish
+ - ironic-standalone-redfish-2024.2
- ironic-standalone-redfish-2024.1
- - ironic-standalone-redfish-2023.2
- - ironic-standalone-redfish-2023.1:
+ - ironic-standalone-redfish-2023.2:
+ voting: false
+ # NOTE(dtantsur): inspector is deprecated and rarely sees any changes,
+ # no point in running many jobs
+ - ironic-inspector-tempest
+ - ironic-inspector-tempest-2024.2:
+ voting: false
+ - ironic-inspector-tempest-2024.1:
voting: false
# NOTE(dtantsur): these jobs cover rarely changed tests and are quite
# unstable, so keep them non-voting.
# NOTE(TheJulia): Except this first one so we can validate fixes to
# the base tests as we make them.
- ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
+ - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.2:
+ voting: false
- ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.1:
voting: false
- ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2023.2:
voting: false
- - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2023.1:
- voting: false
- - ironic-inspector-tempest-discovery
- - ironic-inspector-tempest-discovery-2024.1
- - ironic-inspector-tempest-discovery-2023.2
- - ironic-inspector-tempest-discovery-2023.1:
+ - ironic-inspector-tempest-discovery:
voting: false
gate:
jobs:
- ironic-standalone
+ - ironic-standalone-2024.2
- ironic-standalone-2024.1
- - ironic-standalone-2023.2
- ironic-tempest-functional-python3
+ - ironic-tempest-functional-python3-2024.2
- ironic-tempest-functional-python3-2024.1
- - ironic-tempest-functional-python3-2023.2
+ - ironic-tempest-functional-rbac-scope-enforced-2024.2
- ironic-tempest-functional-rbac-scope-enforced-2024.1
- - ironic-tempest-functional-rbac-scope-enforced-2023.2
- - ironic-tempest-functional-rbac-scope-enforced
- - ironic-inspector-tempest
- - ironic-inspector-tempest-2024.1
- - ironic-inspector-tempest-2023.2
- - ironic-inspector-tempest-rbac-scope-enforced
- - ironic-inspector-tempest-rbac-scope-enforced-2024.1
- - ironic-inspector-tempest-rbac-scope-enforced-2023.2
+ - ironic-standalone-anaconda
+ - ironic-standalone-anaconda-2024.2
+ - ironic-standalone-anaconda-2024.1
- ironic-standalone-redfish
+ - ironic-standalone-redfish-2024.2
- ironic-standalone-redfish-2024.1
- - ironic-standalone-redfish-2023.2
- - ironic-inspector-tempest-discovery
+ - ironic-inspector-tempest
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index 3643bf4..c75f782 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -1,4 +1,9 @@
- job:
+ name: ironic-standalone-2024.2
+ parent: ironic-standalone
+ override-checkout: stable/2024.2
+
+- job:
name: ironic-standalone-2024.1
parent: ironic-standalone
override-checkout: stable/2024.1
@@ -9,47 +14,9 @@
override-checkout: stable/2023.2
- job:
- name: ironic-standalone-2023.1
+ name: ironic-standalone-redfish-2024.2
parent: ironic-standalone
- override-checkout: stable/2023.1
-
-- job:
- name: ironic-standalone-zed
- parent: ironic-standalone
- override-checkout: stable/zed
-
-- job:
- name: ironic-standalone-yoga
- parent: ironic-standalone
- override-checkout: stable/yoga
-
-- job:
- name: ironic-standalone-xena
- parent: ironic-standalone
- override-checkout: stable/xena
-
-- job:
- name: ironic-standalone-wallaby
- parent: ironic-standalone
- override-checkout: stable/wallaby
-
-- job:
- name: ironic-standalone-victoria
- parent: ironic-standalone
- override-checkout: stable/victoria
-
-- job:
- name: ironic-standalone-ussuri
- parent: ironic-standalone
- override-checkout: stable/ussuri
-
-- job:
- name: ironic-standalone-train
- parent: ironic-standalone
- override-checkout: stable/train
- vars:
- devstack_localrc:
- USE_PYTHON3: True
+ override-checkout: stable/2024.2
- job:
name: ironic-standalone-redfish-2024.1
@@ -62,51 +29,9 @@
override-checkout: stable/2023.2
- job:
- name: ironic-standalone-redfish-2023.1
- parent: ironic-standalone
- override-checkout: stable/2023.1
-
-- job:
- name: ironic-standalone-redfish-zed
- parent: ironic-standalone
- override-checkout: stable/zed
-
-- job:
- name: ironic-standalone-redfish-yoga
- parent: ironic-standalone-redfish
- nodeset: openstack-single-node-focal
- override-checkout: stable/yoga
-
-- job:
- name: ironic-standalone-redfish-xena
- parent: ironic-standalone-redfish
- nodeset: openstack-single-node-focal
- override-checkout: stable/xena
-
-- job:
- name: ironic-standalone-redfish-wallaby
- parent: ironic-standalone-redfish
- nodeset: openstack-single-node-focal
- override-checkout: stable/wallaby
-
-- job:
- name: ironic-standalone-redfish-victoria
- parent: ironic-standalone-redfish
- nodeset: openstack-single-node-focal
- override-checkout: stable/victoria
-
-- job:
- name: ironic-standalone-redfish-ussuri
- parent: ironic-standalone-redfish
- override-checkout: stable/ussuri
-
-- job:
- name: ironic-standalone-redfish-train
- parent: ironic-standalone-redfish
- override-checkout: stable/train
- vars:
- devstack_localrc:
- USE_PYTHON3: True
+ name: ironic-standalone-anaconda-2024.2
+ parent: ironic-standalone-anaconda
+ override-checkout: stable/2024.2
- job:
name: ironic-standalone-anaconda-2024.1
@@ -119,9 +44,9 @@
override-checkout: stable/2023.2
- job:
- name: ironic-standalone-anaconda-2023.1
- parent: ironic-standalone-anaconda
- override-checkout: stable/2023.1
+ name: ironic-tempest-functional-python3-2024.2
+ parent: ironic-tempest-functional-python3
+ override-checkout: stable/2024.2
- job:
name: ironic-tempest-functional-python3-2024.1
@@ -134,44 +59,9 @@
override-checkout: stable/2023.2
- job:
- name: ironic-tempest-functional-python3-2023.1
- parent: ironic-tempest-functional-python3
- override-checkout: stable/2023.1
-
-- job:
- name: ironic-tempest-functional-python3-zed
- parent: ironic-tempest-functional-python3
- override-checkout: stable/zed
-
-- job:
- name: ironic-tempest-functional-python3-yoga
- parent: ironic-tempest-functional-python3
- override-checkout: stable/yoga
-
-- job:
- name: ironic-tempest-functional-python3-xena
- parent: ironic-tempest-functional-python3
- override-checkout: stable/xena
-
-- job:
- name: ironic-tempest-functional-python3-wallaby
- parent: ironic-tempest-functional-python3
- override-checkout: stable/wallaby
-
-- job:
- name: ironic-tempest-functional-python3-victoria
- parent: ironic-tempest-functional-python3
- override-checkout: stable/victoria
-
-- job:
- name: ironic-tempest-functional-python3-ussuri
- parent: ironic-tempest-functional-python3
- override-checkout: stable/ussuri
-
-- job:
- name: ironic-tempest-functional-python3-train
- parent: ironic-tempest-functional-python3
- override-checkout: stable/train
+ name: ironic-tempest-functional-rbac-scope-enforced-2024.2
+ parent: ironic-tempest-functional-rbac-scope-enforced
+ override-checkout: stable/2024.2
- job:
name: ironic-tempest-functional-rbac-scope-enforced-2024.1
@@ -184,19 +74,9 @@
override-checkout: stable/2023.2
- job:
- name: ironic-tempest-functional-rbac-scope-enforced-2023.1
- parent: ironic-tempest-functional-rbac-scope-enforced
- override-checkout: stable/2023.1
-
-- job:
- name: ironic-tempest-functional-rbac-scope-enforced-zed
- parent: ironic-tempest-functional-rbac-scope-enforced
- override-checkout: stable/zed
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-zed
+ name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.2
parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/zed
+ override-checkout: stable/2024.2
- job:
name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.1
@@ -209,174 +89,11 @@
override-checkout: stable/2023.2
- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2023.1
- parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/2023.1
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-yoga
- parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/yoga
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-xena
- parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/xena
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-wallaby
- parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/wallaby
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-victoria
- parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/victoria
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-ussuri
- parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/ussuri
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-train
- parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/train
- vars:
- devstack_localrc:
- USE_PYTHON3: True
+ name: ironic-inspector-tempest-2024.2
+ parent: ironic-inspector-tempest
+ override-checkout: stable/2024.2
- job:
name: ironic-inspector-tempest-2024.1
parent: ironic-inspector-tempest
override-checkout: stable/2024.1
-
-- job:
- name: ironic-inspector-tempest-2023.2
- parent: ironic-inspector-tempest
- override-checkout: stable/2023.2
-
-- job:
- name: ironic-inspector-tempest-2023.1
- parent: ironic-inspector-tempest
- override-checkout: stable/2023.1
-
-- job:
- name: ironic-inspector-tempest-zed
- parent: ironic-inspector-tempest
- override-checkout: stable/zed
-
-- job:
- name: ironic-inspector-tempest-yoga
- parent: ironic-inspector-tempest
- override-checkout: stable/yoga
-
-- job:
- name: ironic-inspector-tempest-xena
- parent: ironic-inspector-tempest
- override-checkout: stable/xena
-
-- job:
- name: ironic-inspector-tempest-wallaby
- parent: ironic-inspector-tempest
- override-checkout: stable/wallaby
-
-- job:
- name: ironic-inspector-tempest-victoria
- parent: ironic-inspector-tempest
- override-checkout: stable/victoria
- vars:
- devstack_localrc:
- FIXED_NETWORK_SIZE: 4096
-
-- job:
- name: ironic-inspector-tempest-ussuri
- parent: ironic-inspector-tempest
- override-checkout: stable/ussuri
- vars:
- devstack_localrc:
- FIXED_NETWORK_SIZE: 4096
- EBTABLES_RACE_FIX: True
-
-- job:
- name: ironic-inspector-tempest-train
- parent: ironic-inspector-tempest
- override-checkout: stable/train
- vars:
- devstack_localrc:
- FIXED_NETWORK_SIZE: 4096
- EBTABLES_RACE_FIX: True
- USE_PYTHON3: True
-
-- job:
- name: ironic-inspector-tempest-rbac-scope-enforced-2024.1
- parent: ironic-inspector-tempest-rbac-scope-enforced
- override-checkout: stable/2024.1
-
-- job:
- name: ironic-inspector-tempest-rbac-scope-enforced-2023.2
- parent: ironic-inspector-tempest-rbac-scope-enforced
- override-checkout: stable/2023.2
-
-- job:
- name: ironic-inspector-tempest-rbac-scope-enforced-2023.1
- parent: ironic-inspector-tempest-rbac-scope-enforced
- override-checkout: stable/2023.1
-
-- job:
- name: ironic-inspector-tempest-rbac-scope-enforced-zed
- parent: ironic-inspector-tempest-rbac-scope-enforced
- override-checkout: stable/zed
-
-- job:
- name: ironic-inspector-tempest-discovery-2024.1
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/2024.1
-
-- job:
- name: ironic-inspector-tempest-discovery-2023.2
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/2023.2
-
-- job:
- name: ironic-inspector-tempest-discovery-2023.1
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/2023.1
-
-- job:
- name: ironic-inspector-tempest-discovery-zed
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/zed
-
-- job:
- name: ironic-inspector-tempest-discovery-yoga
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/yoga
-
-- job:
- name: ironic-inspector-tempest-discovery-xena
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/xena
-
-- job:
- name: ironic-inspector-tempest-discovery-wallaby
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/wallaby
-
-- job:
- name: ironic-inspector-tempest-discovery-victoria
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/victoria
-
-- job:
- name: ironic-inspector-tempest-discovery-ussuri
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/ussuri
-
-- job:
- name: ironic-inspector-tempest-discovery-train
- parent: ironic-inspector-tempest-discovery
- override-checkout: stable/train
- vars:
- devstack_localrc:
- USE_PYTHON3: True