Merge "volume: Add 'volume group *' commands"
This commit is contained in:
commit
87369984d1
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.
|
@ -720,6 +720,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…
x
Reference in New Issue
Block a user