volume: Add 'volume group *' commands
These mirror the 'cinder group-*' commands, with arguments copied across essentially verbatim. The only significant departures are the replacement of "tenant" terminology with "project" and the merging of the various volume group replication action commands into the parent volume group (e.g. 'openstack volume group set --enable-replication' instead of 'cinder group enable-replication') volume group create volume group delete volume group list volume group show volume group set volume group failover Change-Id: I3b2c0cb92b8a53cc1c0cefa3313b80f59c9e5835 Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
parent
5faa9ef805
commit
4c2e8523a9
23
doc/source/cli/command-objects/volume-group.rst
Normal file
23
doc/source/cli/command-objects/volume-group.rst
Normal file
@ -0,0 +1,23 @@
|
||||
============
|
||||
volume group
|
||||
============
|
||||
|
||||
Block Storage v3
|
||||
|
||||
.. autoprogram-cliff:: openstack.volume.v3
|
||||
:command: volume group create
|
||||
|
||||
.. autoprogram-cliff:: openstack.volume.v3
|
||||
:command: volume group delete
|
||||
|
||||
.. autoprogram-cliff:: openstack.volume.v3
|
||||
:command: volume group list
|
||||
|
||||
.. autoprogram-cliff:: openstack.volume.v3
|
||||
:command: volume group failover
|
||||
|
||||
.. autoprogram-cliff:: openstack.volume.v3
|
||||
:command: volume group set
|
||||
|
||||
.. autoprogram-cliff:: openstack.volume.v3
|
||||
:command: volume group show
|
@ -159,6 +159,7 @@ referring to both Compute and Volume quotas.
|
||||
* ``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 group``: (**Volume**) group of volumes
|
||||
* ``volume host``: (**Volume**) the physical computer for volumes
|
||||
* ``volume message``: (**Volume**) volume API internal messages detailing volume failure messages
|
||||
* ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes
|
||||
|
@ -44,15 +44,15 @@ force-delete,volume delete --force,"Attempts force-delete of volume regardless o
|
||||
freeze-host,volume host set --disable,Freeze and disable the specified cinder-volume host.
|
||||
get-capabilities,volume backend capability show,Show capabilities of a volume backend. Admin only.
|
||||
get-pools,volume backend pool list,Show pool information for backends. Admin only.
|
||||
group-create,,Creates a group. (Supported by API versions 3.13 - 3.latest)
|
||||
group-create,volume group create,Creates a group. (Supported by API versions 3.13 - 3.latest)
|
||||
group-create-from-src,,Creates a group from a group snapshot or a source group. (Supported by API versions 3.14 - 3.latest)
|
||||
group-delete,,Removes one or more groups. (Supported by API versions 3.13 - 3.latest)
|
||||
group-disable-replication,,Disables replication for group. (Supported by API versions 3.38 - 3.latest)
|
||||
group-enable-replication,,Enables replication for group. (Supported by API versions 3.38 - 3.latest)
|
||||
group-failover-replication,,Fails over replication for group. (Supported by API versions 3.38 - 3.latest)
|
||||
group-list,,Lists all groups. (Supported by API versions 3.13 - 3.latest)
|
||||
group-list-replication-targets,,Lists replication targets for group. (Supported by API versions 3.38 - 3.latest)
|
||||
group-show,,Shows details of a group. (Supported by API versions 3.13 - 3.latest)
|
||||
group-delete,volume group delete,Removes one or more groups. (Supported by API versions 3.13 - 3.latest)
|
||||
group-disable-replication,volume group set --disable-replication,Disables replication for group. (Supported by API versions 3.38 - 3.latest)
|
||||
group-enable-replication,volume group set --enable-replication,Enables replication for group. (Supported by API versions 3.38 - 3.latest)
|
||||
group-failover-replication,volume group failover,Fails over replication for group. (Supported by API versions 3.38 - 3.latest)
|
||||
group-list,volume group list,Lists all groups. (Supported by API versions 3.13 - 3.latest)
|
||||
group-list-replication-targets,volume group list --replication-targets,Lists replication targets for group. (Supported by API versions 3.38 - 3.latest)
|
||||
group-show,volume group show,Shows details of a group. (Supported by API versions 3.13 - 3.latest)
|
||||
group-snapshot-create,,Creates a group snapshot. (Supported by API versions 3.14 - 3.latest)
|
||||
group-snapshot-delete,,Removes one or more group snapshots. (Supported by API versions 3.14 - 3.latest)
|
||||
group-snapshot-list,,Lists all group snapshots. (Supported by API versions 3.14 - 3.latest)
|
||||
@ -65,7 +65,7 @@ group-type-key,,Sets or unsets group_spec for a group type. (Supported by API ve
|
||||
group-type-list,,Lists available 'group types'. (Admin only will see private types) (Supported by API versions 3.11 - 3.latest)
|
||||
group-type-show,,Show group type details. (Supported by API versions 3.11 - 3.latest)
|
||||
group-type-update,,Updates group type name description and/or is_public. (Supported by API versions 3.11 - 3.latest)
|
||||
group-update,,Updates a group. (Supported by API versions 3.13 - 3.latest)
|
||||
group-update,volume group set,Updates a group. (Supported by API versions 3.13 - 3.latest)
|
||||
image-metadata,volume set --image-property,Sets or deletes volume image metadata.
|
||||
image-metadata-show,volume show,Shows volume image metadata.
|
||||
list,volume list,Lists all volumes.
|
||||
|
|
@ -32,10 +32,16 @@ class FakeVolumeClient(object):
|
||||
|
||||
self.attachments = mock.Mock()
|
||||
self.attachments.resource_class = fakes.FakeResource(None, {})
|
||||
self.groups = mock.Mock()
|
||||
self.groups.resource_class = fakes.FakeResource(None, {})
|
||||
self.group_types = mock.Mock()
|
||||
self.group_types.resource_class = fakes.FakeResource(None, {})
|
||||
self.messages = mock.Mock()
|
||||
self.messages.resource_class = fakes.FakeResource(None, {})
|
||||
self.volumes = mock.Mock()
|
||||
self.volumes.resource_class = fakes.FakeResource(None, {})
|
||||
self.volume_types = mock.Mock()
|
||||
self.volume_types.resource_class = fakes.FakeResource(None, {})
|
||||
|
||||
|
||||
class TestVolume(utils.TestCommand):
|
||||
@ -59,6 +65,111 @@ class TestVolume(utils.TestCommand):
|
||||
|
||||
# TODO(stephenfin): Check if the responses are actually the same
|
||||
FakeVolume = volume_v2_fakes.FakeVolume
|
||||
FakeVolumeType = volume_v2_fakes.FakeVolumeType
|
||||
|
||||
|
||||
class FakeVolumeGroup:
|
||||
"""Fake one or more volume groups."""
|
||||
|
||||
@staticmethod
|
||||
def create_one_volume_group(attrs=None):
|
||||
"""Create a fake group.
|
||||
|
||||
:param attrs: A dictionary with all attributes of group
|
||||
:return: A FakeResource object with id, name, status, etc.
|
||||
"""
|
||||
attrs = attrs or {}
|
||||
|
||||
group_type = attrs.pop('group_type', None) or uuid.uuid4().hex
|
||||
volume_types = attrs.pop('volume_types', None) or [uuid.uuid4().hex]
|
||||
|
||||
# Set default attribute
|
||||
group_info = {
|
||||
'id': uuid.uuid4().hex,
|
||||
'status': random.choice([
|
||||
'available',
|
||||
]),
|
||||
'availability_zone': f'az-{uuid.uuid4().hex}',
|
||||
'created_at': '2015-09-16T09:28:52.000000',
|
||||
'name': 'first_group',
|
||||
'description': f'description-{uuid.uuid4().hex}',
|
||||
'group_type': group_type,
|
||||
'volume_types': volume_types,
|
||||
'volumes': [f'volume-{uuid.uuid4().hex}'],
|
||||
'group_snapshot_id': None,
|
||||
'source_group_id': None,
|
||||
'project_id': f'project-{uuid.uuid4().hex}',
|
||||
}
|
||||
|
||||
# Overwrite default attributes if there are some attributes set
|
||||
group_info.update(attrs)
|
||||
|
||||
group = fakes.FakeResource(
|
||||
None,
|
||||
group_info,
|
||||
loaded=True)
|
||||
return group
|
||||
|
||||
@staticmethod
|
||||
def create_volume_groups(attrs=None, count=2):
|
||||
"""Create multiple fake groups.
|
||||
|
||||
:param attrs: A dictionary with all attributes of group
|
||||
:param count: The number of groups to be faked
|
||||
:return: A list of FakeResource objects
|
||||
"""
|
||||
groups = []
|
||||
for n in range(0, count):
|
||||
groups.append(FakeVolumeGroup.create_one_volume_group(attrs))
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
class FakeVolumeGroupType:
|
||||
"""Fake one or more volume group types."""
|
||||
|
||||
@staticmethod
|
||||
def create_one_volume_group_type(attrs=None):
|
||||
"""Create a fake group type.
|
||||
|
||||
:param attrs: A dictionary with all attributes of group type
|
||||
:return: A FakeResource object with id, name, description, etc.
|
||||
"""
|
||||
attrs = attrs or {}
|
||||
|
||||
# Set default attribute
|
||||
group_type_info = {
|
||||
'id': uuid.uuid4().hex,
|
||||
'name': f'group-type-{uuid.uuid4().hex}',
|
||||
'description': f'description-{uuid.uuid4().hex}',
|
||||
'is_public': random.choice([True, False]),
|
||||
'group_specs': {},
|
||||
}
|
||||
|
||||
# Overwrite default attributes if there are some attributes set
|
||||
group_type_info.update(attrs)
|
||||
|
||||
group_type = fakes.FakeResource(
|
||||
None,
|
||||
group_type_info,
|
||||
loaded=True)
|
||||
return group_type
|
||||
|
||||
@staticmethod
|
||||
def create_volume_group_types(attrs=None, count=2):
|
||||
"""Create multiple fake group types.
|
||||
|
||||
:param attrs: A dictionary with all attributes of group type
|
||||
:param count: The number of group types to be faked
|
||||
:return: A list of FakeResource objects
|
||||
"""
|
||||
group_types = []
|
||||
for n in range(0, count):
|
||||
group_types.append(
|
||||
FakeVolumeGroupType.create_one_volume_group_type(attrs)
|
||||
)
|
||||
|
||||
return group_types
|
||||
|
||||
|
||||
class FakeVolumeMessage:
|
||||
|
497
openstackclient/tests/unit/volume/v3/test_volume_group.py
Normal file
497
openstackclient/tests/unit/volume/v3/test_volume_group.py
Normal file
@ -0,0 +1,497 @@
|
||||
# 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 import exceptions
|
||||
|
||||
from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes
|
||||
from openstackclient.volume.v3 import volume_group
|
||||
|
||||
|
||||
class TestVolumeGroup(volume_fakes.TestVolume):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.volume_groups_mock = self.app.client_manager.volume.groups
|
||||
self.volume_groups_mock.reset_mock()
|
||||
|
||||
self.volume_group_types_mock = \
|
||||
self.app.client_manager.volume.group_types
|
||||
self.volume_group_types_mock.reset_mock()
|
||||
|
||||
self.volume_types_mock = self.app.client_manager.volume.volume_types
|
||||
self.volume_types_mock.reset_mock()
|
||||
|
||||
|
||||
class TestVolumeGroupCreate(TestVolumeGroup):
|
||||
|
||||
fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type()
|
||||
fake_volume_group_type = \
|
||||
volume_fakes.FakeVolumeGroupType.create_one_volume_group_type()
|
||||
fake_volume_group = volume_fakes.FakeVolumeGroup.create_one_volume_group(
|
||||
attrs={
|
||||
'group_type': fake_volume_group_type.id,
|
||||
'volume_types': [fake_volume_type.id],
|
||||
},
|
||||
)
|
||||
|
||||
columns = (
|
||||
'ID',
|
||||
'Status',
|
||||
'Name',
|
||||
'Description',
|
||||
'Group Type',
|
||||
'Volume Types',
|
||||
'Availability Zone',
|
||||
'Created At',
|
||||
'Volumes',
|
||||
'Group Snapshot ID',
|
||||
'Source Group ID',
|
||||
)
|
||||
data = (
|
||||
fake_volume_group.id,
|
||||
fake_volume_group.status,
|
||||
fake_volume_group.name,
|
||||
fake_volume_group.description,
|
||||
fake_volume_group.group_type,
|
||||
fake_volume_group.volume_types,
|
||||
fake_volume_group.availability_zone,
|
||||
fake_volume_group.created_at,
|
||||
fake_volume_group.volumes,
|
||||
fake_volume_group.group_snapshot_id,
|
||||
fake_volume_group.source_group_id,
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.volume_types_mock.get.return_value = self.fake_volume_type
|
||||
self.volume_group_types_mock.get.return_value = \
|
||||
self.fake_volume_group_type
|
||||
self.volume_groups_mock.create.return_value = self.fake_volume_group
|
||||
self.volume_groups_mock.get.return_value = self.fake_volume_group
|
||||
|
||||
self.cmd = volume_group.CreateVolumeGroup(self.app, None)
|
||||
|
||||
def test_volume_group_create(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.13')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group_type.id,
|
||||
self.fake_volume_type.id,
|
||||
]
|
||||
verifylist = [
|
||||
('volume_group_type', self.fake_volume_group_type.id),
|
||||
('volume_types', [self.fake_volume_type.id]),
|
||||
('name', None),
|
||||
('description', None),
|
||||
('availability_zone', None),
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.volume_group_types_mock.get.assert_called_once_with(
|
||||
self.fake_volume_group_type.id)
|
||||
self.volume_types_mock.get.assert_called_once_with(
|
||||
self.fake_volume_type.id)
|
||||
self.volume_groups_mock.create.assert_called_once_with(
|
||||
self.fake_volume_group_type.id,
|
||||
self.fake_volume_type.id,
|
||||
None,
|
||||
None,
|
||||
availability_zone=None,
|
||||
)
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertCountEqual(self.data, data)
|
||||
|
||||
def test_volume_group_create_with_options(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.13')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group_type.id,
|
||||
self.fake_volume_type.id,
|
||||
'--name', 'foo',
|
||||
'--description', 'hello, world',
|
||||
'--availability-zone', 'bar',
|
||||
]
|
||||
verifylist = [
|
||||
('volume_group_type', self.fake_volume_group_type.id),
|
||||
('volume_types', [self.fake_volume_type.id]),
|
||||
('name', 'foo'),
|
||||
('description', 'hello, world'),
|
||||
('availability_zone', 'bar'),
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.volume_group_types_mock.get.assert_called_once_with(
|
||||
self.fake_volume_group_type.id)
|
||||
self.volume_types_mock.get.assert_called_once_with(
|
||||
self.fake_volume_type.id)
|
||||
self.volume_groups_mock.create.assert_called_once_with(
|
||||
self.fake_volume_group_type.id,
|
||||
self.fake_volume_type.id,
|
||||
'foo',
|
||||
'hello, world',
|
||||
availability_zone='bar',
|
||||
)
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertCountEqual(self.data, data)
|
||||
|
||||
def test_volume_group_create_pre_v313(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.12')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group_type.id,
|
||||
self.fake_volume_type.id,
|
||||
]
|
||||
verifylist = [
|
||||
('volume_group_type', self.fake_volume_group_type.id),
|
||||
('volume_types', [self.fake_volume_type.id]),
|
||||
('name', None),
|
||||
('description', None),
|
||||
('availability_zone', 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.13 or greater is required',
|
||||
str(exc))
|
||||
|
||||
|
||||
class TestVolumeGroupDelete(TestVolumeGroup):
|
||||
|
||||
fake_volume_group = \
|
||||
volume_fakes.FakeVolumeGroup.create_one_volume_group()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.volume_groups_mock.get.return_value = self.fake_volume_group
|
||||
self.volume_groups_mock.delete.return_value = None
|
||||
|
||||
self.cmd = volume_group.DeleteVolumeGroup(self.app, None)
|
||||
|
||||
def test_volume_group_delete(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.13')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group.id,
|
||||
'--force',
|
||||
]
|
||||
verifylist = [
|
||||
('group', self.fake_volume_group.id),
|
||||
('force', True),
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.volume_groups_mock.delete.assert_called_once_with(
|
||||
self.fake_volume_group.id, delete_volumes=True,
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_volume_group_delete_pre_v313(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.12')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group.id,
|
||||
]
|
||||
verifylist = [
|
||||
('group', self.fake_volume_group.id),
|
||||
('force', False),
|
||||
]
|
||||
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.13 or greater is required',
|
||||
str(exc))
|
||||
|
||||
|
||||
class TestVolumeGroupSet(TestVolumeGroup):
|
||||
|
||||
fake_volume_group = \
|
||||
volume_fakes.FakeVolumeGroup.create_one_volume_group()
|
||||
|
||||
columns = (
|
||||
'ID',
|
||||
'Status',
|
||||
'Name',
|
||||
'Description',
|
||||
'Group Type',
|
||||
'Volume Types',
|
||||
'Availability Zone',
|
||||
'Created At',
|
||||
'Volumes',
|
||||
'Group Snapshot ID',
|
||||
'Source Group ID',
|
||||
)
|
||||
data = (
|
||||
fake_volume_group.id,
|
||||
fake_volume_group.status,
|
||||
fake_volume_group.name,
|
||||
fake_volume_group.description,
|
||||
fake_volume_group.group_type,
|
||||
fake_volume_group.volume_types,
|
||||
fake_volume_group.availability_zone,
|
||||
fake_volume_group.created_at,
|
||||
fake_volume_group.volumes,
|
||||
fake_volume_group.group_snapshot_id,
|
||||
fake_volume_group.source_group_id,
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.volume_groups_mock.get.return_value = self.fake_volume_group
|
||||
self.volume_groups_mock.update.return_value = self.fake_volume_group
|
||||
|
||||
self.cmd = volume_group.SetVolumeGroup(self.app, None)
|
||||
|
||||
def test_volume_group_set(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.13')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group.id,
|
||||
'--name', 'foo',
|
||||
'--description', 'hello, world',
|
||||
]
|
||||
verifylist = [
|
||||
('group', self.fake_volume_group.id),
|
||||
('name', 'foo'),
|
||||
('description', 'hello, world'),
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.volume_groups_mock.update.assert_called_once_with(
|
||||
self.fake_volume_group.id, name='foo', description='hello, world',
|
||||
)
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertCountEqual(self.data, data)
|
||||
|
||||
def test_volume_group_with_enable_replication_option(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.38')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group.id,
|
||||
'--enable-replication',
|
||||
]
|
||||
verifylist = [
|
||||
('group', self.fake_volume_group.id),
|
||||
('enable_replication', True),
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.volume_groups_mock.enable_replication.assert_called_once_with(
|
||||
self.fake_volume_group.id)
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertCountEqual(self.data, data)
|
||||
|
||||
def test_volume_group_set_pre_v313(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.12')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group.id,
|
||||
'--name', 'foo',
|
||||
'--description', 'hello, world',
|
||||
]
|
||||
verifylist = [
|
||||
('group', self.fake_volume_group.id),
|
||||
('name', 'foo'),
|
||||
('description', 'hello, world'),
|
||||
]
|
||||
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.13 or greater is required',
|
||||
str(exc))
|
||||
|
||||
def test_volume_group_with_enable_replication_option_pre_v338(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.37')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group.id,
|
||||
'--enable-replication',
|
||||
]
|
||||
verifylist = [
|
||||
('group', self.fake_volume_group.id),
|
||||
('enable_replication', True),
|
||||
]
|
||||
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.38 or greater is required',
|
||||
str(exc))
|
||||
|
||||
|
||||
class TestVolumeGroupList(TestVolumeGroup):
|
||||
|
||||
fake_volume_groups = \
|
||||
volume_fakes.FakeVolumeGroup.create_volume_groups()
|
||||
|
||||
columns = (
|
||||
'ID',
|
||||
'Status',
|
||||
'Name',
|
||||
)
|
||||
data = [
|
||||
(
|
||||
fake_volume_group.id,
|
||||
fake_volume_group.status,
|
||||
fake_volume_group.name,
|
||||
) for fake_volume_group in fake_volume_groups
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.volume_groups_mock.list.return_value = self.fake_volume_groups
|
||||
|
||||
self.cmd = volume_group.ListVolumeGroup(self.app, None)
|
||||
|
||||
def test_volume_group_list(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.13')
|
||||
|
||||
arglist = [
|
||||
'--all-projects',
|
||||
]
|
||||
verifylist = [
|
||||
('all_projects', True),
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.volume_groups_mock.list.assert_called_once_with(
|
||||
search_opts={
|
||||
'all_tenants': True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertCountEqual(tuple(self.data), data)
|
||||
|
||||
def test_volume_group_list_pre_v313(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.12')
|
||||
|
||||
arglist = [
|
||||
'--all-projects',
|
||||
]
|
||||
verifylist = [
|
||||
('all_projects', True),
|
||||
]
|
||||
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.13 or greater is required',
|
||||
str(exc))
|
||||
|
||||
|
||||
class TestVolumeGroupFailover(TestVolumeGroup):
|
||||
|
||||
fake_volume_group = \
|
||||
volume_fakes.FakeVolumeGroup.create_one_volume_group()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.volume_groups_mock.get.return_value = self.fake_volume_group
|
||||
self.volume_groups_mock.failover_replication.return_value = None
|
||||
|
||||
self.cmd = volume_group.FailoverVolumeGroup(self.app, None)
|
||||
|
||||
def test_volume_group_failover(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.38')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group.id,
|
||||
'--allow-attached-volume',
|
||||
'--secondary-backend-id', 'foo',
|
||||
]
|
||||
verifylist = [
|
||||
('group', self.fake_volume_group.id),
|
||||
('allow_attached_volume', True),
|
||||
('secondary_backend_id', 'foo'),
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.volume_groups_mock.failover_replication.assert_called_once_with(
|
||||
self.fake_volume_group.id,
|
||||
allow_attached_volume=True,
|
||||
secondary_backend_id='foo',
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_volume_group_failover_pre_v338(self):
|
||||
self.app.client_manager.volume.api_version = \
|
||||
api_versions.APIVersion('3.37')
|
||||
|
||||
arglist = [
|
||||
self.fake_volume_group.id,
|
||||
'--allow-attached-volume',
|
||||
'--secondary-backend-id', 'foo',
|
||||
]
|
||||
verifylist = [
|
||||
('group', self.fake_volume_group.id),
|
||||
('allow_attached_volume', True),
|
||||
('secondary_backend_id', 'foo'),
|
||||
]
|
||||
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.38 or greater is required',
|
||||
str(exc))
|
506
openstackclient/volume/v3/volume_group.py
Normal file
506
openstackclient/volume/v3/volume_group.py
Normal file
@ -0,0 +1,506 @@
|
||||
# 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.command import command
|
||||
from osc_lib import exceptions
|
||||
from osc_lib import utils
|
||||
|
||||
from openstackclient.i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_group(group):
|
||||
columns = (
|
||||
'id',
|
||||
'status',
|
||||
'name',
|
||||
'description',
|
||||
'group_type',
|
||||
'volume_types',
|
||||
'availability_zone',
|
||||
'created_at',
|
||||
'volumes',
|
||||
'group_snapshot_id',
|
||||
'source_group_id',
|
||||
)
|
||||
column_headers = (
|
||||
'ID',
|
||||
'Status',
|
||||
'Name',
|
||||
'Description',
|
||||
'Group Type',
|
||||
'Volume Types',
|
||||
'Availability Zone',
|
||||
'Created At',
|
||||
'Volumes',
|
||||
'Group Snapshot ID',
|
||||
'Source Group ID',
|
||||
)
|
||||
|
||||
# TODO(stephenfin): Consider using a formatter for volume_types since it's
|
||||
# a list
|
||||
return (
|
||||
column_headers,
|
||||
utils.get_item_properties(
|
||||
group,
|
||||
columns,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class CreateVolumeGroup(command.ShowOne):
|
||||
"""Create a volume group.
|
||||
|
||||
Generic volume groups enable you to create a group of volumes and manage
|
||||
them together.
|
||||
|
||||
Generic volume groups are more flexible than consistency groups. Currently
|
||||
volume consistency groups only support consistent group snapshot. It
|
||||
cannot be extended easily to serve other purposes. A project may want to
|
||||
put volumes used in the same application together in a group so that it is
|
||||
easier to manage them together, and this group of volumes may or may not
|
||||
support consistent group snapshot. Generic volume group solve this problem.
|
||||
By decoupling the tight relationship between the group construct and the
|
||||
consistency concept, generic volume groups can be extended to support other
|
||||
features in the future.
|
||||
|
||||
This command requires ``--os-volume-api-version`` 3.13 or greater.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super().get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'volume_group_type',
|
||||
metavar='<volume_group_type>',
|
||||
help=_('Name or ID of volume group type to use.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'volume_types',
|
||||
metavar='<volume_type>',
|
||||
nargs='+',
|
||||
default=[],
|
||||
help=_('Name or ID of volume type(s) to use.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--name',
|
||||
metavar='<name>',
|
||||
help=_('Name of the volume group.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--description',
|
||||
metavar='<description>',
|
||||
help=_('Description of a volume group.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--availability-zone',
|
||||
metavar='<availability-zone>',
|
||||
help=_('Availability zone for volume group.'),
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
volume_client = self.app.client_manager.volume
|
||||
|
||||
if volume_client.api_version < api_versions.APIVersion('3.13'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.13 or greater is required to "
|
||||
"support the 'volume group create' command"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
volume_group_type = utils.find_resource(
|
||||
volume_client.group_types,
|
||||
parsed_args.volume_group_type,
|
||||
)
|
||||
|
||||
volume_types = []
|
||||
for volume_type in parsed_args.volume_types:
|
||||
volume_types.append(
|
||||
utils.find_resource(
|
||||
volume_client.volume_types,
|
||||
volume_type,
|
||||
)
|
||||
)
|
||||
|
||||
group = volume_client.groups.create(
|
||||
volume_group_type.id,
|
||||
','.join(x.id for x in volume_types),
|
||||
parsed_args.name,
|
||||
parsed_args.description,
|
||||
availability_zone=parsed_args.availability_zone)
|
||||
|
||||
group = volume_client.groups.get(group.id)
|
||||
|
||||
return _format_group(group)
|
||||
|
||||
|
||||
class DeleteVolumeGroup(command.Command):
|
||||
"""Delete a volume group.
|
||||
|
||||
This command requires ``--os-volume-api-version`` 3.13 or greater.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super().get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'group',
|
||||
metavar='<group>',
|
||||
help=_('Name or ID of volume group to delete'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--force',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=_(
|
||||
'Delete the volume group even if it contains volumes. '
|
||||
'This will delete any remaining volumes in the group.',
|
||||
)
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
volume_client = self.app.client_manager.volume
|
||||
|
||||
if volume_client.api_version < api_versions.APIVersion('3.13'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.13 or greater is required to "
|
||||
"support the 'volume group delete' command"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
group = utils.find_resource(
|
||||
volume_client.groups,
|
||||
parsed_args.group,
|
||||
)
|
||||
|
||||
volume_client.groups.delete(
|
||||
group.id, delete_volumes=parsed_args.force)
|
||||
|
||||
|
||||
class SetVolumeGroup(command.ShowOne):
|
||||
"""Update a volume group.
|
||||
|
||||
This command requires ``--os-volume-api-version`` 3.13 or greater.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super().get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'group',
|
||||
metavar='<group>',
|
||||
help=_('Name or ID of volume group.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--name',
|
||||
metavar='<name>',
|
||||
help=_('New name for group.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--description',
|
||||
metavar='<description>',
|
||||
help=_('New description for group.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--enable-replication',
|
||||
action='store_true',
|
||||
dest='enable_replication',
|
||||
default=None,
|
||||
help=_(
|
||||
'Enable replication for group. '
|
||||
'(supported by --os-volume-api-version 3.38 or above)'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--disable-replication',
|
||||
action='store_false',
|
||||
dest='enable_replication',
|
||||
help=_(
|
||||
'Disable replication for group. '
|
||||
'(supported by --os-volume-api-version 3.38 or above)'
|
||||
),
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
volume_client = self.app.client_manager.volume
|
||||
|
||||
if volume_client.api_version < api_versions.APIVersion('3.13'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.13 or greater is required to "
|
||||
"support the 'volume group set' command"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
group = utils.find_resource(
|
||||
volume_client.groups,
|
||||
parsed_args.group,
|
||||
)
|
||||
|
||||
if parsed_args.enable_replication is not None:
|
||||
if volume_client.api_version < api_versions.APIVersion('3.38'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.38 or greater is required to "
|
||||
"support the '--enable-replication' or "
|
||||
"'--disable-replication' options"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
if parsed_args.enable_replication:
|
||||
volume_client.groups.enable_replication(group.id)
|
||||
else:
|
||||
volume_client.groups.disable_replication(group.id)
|
||||
|
||||
kwargs = {}
|
||||
|
||||
if parsed_args.name is not None:
|
||||
kwargs['name'] = parsed_args.name
|
||||
|
||||
if parsed_args.description is not None:
|
||||
kwargs['description'] = parsed_args.description
|
||||
|
||||
if kwargs:
|
||||
group = volume_client.groups.update(group.id, **kwargs)
|
||||
|
||||
return _format_group(group)
|
||||
|
||||
|
||||
class ListVolumeGroup(command.Lister):
|
||||
"""Lists all volume groups.
|
||||
|
||||
This command requires ``--os-volume-api-version`` 3.13 or greater.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super().get_parser(prog_name)
|
||||
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).'),
|
||||
)
|
||||
# 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
|
||||
|
||||
if volume_client.api_version < api_versions.APIVersion('3.13'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.13 or greater is required to "
|
||||
"support the 'volume group list' command"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
search_opts = {
|
||||
'all_tenants': parsed_args.all_projects,
|
||||
}
|
||||
|
||||
groups = volume_client.groups.list(
|
||||
search_opts=search_opts)
|
||||
|
||||
column_headers = (
|
||||
'ID',
|
||||
'Status',
|
||||
'Name',
|
||||
)
|
||||
columns = (
|
||||
'id',
|
||||
'status',
|
||||
'name',
|
||||
)
|
||||
|
||||
return (
|
||||
column_headers,
|
||||
(
|
||||
utils.get_item_properties(a, columns)
|
||||
for a in groups
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ShowVolumeGroup(command.ShowOne):
|
||||
"""Show detailed information for a volume group.
|
||||
|
||||
This command requires ``--os-volume-api-version`` 3.13 or greater.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super().get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'group',
|
||||
metavar='<group>',
|
||||
help=_('Name or ID of volume group.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--volumes',
|
||||
action='store_true',
|
||||
dest='show_volumes',
|
||||
default=None,
|
||||
help=_(
|
||||
'Show volumes included in the group. '
|
||||
'(supported by --os-volume-api-version 3.25 or above)'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-volumes',
|
||||
action='store_false',
|
||||
dest='show_volumes',
|
||||
help=_(
|
||||
'Do not show volumes included in the group. '
|
||||
'(supported by --os-volume-api-version 3.25 or above)'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--replication-targets',
|
||||
action='store_true',
|
||||
dest='show_replication_targets',
|
||||
default=None,
|
||||
help=_(
|
||||
'Show replication targets for the group. '
|
||||
'(supported by --os-volume-api-version 3.38 or above)'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-replication-targets',
|
||||
action='store_false',
|
||||
dest='show_replication_targets',
|
||||
help=_(
|
||||
'Do not show replication targets for the group. '
|
||||
'(supported by --os-volume-api-version 3.38 or above)'
|
||||
),
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
volume_client = self.app.client_manager.volume
|
||||
|
||||
if volume_client.api_version < api_versions.APIVersion('3.13'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.13 or greater is required to "
|
||||
"support the 'volume group show' command"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
kwargs = {}
|
||||
|
||||
if parsed_args.show_volumes is not None:
|
||||
if volume_client.api_version < api_versions.APIVersion('3.25'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.25 or greater is required to "
|
||||
"support the '--(no-)volumes' option"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
kwargs['list_volume'] = parsed_args.show_volumes
|
||||
|
||||
if parsed_args.show_replication_targets is not None:
|
||||
if volume_client.api_version < api_versions.APIVersion('3.38'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.38 or greater is required to "
|
||||
"support the '--(no-)replication-targets' option"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
group = utils.find_resource(
|
||||
volume_client.groups,
|
||||
parsed_args.group,
|
||||
)
|
||||
|
||||
group = volume_client.groups.show(group.id, **kwargs)
|
||||
|
||||
if parsed_args.show_replication_targets:
|
||||
replication_targets = \
|
||||
volume_client.groups.list_replication_targets(group.id)
|
||||
|
||||
group.replication_targets = replication_targets
|
||||
|
||||
# TODO(stephenfin): Show replication targets
|
||||
return _format_group(group)
|
||||
|
||||
|
||||
class FailoverVolumeGroup(command.Command):
|
||||
"""Failover replication for a volume group.
|
||||
|
||||
This command requires ``--os-volume-api-version`` 3.38 or greater.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super().get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'group',
|
||||
metavar='<group>',
|
||||
help=_('Name or ID of volume group to failover replication for.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--allow-attached-volume',
|
||||
action='store_true',
|
||||
dest='allow_attached_volume',
|
||||
default=False,
|
||||
help=_(
|
||||
'Allow group with attached volumes to be failed over.',
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
'--disallow-attached-volume',
|
||||
action='store_false',
|
||||
dest='allow_attached_volume',
|
||||
default=False,
|
||||
help=_(
|
||||
'Disallow group with attached volumes to be failed over.',
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
'--secondary-backend-id',
|
||||
metavar='<backend_id>',
|
||||
help=_('Secondary backend ID.'),
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
volume_client = self.app.client_manager.volume
|
||||
|
||||
if volume_client.api_version < api_versions.APIVersion('3.38'):
|
||||
msg = _(
|
||||
"--os-volume-api-version 3.38 or greater is required to "
|
||||
"support the 'volume group failover' command"
|
||||
)
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
group = utils.find_resource(
|
||||
volume_client.groups,
|
||||
parsed_args.group,
|
||||
)
|
||||
|
||||
volume_client.groups.failover_replication(
|
||||
group.id,
|
||||
allow_attached_volume=parsed_args.allow_attached_volume,
|
||||
secondary_backend_id=parsed_args.secondary_backend_id,
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add ``volume group create``, ``volume group delete``,
|
||||
``volume group list``, ``volume group failover``,
|
||||
``volume group set/unset`` and ``volume attachment show``
|
||||
commands to create, delete, list, failover, update and show volume groups,
|
||||
respectively.
|
@ -714,6 +714,13 @@ openstack.volume.v3 =
|
||||
volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord
|
||||
volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord
|
||||
|
||||
volume_group_create = openstackclient.volume.v3.volume_group:CreateVolumeGroup
|
||||
volume_group_delete = openstackclient.volume.v3.volume_group:DeleteVolumeGroup
|
||||
volume_group_list = openstackclient.volume.v3.volume_group:ListVolumeGroup
|
||||
volume_group_failover = openstackclient.volume.v3.volume_group:FailoverVolumeGroup
|
||||
volume_group_set = openstackclient.volume.v3.volume_group:SetVolumeGroup
|
||||
volume_group_show = openstackclient.volume.v3.volume_group:ShowVolumeGroup
|
||||
|
||||
volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost
|
||||
|
||||
volume_message_delete = openstackclient.volume.v3.volume_message:DeleteMessage
|
||||
|
Loading…
Reference in New Issue
Block a user