volume: Add 'volume attachment *' commands

These mirror the 'cinder attachment-*' commands, with arguments copied
across essentially verbatim. The only significant departure is the
replacement of "tenant" terminology with "project".

  volume attachment create
  volume attachment delete
  volume attachment list
  volume attachment complete
  volume attachment set
  volume attachment show

Full support for filtering is deferred for now since that's a more
complicated change that requires additional commands be added first.
TODOs are included to this effect.

Change-Id: If47c2b56fe65ee2cee07c000d6ae3688d5ef3b42
Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
Stephen Finucane 2021-05-24 15:56:27 +01:00
parent 0f28588e48
commit 6dc94e1fb8
8 changed files with 1259 additions and 9 deletions

View File

@ -0,0 +1,8 @@
=================
volume attachment
=================
Block Storage v3
.. autoprogram-cliff:: openstack.volume.v3
:command: volume attachment *

View File

@ -153,11 +153,12 @@ referring to both Compute and Volume quotas.
* ``user``: (**Identity**) individual cloud resources users
* ``user role``: (**Identity**) roles assigned to a user
* ``volume``: (**Volume**) block volumes
* ``volume attachment``: (**Volume**) an attachment of a volumes to a server
* ``volume backup``: (**Volume**) backup for volumes
* ``volume backend capability``: (**volume**) volume backend storage capabilities
* ``volume backend pool``: (**volume**) volume backend storage pools
* ``volume backend capability``: (**Volume**) volume backend storage capabilities
* ``volume backend pool``: (**Volume**) volume backend storage pools
* ``volume backup record``: (**Volume**) volume record that can be imported or exported
* ``volume backend``: (**volume**) volume backend storage
* ``volume backend``: (**Volume**) volume backend storage
* ``volume host``: (**Volume**) the physical computer for volumes
* ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes
* ``volume snapshot``: (**Volume**) a point-in-time copy of a volume

View File

@ -1,12 +1,12 @@
absolute-limits,limits show --absolute,Lists absolute limits for a user.
api-version,WONTFIX,Display the server API version information.
availability-zone-list,availability zone list --volume,Lists all availability zones.
attachment-complete,,Complete an attachment for a cinder volume. (Supported by API versions 3.44 - 3.latest)
attachment-create,,Create an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
attachment-delete,,Delete an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
attachment-list,,Lists all attachments. (Supported by API versions 3.27 - 3.latest)
attachment-show,,Show detailed information for attachment. (Supported by API versions 3.27 - 3.latest)
attachment-update,,Update an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
attachment-complete,volume attachment complete,Complete an attachment for a cinder volume. (Supported by API versions 3.44 - 3.latest)
attachment-create,volume attachment create,Create an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
attachment-delete,volume attachment delete,Delete an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
attachment-list,volume attachment list,Lists all attachments. (Supported by API versions 3.27 - 3.latest)
attachment-show,volume attachment show,Show detailed information for attachment. (Supported by API versions 3.27 - 3.latest)
attachment-update,volume attachment update,Update an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
backup-create,volume backup create,Creates a volume backup.
backup-delete,volume backup delete,Removes a backup.
backup-export,volume backup record export,Export backup metadata record.

1 absolute-limits limits show --absolute Lists absolute limits for a user.
2 api-version WONTFIX Display the server API version information.
3 availability-zone-list availability zone list --volume Lists all availability zones.
4 attachment-complete volume attachment complete Complete an attachment for a cinder volume. (Supported by API versions 3.44 - 3.latest)
5 attachment-create volume attachment create Create an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
6 attachment-delete volume attachment delete Delete an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
7 attachment-list volume attachment list Lists all attachments. (Supported by API versions 3.27 - 3.latest)
8 attachment-show volume attachment show Show detailed information for attachment. (Supported by API versions 3.27 - 3.latest)
9 attachment-update volume attachment update Update an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest)
10 backup-create volume backup create Creates a volume backup.
11 backup-delete volume backup delete Removes a backup.
12 backup-export volume backup record export Export backup metadata record.

View File

@ -0,0 +1,155 @@
# 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.
import random
from unittest import mock
import uuid
from cinderclient import api_versions
from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
from openstackclient.tests.unit import fakes
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
from openstackclient.tests.unit import utils
from openstackclient.tests.unit.volume.v2 import fakes as volume_v2_fakes
class FakeVolumeClient(object):
def __init__(self, **kwargs):
self.auth_token = kwargs['token']
self.management_url = kwargs['endpoint']
self.api_version = api_versions.APIVersion('3.0')
self.attachments = mock.Mock()
self.attachments.resource_class = fakes.FakeResource(None, {})
self.volumes = mock.Mock()
self.volumes.resource_class = fakes.FakeResource(None, {})
class TestVolume(utils.TestCommand):
def setUp(self):
super().setUp()
self.app.client_manager.volume = FakeVolumeClient(
endpoint=fakes.AUTH_URL,
token=fakes.AUTH_TOKEN
)
self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client(
endpoint=fakes.AUTH_URL,
token=fakes.AUTH_TOKEN
)
self.app.client_manager.compute = compute_fakes.FakeComputev2Client(
endpoint=fakes.AUTH_URL,
token=fakes.AUTH_TOKEN,
)
# TODO(stephenfin): Check if the responses are actually the same
FakeVolume = volume_v2_fakes.FakeVolume
class FakeVolumeAttachment:
"""Fake one or more volume attachments."""
@staticmethod
def create_one_volume_attachment(attrs=None):
"""Create a fake volume attachment.
:param attrs: A dictionary with all attributes of volume attachment
:return: A FakeResource object with id, status, etc.
"""
attrs = attrs or {}
attachment_id = uuid.uuid4().hex
volume_id = attrs.pop('volume_id', None) or uuid.uuid4().hex
server_id = attrs.pop('instance', None) or uuid.uuid4().hex
# Set default attribute
attachment_info = {
'id': attachment_id,
'volume_id': volume_id,
'instance': server_id,
'status': random.choice([
'attached',
'attaching',
'detached',
'reserved',
'error_attaching',
'error_detaching',
'deleted',
]),
'attach_mode': random.choice(['ro', 'rw']),
'attached_at': '2015-09-16T09:28:52.000000',
'detached_at': None,
'connection_info': {
'access_mode': 'rw',
'attachment_id': attachment_id,
'auth_method': 'CHAP',
'auth_password': 'AcUZ8PpxLHwzypMC',
'auth_username': '7j3EZQWT3rbE6pcSGKvK',
'cacheable': False,
'driver_volume_type': 'iscsi',
'encrypted': False,
'qos_specs': None,
'target_discovered': False,
'target_iqn':
f'iqn.2010-10.org.openstack:volume-{attachment_id}',
'target_lun': '1',
'target_portal': '192.168.122.170:3260',
'volume_id': volume_id,
},
}
# Overwrite default attributes if there are some attributes set
attachment_info.update(attrs)
attachment = fakes.FakeResource(
None,
attachment_info,
loaded=True)
return attachment
@staticmethod
def create_volume_attachments(attrs=None, count=2):
"""Create multiple fake volume attachments.
:param attrs: A dictionary with all attributes of volume attachment
:param count: The number of volume attachments to be faked
:return: A list of FakeResource objects
"""
attachments = []
for n in range(0, count):
attachments.append(
FakeVolumeAttachment.create_one_volume_attachment(attrs))
return attachments
@staticmethod
def get_volume_attachments(attachments=None, count=2):
"""Get an iterable MagicMock object with a list of faked volumes.
If attachments list is provided, then initialize the Mock object with
the list. Otherwise create one.
:param attachments: A list of FakeResource objects faking volume
attachments
:param count: The number of volume attachments to be faked
:return An iterable Mock object with side_effect set to a list of faked
volume attachments
"""
if attachments is None:
attachments = FakeVolumeAttachment.create_volume_attachments(count)
return mock.Mock(side_effect=attachments)

View File

@ -0,0 +1,560 @@
# 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 cinderclient import api_versions
from osc_lib.cli import format_columns
from osc_lib import exceptions
from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes
from openstackclient.volume.v3 import volume_attachment
class TestVolumeAttachment(volume_fakes.TestVolume):
def setUp(self):
super().setUp()
self.volumes_mock = self.app.client_manager.volume.volumes
self.volumes_mock.reset_mock()
self.volume_attachments_mock = \
self.app.client_manager.volume.attachments
self.volume_attachments_mock.reset_mock()
self.projects_mock = self.app.client_manager.identity.projects
self.projects_mock.reset_mock()
self.servers_mock = self.app.client_manager.compute.servers
self.servers_mock.reset_mock()
class TestVolumeAttachmentCreate(TestVolumeAttachment):
volume = volume_fakes.FakeVolume.create_one_volume()
server = compute_fakes.FakeServer.create_one_server()
volume_attachment = \
volume_fakes.FakeVolumeAttachment.create_one_volume_attachment(
attrs={'instance': server.id, 'volume_id': volume.id})
columns = (
'ID',
'Volume ID',
'Instance ID',
'Status',
'Attach Mode',
'Attached At',
'Detached At',
'Properties',
)
data = (
volume_attachment.id,
volume_attachment.volume_id,
volume_attachment.instance,
volume_attachment.status,
volume_attachment.attach_mode,
volume_attachment.attached_at,
volume_attachment.detached_at,
format_columns.DictColumn(volume_attachment.connection_info),
)
def setUp(self):
super().setUp()
self.volumes_mock.get.return_value = self.volume
self.servers_mock.get.return_value = self.server
self.volume_attachments_mock.create.return_value = \
self.volume_attachment
self.cmd = volume_attachment.CreateVolumeAttachment(self.app, None)
def test_volume_attachment_create(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.27')
arglist = [
self.volume.id,
self.server.id,
]
verifylist = [
('volume', self.volume.id),
('server', self.server.id),
('connect', False),
('initiator', None),
('ip', None),
('host', None),
('platform', None),
('os_type', None),
('multipath', False),
('mountpoint', None),
('mode', None),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.volumes_mock.get.assert_called_once_with(self.volume.id)
self.servers_mock.get.assert_called_once_with(self.server.id)
self.volume_attachments_mock.create.assert_called_once_with(
self.volume.id, {}, self.server.id, None,
)
self.assertEqual(self.columns, columns)
self.assertCountEqual(self.data, data)
def test_volume_attachment_create_with_connect(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.54')
arglist = [
self.volume.id,
self.server.id,
'--connect',
'--initiator', 'iqn.1993-08.org.debian:01:cad181614cec',
'--ip', '192.168.1.20',
'--host', 'my-host',
'--platform', 'x86_64',
'--os-type', 'linux2',
'--multipath',
'--mountpoint', '/dev/vdb',
'--mode', 'null',
]
verifylist = [
('volume', self.volume.id),
('server', self.server.id),
('connect', True),
('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'),
('ip', '192.168.1.20'),
('host', 'my-host'),
('platform', 'x86_64'),
('os_type', 'linux2'),
('multipath', True),
('mountpoint', '/dev/vdb'),
('mode', 'null'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
connect_info = dict([
('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'),
('ip', '192.168.1.20'),
('host', 'my-host'),
('platform', 'x86_64'),
('os_type', 'linux2'),
('multipath', True),
('mountpoint', '/dev/vdb'),
])
self.volumes_mock.get.assert_called_once_with(self.volume.id)
self.servers_mock.get.assert_called_once_with(self.server.id)
self.volume_attachments_mock.create.assert_called_once_with(
self.volume.id, connect_info, self.server.id, 'null',
)
self.assertEqual(self.columns, columns)
self.assertCountEqual(self.data, data)
def test_volume_attachment_create_pre_v327(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.26')
arglist = [
self.volume.id,
self.server.id,
]
verifylist = [
('volume', self.volume.id),
('server', self.server.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args)
self.assertIn(
'--os-volume-api-version 3.27 or greater is required',
str(exc))
def test_volume_attachment_create_with_mode_pre_v354(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.53')
arglist = [
self.volume.id,
self.server.id,
'--mode', 'rw',
]
verifylist = [
('volume', self.volume.id),
('server', self.server.id),
('mode', 'rw'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args)
self.assertIn(
'--os-volume-api-version 3.54 or greater is required',
str(exc))
def test_volume_attachment_create_with_connect_missing_arg(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.54')
arglist = [
self.volume.id,
self.server.id,
'--initiator', 'iqn.1993-08.org.debian:01:cad181614cec',
]
verifylist = [
('volume', self.volume.id),
('server', self.server.id),
('connect', False),
('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args)
self.assertIn(
'You must specify the --connect option for any',
str(exc))
class TestVolumeAttachmentDelete(TestVolumeAttachment):
volume_attachment = \
volume_fakes.FakeVolumeAttachment.create_one_volume_attachment()
def setUp(self):
super().setUp()
self.volume_attachments_mock.delete.return_value = None
self.cmd = volume_attachment.DeleteVolumeAttachment(self.app, None)
def test_volume_attachment_delete(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.27')
arglist = [
self.volume_attachment.id,
]
verifylist = [
('attachment', self.volume_attachment.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.volume_attachments_mock.delete.assert_called_once_with(
self.volume_attachment.id,
)
self.assertIsNone(result)
def test_volume_attachment_delete_pre_v327(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.26')
arglist = [
self.volume_attachment.id,
]
verifylist = [
('attachment', self.volume_attachment.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args)
self.assertIn(
'--os-volume-api-version 3.27 or greater is required',
str(exc))
class TestVolumeAttachmentSet(TestVolumeAttachment):
volume_attachment = \
volume_fakes.FakeVolumeAttachment.create_one_volume_attachment()
columns = (
'ID',
'Volume ID',
'Instance ID',
'Status',
'Attach Mode',
'Attached At',
'Detached At',
'Properties',
)
data = (
volume_attachment.id,
volume_attachment.volume_id,
volume_attachment.instance,
volume_attachment.status,
volume_attachment.attach_mode,
volume_attachment.attached_at,
volume_attachment.detached_at,
format_columns.DictColumn(volume_attachment.connection_info),
)
def setUp(self):
super().setUp()
self.volume_attachments_mock.update.return_value = \
self.volume_attachment
self.cmd = volume_attachment.SetVolumeAttachment(self.app, None)
def test_volume_attachment_set(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.27')
arglist = [
self.volume_attachment.id,
'--initiator', 'iqn.1993-08.org.debian:01:cad181614cec',
'--ip', '192.168.1.20',
'--host', 'my-host',
'--platform', 'x86_64',
'--os-type', 'linux2',
'--multipath',
'--mountpoint', '/dev/vdb',
]
verifylist = [
('attachment', self.volume_attachment.id),
('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'),
('ip', '192.168.1.20'),
('host', 'my-host'),
('platform', 'x86_64'),
('os_type', 'linux2'),
('multipath', True),
('mountpoint', '/dev/vdb'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
connect_info = dict([
('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'),
('ip', '192.168.1.20'),
('host', 'my-host'),
('platform', 'x86_64'),
('os_type', 'linux2'),
('multipath', True),
('mountpoint', '/dev/vdb'),
])
self.volume_attachments_mock.update.assert_called_once_with(
self.volume_attachment.id, connect_info,
)
self.assertEqual(self.columns, columns)
self.assertCountEqual(self.data, data)
def test_volume_attachment_set_pre_v327(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.26')
arglist = [
self.volume_attachment.id,
'--initiator', 'iqn.1993-08.org.debian:01:cad181614cec',
]
verifylist = [
('attachment', self.volume_attachment.id),
('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args)
self.assertIn(
'--os-volume-api-version 3.27 or greater is required',
str(exc))
class TestVolumeAttachmentComplete(TestVolumeAttachment):
volume_attachment = \
volume_fakes.FakeVolumeAttachment.create_one_volume_attachment()
def setUp(self):
super().setUp()
self.volume_attachments_mock.complete.return_value = None
self.cmd = volume_attachment.CompleteVolumeAttachment(self.app, None)
def test_volume_attachment_complete(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.44')
arglist = [
self.volume_attachment.id,
]
verifylist = [
('attachment', self.volume_attachment.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.volume_attachments_mock.complete.assert_called_once_with(
self.volume_attachment.id,
)
self.assertIsNone(result)
def test_volume_attachment_complete_pre_v344(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.43')
arglist = [
self.volume_attachment.id,
]
verifylist = [
('attachment', self.volume_attachment.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args)
self.assertIn(
'--os-volume-api-version 3.44 or greater is required',
str(exc))
class TestVolumeAttachmentList(TestVolumeAttachment):
project = identity_fakes.FakeProject.create_one_project()
volume_attachments = \
volume_fakes.FakeVolumeAttachment.create_volume_attachments()
columns = (
'ID',
'Volume ID',
'Server ID',
'Status',
)
data = [
(
volume_attachment.id,
volume_attachment.volume_id,
volume_attachment.instance,
volume_attachment.status,
) for volume_attachment in volume_attachments
]
def setUp(self):
super().setUp()
self.projects_mock.get.return_value = self.project
self.volume_attachments_mock.list.return_value = \
self.volume_attachments
self.cmd = volume_attachment.ListVolumeAttachment(self.app, None)
def test_volume_attachment_list(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.27')
arglist = []
verifylist = [
('project', None),
('all_projects', False),
('volume_id', None),
('status', None),
('marker', None),
('limit', None),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.volume_attachments_mock.list.assert_called_once_with(
search_opts={
'all_tenants': False,
'project_id': None,
'status': None,
'volume_id': None,
},
marker=None,
limit=None,
)
self.assertEqual(self.columns, columns)
self.assertCountEqual(tuple(self.data), data)
def test_volume_attachment_list_with_options(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.27')
arglist = [
'--project', self.project.name,
'--volume-id', 'volume-id',
'--status', 'attached',
'--marker', 'volume-attachment-id',
'--limit', '2',
]
verifylist = [
('project', self.project.name),
('all_projects', False),
('volume_id', 'volume-id'),
('status', 'attached'),
('marker', 'volume-attachment-id'),
('limit', 2),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.volume_attachments_mock.list.assert_called_once_with(
search_opts={
'all_tenants': True,
'project_id': self.project.id,
'status': 'attached',
'volume_id': 'volume-id',
},
marker='volume-attachment-id',
limit=2,
)
self.assertEqual(self.columns, columns)
self.assertCountEqual(tuple(self.data), data)
def test_volume_attachment_list_pre_v327(self):
self.app.client_manager.volume.api_version = \
api_versions.APIVersion('3.26')
arglist = []
verifylist = [
('project', None),
('all_projects', False),
('volume_id', None),
('status', None),
('marker', None),
('limit', None),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args)
self.assertIn(
'--os-volume-api-version 3.27 or greater is required',
str(exc))

View File

@ -0,0 +1,511 @@
# 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.
import logging
from cinderclient import api_versions
from osc_lib.cli import format_columns
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
from openstackclient.i18n import _
from openstackclient.identity import common as identity_common
LOG = logging.getLogger(__name__)
_FILTER_DEPRECATED = _(
"This option is deprecated. Consider using the '--filters' option which "
"was introduced in microversion 3.33 instead."
)
def _format_attachment(attachment):
columns = (
'id',
'volume_id',
'instance',
'status',
'attach_mode',
'attached_at',
'detached_at',
'connection_info',
)
column_headers = (
'ID',
'Volume ID',
'Instance ID',
'Status',
'Attach Mode',
'Attached At',
'Detached At',
'Properties',
)
# TODO(stephenfin): Improve output with the nested connection_info
# field - cinderclient printed two things but that's equally ugly
return (
column_headers,
utils.get_item_properties(
attachment,
columns,
formatters={
'connection_info': format_columns.DictColumn,
},
),
)
class CreateVolumeAttachment(command.ShowOne):
"""Create an attachment for a volume.
This command will only create a volume attachment in the Volume service. It
will not invoke the necessary Compute service actions to actually attach
the volume to the server at the hypervisor level. As a result, it should
typically only be used for troubleshooting issues with an existing server
in combination with other tooling. For all other use cases, the 'server
volume add' command should be preferred.
"""
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
'volume',
metavar='<volume>',
help=_('Name or ID of volume to attach to server.'),
)
parser.add_argument(
'server',
metavar='<server>',
help=_('Name or ID of server to attach volume to.'),
)
parser.add_argument(
'--connect',
action='store_true',
dest='connect',
default=False,
help=_('Make an active connection using provided connector info'),
)
parser.add_argument(
'--no-connect',
action='store_false',
dest='connect',
help=_(
'Do not make an active connection using provided connector '
'info'
),
)
parser.add_argument(
'--initiator',
metavar='<initiator>',
help=_('IQN of the initiator attaching to'),
)
parser.add_argument(
'--ip',
metavar='<ip>',
help=_('IP of the system attaching to'),
)
parser.add_argument(
'--host',
metavar='<host>',
help=_('Name of the host attaching to'),
)
parser.add_argument(
'--platform',
metavar='<platform>',
help=_('Platform type'),
)
parser.add_argument(
'--os-type',
metavar='<ostype>',
help=_('OS type'),
)
parser.add_argument(
'--multipath',
action='store_true',
dest='multipath',
default=False,
help=_('Use multipath'),
)
parser.add_argument(
'--no-multipath',
action='store_false',
dest='multipath',
help=_('Use multipath'),
)
parser.add_argument(
'--mountpoint',
metavar='<mountpoint>',
help=_('Mountpoint volume will be attached at'),
)
parser.add_argument(
'--mode',
metavar='<mode>',
help=_(
'Mode of volume attachment, rw, ro and null, where null '
'indicates we want to honor any existing admin-metadata '
'settings '
'(supported by --os-volume-api-version 3.54 or later)'
),
)
return parser
def take_action(self, parsed_args):
volume_client = self.app.client_manager.volume
compute_client = self.app.client_manager.compute
if volume_client.api_version < api_versions.APIVersion('3.27'):
msg = _(
"--os-volume-api-version 3.27 or greater is required to "
"support the 'volume attachment create' command"
)
raise exceptions.CommandError(msg)
if parsed_args.mode:
if volume_client.api_version < api_versions.APIVersion('3.54'):
msg = _(
"--os-volume-api-version 3.54 or greater is required to "
"support the '--mode' option"
)
raise exceptions.CommandError(msg)
connector = {}
if parsed_args.connect:
connector = {
'initiator': parsed_args.initiator,
'ip': parsed_args.ip,
'platform': parsed_args.platform,
'host': parsed_args.host,
'os_type': parsed_args.os_type,
'multipath': parsed_args.multipath,
'mountpoint': parsed_args.mountpoint,
}
else:
if any({
parsed_args.initiator,
parsed_args.ip,
parsed_args.platform,
parsed_args.host,
parsed_args.host,
parsed_args.multipath,
parsed_args.mountpoint,
}):
msg = _(
'You must specify the --connect option for any of the '
'connection-specific options such as --initiator to be '
'valid'
)
raise exceptions.CommandError(msg)
volume = utils.find_resource(
volume_client.volumes,
parsed_args.volume,
)
server = utils.find_resource(
compute_client.servers,
parsed_args.server,
)
attachment = volume_client.attachments.create(
volume.id, connector, server.id, parsed_args.mode)
return _format_attachment(attachment)
class DeleteVolumeAttachment(command.Command):
"""Delete an attachment for a volume.
Similarly to the 'volume attachment create' command, this command will only
delete the volume attachment record in the Volume service. It will not
invoke the necessary Compute service actions to actually attach the volume
to the server at the hypervisor level. As a result, it should typically
only be used for troubleshooting issues with an existing server in
combination with other tooling. For all other use cases, the 'server volume
remove' command should be preferred.
"""
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
'attachment',
metavar='<attachment>',
help=_('ID of volume attachment to delete'),
)
return parser
def take_action(self, parsed_args):
volume_client = self.app.client_manager.volume
if volume_client.api_version < api_versions.APIVersion('3.27'):
msg = _(
"--os-volume-api-version 3.27 or greater is required to "
"support the 'volume attachment delete' command"
)
raise exceptions.CommandError(msg)
volume_client.attachments.delete(parsed_args.attachment)
class SetVolumeAttachment(command.ShowOne):
"""Update an attachment for a volume.
This call is designed to be more of an volume attachment completion than
anything else. It expects the value of a connector object to notify the
driver that the volume is going to be connected and where it's being
connected to.
"""
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
'attachment',
metavar='<attachment>',
help=_('ID of volume attachment.'),
)
parser.add_argument(
'--initiator',
metavar='<initiator>',
help=_('IQN of the initiator attaching to'),
)
parser.add_argument(
'--ip',
metavar='<ip>',
help=_('IP of the system attaching to'),
)
parser.add_argument(
'--host',
metavar='<host>',
help=_('Name of the host attaching to'),
)
parser.add_argument(
'--platform',
metavar='<platform>',
help=_('Platform type'),
)
parser.add_argument(
'--os-type',
metavar='<ostype>',
help=_('OS type'),
)
parser.add_argument(
'--multipath',
action='store_true',
dest='multipath',
default=False,
help=_('Use multipath'),
)
parser.add_argument(
'--no-multipath',
action='store_false',
dest='multipath',
help=_('Use multipath'),
)
parser.add_argument(
'--mountpoint',
metavar='<mountpoint>',
help=_('Mountpoint volume will be attached at'),
)
return parser
def take_action(self, parsed_args):
volume_client = self.app.client_manager.volume
if volume_client.api_version < api_versions.APIVersion('3.27'):
msg = _(
"--os-volume-api-version 3.27 or greater is required to "
"support the 'volume attachment set' command"
)
raise exceptions.CommandError(msg)
connector = {
'initiator': parsed_args.initiator,
'ip': parsed_args.ip,
'platform': parsed_args.platform,
'host': parsed_args.host,
'os_type': parsed_args.os_type,
'multipath': parsed_args.multipath,
'mountpoint': parsed_args.mountpoint,
}
attachment = volume_client.attachments.update(
parsed_args.attachment, connector)
return _format_attachment(attachment)
class CompleteVolumeAttachment(command.Command):
"""Complete an attachment for a volume."""
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
'attachment',
metavar='<attachment>',
help=_('ID of volume attachment to mark as completed'),
)
return parser
def take_action(self, parsed_args):
volume_client = self.app.client_manager.volume
if volume_client.api_version < api_versions.APIVersion('3.44'):
msg = _(
"--os-volume-api-version 3.44 or greater is required to "
"support the 'volume attachment complete' command"
)
raise exceptions.CommandError(msg)
volume_client.attachments.complete(parsed_args.attachment)
class ListVolumeAttachment(command.Lister):
"""Lists all volume attachments."""
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
'--project',
dest='project',
metavar='<project>',
help=_('Filter results by project (name or ID) (admin only)'),
)
identity_common.add_project_domain_option_to_parser(parser)
parser.add_argument(
'--all-projects',
dest='all_projects',
action='store_true',
default=utils.env('ALL_PROJECTS', default=False),
help=_('Shows details for all projects (admin only).'),
)
parser.add_argument(
'--volume-id',
metavar='<volume-id>',
default=None,
help=_('Filters results by a volume ID. ') + _FILTER_DEPRECATED,
)
parser.add_argument(
'--status',
metavar='<status>',
help=_('Filters results by a status. ') + _FILTER_DEPRECATED,
)
parser.add_argument(
'--marker',
metavar='<marker>',
help=_(
'Begin returning volume attachments that appear later in '
'volume attachment list than that represented by this ID.'
),
)
parser.add_argument(
'--limit',
type=int,
metavar='<limit>',
help=_('Maximum number of volume attachments to return.'),
)
# TODO(stephenfin): Add once we have an equivalent command for
# 'cinder list-filters'
# parser.add_argument(
# '--filter',
# metavar='<key=value>',
# action=parseractions.KeyValueAction,
# dest='filters',
# help=_(
# "Filter key and value pairs. Use 'foo' to "
# "check enabled filters from server. Use 'key~=value' for "
# "inexact filtering if the key supports "
# "(supported by --os-volume-api-version 3.33 or above)"
# ),
# )
return parser
def take_action(self, parsed_args):
volume_client = self.app.client_manager.volume
identity_client = self.app.client_manager.identity
if volume_client.api_version < api_versions.APIVersion('3.27'):
msg = _(
"--os-volume-api-version 3.27 or greater is required to "
"support the 'volume attachment list' command"
)
raise exceptions.CommandError(msg)
project_id = None
if parsed_args.project:
project_id = identity_common.find_project(
identity_client,
parsed_args.project,
parsed_args.project_domain,
).id
search_opts = {
'all_tenants': True if project_id else parsed_args.all_projects,
'project_id': project_id,
'status': parsed_args.status,
'volume_id': parsed_args.volume_id,
}
# Update search option with `filters`
# if AppendFilters.filters:
# search_opts.update(shell_utils.extract_filters(AppendFilters.filters))
# TODO(stephenfin): Implement sorting
attachments = volume_client.attachments.list(
search_opts=search_opts,
marker=parsed_args.marker,
limit=parsed_args.limit)
column_headers = (
'ID',
'Volume ID',
'Server ID',
'Status',
)
columns = (
'id',
'volume_id',
'instance',
'status',
)
return (
column_headers,
(
utils.get_item_properties(a, columns)
for a in attachments
),
)
class ShowVolumeAttachment(command.ShowOne):
"""Show detailed information for a volume attachment."""
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
'attachment',
metavar='<attachment>',
help=_('ID of volume attachment.'),
)
return parser
def take_action(self, parsed_args):
volume_client = self.app.client_manager.volume
if volume_client.api_version < api_versions.APIVersion('3.27'):
msg = _(
"--os-volume-api-version 3.27 or greater is required to "
"support the 'volume attachment show' command"
)
raise exceptions.CommandError(msg)
attachment = volume_client.attachments.show(parsed_args.attachment)
return _format_attachment(attachment)

View File

@ -0,0 +1,8 @@
---
features:
- |
Add ``volume attachment create``, ``volume attachment delete``,
``volume attachment list``, ``volume attachment complete``,
``volume attachment set`` and ``volume attachment show`` commands to
create, delete, list, complete, update and show volume attachments,
respectively.

View File

@ -697,6 +697,13 @@ openstack.volume.v3 =
volume_show = openstackclient.volume.v2.volume:ShowVolume
volume_unset = openstackclient.volume.v2.volume:UnsetVolume
volume_attachment_create = openstackclient.volume.v3.volume_attachment:CreateVolumeAttachment
volume_attachment_delete = openstackclient.volume.v3.volume_attachment:DeleteVolumeAttachment
volume_attachment_list = openstackclient.volume.v3.volume_attachment:ListVolumeAttachment
volume_attachment_complete = openstackclient.volume.v3.volume_attachment:CompleteVolumeAttachment
volume_attachment_set = openstackclient.volume.v3.volume_attachment:SetVolumeAttachment
volume_attachment_show = openstackclient.volume.v3.volume_attachment:ShowVolumeAttachment
volume_backup_create = openstackclient.volume.v2.volume_backup:CreateVolumeBackup
volume_backup_delete = openstackclient.volume.v2.volume_backup:DeleteVolumeBackup
volume_backup_list = openstackclient.volume.v2.volume_backup:ListVolumeBackup