diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 55043c433..350757cd8 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -69,6 +69,14 @@ Volume Connector Operations create_volume_connector, update_volume_connector, patch_volume_connector, delete_volume_connector +Volume Target Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: volume_targets, find_volume_target, get_volume_target, + create_volume_target, update_volume_target, + patch_volume_target, delete_volume_target + Utilities --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index eccda6908..7ccb9a292 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -11,3 +11,4 @@ Baremetal Resources v1/port_group v1/allocation v1/volume_connector + v1/volume_target diff --git a/doc/source/user/resources/baremetal/v1/volume_target.rst b/doc/source/user/resources/baremetal/v1/volume_target.rst new file mode 100644 index 000000000..93525a80f --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/volume_target.rst @@ -0,0 +1,13 @@ +openstack.baremetal.v1.volume_target +======================================= + +.. automodule:: openstack.baremetal.v1.volume_target + +The VolumeTarget Class +------------------------- + +The ``VolumeTarget`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.volume_target.VolumeTarget + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 76d144f2a..356d8ee40 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -18,6 +18,7 @@ from openstack.baremetal.v1 import node as _node from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup from openstack.baremetal.v1 import volume_connector as _volumeconnector +from openstack.baremetal.v1 import volume_target as _volumetarget from openstack import exceptions from openstack import proxy from openstack import utils @@ -231,7 +232,7 @@ class Proxy(proxy.Proxy): provided as the ``sort_key``. * ``sort_key``: Sorts the response by the this attribute value. Default is ``id``. You can specify multiple pairs of sort - key and sort direction query parameters. If you omit the + key and sort direction query pa rameters. If you omit the sort direction in a pair, the API uses the natural sorting direction of the server attribute that is provided as the ``sort_key``. @@ -1166,3 +1167,145 @@ class Proxy(proxy.Proxy): """ return self._delete(_volumeconnector.VolumeConnector, volume_connector, ignore_missing=ignore_missing) + + def volume_targets(self, details=False, **query): + """Retrieve a generator of volume_target. + + :param details: A boolean indicating whether the detailed information + for every volume_target should be returned. + :param dict query: Optional query parameters to be sent to restrict + the volume_targets returned. Available parameters include: + + * ``fields``: A list containing one or more fields to be returned + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. + * ``limit``: Requests at most the specified number of + volume_connector be returned from the query. + * ``marker``: Specifies the ID of the last-seen volume_target. + Use the ``limit`` parameter to make an initial limited request + and use the ID of the last-seen volume_target from the + response as the ``marker`` value in subsequent limited request. + * ``node``:only return the ones associated with this specific node + (name or UUID), or an empty set if not found. + * ``sort_dir``:Sorts the response by the requested sort direction. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. + * ``sort_key``: Sorts the response by the this attribute value. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. + + :returns: A generator of volume_target instances. + """ + if details: + query['detail'] = True + return _volumetarget.VolumeTarget.list(self, **query) + + def create_volume_target(self, **attrs): + """Create a new volume_target from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget`. + + :returns: The results of volume_target creation. + :rtype::class: + `~openstack.baremetal.v1.volume_target.VolumeTarget`. + """ + return self._create(_volumetarget.VolumeTarget, **attrs) + + def find_volume_target(self, vt_id, ignore_missing=True): + """Find a single volume target. + + :param str vt_id: The ID of a volume target. + + :param bool ignore_missing: When set to ``False``, an exception of + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume connector does not exist. When set to `True``, + None will be returned when attempting to find a nonexistent + volume target. + :returns: One :class: + `~openstack.baremetal.v1.volumetarget.VolumeTarget` + object or None. + """ + return self._find(_volumetarget.VolumeTarget, vt_id, + ignore_missing=ignore_missing) + + def get_volume_target(self, volume_target, fields=None): + """Get a specific volume_target. + + :param volume_target: The value can be the ID of a + volume_target or a :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget + instance.` + :param fields: Limit the resource fields to fetch.` + + :returns: One + :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + volume_target matching the name or ID could be found.` + """ + return self._get_with_fields(_volumetarget.VolumeTarget, + volume_target, + fields=fields) + + def update_volume_target(self, volume_target, **attrs): + """Update a volume_target. + + :param volume_target:Either the ID of a volume_target + or an instance of + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget.` + :param dict attrs: The attributes to update on the + volume_target represented by the ``volume_target`` parameter.` + + :returns: The updated volume_target. + :rtype::class: + `~openstack.baremetal.v1.volume_target.VolumeTarget.` + """ + return self._update(_volumetarget.VolumeTarget, + volume_target, **attrs) + + def patch_volume_target(self, volume_target, patch): + """Apply a JSON patch to the volume_target. + + :param volume_target: The value can be the ID of a + volume_target or a :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget` + instance. + :param patch: JSON patch to apply. + + :returns: The updated volume_target. + :rtype::class: + `~openstack.baremetal.v1.volume_target.VolumeTarget.` + """ + return self._get_resource(_volumetarget.VolumeTarget, + volume_target).patch(self, patch) + + def delete_volume_target(self, volume_target, + ignore_missing=True): + """Delete an volume_target. + + :param volume_target: The value can be either the ID of a + volume_target.VolumeTarget or a + :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget` + instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume_target could not be found. + When set to ``True``, no exception will be raised when + attempting to delete a non-existent volume_target. + + :returns: The instance of the volume_target which was deleted. + :rtype::class: + `~openstack.baremetal.v1.volume_target.VolumeTarget`. + """ + return self._delete(_volumetarget.VolumeTarget, + volume_target, ignore_missing=ignore_missing) diff --git a/openstack/baremetal/v1/volume_target.py b/openstack/baremetal/v1/volume_target.py new file mode 100644 index 000000000..a5762a479 --- /dev/null +++ b/openstack/baremetal/v1/volume_target.py @@ -0,0 +1,60 @@ +# 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 openstack.baremetal.v1 import _common +from openstack import resource + + +class VolumeTarget(_common.ListMixin, resource.Resource): + + resources_key = 'targets' + base_path = '/volume/targets' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'node', 'detail', + fields={'type': _common.fields_type}, + ) + + # Volume Targets is available since 1.32 + _max_microversion = '1.32' + + #: The boot index of the Volume target. “0” indicates that this volume is + # used as a boot volume. + boot_index = resource.Body('boot_index') + #: Timestamp at which the port was created. + created_at = resource.Body('created_at') + #: A set of one or more arbitrary metadata key and value pairs. + extra = resource.Body('extra') + #: A list of relative links. Includes the self and bookmark links. + links = resource.Body('links', type=list) + #: The UUID of the Node this resource belongs to. + node_id = resource.Body('node_uuid') + #: A set of physical information of the volume. + properties = resource.Body('properties') + #: Timestamp at which the port was last updated. + updated_at = resource.Body('updated_at') + #: The UUID of the resource. + id = resource.Body('uuid', alternate_id=True) + #: The identifier of the volume. + volume_id = resource.Body('volume_id') + #: The type of Volume target. + volume_type = resource.Body('volume_type') diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index bc0c4cafc..8dc00044d 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -73,3 +73,14 @@ class BaseBaremetalTest(base.BaseFunctionalTest): self.conn.baremetal.delete_volume_connector(volume_connector.id, ignore_missing=True)) return volume_connector + + def create_volume_target(self, node_id=None, **kwargs): + node_id = node_id or self.node_id + volume_target = self.conn.baremetal.create_volume_target( + node_uuid=node_id, **kwargs) + + self.addCleanup( + lambda: + self.conn.baremetal.delete_volume_target(volume_target.id, + ignore_missing=True)) + return volume_target diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py new file mode 100644 index 000000000..7c65e2619 --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py @@ -0,0 +1,179 @@ +# 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 openstack import exceptions +from openstack.tests.functional.baremetal import base + + +class TestBareMetalVolumetarget(base.BaseBaremetalTest): + + min_microversion = '1.32' + + def setUp(self): + super(TestBareMetalVolumetarget, self).setUp() + self.node = self.create_node(provision_state='enroll') + + def test_volume_target_create_get_delete(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_target = self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-de4335ac56d2', + volume_type='iscsi') + + loaded = self.conn.baremetal.get_volume_target( + volume_target.id) + self.assertEqual(loaded.id, volume_target.id) + self.assertIsNotNone(loaded.node_id) + + with_fields = self.conn.baremetal.get_volume_target( + volume_target.id, fields=['uuid', 'extra']) + self.assertEqual(volume_target.id, with_fields.id) + self.assertIsNone(with_fields.node_id) + + self.conn.baremetal.delete_volume_target(volume_target, + ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_target, + volume_target.id) + + def test_volume_target_list(self): + node2 = self.create_node(name='test-node') + self.conn.baremetal.set_node_provision_state( + node2, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(node2, 'power off') + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + vt1 = self.create_volume_target( + boot_index=0, + volume_id='bd4d008c-7d31-463d-abf9-6c23d9d55f7f', + node_id=node2.id, + volume_type='iscsi') + vt2 = self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-de4335ac57c2', + node_id=self.node.id, + volume_type='iscsi') + + vts = self.conn.baremetal.volume_targets( + node=self.node.id) + self.assertEqual([v.id for v in vts], [vt2.id]) + + vts = self.conn.baremetal.volume_targets(node=node2.id) + self.assertEqual([v.id for v in vts], [vt1.id]) + + vts = self.conn.baremetal.volume_targets(node='test-node') + self.assertEqual([v.id for v in vts], [vt1.id]) + + vts_with_details = self.conn.baremetal.volume_targets(details=True) + for i in vts_with_details: + self.assertIsNotNone(i.id) + self.assertIsNotNone(i.volume_type) + + vts_with_fields = self.conn.baremetal.volume_targets( + fields=['uuid', 'node_uuid']) + for i in vts_with_fields: + self.assertIsNotNone(i.id) + self.assertIsNone(i.volume_type) + self.assertIsNotNone(i.node_id) + + def test_volume_target_list_update_delete(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-de4335ac57h3', + node_id=self.node.id, + volume_type='iscsi', + extra={'foo': 'bar'}) + volume_target = next(self.conn.baremetal.volume_targets( + details=True, + node=self.node.id)) + self.assertEqual(volume_target.extra, {'foo': 'bar'}) + + # This test checks that resources returned from listing are usable + self.conn.baremetal.update_volume_target(volume_target, + extra={'foo': 42}) + self.conn.baremetal.delete_volume_target(volume_target, + ignore_missing=False) + + def test_volume_target_update(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_target = self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-de4335ac53h7', + node_id=self.node.id, + volume_type='isci') + volume_target.extra = {'answer': 42} + + volume_target = self.conn.baremetal.update_volume_target( + volume_target) + self.assertEqual({'answer': 42}, volume_target.extra) + + volume_target = self.conn.baremetal.get_volume_target( + volume_target.id) + self.assertEqual({'answer': 42}, volume_target.extra) + + def test_volume_target_patch(self): + vol_targ_id = '04452bed-5367-4202-9cg6-de4335ac53h7' + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_target = self.create_volume_target( + boot_index=0, + volume_id=vol_targ_id, + node_id=self.node.id, + volume_type='isci') + + volume_target = self.conn.baremetal.patch_volume_target( + volume_target, dict(path='/extra/answer', op='add', value=42)) + self.assertEqual({'answer': 42}, volume_target.extra) + self.assertEqual(vol_targ_id, + volume_target.volume_id) + + volume_target = self.conn.baremetal.get_volume_target( + volume_target.id) + self.assertEqual({'answer': 42}, volume_target.extra) + + def test_volume_target_negative_non_existing(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_target, uuid) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.find_volume_target, uuid, + ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.delete_volume_target, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.find_volume_target(uuid)) + self.assertIsNone(self.conn.baremetal.delete_volume_target(uuid)) + + def test_volume_target_fields(self): + self.create_node() + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-99ae634d8971', + node_id=self.node.id, + volume_type='iscsi') + result = self.conn.baremetal.volume_targets( + fields=['uuid', 'node_id']) + for item in result: + self.assertIsNotNone(item.id) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 3b7597672..f9e8d53a6 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -20,6 +20,7 @@ from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port from openstack.baremetal.v1 import port_group from openstack.baremetal.v1 import volume_connector +from openstack.baremetal.v1 import volume_target from openstack import exceptions from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base @@ -205,6 +206,42 @@ class TestBaremetalProxy(test_proxy_base.TestProxyBase): volume_connector.VolumeConnector, True) + @mock.patch.object(volume_target.VolumeTarget, 'list') + def test_volume_target_detailed(self, mock_list): + result = self.proxy.volume_targets(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, detail=True, query=1) + + @mock.patch.object(volume_target.VolumeTarget, 'list') + def test_volume_target_not_detailed(self, mock_list): + result = self.proxy.volume_targets(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, query=1) + + def test_create_volume_target(self): + self.verify_create(self.proxy.create_volume_target, + volume_target.VolumeTarget) + + def test_find_volume_target(self): + self.verify_find(self.proxy.find_volume_target, + volume_target.VolumeTarget) + + def test_get_volume_target(self): + self.verify_get(self.proxy.get_volume_target, + volume_target.VolumeTarget, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}) + + def test_delete_volume_target(self): + self.verify_delete(self.proxy.delete_volume_target, + volume_target.VolumeTarget, + False) + + def test_delete_volume_target_ignore(self): + self.verify_delete(self.proxy.delete_volume_target, + volume_target.VolumeTarget, + True) + @mock.patch.object(node.Node, 'fetch', autospec=True) def test__get_with_fields_none(self, mock_fetch): result = self.proxy._get_with_fields(node.Node, 'value') diff --git a/openstack/tests/unit/baremetal/v1/test_volume_target.py b/openstack/tests/unit/baremetal/v1/test_volume_target.py new file mode 100644 index 000000000..4598858a3 --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_volume_target.py @@ -0,0 +1,65 @@ +# 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 openstack.tests.unit import base + +from openstack.baremetal.v1 import volume_target + +FAKE = { + "boot_index": 0, + "created_at": "2016-08-18T22:28:48.643434+11:11", + "extra": {}, + "links": [ + { + "href": "http://127.0.0.1:6385/v1/volume/targets/", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/volume/targets/", + "rel": "bookmark" + } + ], + "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", + "properties": {}, + "updated_at": None, + "uuid": "bd4d008c-7d31-463d-abf9-6c23d9d55f7f", + "volume_id": "04452bed-5367-4202-8bf5-de4335ac56d2", + "volume_type": "iscsi" +} + + +class TestVolumeTarget(base.TestCase): + + def test_basic(self): + sot = volume_target.VolumeTarget() + self.assertIsNone(sot.resource_key) + self.assertEqual('targets', sot.resources_key) + self.assertEqual('/volume/targets', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + def test_instantiate(self): + sot = volume_target.VolumeTarget(**FAKE) + self.assertEqual(FAKE['boot_index'], sot.boot_index) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['extra'], sot.extra) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['node_uuid'], sot.node_id) + self.assertEqual(FAKE['properties'], sot.properties) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) + self.assertEqual(FAKE['volume_id'], sot.volume_id) + self.assertEqual(FAKE['volume_type'], sot.volume_type) diff --git a/releasenotes/notes/ironic-volume_target-support-8130361804366787.yaml b/releasenotes/notes/ironic-volume_target-support-8130361804366787.yaml new file mode 100644 index 000000000..eed88c50b --- /dev/null +++ b/releasenotes/notes/ironic-volume_target-support-8130361804366787.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Support for Ironic Volume Target API.