diff --git a/cinderclient/tests/unit/v2/fakes.py b/cinderclient/tests/unit/v2/fakes.py index a9e404143..afa316624 100644 --- a/cinderclient/tests/unit/v2/fakes.py +++ b/cinderclient/tests/unit/v2/fakes.py @@ -40,6 +40,7 @@ def _stub_volume(**kwargs): "snapshot_id": None, "status": "available", "volume_type": "None", + "multiattach": "false", "links": [ { "href": "http://localhost/v2/fake/volumes/1234", @@ -415,7 +416,7 @@ class FakeHTTPClient(base_client.HTTPClient): assert (keys == ['instance_uuid', 'mode', 'mountpoint'] or keys == ['host_name', 'mode', 'mountpoint']) elif action == 'os-detach': - assert body[action] is None + assert list(body[action]) == ['attachment_id'] elif action == 'os-reserve': assert body[action] is None elif action == 'os-unreserve': diff --git a/cinderclient/tests/unit/v2/test_shell.py b/cinderclient/tests/unit/v2/test_shell.py index 07f44ebbc..dff0f1b92 100644 --- a/cinderclient/tests/unit/v2/test_shell.py +++ b/cinderclient/tests/unit/v2/test_shell.py @@ -125,7 +125,8 @@ class ShellTest(utils.TestCase): 'snapshot_id': None, 'metadata': {'key1': '"--test1"'}, 'volume_type': None, - 'description': None}} + 'description': None, + 'multiattach': False}} self.assert_called_anytime('POST', '/volumes', expected) def test_metadata_args_limiter_display_name(self): @@ -145,7 +146,8 @@ class ShellTest(utils.TestCase): 'snapshot_id': None, 'metadata': {'key1': '"--t1"'}, 'volume_type': None, - 'description': None}} + 'description': None, + 'multiattach': False}} self.assert_called_anytime('POST', '/volumes', expected) def test_delimit_metadata_args(self): @@ -165,7 +167,8 @@ class ShellTest(utils.TestCase): 'metadata': {'key1': '"test1"', 'key2': '"test2"'}, 'volume_type': None, - 'description': None}} + 'description': None, + 'multiattach': False}} self.assert_called_anytime('POST', '/volumes', expected) def test_delimit_metadata_args_display_name(self): @@ -185,7 +188,8 @@ class ShellTest(utils.TestCase): 'snapshot_id': None, 'metadata': {'key1': '"t1"'}, 'volume_type': None, - 'description': None}} + 'description': None, + 'multiattach': False}} self.assert_called_anytime('POST', '/volumes', expected) def test_list_filter_status(self): diff --git a/cinderclient/tests/unit/v2/test_volumes.py b/cinderclient/tests/unit/v2/test_volumes.py index 8ddb59ed8..0a3f4e546 100644 --- a/cinderclient/tests/unit/v2/test_volumes.py +++ b/cinderclient/tests/unit/v2/test_volumes.py @@ -95,7 +95,8 @@ class VolumesTest(utils.TestCase): 'project_id': None, 'metadata': {}, 'source_replica': None, - 'consistencygroup_id': None}, + 'consistencygroup_id': None, + 'multiattach': False}, 'OS-SCH-HNT:scheduler_hints': 'uuid'} cs.assert_called('POST', '/volumes', body=expected) @@ -111,7 +112,7 @@ class VolumesTest(utils.TestCase): def test_detach(self): v = cs.volumes.get('1234') - cs.volumes.detach(v) + cs.volumes.detach(v, 'abc123') cs.assert_called('POST', '/volumes/1234/action') def test_reserve(self): diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 3c13636b3..0e6bcac4c 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -234,10 +234,12 @@ def do_list(cs, args): if all_tenants: key_list = ['ID', 'Tenant ID', 'Status', 'Name', - 'Size', 'Volume Type', 'Bootable', 'Attached to'] + 'Size', 'Volume Type', 'Bootable', 'Multiattach', + 'Attached to'] else: key_list = ['ID', 'Status', 'Name', - 'Size', 'Volume Type', 'Bootable', 'Attached to'] + 'Size', 'Volume Type', 'Bootable', + 'Multiattach', 'Attached to'] if args.sort_key or args.sort_dir or args.sort: sortby_index = None else: @@ -348,6 +350,12 @@ class CheckSizeArgForCreate(argparse.Action): action='append', default=[], help='Scheduler hint, like in nova.') +@utils.arg('--allow-multiattach', + dest='multiattach', + action="store_true", + help=('Allow volume to be attached more than once.' + ' Default=False'), + default=False) @utils.service_type('volumev2') def do_create(cs, args): """Creates a volume.""" @@ -391,7 +399,8 @@ def do_create(cs, args): imageRef=image_ref, metadata=volume_metadata, scheduler_hints=hints, - source_replica=args.source_replica) + source_replica=args.source_replica, + multiattach=args.multiattach) info = dict() volume = cs.volumes.get(volume.id) diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index 484ea2d6c..e8181f775 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -179,8 +179,8 @@ class VolumeManager(base.ManagerWithFind): volume_type=None, user_id=None, project_id=None, availability_zone=None, metadata=None, imageRef=None, scheduler_hints=None, - source_replica=None): - """Creates a volume. + source_replica=None, multiattach=False): + """Create a volume. :param size: Size of volume in GB :param consistencygroup_id: ID of the consistencygroup @@ -197,6 +197,8 @@ class VolumeManager(base.ManagerWithFind): :param source_replica: ID of source volume to clone replica :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance + :param multiattach: Allow the volume to be attached to more than + one instance :rtype: :class:`Volume` """ if metadata is None: @@ -219,6 +221,7 @@ class VolumeManager(base.ManagerWithFind): 'imageRef': imageRef, 'source_volid': source_volid, 'source_replica': source_replica, + 'multiattach': multiattach, }} if scheduler_hints: @@ -393,13 +396,15 @@ class VolumeManager(base.ManagerWithFind): body.update({'host_name': host_name}) return self._action('os-attach', volume, body) - def detach(self, volume): + def detach(self, volume, attachment_uuid=None): """Clear attachment metadata. :param volume: The :class:`Volume` (or its ID) you would like to detach. + :param attachment_uuid: The uuid of the volume attachment. """ - return self._action('os-detach', volume) + return self._action('os-detach', volume, + {'attachment_id': attachment_uuid}) def reserve(self, volume): """Reserve this volume.