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

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.