Add and modify options for "volume create" command

1.Add mutually exclusive options into a mutually
exclusive group.
2.Add "--source-replicated", "--consistency-group",
"--hint" and "multi-attach" options
3.Make --size option to be optional under some cases

Closes-Bug: #1568005
Closes-Bug: #1627913
Implements: bp implement-cinder-features
Co-Authored-By: Roman Vasilets <rvasilets@mirantis.com>
Change-Id: I2c4c3073195d33774e477f4d7f22e383b14b41dd
This commit is contained in:
Huanxuan Ao 2016-09-27 12:27:05 +08:00
parent 8d63b8b263
commit c9e0c01f67
6 changed files with 296 additions and 30 deletions

View File

@ -13,21 +13,23 @@ Create new volume
.. code:: bash .. code:: bash
os volume create os volume create
--size <size> [--size <size>]
[--type <volume-type>] [--type <volume-type>]
[--image <image>] [--image <image> | --snapshot <snapshot> | --source <volume> | --source-replicated <replicated-volume>]
[--snapshot <snapshot>]
[--source <volume>]
[--description <description>] [--description <description>]
[--user <user>] [--user <user>]
[--project <project>] [--project <project>]
[--availability-zone <availability-zone>] [--availability-zone <availability-zone>]
[--consistency-group <consistency-group>]
[--property <key=value> [...] ] [--property <key=value> [...] ]
[--hint <key=value> [...] ]
[--multi-attach]
<name> <name>
.. option:: --size <size> (required) .. option:: --size <size>
Volume size in GB Volume size in GB
(Required unless --snapshot or --source or --source-replicated is specified)
.. option:: --type <volume-type> .. option:: --type <volume-type>
@ -46,10 +48,14 @@ Create new volume
Use :option:`\<snapshot\>` as source of volume (name or ID) Use :option:`\<snapshot\>` as source of volume (name or ID)
.. option:: --source <source> .. option:: --source <volume>
Volume to clone (name or ID) Volume to clone (name or ID)
.. option:: --source-replicated <replicated-volume>
Replicated volume to clone (name or ID)
.. option:: --description <description> .. option:: --description <description>
Volume description Volume description
@ -66,10 +72,23 @@ Create new volume
Create volume in :option:`\<availability-zone\>` Create volume in :option:`\<availability-zone\>`
.. option:: --consistency-group <consistency-group>
Consistency group where the new volume belongs to
.. option:: --property <key=value> .. option:: --property <key=value>
Set a property on this volume (repeat option to set multiple properties) Set a property on this volume (repeat option to set multiple properties)
.. option:: --hint <key=value>
Arbitrary scheduler hint key-value pairs to help boot an instance
(repeat option to set multiple hints)
.. option:: --multi-attach
Allow volume to be attached more than once (default to False)
.. _volume_create-name: .. _volume_create-name:
.. describe:: <name> .. describe:: <name>

View File

@ -23,6 +23,7 @@ from osc_lib import utils
from openstackclient.tests.unit import fakes from openstackclient.tests.unit import fakes
from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes
from openstackclient.tests.unit import utils as tests_utils
from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes
from openstackclient.volume.v1 import volume from openstackclient.volume.v1 import volume
@ -411,6 +412,67 @@ class TestVolumeCreate(TestVolume):
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist, data) self.assertEqual(self.datalist, data)
def test_volume_create_with_source(self):
self.volumes_mock.get.return_value = self.new_volume
arglist = [
'--source', self.new_volume.id,
self.new_volume.display_name,
]
verifylist = [
('source', self.new_volume.id),
('name', self.new_volume.display_name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.volumes_mock.create.assert_called_with(
None,
None,
self.new_volume.id,
self.new_volume.display_name,
None,
None,
None,
None,
None,
None,
None,
)
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist, data)
def test_volume_create_without_size(self):
arglist = [
self.new_volume.display_name,
]
verifylist = [
('name', self.new_volume.display_name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
parsed_args)
def test_volume_create_with_multi_source(self):
arglist = [
'--image', 'source_image',
'--source', 'source_volume',
'--snapshot', 'source_snapshot',
'--size', str(self.new_volume.size),
self.new_volume.display_name,
]
verifylist = [
('image', 'source_image'),
('source', 'source_volume'),
('snapshot', 'source_snapshot'),
('size', self.new_volume.size),
('name', self.new_volume.display_name),
]
self.assertRaises(tests_utils.ParserException, self.check_parser,
self.cmd, arglist, verifylist)
class TestVolumeDelete(TestVolume): class TestVolumeDelete(TestVolume):

View File

@ -21,6 +21,7 @@ from osc_lib import utils
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
from openstackclient.tests.unit.image.v2 import fakes as image_fakes from openstackclient.tests.unit.image.v2 import fakes as image_fakes
from openstackclient.tests.unit import utils as tests_utils
from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes
from openstackclient.volume.v2 import volume from openstackclient.volume.v2 import volume
@ -45,6 +46,10 @@ class TestVolume(volume_fakes.TestVolume):
self.snapshots_mock = self.app.client_manager.volume.volume_snapshots self.snapshots_mock = self.app.client_manager.volume.volume_snapshots
self.snapshots_mock.reset_mock() self.snapshots_mock.reset_mock()
self.consistencygroups_mock = (
self.app.client_manager.volume.consistencygroups)
self.consistencygroups_mock.reset_mock()
def setup_volumes_mock(self, count): def setup_volumes_mock(self, count):
volumes = volume_fakes.FakeVolume.create_volumes(count=count) volumes = volume_fakes.FakeVolume.create_volumes(count=count)
@ -123,18 +128,28 @@ class TestVolumeCreate(TestVolume):
availability_zone=None, availability_zone=None,
metadata=None, metadata=None,
imageRef=None, imageRef=None,
source_volid=None source_volid=None,
consistencygroup_id=None,
source_replica=None,
multiattach=False,
scheduler_hints=None,
) )
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist, data) self.assertEqual(self.datalist, data)
def test_volume_create_options(self): def test_volume_create_options(self):
consistency_group = (
volume_fakes.FakeConsistencyGroup.create_one_consistency_group())
self.consistencygroups_mock.get.return_value = consistency_group
arglist = [ arglist = [
'--size', str(self.new_volume.size), '--size', str(self.new_volume.size),
'--description', self.new_volume.description, '--description', self.new_volume.description,
'--type', self.new_volume.volume_type, '--type', self.new_volume.volume_type,
'--availability-zone', self.new_volume.availability_zone, '--availability-zone', self.new_volume.availability_zone,
'--consistency-group', consistency_group.id,
'--hint', 'k=v',
'--multi-attach',
self.new_volume.name, self.new_volume.name,
] ]
verifylist = [ verifylist = [
@ -142,6 +157,9 @@ class TestVolumeCreate(TestVolume):
('description', self.new_volume.description), ('description', self.new_volume.description),
('type', self.new_volume.volume_type), ('type', self.new_volume.volume_type),
('availability_zone', self.new_volume.availability_zone), ('availability_zone', self.new_volume.availability_zone),
('consistency_group', consistency_group.id),
('hint', {'k': 'v'}),
('multi_attach', True),
('name', self.new_volume.name), ('name', self.new_volume.name),
] ]
parsed_args = self.check_parser(self.cmd, arglist, verifylist) parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@ -162,7 +180,11 @@ class TestVolumeCreate(TestVolume):
availability_zone=self.new_volume.availability_zone, availability_zone=self.new_volume.availability_zone,
metadata=None, metadata=None,
imageRef=None, imageRef=None,
source_volid=None source_volid=None,
consistencygroup_id=consistency_group.id,
source_replica=None,
multiattach=True,
scheduler_hints={'k': 'v'},
) )
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
@ -204,7 +226,11 @@ class TestVolumeCreate(TestVolume):
availability_zone=None, availability_zone=None,
metadata=None, metadata=None,
imageRef=None, imageRef=None,
source_volid=None source_volid=None,
consistencygroup_id=None,
source_replica=None,
multiattach=False,
scheduler_hints=None,
) )
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
@ -246,7 +272,11 @@ class TestVolumeCreate(TestVolume):
availability_zone=None, availability_zone=None,
metadata=None, metadata=None,
imageRef=None, imageRef=None,
source_volid=None source_volid=None,
consistencygroup_id=None,
source_replica=None,
multiattach=False,
scheduler_hints=None,
) )
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
@ -282,7 +312,11 @@ class TestVolumeCreate(TestVolume):
availability_zone=None, availability_zone=None,
metadata={'Alpha': 'a', 'Beta': 'b'}, metadata={'Alpha': 'a', 'Beta': 'b'},
imageRef=None, imageRef=None,
source_volid=None source_volid=None,
consistencygroup_id=None,
source_replica=None,
multiattach=False,
scheduler_hints=None,
) )
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
@ -321,6 +355,10 @@ class TestVolumeCreate(TestVolume):
metadata=None, metadata=None,
imageRef=image.id, imageRef=image.id,
source_volid=None, source_volid=None,
consistencygroup_id=None,
source_replica=None,
multiattach=False,
scheduler_hints=None,
) )
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
@ -358,7 +396,11 @@ class TestVolumeCreate(TestVolume):
availability_zone=None, availability_zone=None,
metadata=None, metadata=None,
imageRef=image.id, imageRef=image.id,
source_volid=None source_volid=None,
consistencygroup_id=None,
source_replica=None,
multiattach=False,
scheduler_hints=None,
) )
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
@ -368,12 +410,10 @@ class TestVolumeCreate(TestVolume):
snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() snapshot = volume_fakes.FakeSnapshot.create_one_snapshot()
self.new_volume.snapshot_id = snapshot.id self.new_volume.snapshot_id = snapshot.id
arglist = [ arglist = [
'--size', str(self.new_volume.size),
'--snapshot', self.new_volume.snapshot_id, '--snapshot', self.new_volume.snapshot_id,
self.new_volume.name, self.new_volume.name,
] ]
verifylist = [ verifylist = [
('size', self.new_volume.size),
('snapshot', self.new_volume.snapshot_id), ('snapshot', self.new_volume.snapshot_id),
('name', self.new_volume.name), ('name', self.new_volume.name),
] ]
@ -387,7 +427,7 @@ class TestVolumeCreate(TestVolume):
columns, data = self.cmd.take_action(parsed_args) columns, data = self.cmd.take_action(parsed_args)
self.volumes_mock.create.assert_called_once_with( self.volumes_mock.create.assert_called_once_with(
size=self.new_volume.size, size=None,
snapshot_id=snapshot.id, snapshot_id=snapshot.id,
name=self.new_volume.name, name=self.new_volume.name,
description=None, description=None,
@ -397,12 +437,83 @@ class TestVolumeCreate(TestVolume):
availability_zone=None, availability_zone=None,
metadata=None, metadata=None,
imageRef=None, imageRef=None,
source_volid=None source_volid=None,
consistencygroup_id=None,
source_replica=None,
multiattach=False,
scheduler_hints=None,
) )
self.assertEqual(self.columns, columns) self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist, data) self.assertEqual(self.datalist, data)
def test_volume_create_with_source_replicated(self):
self.volumes_mock.get.return_value = self.new_volume
arglist = [
'--source-replicated', self.new_volume.id,
self.new_volume.name,
]
verifylist = [
('source_replicated', self.new_volume.id),
('name', self.new_volume.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.volumes_mock.create.assert_called_once_with(
size=None,
snapshot_id=None,
name=self.new_volume.name,
description=None,
volume_type=None,
user_id=None,
project_id=None,
availability_zone=None,
metadata=None,
imageRef=None,
source_volid=None,
consistencygroup_id=None,
source_replica=self.new_volume.id,
multiattach=False,
scheduler_hints=None,
)
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist, data)
def test_volume_create_without_size(self):
arglist = [
self.new_volume.name,
]
verifylist = [
('name', self.new_volume.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
parsed_args)
def test_volume_create_with_multi_source(self):
arglist = [
'--image', 'source_image',
'--source', 'source_volume',
'--snapshot', 'source_snapshot',
'--source-replicated', 'source_replicated_volume',
'--size', str(self.new_volume.size),
self.new_volume.name,
]
verifylist = [
('image', 'source_image'),
('source', 'source_volume'),
('snapshot', 'source_snapshot'),
('source-replicated', 'source_replicated_volume'),
('size', self.new_volume.size),
('name', self.new_volume.name),
]
self.assertRaises(tests_utils.ParserException, self.check_parser,
self.cmd, arglist, verifylist)
class TestVolumeDelete(TestVolume): class TestVolumeDelete(TestVolume):

View File

@ -30,6 +30,20 @@ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def _check_size_arg(args):
"""Check whether --size option is required or not.
Require size parameter only in case when snapshot or source
volume is not specified.
"""
if ((args.snapshot or args.source)
is None and args.size is None):
msg = _("--size is a required option if snapshot "
"or source volume is not specified.")
raise exceptions.CommandError(msg)
class CreateVolume(command.ShowOne): class CreateVolume(command.ShowOne):
"""Create new volume""" """Create new volume"""
@ -43,32 +57,32 @@ class CreateVolume(command.ShowOne):
parser.add_argument( parser.add_argument(
'--size', '--size',
metavar='<size>', metavar='<size>',
required=True,
type=int, type=int,
help=_('Volume size in GB'), help=_("Volume size in GB (Required unless --snapshot or "
"--source is specified)"),
) )
parser.add_argument( parser.add_argument(
'--type', '--type',
metavar='<volume-type>', metavar='<volume-type>',
help=_("Set the type of volume"), help=_("Set the type of volume"),
) )
parser.add_argument( source_group = parser.add_mutually_exclusive_group()
source_group.add_argument(
'--image', '--image',
metavar='<image>', metavar='<image>',
help=_('Use <image> as source of volume (name or ID)'), help=_('Use <image> as source of volume (name or ID)'),
) )
snapshot_group = parser.add_mutually_exclusive_group() source_group.add_argument(
snapshot_group.add_argument(
'--snapshot', '--snapshot',
metavar='<snapshot>', metavar='<snapshot>',
help=_('Use <snapshot> as source of volume (name or ID)'), help=_('Use <snapshot> as source of volume (name or ID)'),
) )
snapshot_group.add_argument( source_group.add_argument(
'--snapshot-id', '--snapshot-id',
metavar='<snapshot-id>', metavar='<snapshot-id>',
help=argparse.SUPPRESS, help=argparse.SUPPRESS,
) )
parser.add_argument( source_group.add_argument(
'--source', '--source',
metavar='<volume>', metavar='<volume>',
help=_('Volume to clone (name or ID)'), help=_('Volume to clone (name or ID)'),
@ -104,7 +118,7 @@ class CreateVolume(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
_check_size_arg(parsed_args)
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
volume_client = self.app.client_manager.volume volume_client = self.app.client_manager.volume

View File

@ -30,6 +30,20 @@ from openstackclient.identity import common as identity_common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def _check_size_arg(args):
"""Check whether --size option is required or not.
Require size parameter only in case when snapshot or source
volume is not specified.
"""
if ((args.snapshot or args.source or args.source_replicated)
is None and args.size is None):
msg = _("--size is a required option if snapshot "
"or source volume is not specified.")
raise exceptions.CommandError(msg)
class CreateVolume(command.ShowOne): class CreateVolume(command.ShowOne):
"""Create new volume""" """Create new volume"""
@ -44,29 +58,35 @@ class CreateVolume(command.ShowOne):
"--size", "--size",
metavar="<size>", metavar="<size>",
type=int, type=int,
required=True, help=_("Volume size in GB (Required unless --snapshot or "
help=_("Volume size in GB"), "--source or --source-replicated is specified)"),
) )
parser.add_argument( parser.add_argument(
"--type", "--type",
metavar="<volume-type>", metavar="<volume-type>",
help=_("Set the type of volume"), help=_("Set the type of volume"),
) )
parser.add_argument( source_group = parser.add_mutually_exclusive_group()
source_group.add_argument(
"--image", "--image",
metavar="<image>", metavar="<image>",
help=_("Use <image> as source of volume (name or ID)"), help=_("Use <image> as source of volume (name or ID)"),
) )
parser.add_argument( source_group.add_argument(
"--snapshot", "--snapshot",
metavar="<snapshot>", metavar="<snapshot>",
help=_("Use <snapshot> as source of volume (name or ID)"), help=_("Use <snapshot> as source of volume (name or ID)"),
) )
parser.add_argument( source_group.add_argument(
"--source", "--source",
metavar="<volume>", metavar="<volume>",
help=_("Volume to clone (name or ID)"), help=_("Volume to clone (name or ID)"),
) )
source_group.add_argument(
"--source-replicated",
metavar="<replicated-volume>",
help=_("Replicated volume to clone (name or ID)"),
)
parser.add_argument( parser.add_argument(
"--description", "--description",
metavar="<description>", metavar="<description>",
@ -87,6 +107,11 @@ class CreateVolume(command.ShowOne):
metavar="<availability-zone>", metavar="<availability-zone>",
help=_("Create volume in <availability-zone>"), help=_("Create volume in <availability-zone>"),
) )
parser.add_argument(
"--consistency-group",
metavar="consistency-group>",
help=_("Consistency group where the new volume belongs to"),
)
parser.add_argument( parser.add_argument(
"--property", "--property",
metavar="<key=value>", metavar="<key=value>",
@ -94,9 +119,23 @@ class CreateVolume(command.ShowOne):
help=_("Set a property to this volume " help=_("Set a property to this volume "
"(repeat option to set multiple properties)"), "(repeat option to set multiple properties)"),
) )
parser.add_argument(
"--hint",
metavar="<key=value>",
action=parseractions.KeyValueAction,
help=_("Arbitrary scheduler hint key-value pairs to help boot "
"an instance (repeat option to set multiple hints)"),
)
parser.add_argument(
"--multi-attach",
action="store_true",
help=_("Allow volume to be attached more than once "
"(default to False)")
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
_check_size_arg(parsed_args)
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
volume_client = self.app.client_manager.volume volume_client = self.app.client_manager.volume
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
@ -107,6 +146,18 @@ class CreateVolume(command.ShowOne):
volume_client.volumes, volume_client.volumes,
parsed_args.source).id parsed_args.source).id
replicated_source_volume = None
if parsed_args.source_replicated:
replicated_source_volume = utils.find_resource(
volume_client.volumes,
parsed_args.source_replicated).id
consistency_group = None
if parsed_args.consistency_group:
consistency_group = utils.find_resource(
volume_client.consistencygroups,
parsed_args.consistency_group).id
image = None image = None
if parsed_args.image: if parsed_args.image:
image = utils.find_resource( image = utils.find_resource(
@ -142,7 +193,11 @@ class CreateVolume(command.ShowOne):
availability_zone=parsed_args.availability_zone, availability_zone=parsed_args.availability_zone,
metadata=parsed_args.property, metadata=parsed_args.property,
imageRef=image, imageRef=image,
source_volid=source_volume source_volid=source_volume,
consistencygroup_id=consistency_group,
source_replica=replicated_source_volume,
multiattach=parsed_args.multi_attach,
scheduler_hints=parsed_args.hint,
) )
# Remove key links from being displayed # Remove key links from being displayed
volume._info.update( volume._info.update(

View File

@ -0,0 +1,5 @@
---
features:
- Add ``--source-replicated``, ``--consistency-group``, ``--hint`` and
``--multi-attach`` options to ``volume create`` command in volume v2.
[Bug `1627913 <https://bugs.launchpad.net/python-openstackclient/+bug/1627913>`_]