Merge "list detailed ports by node"
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/ironic_tempest_plugin/common/waiters.py b/ironic_tempest_plugin/common/waiters.py
index e538cd8..313b4c3 100644
--- a/ironic_tempest_plugin/common/waiters.py
+++ b/ironic_tempest_plugin/common/waiters.py
@@ -18,6 +18,7 @@
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.common import utils
+from ironic_tempest_plugin import exceptions as ironic_exc
LOG = log.getLogger(__name__)
@@ -164,7 +165,8 @@
def wait_node_value_in_field(client, node_id, field, value,
raise_if_insufficent_access=True,
- timeout=None, interval=None):
+ timeout=None, interval=None,
+ abort_on_error_state=False):
"""Waits for a node to have a field value appear.
:param client: an instance of tempest plugin BaremetalClient.
@@ -173,6 +175,8 @@
:param value: the value/key with-in the field to look for.
:param timeout: the timeout after which the check is considered as failed.
:param interval: an interval between show_node calls for status check.
+ :param abort_on_error_state: whether to abort waiting if the node reaches
+ an error state.
"""
def is_field_updated():
@@ -180,8 +184,17 @@
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.')
- raise lib_exc.InsufficientAPIAccess(msg)
+ 'indicating insufficient access to execute this test.')
+ raise ironic_exc.InsufficientAPIAccess(msg)
+ elif (abort_on_error_state
+ and (node['provision_state'].endswith('failed')
+ or node['provision_state'] == 'error')):
+ msg = ('Node %(node)s reached failure state %(state)s while '
+ 'waiting Error: %(error)s' %
+ {'node': node_id, 'state': node['provision_state'],
+ 'error': node.get('last_error')})
+ LOG.debug(msg)
+ raise lib_exc.TempestException(msg)
return value in field_value
if not test_utils.call_until_true(is_field_updated, timeout,
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index 9e96863..38a67eb 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -20,6 +20,10 @@
from tempest import config # noqa
+_INSPECTOR_REASON = ('ironic-inspector was retired in favor of the built-in'
+ 'agent inspect interface.')
+
+
# NOTE(TheJulia): The following options are loaded into a tempest
# plugin configuration option via plugin.py.
ironic_service_option = cfg.BoolOpt('ironic',
@@ -29,6 +33,8 @@
inspector_service_option = cfg.BoolOpt("ironic_inspector",
default=False,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Whether or not ironic-inspector "
"is expected to be available")
@@ -40,6 +46,8 @@
inspector_scope_enforcement = cfg.BoolOpt('ironic_inspector',
default=True,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help='Whether or not '
'ironic-inspector is expected '
'to enforce auth scope.')
@@ -224,9 +232,6 @@
min=0,
help="Ironic adjusted disk size to use in the standalone tests "
"as instance_info/root_gb value."),
- cfg.IntOpt('available_nodes', min=0, default=None,
- help="The number of baremetal hosts available to use for "
- "the tests."),
cfg.BoolOpt('partition_netboot',
default=True,
help="Treat partition images as netbooted as opposed to "
@@ -282,33 +287,51 @@
"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 = [
cfg.StrOpt('catalog_type',
default='baremetal-introspection',
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Catalog type of the baremetal provisioning service"),
cfg.StrOpt('endpoint_type',
default='publicURL',
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="The endpoint type to use for the baremetal introspection"
" service"),
cfg.IntOpt('introspection_sleep',
default=30,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Introspection sleep before check status"),
cfg.IntOpt('introspection_timeout',
default=600,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Introspection time out"),
cfg.IntOpt('introspection_start_timeout',
default=90,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Timeout to start introspection"),
cfg.IntOpt('hypervisor_update_sleep',
default=60,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Time to wait until nova becomes aware of "
"bare metal instances"),
cfg.IntOpt('hypervisor_update_timeout',
default=300,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Time out for wait until nova becomes aware of "
"bare metal instances"),
# NOTE(aarefiev): status_check_period default is 60s, but checking
@@ -316,14 +339,20 @@
# 80s would be enough to make one more check.
cfg.IntOpt('ironic_sync_timeout',
default=80,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Time it might take for Ironic--Inspector "
"sync to happen"),
cfg.IntOpt('discovery_timeout',
default=300,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Time to wait until new node would enrolled in "
"ironic"),
cfg.BoolOpt('auto_discovery_feature',
default=False,
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="Is the auto-discovery feature enabled. Enroll hook "
"should be specified in node_not_found_hook - processing "
"section of inspector.conf"),
@@ -331,11 +360,17 @@
# TODO(dtantsur): change to fake-hardware when Queens is no
# longer supported.
default='fake',
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="The driver expected to be set on newly discovered nodes. "
"Only has effect with auto_discovery_feature is True."),
cfg.StrOpt('auto_discovery_target_driver',
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="The driver to set on the newly discovered nodes. "
"Only has effect with auto_discovery_feature is True."),
cfg.StrOpt('data_store',
+ deprecated_for_removal=True,
+ deprecated_reason=_INSPECTOR_REASON,
help="The storage backend for storing introspection data."),
]
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/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index 599f448..d12ece4 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -331,7 +331,6 @@
:param description: The description of the chassis.
Default: test-chassis
:return: A tuple with the server response and the created chassis.
-
"""
chassis = {'description': kwargs.get('description', 'test-chassis')}
@@ -1029,3 +1028,69 @@
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)
+
+ @base.handle_errors
+ def list_portgroups_detail(self):
+ """List detailed portgroups."""
+ return self._list_request('portgroups/detail')
+
+ @base.handle_errors
+ def list_portgroups_by_node(self, node_ident):
+ """List portgroups filtered by node."""
+ return self._list_request(f'nodes/{node_ident}/portgroups')
+
+ @base.handle_errors
+ def list_portgroups_details_by_node(self, node_ident):
+ """List detailed portgroups filtered by node."""
+ return self._list_request(f'nodes/{node_ident}/portgroups/detail')
+
+ @base.handle_errors
+ def create_inspection_rule(self, payload):
+ """Create Inspection rule.
+
+ :param payload: Inspection rule JSON
+ """
+ return self._create_request('inspection_rules', payload)
+
+ @base.handle_errors
+ def show_inspection_rule(self, rule_uuid):
+ """Show Inspection rule."""
+ return self._show_request('inspection_rules', rule_uuid)
+
+ @base.handle_errors
+ def list_inspection_rule(self, **kwargs):
+ """List all Inspection rules."""
+ return self._list_request('inspection_rules', **kwargs)
+
+ @base.handle_errors
+ def update_inspection_rule(self, rule_uuid, patch):
+ """Update the specified Inspection rule.
+
+ :param rule_uuid: The unique identifier of the inspection rule.
+ :param patch: List of dicts representing json patches.
+ """
+ return self._patch_request('inspection_rules', rule_uuid, patch)
+
+ @base.handle_errors
+ def delete_inspection_rule(self, rule_uuid):
+ """Delete Inspection rule.
+
+ :param rule_uuid: uuid of the inspection rule.
+ """
+ return self._delete_request('inspection_rules', rule_uuid)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py
new file mode 100644
index 0000000..3d00ce3
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py
@@ -0,0 +1,172 @@
+# 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 ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+class TestInspectionRules(base.BaseBaremetalTest):
+ """API tests for Inspection Rules endpoints"""
+
+ # Inspection rules API was introduced in microversion 1.96
+ # We will be skipping this test for the older version.
+ min_microversion = '1.96'
+
+ def setUp(self):
+ super(TestInspectionRules, self).setUp()
+
+ _, self.node = self.create_node(None)
+
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture('1.96'))
+
+ def _create_inspection_rule_payload(self, **kwargs):
+ """Create a Inspection rule payload."""
+ payload = {
+ "description": "Inspection rule to log node UUID",
+ "conditions": [],
+ "actions": [
+ {
+ "op": "log",
+ "args": {
+ "msg": "Node with UUID {node.uuid} is being inspected"
+ }
+ }
+ ],
+ "phase": "main",
+ "priority": 0,
+ "sensitive": False
+ }
+
+ payload.update(kwargs)
+
+ return payload
+
+ @decorators.idempotent_id('7fb771cd-b011-409e-a255-3c71cf7251e8')
+ def test_create_rule_sensitive_true(self):
+ """Test creating rule with sensitive=True."""
+ rule_uuid = data_utils.rand_uuid()
+ payload = self._create_inspection_rule_payload(sensitive=True)
+
+ self.create_inspection_rule(rule_uuid, payload)
+
+ _, fetched_rule = self.client.show_inspection_rule(rule_uuid)
+
+ self.assertTrue(fetched_rule.get('sensitive'))
+ self.assertIsNone(fetched_rule.get('conditions'))
+ self.assertIsNone(fetched_rule.get('actions'))
+
+ @decorators.idempotent_id('e60b4513-7c3d-4b2c-b485-17443bf6485f')
+ def test_create_rule_complex_logging_conditions_actions(self):
+ """Test creating rule with loop conditions and actions"""
+ complex_log_conditions = [
+ {
+ "op": "eq",
+ "args": [
+ "{inventory.system.product_name}",
+ "{item}"
+ ],
+ "loop": [
+ "product_name_1",
+ "product_name_2",
+ "product_name_3"
+ ],
+ "multiple": "any"
+ }
+ ]
+
+ complex_log_actions = [
+ {
+ "op": "set-attribute",
+ "args": [
+ "{item[path]}",
+ "{item[value]}"
+ ],
+ "loop": [
+ {
+ "path": "/driver_info/ipmi_username",
+ "value": "admin"
+ },
+ {
+ "path": "/driver_info/ipmi_password",
+ "value": "password"
+ },
+ {
+ "path": "/driver_info/ipmi_address",
+ "value": "{inventory[bmc_address]}"
+ }
+ ]
+ }
+ ]
+
+ payload = self._create_inspection_rule_payload(
+ conditions=complex_log_conditions,
+ actions=complex_log_actions,
+ )
+
+ _, created_rule = self.create_inspection_rule(None, payload)
+
+ self.assertEqual(complex_log_conditions,
+ created_rule.get('conditions'))
+ self.assertEqual(complex_log_actions,
+ created_rule.get('actions'))
+
+ @decorators.idempotent_id('a786a4ec-1e43-4fb9-8fc3-c53aa4e1f52f')
+ def test_patch_conditions_actions_priority(self):
+ """Test Updating rule'si priority, condition and actions"""
+ payload = self._create_inspection_rule_payload()
+
+ patch = [
+ {
+ "op": "replace",
+ "path": "/priority",
+ "value": 200
+ },
+ {
+ "op": "replace",
+ "path": "/conditions",
+ "value": [
+ {
+ "op": "eq",
+ "args": ["{{ inventory.cpu.count }}", 8]
+ }
+ ]
+ },
+ {
+ "op": "replace",
+ "path": "/actions",
+ "value": [
+ {
+ "op": "set-attribute",
+ "args": ["{{ /properties/cpu_model }}", "cpu_xyz"]
+ },
+ {
+ "op": "log",
+ "args": ["CPU model updated via rule."]
+ }
+ ]
+ }
+ ]
+
+ _, created_rule = self.create_inspection_rule(None, payload)
+ _, fetched_rule = self.client.update_inspection_rule(
+ created_rule.get('uuid'), patch)
+
+ self.assertEqual(fetched_rule.get('priority'),
+ patch[0]['value'])
+ self.assertEqual(fetched_rule.get('conditions'),
+ patch[1]['value'])
+ self.assertEqual(fetched_rule.get('actions'),
+ patch[2]['value'])
diff --git a/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py
new file mode 100644
index 0000000..a92728b
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py
@@ -0,0 +1,79 @@
+# 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.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+class TestInspectionRulesNegative(base.BaseBaremetalTest):
+ """Negative Inspection Rules test"""
+
+ # Inspection rules API was introduced in microversion 1.96
+ # We will be skipping this test for the older version.
+ min_microversion = '1.96'
+
+ def setUp(self):
+ super(TestInspectionRulesNegative, self).setUp()
+
+ _, self.node = self.create_node(None)
+
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture('1.96'))
+
+ def _create_inspection_rule_payload(self, **kwargs):
+ """Create a Inspection rule payload."""
+ payload = {
+ "description": "Inspection rule to log node UUID",
+ "conditions": [],
+ "actions": [
+ {
+ "op": "log",
+ "args": {
+ "msg": "Node with UUID {node.uuid} is being inspected"
+ }
+ }
+ ],
+ "phase": "main",
+ "priority": 0,
+ "sensitive": False
+ }
+
+ payload.update(kwargs)
+
+ return payload
+
+ @decorators.idempotent_id('55403d94-53ce-41ab-989a-da3399314c9d')
+ @decorators.attr(type=['negative'])
+ def test_create_invalid_priority_fails(self):
+ """Test to create Inspection rule with invalid priorities"""
+ invalid_priorities = [-1, 10000, 5000.50]
+
+ for priority_val in invalid_priorities:
+ payload = self._create_inspection_rule_payload(
+ priority=priority_val)
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.create_inspection_rule,
+ rule_uuid=None, payload=payload)
+
+ @decorators.idempotent_id('cf9615b3-904e-4456-b00a-622d39892b88')
+ @decorators.attr(type=['negative'])
+ def test_delete_by_wrong_uiid(self):
+ """Test to delete Inspection Rule with wrong uuid"""
+ rule_uuid = data_utils.rand_uuid()
+ self.assertRaises(lib_exc.NotFound,
+ self.delete_inspection_rule,
+ rule_uuid=rule_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..6dd9982
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_microversion_enforcement.py
@@ -0,0 +1,1499 @@
+# 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 random
+
+from oslo_utils import timeutils
+from oslo_utils import uuidutils
+from tempest import config
+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.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
+
+CONF = config.CONF
+
+
+class MicroversionTestMixin:
+ """Mixin class containing shared microversion test functionality."""
+
+ def _microversion_test(
+ self,
+ method_name,
+ min_version,
+ expected_error,
+ required_args,
+ ignore_positive=False,
+ ):
+ """Test methods with invalid API versions"""
+
+ major, minor = map(int, min_version.split("."))
+ invalid_versions = []
+ if minor >= 11:
+ invalid_versions.append(f"{major}.{minor - 10}")
+ if minor >= 2:
+ invalid_versions.append(f"{major}.{minor - 1}")
+ elif minor == 0 and major > 1:
+ invalid_versions.append(f"{major - 1}.99")
+ elif minor == 1:
+ invalid_versions.append(f"{major}.0")
+ else:
+ # We expect fails for v1.0 and below
+ raise ValueError(f"Invalid microversion {min_version}")
+
+ # Get method name from method object
+ method_name = method_name.__name__
+
+ # Test with invalid versions (should fail)
+ for microversion in invalid_versions:
+ msg = (
+ f"Testing {method_name} with version {microversion} "
+ f"and arguments {required_args} - should fail"
+ )
+ with self.subTest(
+ msg=msg, method=method_name, version=microversion
+ ):
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture(
+ microversion
+ )
+ )
+ method = getattr(self.client, method_name)
+
+ try:
+ method(**required_args)
+ self.fail(
+ f"Request for microversion {microversion} for "
+ f"{method_name} unexpectedly succeeded. We expected "
+ f"{expected_error.__name__}."
+ )
+ except expected_error:
+ pass # Expected error, test passes
+ except Exception as e:
+ self.fail(
+ f"Request for microversion {microversion} for "
+ f"{method_name} raised unexpected exception: {e}"
+ )
+
+ if ignore_positive:
+ return True
+
+ # Test with valid version (should succeed)
+ # Use the minimum required version
+ msg = (
+ f"Testing {method_name} with version {min_version} "
+ f"and arguments {required_args} - should succeed"
+ )
+ with self.subTest(
+ msg=msg, method=method_name, version=min_version
+ ):
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture(
+ min_version
+ )
+ )
+ method = getattr(self.client, method_name)
+
+ try:
+ # We don't check the actual response, just
+ # that it doesn't raise
+ # the expected error from the negative test
+ method(**required_args)
+ return True
+
+ except expected_error as e:
+ self.fail(
+ f"Method {method_name} failed with valid "
+ f"microversion {min_version}: {e}"
+ )
+ except Exception as e:
+ # Other exceptions might be expected due to invalid test data
+ # For example, a 404 might be expected if we're using fake IDs
+ self.assertNotIsInstance(
+ e,
+ expected_error,
+ (
+ f"Got unexpected {expected_error.__name__} with valid "
+ f"microversion {min_version}: {e}"
+ ),
+ )
+
+
+class BaseTestMicroversionEnforcement(base.BaseBaremetalTest):
+ """Base class for microversion enforcement tests."""
+
+ def setUp(self):
+ super().setUp()
+ self.resource_class = uuidutils.generate_uuid()
+
+
+class TestNodesMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for nodes-related API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ def setUp(self):
+ super(TestNodesMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+
+ # NOTE(adamcarthur) - We test the positive case in the other
+ # tests. Doing it here would require changing the test function
+ # to allow returns.
+ @decorators.idempotent_id("be29d566-43d5-41ce-8c8a-ce360af9995b")
+ def test_create_node(self):
+ self._microversion_test(
+ BaremetalClient.create_node,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"chassis_id": self.chassis["uuid"]},
+ ignore_positive=True
+ )
+
+ @decorators.idempotent_id("3ddd0ae9-979b-44ac-9cdc-2cabd2f61118")
+ def test_list_nodes(self):
+ self._microversion_test(
+ BaremetalClient.list_nodes, "1.1", lib_exc.NotAcceptable, {}
+ )
+
+ @decorators.idempotent_id("d47160dd-654e-427b-91b0-9a4298a7ff54")
+ def test_list_nodes_detail(self):
+ self._microversion_test(
+ BaremetalClient.list_nodes_detail, "1.1", lib_exc.NotAcceptable, {}
+ )
+
+ @decorators.idempotent_id("a612b489-d4c6-4f29-9dcd-ca7a71d93f77")
+ def test_show_node(self):
+ _, node = self.create_node(self.chassis["uuid"])
+ self._microversion_test(
+ BaremetalClient.show_node,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": node["uuid"]},
+ )
+ # delete the node
+ self.client.delete_node(node["uuid"])
+
+ @decorators.idempotent_id("02623737-84f5-42f1-a8f3-0f3a8c1319da")
+ def test_update_node(self):
+ _, node = self.create_node(self.chassis["uuid"])
+ instance_uuid = data_utils.rand_uuid()
+ self._microversion_test(
+ BaremetalClient.update_node,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": node["uuid"], "patch": instance_uuid},
+ )
+ # delete the node
+ self.client.delete_node(node["uuid"])
+
+ @decorators.idempotent_id("59f433f1-05b8-47a9-b14a-f4aba337698d")
+ def test_delete_node(self):
+ _, node = self.create_node(self.chassis["uuid"])
+ self._microversion_test(
+ BaremetalClient.delete_node,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": node["uuid"]},
+ )
+
+
+class TestNodeManagementMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node management-related API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ def setUp(self):
+ super(TestNodeManagementMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ def _validate_provision_state(self, node_uuid, target_state):
+ # Validate that provision state is set within timeout
+ start = timeutils.utcnow()
+ while timeutils.delta_seconds(
+ start, timeutils.utcnow()) < self.unprovision_timeout:
+ _, node = self.client.show_node(node_uuid)
+ if node['provision_state'] == target_state:
+ return
+ message = ('Failed to set provision state %(state)s within '
+ 'the required time: %(timeout)s sec.',
+ {'state': target_state,
+ 'timeout': self.unprovision_timeout})
+ raise lib_exc.TimeoutException(message)
+
+ @decorators.idempotent_id("54a58e1e-8334-4152-84b4-2974cabb74e8")
+ def test_validate_driver_interface(self):
+ self._microversion_test(
+ BaremetalClient.validate_driver_interface,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+ @decorators.idempotent_id("9adfa595-8937-4a99-83e5-6012ddcbddc0")
+ def test_set_node_power_state(self):
+ self._microversion_test(
+ BaremetalClient.set_node_power_state,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"], "state": "power off"},
+ )
+
+ @decorators.idempotent_id("efd978ce-2ce5-4dc3-8f69-ff063644a550")
+ def test_set_node_provision_state(self):
+ contd = self._microversion_test(
+ BaremetalClient.set_node_provision_state,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"], "state": "manageable"},
+ )
+
+ if contd:
+ self._validate_provision_state(self.node["uuid"], "manageable")
+
+ @decorators.idempotent_id("893e4815-2c55-40f6-84e3-1816e28f6803")
+ def test_set_node_raid_config(self):
+ self._microversion_test(
+ BaremetalClient.set_node_raid_config,
+ "1.12",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"], "target_raid_config": {}},
+ )
+
+ @decorators.idempotent_id("f6b835da-dea3-46b2-8e16-6f910ad7cc86")
+ def test_get_console(self):
+ self._microversion_test(
+ BaremetalClient.get_console,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+ @decorators.idempotent_id("2dc96b6b-5713-467d-9707-389292ae5460")
+ def test_set_console_mode(self):
+ self._microversion_test(
+ BaremetalClient.set_console_mode,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"], "enabled": True},
+ )
+
+ @decorators.idempotent_id("4b4e23ae-3d18-4f35-a140-d10abcf22110")
+ def test_set_node_boot_device(self):
+ self._microversion_test(
+ BaremetalClient.set_node_boot_device,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"], "boot_device": "pxe"},
+ )
+
+ @decorators.idempotent_id("f13b398e-cde1-4862-a183-f20eb56d3234")
+ def test_get_node_boot_device(self):
+ self._microversion_test(
+ BaremetalClient.get_node_boot_device,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+ @decorators.idempotent_id("88985d9e-ccd1-4894-bc5d-e712ce29b892")
+ def test_get_node_supported_boot_devices(self):
+ self._microversion_test(
+ BaremetalClient.get_node_supported_boot_devices,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+
+class TestNodeVMediaMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node vmedia-related API microversion enforcement."""
+
+ min_microversion = "1.89"
+
+ def setUp(self):
+ super(TestNodeVMediaMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("305c0689-c43b-407c-8b78-932ff86e4b3d")
+ def test_attach_node_vmedia(self):
+ self._microversion_test(
+ BaremetalClient._put_request,
+ "1.89",
+ lib_exc.NotFound,
+ {
+ "resource": f"nodes/{self.node['uuid']}/vmedia",
+ "put_object": {
+ "device_type": "CDROM", "image_url": "http://image"
+ },
+ },
+ )
+
+ @decorators.idempotent_id("1d4d15bb-a66c-47f9-bf41-cc5e09854bcc")
+ def test_detach_node_vmedia(self):
+ self._microversion_test(
+ BaremetalClient._delete_request,
+ "1.89",
+ lib_exc.NotFound,
+ {"resource": f"nodes/{self.node['uuid']}/vmedia", "uuid": ""},
+ )
+
+
+class TestNodeVendorPassthruMethodsMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node vendor passthru methods API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ def setUp(self):
+ super(TestNodeVendorPassthruMethodsMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("8599e266-debc-4548-af5a-cd2223a5e051")
+ def test_list_vendor_passthru_methods(self):
+ self._microversion_test(
+ BaremetalClient.list_vendor_passthru_methods,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+
+class TestNodeVendorPassthruCallMethodMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for vendor passthru call method API microversion"""
+
+ min_microversion = "1.1"
+
+ def setUp(self):
+ super(TestNodeVendorPassthruCallMethodMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("35b34e5d-78d4-49e6-81b3-48b536f9fb60")
+ def test_call_vendor_passthru_method(self):
+ self._microversion_test(
+ BaremetalClient._put_request,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {
+ "resource": f"nodes/{self.node['uuid']}/vendor_passthru",
+ "put_object": {"method": "fakemethod"},
+ },
+ )
+
+
+class TestNodeTraitsMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node traits-related API microversion enforcement."""
+
+ min_microversion = "1.37"
+
+ def setUp(self):
+ super(TestNodeTraitsMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+ self.traits = ["CUSTOM_TRAIT1", "HW_CPU_X86_VMX"]
+
+ @decorators.idempotent_id("3f4ba6c1-5b39-4cb1-a823-21dcea802d61")
+ def test_list_node_traits(self):
+ self._microversion_test(
+ BaremetalClient.list_node_traits,
+ "1.37",
+ lib_exc.NotAcceptable,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+ @decorators.idempotent_id("10d77871-cd8a-4ecb-b6a4-f38d6ad719fe")
+ def test_set_node_traits(self):
+ self._microversion_test(
+ BaremetalClient.set_node_traits,
+ "1.37",
+ lib_exc.UnexpectedResponseCode,
+ {"node_uuid": self.node["uuid"], "traits": self.traits},
+ )
+
+ @decorators.idempotent_id("12ca1f62-9d4a-490c-8f8a-a10fc742630a")
+ def test_add_node_trait(self):
+ self._microversion_test(
+ BaremetalClient.add_node_trait,
+ "1.37",
+ lib_exc.UnexpectedResponseCode,
+ {"node_uuid": self.node["uuid"], "trait": "CUSTOM_TRAIT1"},
+ )
+
+ @decorators.idempotent_id("dd2f3b89-3fe3-4861-ba11-608b365ff484")
+ def test_remove_node_traits(self):
+ # add a trait to remove it
+ self.client.add_node_trait(self.node["uuid"], "CUSTOM_TRAIT1")
+ self._microversion_test(
+ BaremetalClient.remove_node_traits,
+ "1.37",
+ lib_exc.NotFound,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+ @decorators.idempotent_id("be438903-26d0-4ee6-970e-2d6d66058842")
+ def test_remove_node_trait(self):
+ self.client.add_node_trait(self.node["uuid"], "CUSTOM_TRAIT1")
+ self._microversion_test(
+ BaremetalClient.remove_node_trait,
+ "1.37",
+ lib_exc.NotFound,
+ {"node_uuid": self.node["uuid"], "trait": "CUSTOM_TRAIT1"},
+ )
+
+
+class TestNodeVIFsMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node VIFs-related API microversion enforcement."""
+
+ min_microversion = "1.28"
+
+ def setUp(self):
+ super(TestNodeVIFsMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+ self.vif_id = uuidutils.generate_uuid()
+
+ @decorators.idempotent_id("9b26c364-0623-4bd4-ac53-a3eca8d58fea")
+ def test_list_node_vifs(self):
+ self._microversion_test(
+ BaremetalClient.vif_list,
+ "1.28",
+ lib_exc.NotFound,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+ @decorators.idempotent_id("4e91a305-145d-408d-9aea-08d73ef8d082")
+ def test_attach_node_vif(self):
+ self._microversion_test(
+ BaremetalClient.vif_attach,
+ "1.28",
+ lib_exc.NotFound,
+ {"node_uuid": self.node["uuid"], "vif_id": self.vif_id},
+ )
+
+ @decorators.idempotent_id("ce93713b-5df8-44ce-815c-c18ae0203c63")
+ def test_detach_node_vif(self):
+ self._microversion_test(
+ BaremetalClient.vif_detach,
+ "1.28",
+ lib_exc.NotFound,
+ {"node_uuid": self.node["uuid"], "vif_id": self.vif_id},
+ )
+
+
+class TestNodeIndicatorsMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node indicators-related API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ def setUp(self):
+ super(TestNodeIndicatorsMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+ self.component = "system"
+ self.ind_ident = "led"
+ self.state = "ON"
+
+ @decorators.idempotent_id("79194e54-c98d-4e55-a368-2a2ba2199844")
+ def test_get_node_indicator_state(self):
+ self._microversion_test(
+ BaremetalClient.get_node_indicator_state,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {
+ "node_uuid": self.node["uuid"],
+ "component": self.component,
+ "ind_ident": self.ind_ident,
+ },
+ )
+
+
+class TestPortgroupMicroversions(BaseTestMicroversionEnforcement,
+ MicroversionTestMixin):
+ """Tests for Portgroup-related API microversion enforcement.
+
+ Portgroup APIs (e.g. listing, creating, showing, updating and deleting)
+ were introduced in microversion 1.23.
+ """
+
+ min_microversion = "1.23"
+
+ def setUp(self):
+ super(TestPortgroupMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis['uuid'])
+
+ @decorators.idempotent_id("a8b52cdf-8471-40d7-9bc0-6ecfefa33adf")
+ def test_list_portgroups(self):
+ self._microversion_test(
+ BaremetalClient.list_portgroups,
+ "1.23",
+ lib_exc.NotFound,
+ {} # Empty dict for no required args
+ )
+
+ @decorators.idempotent_id("cb5b92f0-fdb6-4789-b9b5-ae147cbe288b")
+ def test_list_portgroups_detail(self):
+ self._microversion_test(
+ BaremetalClient.list_portgroups_detail,
+ "1.23",
+ lib_exc.NotFound,
+ {} # Empty dict for no required args
+ )
+
+ @decorators.idempotent_id("aafb5f2d-22e4-4b07-98c6-7f8f5f4a6699")
+ def test_create_portgroup(self):
+ self._microversion_test(
+ BaremetalClient.create_portgroup,
+ "1.23",
+ lib_exc.NotFound,
+ {
+ "node_uuid": self.node['uuid'],
+ "address": "11:11:11:11:11:11",
+ "name": "test_portgroup"
+ }
+ )
+
+ @decorators.idempotent_id("ea27635c-132f-4cff-9f93-b11bd4baa5a2")
+ def test_show_portgroup(self):
+ _, portgroup = self.client.create_portgroup(self.node['uuid'])
+ self._microversion_test(
+ BaremetalClient.show_portgroup,
+ "1.23",
+ lib_exc.NotFound,
+ {"portgroup_ident": portgroup['uuid']}
+ )
+
+ @decorators.idempotent_id("48dfef1f-b95f-4767-86ee-d6257c52cefd")
+ def test_update_portgroup(self):
+ patch_doc = [
+ {"op": "replace", "path": "/address", "value": "22:22:22:22:22:22"}
+ ]
+ # Need to create a portgroup to delete it
+ _, portgroup = self.create_portgroup(self.node['uuid'])
+ self._microversion_test(
+ BaremetalClient.update_portgroup,
+ "1.23",
+ lib_exc.NotFound,
+ {
+ "uuid": portgroup['uuid'],
+ "patch": patch_doc
+ }
+ )
+
+ @decorators.idempotent_id("e0884cbe-bacc-4347-be14-72e475855eff")
+ def test_delete_portgroup(self):
+ # Need to create a portgroup to delete it
+ _, portgroup = self.create_portgroup(self.node['uuid'])
+
+ self._microversion_test(
+ BaremetalClient.delete_portgroup,
+ "1.23",
+ lib_exc.NotFound,
+ {"portgroup_ident": portgroup['uuid']}
+ )
+
+
+class TestPortgroupByNodeMicroversions(BaseTestMicroversionEnforcement,
+ MicroversionTestMixin):
+ """Tests for listing Portgroups by Node.
+
+ These endpoints (e.g. /v1/nodes/{node_ident}/portgroups and
+ /v1/nodes/{node_ident}/portgroups/detail) were introduced
+ in microversion 1.24.
+ """
+
+ min_microversion = "1.24"
+
+ def setUp(self):
+ super(TestPortgroupByNodeMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis['uuid'])
+
+ @decorators.idempotent_id("c967b02f-1acc-4714-98b0-07e49e9b3482")
+ def test_list_portgroups_by_node(self):
+ self._microversion_test(
+ BaremetalClient.list_portgroups_by_node,
+ "1.24",
+ lib_exc.NotFound,
+ {"node_ident": self.node['uuid']}
+ )
+
+ @decorators.idempotent_id("8c38415b-1aad-4519-81fb-0792ebf3731a")
+ def test_list_portgroups_details_by_node(self):
+ self._microversion_test(
+ BaremetalClient.list_portgroups_details_by_node,
+ "1.24",
+ lib_exc.NotFound,
+ {"node_ident": self.node['uuid']}
+ )
+
+
+class TestPortMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for port-related API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ def setUp(self):
+ super(TestPortMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+ random_mac = (
+ f"02:00:00:{random.randint(0, 255):02x}"
+ f":{random.randint(0, 255):02x}"
+ f":{random.randint(0, 255):02x}"
+ )
+
+ _, self.port = self.create_port(
+ self.node["uuid"], address=random_mac
+ )
+
+ @decorators.idempotent_id("99e628a4-0d10-44a1-aed5-0b82d0155103")
+ def test_list_ports(self):
+ self._microversion_test(
+ BaremetalClient.list_ports,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {}, # Empty dict for no required args
+ )
+
+ @decorators.idempotent_id("0c01ce9a-8945-4604-b315-3f8a1856c621")
+ def test_list_ports_detail(self):
+ self._microversion_test(
+ BaremetalClient.list_ports_detail,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {}, # Empty dict for no required args
+ )
+
+ @decorators.idempotent_id("15b3f4d5-d8ca-44af-b0fe-75f9a86896b1")
+ def test_create_port(self):
+ self._microversion_test(
+ BaremetalClient.create_port,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {
+ "node_id": self.node["uuid"],
+ "address": "11:11:11:11:11:11",
+ },
+ )
+
+ @decorators.idempotent_id("637f088f-1460-46c4-a3cf-3568c1997461")
+ def test_show_port(self):
+ self._microversion_test(
+ BaremetalClient.show_port,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": "fake_port"},
+ )
+
+ @decorators.idempotent_id("5cf7930d-3e0b-44f7-b9b8-a941dea2782e")
+ def test_update_port(self):
+ patch_doc = [
+ {"op": "replace", "path": "/address", "value": "22:22:22:22:22:22"}
+ ]
+ self._microversion_test(
+ BaremetalClient.update_port,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": self.port["uuid"], "patch": patch_doc},
+ )
+
+ @decorators.idempotent_id("b5d92c53-9d2e-485f-ac6c-1e0d020d5af3")
+ def test_delete_port(self):
+ self._microversion_test(
+ BaremetalClient.delete_port,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": self.port["uuid"]},
+ )
+
+
+class TestNodePortsMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for listing Ports by Node API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ def setUp(self):
+ super(TestNodePortsMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("7a0acf09-d527-4d77-92bd-3eac81687bc7")
+ def test_list_ports_by_node(self):
+ self._microversion_test(
+ BaremetalClient.list_node_ports,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": self.node["uuid"]},
+ )
+
+ @decorators.idempotent_id("df017231-a8d3-4b6b-8583-0e6fbf61e834")
+ def test_list_ports_details_by_node(self):
+ self._microversion_test(
+ BaremetalClient.list_node_ports,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": self.node["uuid"]},
+ )
+
+
+class TestPortgroupPortsMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for listing Ports by Portgroup API microversion enforcement."""
+
+ min_microversion = "1.24"
+
+ def setUp(self):
+ super(TestPortgroupPortsMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+ _, self.portgroup = self.create_portgroup(self.node["uuid"])
+
+ @decorators.idempotent_id("bb3a1fa6-52cf-4e19-98d0-746c2421ff9a")
+ def test_list_ports_by_portgroup(self):
+ self._microversion_test(
+ BaremetalClient.list_ports,
+ "1.24",
+ lib_exc.NotAcceptable,
+ {"portgroup": self.portgroup["uuid"]},
+ )
+
+ @decorators.idempotent_id("6740fe09-539d-415b-bc44-f38f458f2547")
+ def test_list_ports_details_by_portgroup(self):
+ self._microversion_test(
+ BaremetalClient.list_ports_detail,
+ "1.24",
+ lib_exc.NotAcceptable,
+ {"portgroup": self.portgroup["uuid"]},
+ )
+
+
+class TestVolumeConnectorMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for volume connector-related API microversion enforcement."""
+
+ min_microversion = "1.32"
+
+ def setUp(self):
+ super(TestVolumeConnectorMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("1bde0eb4-c63c-4d05-897d-b2247dd97863")
+ def test_list_volume_connectors(self):
+ self._microversion_test(
+ BaremetalClient.list_volume_connectors,
+ "1.32",
+ lib_exc.NotFound,
+ {},
+ )
+
+ @decorators.idempotent_id("7673b980-b882-45d7-bc89-8e255e2228cc")
+ def test_show_volume_connector(self):
+ self._microversion_test(
+ BaremetalClient.show_volume_connector,
+ "1.32",
+ lib_exc.NotFound,
+ {"volume_connector_ident": "fake_volume_connector"},
+ )
+
+ @decorators.idempotent_id("3e7d6a89-5302-4087-a10a-200b8d9dc026")
+ def test_create_volume_connector(self):
+ self._microversion_test(
+ BaremetalClient.create_volume_connector,
+ "1.32",
+ lib_exc.NotFound,
+ {
+ "node_uuid": self.node["uuid"],
+ "type": "iqn",
+ "connector_id": "fake_connector_id",
+ },
+ )
+
+ @decorators.idempotent_id("cca4b764-12cc-4d4d-a702-0b52ef1b0e94")
+ def test_update_volume_connector(self):
+ patch = [{"op": "replace", "path": "/connector_id", "value": "new_id"}]
+ self._microversion_test(
+ BaremetalClient.update_volume_connector,
+ "1.32",
+ lib_exc.NotFound,
+ {"uuid": "fake_volume_connector", "patch": patch},
+ )
+
+ @decorators.idempotent_id("e05e85d1-da6b-43b6-adc4-5e4bc8e4c72d")
+ def test_delete_volume_connector(self):
+ self._microversion_test(
+ BaremetalClient.delete_volume_connector,
+ "1.32",
+ lib_exc.NotFound,
+ {"volume_connector_ident": "fake_volume_connector"},
+ )
+
+
+class TestVolumeTargetMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for volume target-related API microversion enforcement."""
+
+ min_microversion = "1.32"
+
+ def setUp(self):
+ super(TestVolumeTargetMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("c7511a8a-4d6b-4439-bfb1-c659808c0967")
+ def test_list_volume_targets(self):
+ self._microversion_test(
+ BaremetalClient.list_volume_targets, "1.32", lib_exc.NotFound, {}
+ )
+
+ @decorators.idempotent_id("ddb8223e-2f63-4a74-aa2f-229a522ae3e8")
+ def test_show_volume_target(self):
+ self._microversion_test(
+ BaremetalClient.show_volume_target,
+ "1.32",
+ lib_exc.NotFound,
+ {"volume_target_ident": "fake_volume_target"},
+ )
+
+ @decorators.idempotent_id("3fb1e9c5-8154-4308-a765-393ab80762c8")
+ def test_create_volume_target(self):
+ self._microversion_test(
+ BaremetalClient.create_volume_target,
+ "1.32",
+ lib_exc.NotFound,
+ {
+ "node_uuid": self.node["uuid"],
+ "volume_type": "iscsi",
+ "volume_id": "fake_volume_id",
+ "boot_index": 0,
+ },
+ )
+
+ @decorators.idempotent_id("16d83cd1-0425-4e28-bfd4-33199e5a87c7")
+ def test_update_volume_target(self):
+ patch = [{"op": "replace", "path": "/volume_id", "value": "new_id"}]
+ self._microversion_test(
+ BaremetalClient.update_volume_target,
+ "1.32",
+ lib_exc.NotFound,
+ {"uuid": "fake_volume_target", "patch": patch},
+ )
+
+ @decorators.idempotent_id("697b67be-ccc5-4a7b-99ca-276e5e8c931a")
+ def test_delete_volume_target(self):
+ self._microversion_test(
+ BaremetalClient.delete_volume_target,
+ "1.32",
+ lib_exc.NotFound,
+ {"volume_target_ident": "fake_volume_target"},
+ )
+
+
+class TestNodeVolumeMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node volume-related API microversion enforcement."""
+
+ min_microversion = "1.32"
+
+ def setUp(self):
+ super(TestNodeVolumeMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("2ff700e6-4914-4eed-8342-308a6f2134a5")
+ def test_list_node_volume(self):
+ self._microversion_test(
+ BaremetalClient._list_request,
+ "1.32",
+ lib_exc.NotFound,
+ {"resource": f"nodes/{self.node['uuid']}/volume"},
+ )
+
+ @decorators.idempotent_id("3e56f64f-6bac-4126-ba7d-d89275318975")
+ def test_list_node_volume_connectors(self):
+ self._microversion_test(
+ BaremetalClient._list_request,
+ "1.32",
+ lib_exc.NotFound,
+ {"resource": f"nodes/{self.node['uuid']}/volume/connectors"},
+ )
+
+ @decorators.idempotent_id("060008fe-d478-49d2-bdf0-d327f1e360a5")
+ def test_list_node_volume_targets(self):
+ self._microversion_test(
+ BaremetalClient._list_request,
+ "1.32",
+ lib_exc.NotFound,
+ {"resource": f"nodes/{self.node['uuid']}/volume/targets"},
+ )
+
+
+class TestDriverMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for driver-related API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ @decorators.idempotent_id("c5f179c9-3b8a-49c6-bee0-734979d544bc")
+ def test_list_drivers(self):
+ self._microversion_test(
+ BaremetalClient.list_drivers, "1.1", lib_exc.NotAcceptable, {}
+ )
+
+ @decorators.idempotent_id("f79931be-dc7b-4e60-89ce-90a4e2793714")
+ def test_show_driver(self):
+ self._microversion_test(
+ BaremetalClient.show_driver,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"driver_name": "testdriver"},
+ )
+
+ @decorators.idempotent_id("daef2f42-e343-4640-a5d4-d8938e956ddb")
+ def test_show_driver_properties(self):
+ self._microversion_test(
+ BaremetalClient.get_driver_properties,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"driver_name": "testdriver"},
+ )
+
+ @decorators.idempotent_id("bade310a-0699-4470-8da3-6f8ae26e9d71")
+ def test_show_driver_logical_disk_properties(self):
+ self._microversion_test(
+ BaremetalClient.get_driver_logical_disk_properties,
+ "1.12",
+ lib_exc.NotAcceptable,
+ {"driver_name": "testdriver"},
+ )
+
+
+class TestDriverVendorPassthruMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for driver vendor passthru API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ @decorators.idempotent_id("27c0157d-758a-40c9-b0fd-bde05ed8a6fc")
+ def test_list_vendor_passthru_methods(self):
+ self._microversion_test(
+ BaremetalClient.list_vendor_passthru_methods,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"node_uuid": "fake_node"},
+ )
+
+
+class TestNodeBiosMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node bios-related API microversion enforcement."""
+
+ min_microversion = "1.40"
+
+ def setUp(self):
+ super(TestNodeBiosMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("126de288-71f9-4e4f-8591-6ddfc9ac3095")
+ def test_list_node_bios(self):
+ self._microversion_test(
+ BaremetalClient.list_node_bios_settings,
+ "1.40",
+ lib_exc.NotFound,
+ {"uuid": self.node["uuid"]},
+ )
+
+
+class TestNodeBiosDetailsMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node bios details API microversion enforcement."""
+
+ min_microversion = "1.40"
+
+ def setUp(self):
+ super(TestNodeBiosDetailsMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("68583cd5-63d0-4bda-a836-9bda6d5b2ab4")
+ def test_list_node_bios_details(self):
+ self._microversion_test(
+ BaremetalClient.list_node_bios_settings,
+ "1.40",
+ lib_exc.NotFound,
+ {"uuid": self.node["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']}
+ )
+
+
+class TestConductorMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+
+ min_microversion = "1.49"
+
+ def setUp(self):
+ super(TestConductorMicroversions, self).setUp()
+
+ @decorators.idempotent_id("412fde59-afe7-442d-83c9-538c87b95687")
+ def test_list_conductors(self):
+ self._microversion_test(
+ BaremetalClient.list_conductors, "1.49", lib_exc.NotFound, {}
+ )
+
+ @decorators.idempotent_id("ad496ff6-2d6e-4e63-9c64-60c5184a18a7")
+ def test_show_conductor(self):
+ _, conductors = self.client.list_conductors()
+ self.assertTrue(len(conductors['conductors']) > 0)
+ conductor = conductors['conductors'].pop()
+
+ _, conductor = self.client.show_conductor(conductor['hostname'])
+ self._microversion_test(
+ BaremetalClient.show_conductor,
+ "1.49",
+ lib_exc.NotFound,
+ {"hostname": conductor['hostname']},
+ )
+
+
+class TestAllocationMicroversions(
+ BaseTestMicroversionEnforcement,
+ MicroversionTestMixin):
+ """Tests for allocation-related API microversion enforcement."""
+
+ min_microversion = "1.52"
+
+ @decorators.idempotent_id('ef7bd40a-c1ef-4d40-a0b6-f388d23943c3')
+ 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 TestNodeAllocationMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node allocation-related API microversion enforcement."""
+
+ min_microversion = "1.52"
+
+ def setUp(self):
+ super(TestNodeAllocationMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("9b170cac-18f7-4a17-be2e-314b548244ba")
+ def test_show_allocation_by_node(self):
+ _, body = self.create_allocation(self.resource_class)
+
+ self._microversion_test(
+ BaremetalClient.show_node_allocation,
+ "1.52",
+ lib_exc.NotFound,
+ {"node_ident": body["node_uuid"]},
+ )
+
+
+class TestDeployTemplateMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for deploy template-related API microversion enforcement."""
+
+ min_microversion = "1.55"
+
+ def setUp(self):
+ super(TestDeployTemplateMicroversions, self).setUp()
+ self.deploy_template_name = "CUSTOM_TEMPLATE_123"
+ self.steps = [
+ {
+ "interface": "bios",
+ "step": "apply_configuration",
+ "args": {
+ "settings": [
+ {"name": "LogicalProc", "value": "Enabled"}
+ ]
+ },
+ "priority": 150,
+ }
+ ]
+
+ @decorators.idempotent_id("52806f1d-d64a-4b7f-a799-e623ecc09bda")
+ def test_create_deploy_template(self):
+ self._microversion_test(
+ BaremetalClient.create_deploy_template,
+ "1.55",
+ lib_exc.UnexpectedResponseCode,
+ {"name": self.deploy_template_name, "steps": self.steps},
+ )
+
+ @decorators.idempotent_id("1a3bb543-faa9-4da6-a1c4-20cb8177f53d")
+ def test_list_deploy_templates(self):
+ self._microversion_test(
+ BaremetalClient.list_deploy_templates, "1.55", lib_exc.NotFound, {}
+ )
+
+ @decorators.idempotent_id("a0933551-8569-4638-acd4-fad0a9daefd9")
+ def test_show_deploy_template(self):
+ _, deploy_template = self.client.create_deploy_template(
+ "CUSTOM_TEMPLATE_456", steps=self.steps
+ )
+ self._microversion_test(
+ BaremetalClient.show_deploy_template,
+ "1.55",
+ lib_exc.NotFound,
+ {"deploy_template_ident": deploy_template["uuid"]},
+ )
+
+ @decorators.idempotent_id("f47a12a7-71b7-4ff0-ab17-9f45c9ac68e7")
+ def test_update_deploy_template(self):
+ patch = [{"op": "replace", "path": "/name", "value": "new_name"}]
+ self._microversion_test(
+ BaremetalClient.update_deploy_template,
+ "1.55",
+ lib_exc.UnexpectedResponseCode,
+ {"deploy_template_ident": "fakedt", "patch": patch},
+ )
+
+ @decorators.idempotent_id("b987a887-22ca-4ec9-a074-0a9bd3eac7ac")
+ def test_delete_deploy_template(self):
+ self._microversion_test(
+ BaremetalClient.delete_deploy_template,
+ "1.55",
+ lib_exc.UnexpectedResponseCode,
+ {"deploy_template_ident": "fakedt"},
+ )
+
+
+class TestRunbookMicroversions(BaseTestMicroversionEnforcement,
+ MicroversionTestMixin):
+ """Tests for runbook-related API microversion enforcement.
+
+ The runbook API was introduced in microversion 1.92. Operations such as
+ creating, updating, and deleting runbooks are expected to fail with an
+ error if used with an unsupported microversion.
+ """
+
+ min_microversion = "1.92"
+
+ @decorators.idempotent_id("e37be789-fcdf-435a-8a16-3d2f8f21e6e2")
+ def test_create_runbook(self):
+ # Define a minimal valid runbook payload.
+ steps = [
+ {
+ "interface": "bios",
+ "step": "apply_configuration",
+ "order": 1,
+ "args": {
+ "settings": [
+ {"name": "LogicalProc", "value": "Enabled"}
+ ]
+ }
+ }
+ ]
+ self._microversion_test(
+ BaremetalClient.create_runbook,
+ "1.92",
+ lib_exc.UnexpectedResponseCode,
+ {"name": "CUSTOM_TEST", "steps": steps}
+ )
+
+ @decorators.idempotent_id("f1b5b500-6b9c-41f6-bb99-3a77e3bd6ac4")
+ def test_list_runbooks(self):
+ self._microversion_test(
+ BaremetalClient.list_runbooks,
+ "1.92",
+ lib_exc.NotFound,
+ {}
+ )
+
+ @decorators.idempotent_id("3d12345a-3d56-4fbb-afd2-7bd0ebc32e19")
+ def test_show_runbook(self):
+ steps = [
+ {
+ "interface": "bios",
+ "step": "apply_configuration",
+ "order": 1,
+ "args": {
+ "settings": [
+ {"name": "LogicalProc", "value": "Enabled"}
+ ]
+ }
+ }
+ ]
+ _, runbook = self.client.create_runbook("CUSTOM_TEST_2", steps=steps)
+ self._microversion_test(
+ BaremetalClient.show_runbook,
+ "1.92",
+ lib_exc.NotFound,
+ {"runbook_ident": runbook['uuid']}
+ )
+
+ @decorators.idempotent_id("a1e4cd23-56be-4b39-b756-9a9b73f21492")
+ def test_update_runbook(self):
+ # Using an example JSON PATCH document to update the runbook.
+ patch_doc = [
+ {"op": "replace", "path": "/name", "value": "CUSTOM_TEST2"}
+ ]
+ self._microversion_test(
+ BaremetalClient.update_runbook,
+ "1.92",
+ lib_exc.UnexpectedResponseCode,
+ {"runbook_ident": "fake_runbook", "patch": patch_doc}
+ )
+
+ @decorators.idempotent_id("cb9cf9a6-2a3f-4aa7-9488-098f8aeeb5d9")
+ def test_delete_runbook(self):
+ self._microversion_test(
+ BaremetalClient.delete_runbook,
+ "1.92",
+ lib_exc.UnexpectedResponseCode,
+ {"runbook_ident": "fake_runbook"}
+ )
+
+
+class TestNodeHistoryMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node history-related API microversion enforcement."""
+
+ min_microversion = "1.78"
+
+ def setUp(self):
+ super(TestNodeHistoryMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("aea6cb45-6519-44d9-a33e-aab7cd72d769")
+ def test_list_node_history(self):
+ self._microversion_test(
+ BaremetalClient.list_node_history,
+ "1.78",
+ lib_exc.NotFound,
+ {"node_uuid": self.node["uuid"]},
+ )
+
+
+# NOTE(adamcarthur) - The positive test for this will need a configured
+# node to have history.
+class TestNodeInventoryMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for node inventory-related API microversion enforcement."""
+
+ min_microversion = "1.81"
+
+ def setUp(self):
+ super(TestNodeInventoryMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+
+ @decorators.idempotent_id("ca557dca-9b3e-44cb-a101-f31d58a0dedd")
+ def test_show_node_inventory(self):
+ _, node = self.client.show_node(self.node['uuid'])
+ self._microversion_test(
+ BaremetalClient.show_inventory,
+ "1.81",
+ lib_exc.NotFound,
+ {"uuid": node["uuid"]},
+ ignore_positive=True
+ )
+
+
+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 TestChassisMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for chassis-related API microversion enforcement."""
+
+ min_microversion = "1.1"
+
+ def setUp(self):
+ super(TestChassisMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+
+ @decorators.idempotent_id("59c20dbf-280e-4148-ac8e-be3b8d75d021")
+ def test_create_chassis(self):
+ self._microversion_test(
+ BaremetalClient.create_chassis, "1.1", lib_exc.NotAcceptable, {}
+ )
+
+ @decorators.idempotent_id("0e840e17-5659-4ad9-a218-84b14bc8b013")
+ def test_list_chassis(self):
+ self._microversion_test(
+ BaremetalClient.list_chassis, "1.1", lib_exc.NotAcceptable, {}
+ )
+
+ @decorators.idempotent_id("6c3a5728-0d96-41aa-a3ca-295bfbbe341e")
+ def test_show_chassis(self):
+ self._microversion_test(
+ BaremetalClient.show_chassis,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": self.chassis["uuid"]},
+ )
+
+ @decorators.idempotent_id("7ae8ca94-b7d9-4975-87c6-37d89779b8b5")
+ def test_update_chassis(self):
+ patch = [
+ {"op": "replace", "path": "/description", "value": "new_desc"}
+ ]
+ self._microversion_test(
+ BaremetalClient.update_chassis,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": self.chassis["uuid"], "patch": patch},
+ )
+
+ @decorators.idempotent_id("f6d70a3a-9212-46af-bb7b-c95a3043a722")
+ def test_delete_chassis(self):
+ self._microversion_test(
+ BaremetalClient.delete_chassis,
+ "1.1",
+ lib_exc.NotAcceptable,
+ {"uuid": self.chassis["uuid"]},
+ )
+
+
+class TestUtilityMicroversions(
+ BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+ """Tests for utility-related API microversion enforcement."""
+
+ min_microversion = "1.22"
+
+ def setUp(self):
+ super(TestUtilityMicroversions, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis["uuid"])
+ self.callback_url = "http://example.com/callback"
+ self.agent_version = "1.0"
+ self.agent_token = "test_token"
+
+ @decorators.idempotent_id("dd2b17d9-c847-49bd-acfd-b3d63ae59f2f")
+ def test_ipa_heartbeat(self):
+ self._microversion_test(
+ BaremetalClient.ipa_heartbeat,
+ "1.22",
+ lib_exc.NotFound,
+ {
+ "node_uuid": self.node["uuid"],
+ "callback_url": self.callback_url,
+ "agent_version": self.agent_version,
+ "agent_token": self.agent_token,
+ },
+ )
diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodes.py b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
index 25fa0a3..0d82577 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
@@ -384,7 +384,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')
@@ -392,7 +392,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)
@@ -402,10 +402,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)
@@ -1055,10 +1055,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')
@@ -1108,10 +1107,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_portgroups.py b/ironic_tempest_plugin/tests/api/admin/test_portgroups.py
index 332c791..80724fe 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_portgroups.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_portgroups.py
@@ -21,7 +21,7 @@
class TestPortGroups(base.BaseBaremetalTest):
"""Basic positive test cases for port groups."""
- min_microversion = '1.23'
+ min_microversion = '1.26' # portgroup mode and properties introduced
def setUp(self):
super(TestPortGroups, self).setUp()
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_shards.py b/ironic_tempest_plugin/tests/api/admin/test_shards.py
index ffe6914..5a5e4ba 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
@@ -91,7 +92,7 @@
_, fetched_nodes = self.client.list_nodes(shard=shard)
fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
- self.assertItemsEqual(good_node_ids, fetched_node_ids)
+ self.assertCountEqual(good_node_ids, fetched_node_ids)
@decorators.idempotent_id('6f1e241d-4386-4730-b9ff-28c6a3dcad31')
def test_only_show_multiple_requested_shards(self):
@@ -107,7 +108,7 @@
shard=','.join([shard, second_shard]))
fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
- self.assertItemsEqual(good_node_ids, fetched_node_ids)
+ self.assertCountEqual(good_node_ids, fetched_node_ids)
self.assertNotIn(self.bad_node_id, fetched_node_ids)
self.assertNotIn(self.none_node_id, fetched_node_ids)
@@ -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.assertCountEqual(self.shards, fetched_shards)
diff --git a/ironic_tempest_plugin/tests/api/base.py b/ironic_tempest_plugin/tests/api/base.py
index 11ce859..7744aee 100644
--- a/ironic_tempest_plugin/tests/api/base.py
+++ b/ironic_tempest_plugin/tests/api/base.py
@@ -38,7 +38,8 @@
# 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', 'runbook']
+ 'volume_target', 'chassis', 'deploy_template',
+ 'runbook', 'inspection_rule']
def creates(resource):
@@ -520,11 +521,37 @@
resp, body = cls.client.create_allocation(resource_class, **kwargs)
return resp, body
+ @classmethod
+ @creates('inspection_rule')
+ def create_inspection_rule(cls, rule_uuid, payload):
+ """Wrapper utility for creating Inspection rule.
+
+ :param rule_uuid: UUID of the Inspection rule.
+ :param payload: Inspection rule other fields.
+ :return: Server response.
+ """
+ if rule_uuid is not None:
+ payload['uuid'] = rule_uuid
+
+ resp, body = cls.client.create_inspection_rule(payload)
+
+ return resp, body
+
+ @classmethod
+ def delete_inspection_rule(cls, rule_uuid):
+ """Delete a inspection rules having the specified UUID.
+
+ :param rule_uuid: UUID of the Inspection rule.
+ """
+ resp, body = cls.client.delete_inspection_rule(rule_uuid)
+
+ return resp
+
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/scenario/baremetal_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
index e9090cb..7e93fe0 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):
@@ -136,14 +139,16 @@
instance_id)
@classmethod
- def wait_for_agent_heartbeat(cls, node_id, timeout=None):
+ def wait_for_agent_heartbeat(cls, node_id, timeout=None,
+ abort_on_error_state=True):
ironic_waiters.wait_node_value_in_field(
cls.baremetal_client,
node_id=node_id,
field='driver_internal_info',
value='agent_last_heartbeat',
timeout=timeout or CONF.baremetal.deploywait_timeout,
- interval=10)
+ interval=10,
+ abort_on_error_state=abort_on_error_state)
@classmethod
def get_node(cls, node_id=None, instance_id=None, api_version=None):
@@ -307,3 +312,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 ddb550b..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
@@ -583,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
@@ -719,7 +719,7 @@
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_advanced_ops.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_advanced_ops.py
index 0a7116a..b66309b 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_advanced_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_advanced_ops.py
@@ -27,6 +27,7 @@
driver = 'redfish'
deploy_interface = 'direct'
boot_interface = 'redfish-virtual-media'
+ # To force interface retoggling.
image_ref = CONF.baremetal.whole_disk_image_ref
image_checksum = CONF.baremetal.whole_disk_image_checksum
wholedisk_image = True
@@ -95,7 +96,23 @@
# Get the latest state for the node.
self.node = self.get_node(self.node['uuid'])
+ # This test, as far as I'm remembering after the fact, was developed
+ # in an environment where neutron was the default network interface.
+ # so we must try to set it to properly ensure dhcp-less operation.
prior_prov_net = self.node['driver_info'].get('provisioning_network')
+ try:
+ self.client.update_node(self.node['uuid'],
+ [{'path': '/network_interface',
+ 'op': 'replace',
+ 'value': 'neutron'}])
+ self.addCleanup(self.update_node,
+ self.node['uuid'],
+ [{'op': 'replace',
+ 'path': '/network_interface',
+ 'value': 'flat'}])
+ except Exception:
+ raise self.skipException(
+ "Ironic configuration incorrect to exercise this test.")
ip_version = CONF.validation.ip_version_for_ssh
tenant_cidr = '10.0.6.0/24'
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/test_baremetal_basic_ops.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
index 2a36c6a..e3a9a58 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
@@ -50,6 +50,7 @@
TEST_RESCUE_MODE = False
image_ref = None
wholedisk_image = None
+ auto_lease = False
@classmethod
def skip_checks(cls):
@@ -177,6 +178,9 @@
# CUSTOM_ prefix. Normalise it.
node_resource_class = node['resource_class']
node_resource_class = node_resource_class.upper()
+ node_resource_class = node_resource_class.translate(
+ str.maketrans(" -.:", "____", "!@#$%^&*()+=/\\?<>|\"'")
+ )
node_resource_class = 'CUSTOM_' + node_resource_class
self.assertEqual(resource_class, node_resource_class)
@@ -210,13 +214,25 @@
output = client.exec_command(cmd).rstrip()
self.assertEqual(success_string, output)
+ def validate_lessee(self):
+ iinfo = self.node.get('instance_info')
+ dii = self.node.get('driver_internal_info', {})
+ if 'automatic_lessee' in dii and iinfo:
+ # NOTE(JayF): This item not being in instance_info tells us we
+ # set the lessee.
+ self.assertEqual(iinfo['project_id'], self.node['lessee'])
+
def baremetal_server_ops(self):
self.add_keypair()
self.instance, self.node = self.boot_instance(image_id=self.image_ref)
self.validate_image()
self.validate_ports()
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
@@ -237,20 +253,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 have some sort of connectivity
- # Attempt to ping, if all else fails fall back to an ssh connection
- # which worked previously.
- pinging = self.ping_ip_address(ip_address)
- if not pinging:
- self.get_remote_client(ip_address, server=self.instance)
- else:
- # If we're here, this is successful. If ssh fails above,
- # the job will ultimately fail.
- self.assertTrue(pinging)
-
self.terminate_instance(self.instance)
@decorators.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
@@ -272,6 +274,7 @@
def test_baremetal_server_ops_wholedisk_image(self):
self.image_ref = CONF.baremetal.whole_disk_image_ref
self.wholedisk_image = True
+ self.auto_lease = True
self.baremetal_server_ops()
diff --git a/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
index 086bf21..5bb7df5 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
@@ -44,19 +47,17 @@
if not CONF.baremetal.use_provision_network:
msg = 'Ironic/Neutron tenant isolation is not configured.'
raise cls.skipException(msg)
- if (CONF.baremetal.available_nodes is not None
- and CONF.baremetal.available_nodes < 2):
- msg = ('Not enough baremetal nodes, %d configured, test requires '
- '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 +66,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'
@@ -98,6 +113,7 @@
clients=self.os_primary,
keypair=keypair,
net_id=network['id'],
+ fixed_ip='10.0.100.101',
)
fixed_ip1 = instance1['addresses'][network['name']][0]['addr']
floating_ip1 = self.create_floating_ip(
@@ -112,7 +128,8 @@
clients=self.os_alt,
key_name=alt_keypair['name'],
flavor=CONF.compute.flavor_ref_alt,
- networks=[{'uuid': alt_network['id']}]
+ networks=[{'uuid': alt_network['id'],
+ 'fixed_ip': '10.0.100.102'}],
)
else:
# Create BM
@@ -120,6 +137,7 @@
keypair=alt_keypair,
clients=self.os_alt,
net_id=alt_network['id'],
+ fixed_ip='10.0.100.102',
)
fixed_ip2 = alt_instance['addresses'][alt_network['name']][0]['addr']
alt_floating_ip = self.create_floating_ip(
@@ -165,3 +183,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_baremetal_single_tenant.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_single_tenant.py
index 2138d3e..1be419b 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_single_tenant.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_single_tenant.py
@@ -44,11 +44,6 @@
if not CONF.baremetal.use_provision_network:
msg = 'Ironic/Neutron tenant isolation is not configured.'
raise cls.skipException(msg)
- if (CONF.baremetal.available_nodes is not None
- and CONF.baremetal.available_nodes < 2):
- msg = ('Not enough baremetal nodes, %d configured, test requires '
- 'a minimum of 2') % CONF.baremetal.available_nodes
- raise cls.skipException(msg)
def create_tenant_network(self, clients, tenant_cidr):
network = self.create_network(
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/deprecate-inspector-0de255090cd2cb82.yaml b/releasenotes/notes/deprecate-inspector-0de255090cd2cb82.yaml
new file mode 100644
index 0000000..0607071
--- /dev/null
+++ b/releasenotes/notes/deprecate-inspector-0de255090cd2cb82.yaml
@@ -0,0 +1,10 @@
+---
+deprecations:
+ - |
+ Support for ironic-inspector has been deprecated, because ironic-inspector
+ was already retired. Due to this deprecation, the following options are
+ also deprecated.
+
+ - ``[service_available] ironic_inspector``
+ - ``[enforce_scope] ironic_inspector``
+ - All options in the ``[baremetal_introspection]`` section.
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.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/test-requirements.txt b/test-requirements.txt
index 0ce003e..a080de9 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,2 +1 @@
stestr>=1.0.0 # Apache-2.0
-coverage>=4.0 # Apache-2.0
diff --git a/tox.ini b/tox.ini
index 3a67abc..79892f5 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,25 +14,13 @@
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}
-[testenv:cover]
-setenv =
- {[testenv]setenv}
- PYTHON=coverage run --source ironic_tempest_plugin --parallel-mode
-commands =
- stestr run {posargs}
- coverage combine
- coverage html -d cover
- coverage xml -o cover/coverage.xml
-
[testenv:docs]
deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/doc/requirements.txt
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 4d9da27..c89b63e 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -6,64 +6,30 @@
check:
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:
- 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-2024.2
- - ironic-tempest-functional-rbac-scope-enforced-2024.1
- - ironic-tempest-functional-rbac-scope-enforced-2023.2:
- voting: false
+ - ironic-tempest-functional-python3-2025.2
+ - ironic-tempest-functional-python3-2025.1
+ # NOTE(TheJulia): Consensus in the 2026.1 PTG is that we don't need
+ # to run the anaconda job as heavily as we need to run the normal
+ # tempest jobs.
- ironic-standalone-anaconda
- - ironic-standalone-anaconda-2024.2
- - ironic-standalone-anaconda-2024.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:
- 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
+ - ironic-standalone-redfish-2025.2
+ - ironic-standalone-redfish-2025.1
# 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:
+ - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode:
voting: false
- - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.1:
- voting: false
- - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2023.2:
+ - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2025.2:
voting: false
- ironic-inspector-tempest-discovery:
voting: false
gate:
jobs:
- - ironic-standalone
- - ironic-standalone-2024.2
- - ironic-standalone-2024.1
- ironic-tempest-functional-python3
- - ironic-tempest-functional-python3-2024.2
- - ironic-tempest-functional-python3-2024.1
- - ironic-tempest-functional-rbac-scope-enforced-2024.2
- - ironic-tempest-functional-rbac-scope-enforced-2024.1
+ - ironic-tempest-functional-python3-2025.2
+ - ironic-tempest-functional-python3-2025.1
- 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-inspector-tempest
+ - ironic-standalone-redfish-2025.2
+ - ironic-standalone-redfish-2025.1
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index c75f782..f67694b 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -1,99 +1,39 @@
- job:
- name: ironic-standalone-2024.2
- parent: ironic-standalone
- override-checkout: stable/2024.2
+ name: ironic-standalone-redfish-2025.2
+ parent: ironic-standalone-redfish
+ override-checkout: stable/2025.2
- job:
- name: ironic-standalone-2024.1
- parent: ironic-standalone
- override-checkout: stable/2024.1
+ name: ironic-standalone-redfish-2025.1
+ parent: ironic-standalone-redfish
+ override-checkout: stable/2025.1
- job:
- name: ironic-standalone-2023.2
- parent: ironic-standalone
- override-checkout: stable/2023.2
-
-- job:
- name: ironic-standalone-redfish-2024.2
- parent: ironic-standalone
- override-checkout: stable/2024.2
-
-- job:
- name: ironic-standalone-redfish-2024.1
- parent: ironic-standalone
- override-checkout: stable/2024.1
-
-- job:
- name: ironic-standalone-redfish-2023.2
- parent: ironic-standalone
- override-checkout: stable/2023.2
-
-- job:
- name: ironic-standalone-anaconda-2024.2
+ name: ironic-standalone-anaconda-2025.2
parent: ironic-standalone-anaconda
- override-checkout: stable/2024.2
+ override-checkout: stable/2025.2
- job:
- name: ironic-standalone-anaconda-2024.1
+ name: ironic-standalone-anaconda-2025.1
parent: ironic-standalone-anaconda
- override-checkout: stable/2024.1
+ override-checkout: stable/2025.1
- job:
- name: ironic-standalone-anaconda-2023.2
- parent: ironic-standalone-anaconda
- override-checkout: stable/2023.2
-
-- job:
- name: ironic-tempest-functional-python3-2024.2
+ name: ironic-tempest-functional-python3-2025.2
parent: ironic-tempest-functional-python3
- override-checkout: stable/2024.2
+ override-checkout: stable/2025.2
- job:
- name: ironic-tempest-functional-python3-2024.1
+ name: ironic-tempest-functional-python3-2025.1
parent: ironic-tempest-functional-python3
- override-checkout: stable/2024.1
+ override-checkout: stable/2025.1
- job:
- name: ironic-tempest-functional-python3-2023.2
- parent: ironic-tempest-functional-python3
- override-checkout: stable/2023.2
-
-- job:
- 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
- parent: ironic-tempest-functional-rbac-scope-enforced
- override-checkout: stable/2024.1
-
-- job:
- name: ironic-tempest-functional-rbac-scope-enforced-2023.2
- parent: ironic-tempest-functional-rbac-scope-enforced
- override-checkout: stable/2023.2
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.2
+ name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2025.2
parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/2024.2
+ override-checkout: stable/2025.2
- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.1
+ name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2025.1
parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/2024.1
-
-- job:
- name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2023.2
- parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
- override-checkout: stable/2023.2
-
-- job:
- 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
+ override-checkout: stable/2025.1