diff --git a/ironicclient/common/base.py b/ironicclient/common/base.py index 376680eaa..54d857f57 100644 --- a/ironicclient/common/base.py +++ b/ironicclient/common/base.py @@ -67,11 +67,13 @@ class Manager(object): """ - def _get(self, resource_id, fields=None): + def _get(self, resource_id, fields=None, os_ironic_api_version=None): """Retrieve a resource. :param resource_id: Identifier of the resource. :param fields: List of specific fields to be returned. + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. :raises exc.ValidationError: For invalid resource_id arg value. """ @@ -85,19 +87,25 @@ class Manager(object): resource_id += ','.join(fields) try: - return self._list(self._path(resource_id))[0] + return self._list( + self._path(resource_id), + os_ironic_api_version=os_ironic_api_version)[0] except IndexError: return None - def _get_as_dict(self, resource_id, fields=None): + def _get_as_dict(self, resource_id, fields=None, + os_ironic_api_version=None): """Retrieve a resource as a dictionary :param resource_id: Identifier of the resource. :param fields: List of specific fields to be returned. + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. :returns: a dictionary representing the resource; may be empty """ - resource = self._get(resource_id, fields=fields) + resource = self._get(resource_id, fields=fields, + os_ironic_api_version=os_ironic_api_version) if resource: return resource.to_dict() else: @@ -118,7 +126,7 @@ class Manager(object): return data def _list_pagination(self, url, response_key=None, obj_class=None, - limit=None): + limit=None, os_ironic_api_version=None): """Retrieve a list of items. The Ironic API is configured to return a maximum number of @@ -134,19 +142,24 @@ class Manager(object): :param obj_class: class for constructing the returned objects. :param limit: maximum number of items to return. If None returns everything. - + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. """ if obj_class is None: obj_class = self.resource_class if limit is not None: limit = int(limit) + kwargs = {} + if os_ironic_api_version is not None: + kwargs['headers'] = {'X-OpenStack-Ironic-API-Version': + os_ironic_api_version} object_list = [] object_count = 0 limit_reached = False while url: - resp, body = self.api.json_request('GET', url) + resp, body = self.api.json_request('GET', url, **kwargs) data = self._format_body_data(body, response_key) for obj in data: object_list.append(obj_class(self, obj, loaded=True)) @@ -170,16 +183,26 @@ class Manager(object): return object_list - def __list(self, url, response_key=None, body=None): - resp, body = self.api.json_request('GET', url) + def __list(self, url, response_key=None, body=None, + os_ironic_api_version=None): + kwargs = {} + + if os_ironic_api_version is not None: + kwargs['headers'] = {'X-OpenStack-Ironic-API-Version': + os_ironic_api_version} + + resp, body = self.api.json_request('GET', url, **kwargs) + data = self._format_body_data(body, response_key) return data - def _list(self, url, response_key=None, obj_class=None, body=None): + def _list(self, url, response_key=None, obj_class=None, body=None, + os_ironic_api_version=None): if obj_class is None: obj_class = self.resource_class - data = self.__list(url, response_key=response_key, body=body) + data = self.__list(url, response_key=response_key, body=body, + os_ironic_api_version=os_ironic_api_version) return [obj_class(self, res, loaded=True) for res in data if res] def _list_primitives(self, url, response_key=None): diff --git a/ironicclient/tests/unit/common/test_base.py b/ironicclient/tests/unit/common/test_base.py index 3575e33ce..0f59ddf31 100644 --- a/ironicclient/tests/unit/common/test_base.py +++ b/ironicclient/tests/unit/common/test_base.py @@ -87,16 +87,16 @@ class TestableManager(base.CreateManager): return ('/v1/testableresources/%s' % id if id else '/v1/testableresources') - def get(self, testable_resource_id, fields=None): + def get(self, testable_resource_id, fields=None, **kwargs): return self._get(resource_id=testable_resource_id, - fields=fields) + fields=fields, **kwargs) - def delete(self, testable_resource_id): - return self._delete(resource_id=testable_resource_id) + def delete(self, testable_resource_id, **kwargs): + return self._delete(resource_id=testable_resource_id, **kwargs) - def update(self, testable_resource_id, patch): + def update(self, testable_resource_id, patch, **kwargs): return self._update(resource_id=testable_resource_id, - patch=patch) + patch=patch, **kwargs) class ManagerTestCase(testtools.TestCase): @@ -120,12 +120,13 @@ class ManagerTestCase(testtools.TestCase): self.manager.create, **INVALID_ATTRIBUTE_TESTABLE_RESOURCE) - def test__get(self): + def test__get_microversion_override(self): resource_id = TESTABLE_RESOURCE['uuid'] - resource = self.manager._get(resource_id) + resource = self.manager._get(resource_id, + os_ironic_api_version='1.22') expect = [ ('GET', '/v1/testableresources/%s' % resource_id, - {}, None), + {'X-OpenStack-Ironic-API-Version': '1.22'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(resource_id, resource.uuid) @@ -147,12 +148,24 @@ class ManagerTestCase(testtools.TestCase): self.assertEqual(expect, self.api.calls) self.assertEqual(TESTABLE_RESOURCE, resource) + def test__get_as_dict_microversion_override(self): + resource_id = TESTABLE_RESOURCE['uuid'] + resource = self.manager._get_as_dict(resource_id, + os_ironic_api_version='1.21') + expect = [ + ('GET', '/v1/testableresources/%s' % resource_id, + {'X-OpenStack-Ironic-API-Version': '1.21'}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(TESTABLE_RESOURCE, resource) + @mock.patch.object(base.Manager, '_get', autospec=True) def test__get_as_dict_empty(self, mock_get): mock_get.return_value = None resource_id = TESTABLE_RESOURCE['uuid'] resource = self.manager._get_as_dict(resource_id) - mock_get.assert_called_once_with(mock.ANY, resource_id, fields=None) + mock_get.assert_called_once_with(mock.ANY, resource_id, fields=None, + os_ironic_api_version=None) self.assertEqual({}, resource) def test_get(self): @@ -165,6 +178,17 @@ class ManagerTestCase(testtools.TestCase): self.assertEqual(TESTABLE_RESOURCE['uuid'], resource.uuid) self.assertEqual(TESTABLE_RESOURCE['attribute1'], resource.attribute1) + def test_get_microversion_override(self): + resource = self.manager.get(TESTABLE_RESOURCE['uuid'], + os_ironic_api_version='1.10') + expect = [ + ('GET', '/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'], + {'X-OpenStack-Ironic-API-Version': '1.10'}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(TESTABLE_RESOURCE['uuid'], resource.uuid) + self.assertEqual(TESTABLE_RESOURCE['attribute1'], resource.attribute1) + def test_update(self): patch = {'op': 'replace', 'value': NEW_ATTRIBUTE_VALUE, @@ -180,6 +204,21 @@ class ManagerTestCase(testtools.TestCase): self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_ATTRIBUTE_VALUE, resource.attribute1) + def test_update_microversion_override(self): + patch = {'op': 'replace', + 'value': NEW_ATTRIBUTE_VALUE, + 'path': '/attribute1'} + resource = self.manager.update( + testable_resource_id=TESTABLE_RESOURCE['uuid'], + patch=patch, os_ironic_api_version='1.9' + ) + expect = [ + ('PATCH', '/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'], + {'X-OpenStack-Ironic-API-Version': '1.9'}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_ATTRIBUTE_VALUE, resource.attribute1) + def test_delete(self): resource = self.manager.delete( testable_resource_id=TESTABLE_RESOURCE['uuid'] diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 87281ee3f..504b27284 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -817,6 +817,16 @@ class NodeManagerTest(testtools.TestCase): self.assertEqual(2, len(nodes)) self.assertEqual(nodes[0].extra, {}) + def test_node_list_detail_microversion_override(self): + nodes = self.mgr.list(detail=True, os_ironic_api_version='1.30') + expect = [ + ('GET', '/v1/nodes/detail', + {'X-OpenStack-Ironic-API-Version': '1.30'}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(nodes)) + self.assertEqual(nodes[0].extra, {}) + def test_node_list_fields(self): nodes = self.mgr.list(fields=['uuid', 'extra']) expect = [ @@ -898,6 +908,19 @@ class NodeManagerTest(testtools.TestCase): self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_DRIVER, node.driver) + def test_update_microversion_override(self): + patch = {'op': 'replace', + 'value': NEW_DRIVER, + 'path': '/driver'} + node = self.mgr.update(node_id=NODE1['uuid'], patch=patch, + os_ironic_api_version='1.24') + expect = [ + ('PATCH', '/v1/nodes/%s' % NODE1['uuid'], + {'X-OpenStack-Ironic-API-Version': '1.24'}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_DRIVER, node.driver) + def test_node_port_list_with_uuid(self): ports = self.mgr.list_ports(NODE1['uuid']) expect = [ diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 90dbeb4e8..a79b2495d 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -58,7 +58,7 @@ class NodeManager(base.CreateManager): def list(self, associated=None, maintenance=None, marker=None, limit=None, detail=False, sort_key=None, sort_dir=None, fields=None, provision_state=None, driver=None, resource_class=None, - chassis=None, fault=None): + chassis=None, fault=None, os_ironic_api_version=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string @@ -107,6 +107,8 @@ class NodeManager(base.CreateManager): :param fault: Optional. String value to get only nodes with specified fault. + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. :returns: A list of nodes. @@ -142,10 +144,12 @@ class NodeManager(base.CreateManager): path += '?' + '&'.join(filters) if limit is None: - return self._list(self._path(path), "nodes") + return self._list(self._path(path), "nodes", + os_ironic_api_version=os_ironic_api_version) else: - return self._list_pagination(self._path(path), "nodes", - limit=limit) + return self._list_pagination( + self._path(path), "nodes", limit=limit, + os_ironic_api_version=os_ironic_api_version) def list_ports(self, node_id, marker=None, limit=None, sort_key=None, sort_dir=None, detail=False, fields=None): @@ -314,8 +318,9 @@ class NodeManager(base.CreateManager): self._path(path), response_key="targets", limit=limit, obj_class=volume_target.VolumeTarget) - def get(self, node_id, fields=None): - return self._get(resource_id=node_id, fields=fields) + def get(self, node_id, fields=None, os_ironic_api_version=None): + return self._get(resource_id=node_id, fields=fields, + os_ironic_api_version=os_ironic_api_version) def get_by_instance_uuid(self, instance_uuid, fields=None): path = '?instance_uuid=%s' % instance_uuid diff --git a/releasenotes/notes/version-overrides-4e9ba1266a238c6a.yaml b/releasenotes/notes/version-overrides-4e9ba1266a238c6a.yaml new file mode 100644 index 000000000..721c43ec8 --- /dev/null +++ b/releasenotes/notes/version-overrides-4e9ba1266a238c6a.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds support for ``NodeManager.set_provision_state``, + ``NodeManager.update``, ``NodeManager.get``, and ``NodeManager.list`` + to accept an ``os_ironic_api_version`` keyword argument to override + the API version for that specific call to the REST API. + + When overridden, the API version is not preserved, and if an unsupported + version is requested from the remote API, an ``UnsupportedVersion`` + exception is raised.