diff --git a/.gitreview b/.gitreview index 130a080b..c98eec1d 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/python-ironicclient.git +defaultbranch=stable/2025.2 diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 055af237..057952c3 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 @@ -36,7 +37,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__) @@ -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) diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index 632b4bef..cd7b315e 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/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index b892f035..0ee40822 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/ironicclient/tests/unit/osc/v1/test_baremetal_port.py b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py index fa05df12..467bf78c 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 a2b41e89..95f51df8 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 4854db3a..348638a1 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 00000000..e270b5a5 --- /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. diff --git a/releasenotes/notes/fix-api-version-9542bf9df71f809e.yaml b/releasenotes/notes/fix-api-version-9542bf9df71f809e.yaml new file mode 100644 index 00000000..e6564e63 --- /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. diff --git a/tox.ini b/tox.ini index a3687e16..7cc7142f 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