From 517237553057b1238469bf259644d6bdb596e26e Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 5 Sep 2025 12:14:38 +0000 Subject: [PATCH 1/5] Update .gitreview for stable/2025.2 Change-Id: I054aca28ecb753e624a9e0a04f1cb76fb1e5806a Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/functions --- .gitreview | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitreview b/.gitreview index 130a080b0..c98eec1dc 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/python-ironicclient.git +defaultbranch=stable/2025.2 From d6eca3931f543955969a29f8bbb2eb2a04f787ba Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 5 Sep 2025 12:14:39 +0000 Subject: [PATCH 2/5] Update TOX_CONSTRAINTS_FILE for stable/2025.2 Update the URL to the upper-constraints file to point to the redirect rule on releases.openstack.org so that anyone working on this branch will switch to the correct upper-constraints list automatically when the requirements repository branches. Until the requirements repository has as stable/2025.2 branch, tests will continue to use the upper-constraints list on master. Change-Id: Ia0ae5340687eac3d2dc6d050a40c4c02e80279ed Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/functions --- tox.ini | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index a3687e16b..7cc7142f0 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ setenv = VIRTUAL_ENV={envdir} TESTS_DIR=./ironicclient/tests/unit usedevelop = True deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = @@ -19,7 +19,7 @@ commands = [testenv:releasenotes] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html @@ -30,7 +30,7 @@ deps = flake8-import-order>=0.17.1 # LGPLv3 pycodestyle>=2.0.0,<3.0.0 # MIT Pygments>=2.2.0 # BSD - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} commands = flake8 {posargs} doc8 doc/source CONTRIBUTING.rst README.rst @@ -47,7 +47,7 @@ commands = [testenv:venv] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt @@ -55,7 +55,7 @@ commands = {posargs} [testenv:docs] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html From b2c1dfac697b3270032f0417ff1b38ee516aeda5 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Tue, 13 Jan 2026 12:24:32 -0600 Subject: [PATCH 3/5] feat: add 'vendor' and 'category' for port object Added support for the 'vendor' and 'category' fields for the port object. Change-Id: Id2ec4308b1ab4c9fba538c811af52b32206730f8 Signed-off-by: Doug Goldstein (cherry picked from commit 924107bcd2b1b0676ac81392a2feb3644e37827d) --- ironicclient/osc/v1/baremetal_port.py | 53 +++++++- .../tests/unit/osc/v1/test_baremetal_port.py | 113 ++++++++++++++++++ ironicclient/v1/port.py | 2 +- ironicclient/v1/resource_fields.py | 4 + ...port-vendor-category-4f046b55eec698bf.yaml | 6 + 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-port-vendor-category-4f046b55eec698bf.yaml diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index 632b4befc..cd7b315e8 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -118,6 +118,18 @@ def get_parser(self, prog_name): metavar='', help=_("An optional description for the port.")) + parser.add_argument( + '--vendor', + dest='vendor', + metavar='', + help=_("An optional vendor for the port.")) + + parser.add_argument( + '--category', + dest='category', + metavar='', + help=_("An optional category for the port.")) + return parser def take_action(self, parsed_args): @@ -138,7 +150,8 @@ def take_action(self, parsed_args): field_list = ['address', 'uuid', 'extra', 'node_uuid', 'pxe_enabled', 'local_link_connection', 'portgroup_uuid', - 'physical_network', 'name', 'description'] + 'physical_network', 'name', 'description', 'vendor', + 'category'] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and v is not None) fields = utils.args_array_to_dict(fields, 'extra') @@ -255,6 +268,18 @@ def get_parser(self, prog_name): action='store_true', help=_("Unset the description for this port.")) + parser.add_argument( + '--vendor', + dest='vendor', + action='store_true', + help=_("Unset the vendor for this port.")) + + parser.add_argument( + '--category', + dest='category', + action='store_true', + help=_("Unset the category for this port.")) + return parser def take_action(self, parsed_args): @@ -281,6 +306,12 @@ def take_action(self, parsed_args): if parsed_args.description: properties.extend(utils.args_array_to_patch('remove', ['description'])) + if parsed_args.vendor: + properties.extend(utils.args_array_to_patch('remove', + ['vendor'])) + if parsed_args.category: + properties.extend(utils.args_array_to_patch('remove', + ['category'])) if properties: baremetal_client.port.update(parsed_args.port, properties) @@ -378,6 +409,18 @@ def get_parser(self, prog_name): dest='description', help=_("Set a description for this port")) + parser.add_argument( + '--vendor', + metavar='', + dest='vendor', + help=_("Set a vendor for this port")) + + parser.add_argument( + '--category', + metavar='', + dest='category', + help=_("Set a category for this port")) + return parser def take_action(self, parsed_args): @@ -421,6 +464,14 @@ def take_action(self, parsed_args): port_description = ["description=%s" % parsed_args.description] properties.extend(utils.args_array_to_patch('add', port_description)) + if parsed_args.vendor: + port_vendor = ["vendor=%s" % parsed_args.vendor] + properties.extend(utils.args_array_to_patch('add', + port_vendor)) + if parsed_args.category: + port_category = ["category=%s" % parsed_args.category] + properties.extend(utils.args_array_to_patch('add', + port_category)) if properties: baremetal_client.port.update(parsed_args.port, properties) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py index fa05df128..467bf78cc 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py @@ -326,6 +326,60 @@ def test_baremetal_port_create_description(self): self.baremetal_mock.port.create.assert_called_once_with(**args) + def test_baremetal_port_create_vendor(self): + arglist = [ + baremetal_fakes.baremetal_port_address, + '--node', baremetal_fakes.baremetal_uuid, + '--vendor', 'VendorA' + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('address', baremetal_fakes.baremetal_port_address), + ('vendor', 'VendorA') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + args = { + 'address': baremetal_fakes.baremetal_port_address, + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'vendor': 'VendorA' + } + + self.baremetal_mock.port.create.assert_called_once_with(**args) + + def test_baremetal_port_create_category(self): + arglist = [ + baremetal_fakes.baremetal_port_address, + '--node', baremetal_fakes.baremetal_uuid, + '--category', 'Green' + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('address', baremetal_fakes.baremetal_port_address), + ('category', 'Green') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + args = { + 'address': baremetal_fakes.baremetal_port_address, + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'category': 'Green' + } + + self.baremetal_mock.port.create.assert_called_once_with(**args) + class TestShowBaremetalPort(TestBaremetalPort): def setUp(self): @@ -505,6 +559,30 @@ def test_baremetal_port_unset_description(self): 'port', [{'path': '/description', 'op': 'remove'}]) + def test_baremetal_port_unset_vendor(self): + arglist = ['port', '--vendor'] + verifylist = [('port', 'port'), + ('vendor', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + 'port', + [{'path': '/vendor', 'op': 'remove'}]) + + def test_baremetal_port_unset_category(self): + arglist = ['port', '--category'] + verifylist = [('port', 'port'), + ('category', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + 'port', + [{'path': '/category', 'op': 'remove'}]) + class TestBaremetalPortSet(TestBaremetalPort): def setUp(self): @@ -705,6 +783,38 @@ def test_baremetal_port_set_description(self): [{'path': '/description', 'value': 'Public Network', 'op': 'add'}]) + def test_baremetal_port_set_vendor(self): + arglist = [ + baremetal_fakes.baremetal_port_uuid, + '--vendor', 'VendorA'] + verifylist = [ + ('port', baremetal_fakes.baremetal_port_uuid), + ('vendor', 'VendorA')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + baremetal_fakes.baremetal_port_uuid, + [{'path': '/vendor', 'value': 'VendorA', + 'op': 'add'}]) + + def test_baremetal_port_set_category(self): + arglist = [ + baremetal_fakes.baremetal_port_uuid, + '--category', 'Green'] + verifylist = [ + ('port', baremetal_fakes.baremetal_port_uuid), + ('category', 'Green')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + baremetal_fakes.baremetal_port_uuid, + [{'path': '/category', 'value': 'Green', + 'op': 'add'}]) + class TestBaremetalPortDelete(TestBaremetalPort): def setUp(self): @@ -858,6 +968,7 @@ def test_baremetal_port_list_long(self): self.baremetal_mock.port.list.assert_called_with(**kwargs) collist = ('UUID', 'Address', 'Created At', 'Extra', 'Node UUID', + 'Category', 'Vendor', 'Local Link Connection', 'Portgroup UUID', 'PXE boot enabled', 'Physical Network', 'Updated At', 'Internal Info', 'Is Smart NIC port', @@ -878,6 +989,8 @@ def test_baremetal_port_list_long(self): '', '', '', + '', + '', ), ) self.assertEqual(datalist, tuple(data)) diff --git a/ironicclient/v1/port.py b/ironicclient/v1/port.py index a2b41e898..95f51df8e 100644 --- a/ironicclient/v1/port.py +++ b/ironicclient/v1/port.py @@ -30,7 +30,7 @@ class PortManager(base.CreateManager): _creation_attributes = ['address', 'extra', 'local_link_connection', 'node_uuid', 'physical_network', 'portgroup_uuid', 'pxe_enabled', 'uuid', 'is_smartnic', 'name', - 'description'] + 'description', 'vendor', 'category'] _resource_name = 'ports' def list(self, address=None, limit=None, marker=None, sort_key=None, diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 4854db3ac..348638a17 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -131,6 +131,8 @@ class Resource(object): 'value': 'Value', 'volume_id': 'Volume ID', 'volume_type': 'Driver Volume Type', + 'category': 'Category', + 'vendor': 'Vendor', 'local_link_connection': 'Local Link Connection', 'pxe_enabled': 'PXE boot enabled', 'portgroup_uuid': 'Portgroup UUID', @@ -347,6 +349,8 @@ def sort_labels(self): 'created_at', 'extra', 'node_uuid', + 'category', + 'vendor', 'local_link_connection', 'portgroup_uuid', 'pxe_enabled', diff --git a/releasenotes/notes/add-port-vendor-category-4f046b55eec698bf.yaml b/releasenotes/notes/add-port-vendor-category-4f046b55eec698bf.yaml new file mode 100644 index 000000000..e270b5a5e --- /dev/null +++ b/releasenotes/notes/add-port-vendor-category-4f046b55eec698bf.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds ``vendor`` field and ``category`` field support, which is introduced + in ironic API 1.100 and 1.101 respectively. These field is used to store + informational text about the port for trait based scheduling. From b5125b4d3b84daa881c5e805ca906cb1dc8c7433 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Tue, 13 Jan 2026 13:53:20 -0600 Subject: [PATCH 4/5] fix: report compatibility with 2025.2 release of Ironic This check determines the maximum API version that the client can use. There is no way for the user to override this maximum value. The current API that the client is pinned to is 1.96 but this was the Ironic API for 2025.1. The API for 2025.2 was 1.101. This means that the client released for 2025.2 cannot use any of the API features released with 2025.2 unless this value is changed. The 2025.2 client code includes support for changes up to 1.101 but is unable to do so. The change to the test is for confirming the API clamping behavior of the client. The mock server reported that it supported 1.1 through 1.99. The test requests the maximum version the client supports and then confirms that the server accepted the client's maximum. By changing the client to 1.101 the test fails when the mock server returns back 1.99. The original value of 1.99 was selected many years ago and deemed to be 'very large' but has now proven not large enough. Change-Id: I2d242d64f03e8252d6f6d8c713430c654e6059ad Signed-off-by: Doug Goldstein --- ironicclient/common/http.py | 2 +- ironicclient/tests/unit/common/test_http.py | 2 +- releasenotes/notes/fix-api-version-9542bf9df71f809e.yaml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-api-version-9542bf9df71f809e.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 055af2377..4207594fe 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -36,7 +36,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 96 +LAST_KNOWN_API_VERSION = 101 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index b892f0356..0ee40822f 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -205,7 +205,7 @@ def test_negotiate_version_strict_version_comparison(self, mock_pvh, def test_negotiate_version_server_user_latest( self, mock_pvh, mock_msr, mock_save_data): # have to retry with simple get - mock_pvh.side_effect = iter([(None, None), ('1.1', '1.99')]) + mock_pvh.side_effect = iter([(None, None), ('1.1', '1.999')]) mock_conn = mock.MagicMock() self.test_object.api_version_select_state = 'user' self.test_object.os_ironic_api_version = 'latest' diff --git a/releasenotes/notes/fix-api-version-9542bf9df71f809e.yaml b/releasenotes/notes/fix-api-version-9542bf9df71f809e.yaml new file mode 100644 index 000000000..e6564e634 --- /dev/null +++ b/releasenotes/notes/fix-api-version-9542bf9df71f809e.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Update the Ironic API that the client supports to the 2025.2 release of + Ironic. From c25cb54d5d17f4832a37bd6a1a74cca6262134bc Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Mon, 4 Aug 2025 11:12:14 +0200 Subject: [PATCH 5/5] Fix parallel initial version negotiation If two parallel greenthreads use the same, uninitialized client, there's a race-condition when both enter `negotiate_version()`: there is a request made to Ironic, which hands over execution to the other greenthread. The first greenthread returning from the request changes the state of the client and the second one reads the updated state and thus thinks it's in an error-handling call instead of the initial negotiation - and errors out. We fix this by adding a lock around the initial call to `negotiate_version()`. Change-Id: I9ec03d2bc34017c7670fd6903e5353a8c91e9f17 Closes-Bug: #2119323 Signed-off-by: Johannes Kulik (cherry picked from commit ad1291cc9cb4fc6560a1f82d73a496168a6770c6) --- ironicclient/common/http.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 4207594fe..057952c32 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -19,6 +19,7 @@ import logging import re import textwrap +import threading import time from urllib import parse as urlparse @@ -345,6 +346,7 @@ def __init__(self, _('The Bare Metal API endpoint cannot be detected and was ' 'not provided explicitly')) self.endpoint_trimmed = _trim_endpoint_api_version(endpoint) + self._first_negotiation_lock = threading.Lock() def _parse_version_headers(self, resp): return self._generic_parse_version_headers(resp.headers.get) @@ -369,7 +371,9 @@ def _http_request(self, url, method, **kwargs): # NOTE(TheJulia): self.os_ironic_api_version is reset in # the self.negotiate_version() call if negotiation occurs. if self.os_ironic_api_version and self._must_negotiate_version(): - self.negotiate_version(self.session, None) + with self._first_negotiation_lock: + if self._must_negotiate_version(): + self.negotiate_version(self.session, None) kwargs.setdefault('user_agent', USER_AGENT) kwargs.setdefault('auth', self.auth)