Volume Target support for Ironic on OpenStack SDK

This patch adds support for Ironic Volume Target API.

Change-Id: Ic1e080cfc2c6439fddbb41bc6015abcb59291667
Story: #2008169
Task: #40924
This commit is contained in:
MartaLais 2020-09-21 13:39:35 -03:00
parent 256e25e321
commit 06db9e37fa
10 changed files with 522 additions and 1 deletions

View File

@ -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
---------

View File

@ -11,3 +11,4 @@ Baremetal Resources
v1/port_group
v1/allocation
v1/volume_connector
v1/volume_target

View File

@ -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:

View File

@ -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
@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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/<ID>",
"rel": "self"
},
{
"href": "http://127.0.0.1:6385/volume/targets/<ID>",
"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)

View File

@ -0,0 +1,4 @@
---
features:
- |
Support for Ironic Volume Target API.