Merge "volume: Add 'volume attachment *' commands"
This commit is contained in:
commit
673fec4f64
8
doc/source/cli/command-objects/volume-attachment.rst
Normal file
8
doc/source/cli/command-objects/volume-attachment.rst
Normal file
@ -0,0 +1,8 @@
|
||||
=================
|
||||
volume attachment
|
||||
=================
|
||||
|
||||
Block Storage v3
|
||||
|
||||
.. autoprogram-cliff:: openstack.volume.v3
|
||||
:command: volume attachment *
|
@ -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
|
||||
|
@ -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.
|
||||
|
|
155
openstackclient/tests/unit/volume/v3/fakes.py
Normal file
155
openstackclient/tests/unit/volume/v3/fakes.py
Normal 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)
|
560
openstackclient/tests/unit/volume/v3/test_volume_attachment.py
Normal file
560
openstackclient/tests/unit/volume/v3/test_volume_attachment.py
Normal 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))
|
511
openstackclient/volume/v3/volume_attachment.py
Normal file
511
openstackclient/volume/v3/volume_attachment.py
Normal 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)
|
@ -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.
|
@ -703,6 +703,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
|
||||
|
Loading…
x
Reference in New Issue
Block a user