1# Copyright (c) 2013 OpenStack Foundation
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15
16from unittest import mock
17
18import ddt
19import fixtures
20from requests_mock.contrib import fixture as requests_mock_fixture
21from six.moves.urllib import parse
22
23from cinderclient import client
24from cinderclient import exceptions
25from cinderclient import shell
26from cinderclient.tests.unit.fixture_data import keystone_client
27from cinderclient.tests.unit import utils
28from cinderclient.tests.unit.v2 import fakes
29from cinderclient.v2 import shell as test_shell
30from cinderclient.v2 import volume_backups
31from cinderclient.v2 import volumes
32
33
34@ddt.ddt
35@mock.patch.object(client, 'Client', fakes.FakeClient)
36class ShellTest(utils.TestCase):
37
38    FAKE_ENV = {
39        'CINDER_USERNAME': 'username',
40        'CINDER_PASSWORD': 'password',
41        'CINDER_PROJECT_ID': 'project_id',
42        'OS_VOLUME_API_VERSION': '2',
43        'CINDER_URL': keystone_client.BASE_URL,
44    }
45
46    # Patch os.environ to avoid required auth info.
47    def setUp(self):
48        """Run before each test."""
49        super(ShellTest, self).setUp()
50        for var in self.FAKE_ENV:
51            self.useFixture(fixtures.EnvironmentVariable(var,
52                                                         self.FAKE_ENV[var]))
53
54        self.mock_completion()
55
56        self.shell = shell.OpenStackCinderShell()
57
58        self.requests = self.useFixture(requests_mock_fixture.Fixture())
59        self.requests.register_uri(
60            'GET', keystone_client.BASE_URL,
61            text=keystone_client.keystone_request_callback)
62
63        self.cs = mock.Mock()
64
65    def _make_args(self, args):
66        class Args(object):
67            def __init__(self, entries):
68                self.__dict__.update(entries)
69
70        return Args(args)
71
72    def run_command(self, cmd):
73        self.shell.main(cmd.split())
74
75    def assert_called(self, method, url, body=None,
76                      partial_body=None, **kwargs):
77        return self.shell.cs.assert_called(method, url, body,
78                                           partial_body, **kwargs)
79
80    def test_list(self):
81        self.run_command('list')
82        # NOTE(jdg): we default to detail currently
83        self.assert_called('GET', '/volumes/detail')
84
85    def test_list_filter_tenant_with_all_tenants(self):
86        self.run_command('list --all-tenants=1 --tenant 123')
87        self.assert_called('GET',
88                           '/volumes/detail?all_tenants=1&project_id=123')
89
90    def test_list_filter_tenant_without_all_tenants(self):
91        self.run_command('list --tenant 123')
92        self.assert_called('GET',
93                           '/volumes/detail?all_tenants=1&project_id=123')
94
95    def test_metadata_args_with_limiter(self):
96        self.run_command('create --metadata key1="--test1" 1')
97        self.assert_called('GET', '/volumes/1234')
98        expected = {'volume': {'imageRef': None,
99                               'size': 1,
100                               'availability_zone': None,
101                               'source_volid': None,
102                               'consistencygroup_id': None,
103                               'name': None,
104                               'snapshot_id': None,
105                               'metadata': {'key1': '"--test1"'},
106                               'volume_type': None,
107                               'description': None,
108                               }}
109        self.assert_called_anytime('POST', '/volumes', expected)
110
111    def test_metadata_args_limiter_display_name(self):
112        self.run_command('create --metadata key1="--t1" --name="t" 1')
113        self.assert_called('GET', '/volumes/1234')
114        expected = {'volume': {'imageRef': None,
115                               'size': 1,
116                               'availability_zone': None,
117                               'source_volid': None,
118                               'consistencygroup_id': None,
119                               'name': '"t"',
120                               'snapshot_id': None,
121                               'metadata': {'key1': '"--t1"'},
122                               'volume_type': None,
123                               'description': None,
124                               }}
125        self.assert_called_anytime('POST', '/volumes', expected)
126
127    def test_delimit_metadata_args(self):
128        self.run_command('create --metadata key1="test1" key2="test2" 1')
129        expected = {'volume': {'imageRef': None,
130                               'size': 1,
131                               'availability_zone': None,
132                               'source_volid': None,
133                               'consistencygroup_id': None,
134                               'name': None,
135                               'snapshot_id': None,
136                               'metadata': {'key1': '"test1"',
137                                            'key2': '"test2"'},
138                               'volume_type': None,
139                               'description': None,
140                               }}
141        self.assert_called_anytime('POST', '/volumes', expected)
142
143    def test_delimit_metadata_args_display_name(self):
144        self.run_command('create --metadata key1="t1" --name="t" 1')
145        self.assert_called('GET', '/volumes/1234')
146        expected = {'volume': {'imageRef': None,
147                               'size': 1,
148                               'availability_zone': None,
149                               'source_volid': None,
150                               'consistencygroup_id': None,
151                               'name': '"t"',
152                               'snapshot_id': None,
153                               'metadata': {'key1': '"t1"'},
154                               'volume_type': None,
155                               'description': None,
156                               }}
157        self.assert_called_anytime('POST', '/volumes', expected)
158
159    def test_list_filter_status(self):
160        self.run_command('list --status=available')
161        self.assert_called('GET', '/volumes/detail?status=available')
162
163    def test_list_filter_bootable_true(self):
164        self.run_command('list --bootable=true')
165        self.assert_called('GET', '/volumes/detail?bootable=true')
166
167    def test_list_filter_bootable_false(self):
168        self.run_command('list --bootable=false')
169        self.assert_called('GET', '/volumes/detail?bootable=false')
170
171    def test_list_filter_name(self):
172        self.run_command('list --name=1234')
173        self.assert_called('GET', '/volumes/detail?name=1234')
174
175    def test_list_all_tenants(self):
176        self.run_command('list --all-tenants=1')
177        self.assert_called('GET', '/volumes/detail?all_tenants=1')
178
179    def test_list_marker(self):
180        self.run_command('list --marker=1234')
181        self.assert_called('GET', '/volumes/detail?marker=1234')
182
183    def test_list_limit(self):
184        self.run_command('list --limit=10')
185        self.assert_called('GET', '/volumes/detail?limit=10')
186
187    @mock.patch("cinderclient.utils.print_list")
188    def test_list_field(self, mock_print):
189        self.run_command('list --field Status,Name,Size,Bootable')
190        self.assert_called('GET', '/volumes/detail')
191        key_list = ['ID', 'Status', 'Name', 'Size', 'Bootable']
192        mock_print.assert_called_once_with(mock.ANY, key_list,
193            exclude_unavailable=True, sortby_index=0)
194
195    @mock.patch("cinderclient.utils.print_list")
196    def test_list_field_with_all_tenants(self, mock_print):
197        self.run_command('list --field Status,Name,Size,Bootable '
198                         '--all-tenants 1')
199        self.assert_called('GET', '/volumes/detail?all_tenants=1')
200        key_list = ['ID', 'Status', 'Name', 'Size', 'Bootable']
201        mock_print.assert_called_once_with(mock.ANY, key_list,
202            exclude_unavailable=True, sortby_index=0)
203
204    @mock.patch("cinderclient.utils.print_list")
205    def test_list_duplicate_fields(self, mock_print):
206        self.run_command('list --field Status,id,Size,status')
207        self.assert_called('GET', '/volumes/detail')
208        key_list = ['ID', 'Status', 'Size']
209        mock_print.assert_called_once_with(mock.ANY, key_list,
210            exclude_unavailable=True, sortby_index=0)
211
212    @mock.patch("cinderclient.utils.print_list")
213    def test_list_field_with_tenant(self, mock_print):
214        self.run_command('list --field Status,Name,Size,Bootable '
215                         '--tenant 123')
216        self.assert_called('GET',
217            '/volumes/detail?all_tenants=1&project_id=123')
218        key_list = ['ID', 'Status', 'Name', 'Size', 'Bootable']
219        mock_print.assert_called_once_with(mock.ANY, key_list,
220            exclude_unavailable=True, sortby_index=0)
221
222    def test_list_sort_name(self):
223        # Client 'name' key is mapped to 'display_name'
224        self.run_command('list --sort=name')
225        self.assert_called('GET', '/volumes/detail?sort=display_name')
226
227    def test_list_sort_single_key_only(self):
228        self.run_command('list --sort=id')
229        self.assert_called('GET', '/volumes/detail?sort=id')
230
231    def test_list_sort_single_key_trailing_colon(self):
232        self.run_command('list --sort=id:')
233        self.assert_called('GET', '/volumes/detail?sort=id')
234
235    def test_list_sort_single_key_and_dir(self):
236        self.run_command('list --sort=id:asc')
237        url = '/volumes/detail?%s' % parse.urlencode([('sort', 'id:asc')])
238        self.assert_called('GET', url)
239
240    def test_list_sort_multiple_keys_only(self):
241        self.run_command('list --sort=id,status,size')
242        url = ('/volumes/detail?%s' %
243               parse.urlencode([('sort', 'id,status,size')]))
244        self.assert_called('GET', url)
245
246    def test_list_sort_multiple_keys_and_dirs(self):
247        self.run_command('list --sort=id:asc,status,size:desc')
248        url = ('/volumes/detail?%s' %
249               parse.urlencode([('sort', 'id:asc,status,size:desc')]))
250        self.assert_called('GET', url)
251
252    def test_list_reorder_with_sort(self):
253        # sortby_index is None if there is sort information
254        for cmd in ['list --sort=name',
255                    'list --sort=name:asc']:
256            with mock.patch('cinderclient.utils.print_list') as mock_print:
257                self.run_command(cmd)
258                mock_print.assert_called_once_with(
259                    mock.ANY, mock.ANY, exclude_unavailable=True,
260                    sortby_index=None)
261
262    def test_list_reorder_without_sort(self):
263        # sortby_index is 0 without sort information
264        for cmd in ['list', 'list --all-tenants']:
265            with mock.patch('cinderclient.utils.print_list') as mock_print:
266                self.run_command(cmd)
267                mock_print.assert_called_once_with(
268                    mock.ANY, mock.ANY, exclude_unavailable=True,
269                    sortby_index=0)
270
271    def test_list_availability_zone(self):
272        self.run_command('availability-zone-list')
273        self.assert_called('GET', '/os-availability-zone')
274
275    def test_create_volume_from_snapshot(self):
276        expected = {'volume': {'size': None}}
277
278        expected['volume']['snapshot_id'] = '1234'
279        self.run_command('create --snapshot-id=1234')
280        self.assert_called_anytime('POST', '/volumes', partial_body=expected)
281        self.assert_called('GET', '/volumes/1234')
282
283        expected['volume']['size'] = 2
284        self.run_command('create --snapshot-id=1234 2')
285        self.assert_called_anytime('POST', '/volumes', partial_body=expected)
286        self.assert_called('GET', '/volumes/1234')
287
288    def test_create_volume_from_volume(self):
289        expected = {'volume': {'size': None}}
290
291        expected['volume']['source_volid'] = '1234'
292        self.run_command('create --source-volid=1234')
293        self.assert_called_anytime('POST', '/volumes', partial_body=expected)
294        self.assert_called('GET', '/volumes/1234')
295
296        expected['volume']['size'] = 2
297        self.run_command('create --source-volid=1234 2')
298        self.assert_called_anytime('POST', '/volumes', partial_body=expected)
299        self.assert_called('GET', '/volumes/1234')
300
301    def test_create_volume_from_image(self):
302        expected = {'volume': {'size': 1,
303                               'imageRef': '1234'}}
304        self.run_command('create --image=1234 1')
305        self.assert_called_anytime('POST', '/volumes', partial_body=expected)
306        self.assert_called('GET', '/volumes/1234')
307
308    def test_upload_to_image(self):
309        expected = {'os-volume_upload_image': {'force': False,
310                                               'container_format': 'bare',
311                                               'disk_format': 'raw',
312                                               'image_name': 'test-image'}}
313        self.run_command('upload-to-image 1234 test-image')
314        self.assert_called_anytime('GET', '/volumes/1234')
315        self.assert_called_anytime('POST', '/volumes/1234/action',
316                                   body=expected)
317
318    def test_upload_to_image_force(self):
319        expected = {'os-volume_upload_image': {'force': 'True',
320                                               'container_format': 'bare',
321                                               'disk_format': 'raw',
322                                               'image_name': 'test-image'}}
323        self.run_command('upload-to-image --force=True 1234 test-image')
324        self.assert_called_anytime('GET', '/volumes/1234')
325        self.assert_called_anytime('POST', '/volumes/1234/action',
326                                   body=expected)
327
328    def test_create_size_required_if_not_snapshot_or_clone(self):
329        self.assertRaises(SystemExit, self.run_command, 'create')
330
331    def test_create_size_zero_if_not_snapshot_or_clone(self):
332        expected = {'volume': {'size': 0}}
333        self.run_command('create 0')
334        self.assert_called_anytime('POST', '/volumes', partial_body=expected)
335        self.assert_called('GET', '/volumes/1234')
336
337    def test_show(self):
338        self.run_command('show 1234')
339        self.assert_called('GET', '/volumes/1234')
340
341    def test_delete(self):
342        self.run_command('delete 1234')
343        self.assert_called('DELETE', '/volumes/1234')
344
345    def test_delete_by_name(self):
346        self.run_command('delete sample-volume')
347        self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1&'
348                                          'name=sample-volume')
349        self.assert_called('DELETE', '/volumes/1234')
350
351    def test_delete_multiple(self):
352        self.run_command('delete 1234 5678')
353        self.assert_called_anytime('DELETE', '/volumes/1234')
354        self.assert_called('DELETE', '/volumes/5678')
355
356    def test_delete_with_cascade_true(self):
357        self.run_command('delete 1234 --cascade')
358        self.assert_called('DELETE', '/volumes/1234?cascade=True')
359        self.run_command('delete --cascade 1234')
360        self.assert_called('DELETE', '/volumes/1234?cascade=True')
361
362    def test_delete_with_cascade_with_invalid_value(self):
363        self.assertRaises(SystemExit, self.run_command,
364                          'delete 1234 --cascade 1234')
365
366    def test_backup(self):
367        self.run_command('backup-create 1234')
368        self.assert_called('POST', '/backups')
369
370    def test_backup_incremental(self):
371        self.run_command('backup-create 1234 --incremental')
372        self.assert_called('POST', '/backups')
373
374    def test_backup_force(self):
375        self.run_command('backup-create 1234 --force')
376        self.assert_called('POST', '/backups')
377
378    def test_backup_snapshot(self):
379        self.run_command('backup-create 1234 --snapshot-id 4321')
380        self.assert_called('POST', '/backups')
381
382    def test_multiple_backup_delete(self):
383        self.run_command('backup-delete 1234 5678')
384        self.assert_called_anytime('DELETE', '/backups/1234')
385        self.assert_called('DELETE', '/backups/5678')
386
387    def test_restore(self):
388        self.run_command('backup-restore 1234')
389        self.assert_called('POST', '/backups/1234/restore')
390
391    def test_restore_with_name(self):
392        self.run_command('backup-restore 1234 --name restore_vol')
393        expected = {'restore': {'volume_id': None, 'name': 'restore_vol'}}
394        self.assert_called('POST', '/backups/1234/restore',
395                           body=expected)
396
397    def test_restore_with_name_error(self):
398        self.assertRaises(exceptions.CommandError, self.run_command,
399                          'backup-restore 1234 --volume fake_vol --name '
400                          'restore_vol')
401
402    @ddt.data('backup_name', '1234')
403    @mock.patch('cinderclient.shell_utils.find_backup')
404    @mock.patch('cinderclient.utils.print_dict')
405    @mock.patch('cinderclient.utils.find_volume')
406    def test_do_backup_restore_with_name(self,
407                                         value,
408                                         mock_find_volume,
409                                         mock_print_dict,
410                                         mock_find_backup):
411        backup_id = '1234'
412        volume_id = '5678'
413        name = None
414        input = {
415            'backup': value,
416            'volume': volume_id,
417            'name': None
418        }
419
420        args = self._make_args(input)
421        with mock.patch.object(self.cs.restores,
422                               'restore') as mocked_restore:
423            mock_find_volume.return_value = volumes.Volume(self,
424                                                           {'id': volume_id},
425                                                           loaded=True)
426            mock_find_backup.return_value = volume_backups.VolumeBackup(
427                self,
428                {'id': backup_id},
429                loaded=True)
430            test_shell.do_backup_restore(self.cs, args)
431            mock_find_backup.assert_called_once_with(
432                self.cs,
433                value)
434            mocked_restore.assert_called_once_with(
435                backup_id,
436                volume_id,
437                name)
438            self.assertTrue(mock_print_dict.called)
439
440    def test_record_export(self):
441        self.run_command('backup-export 1234')
442        self.assert_called('GET', '/backups/1234/export_record')
443
444    def test_record_import(self):
445        self.run_command('backup-import fake.driver URL_STRING')
446        expected = {'backup-record': {'backup_service': 'fake.driver',
447                                      'backup_url': 'URL_STRING'}}
448        self.assert_called('POST', '/backups/import_record', expected)
449
450    def test_snapshot_list_filter_volume_id(self):
451        self.run_command('snapshot-list --volume-id=1234')
452        self.assert_called('GET', '/snapshots/detail?volume_id=1234')
453
454    def test_snapshot_list_filter_status_and_volume_id(self):
455        self.run_command('snapshot-list --status=available --volume-id=1234')
456        self.assert_called('GET', '/snapshots/detail?'
457                           'status=available&volume_id=1234')
458
459    def test_snapshot_list_filter_name(self):
460        self.run_command('snapshot-list --name abc')
461        self.assert_called('GET', '/snapshots/detail?name=abc')
462
463    @mock.patch("cinderclient.utils.print_list")
464    def test_snapshot_list_sort(self, mock_print_list):
465        self.run_command('snapshot-list --sort id')
466        self.assert_called('GET', '/snapshots/detail?sort=id')
467        columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size']
468        mock_print_list.assert_called_once_with(mock.ANY, columns,
469            sortby_index=None)
470
471    def test_snapshot_list_filter_tenant_with_all_tenants(self):
472        self.run_command('snapshot-list --all-tenants=1 --tenant 123')
473        self.assert_called('GET',
474                           '/snapshots/detail?all_tenants=1&project_id=123')
475
476    def test_snapshot_list_filter_tenant_without_all_tenants(self):
477        self.run_command('snapshot-list --tenant 123')
478        self.assert_called('GET',
479                           '/snapshots/detail?all_tenants=1&project_id=123')
480
481    def test_rename(self):
482        # basic rename with positional arguments
483        self.run_command('rename 1234 new-name')
484        expected = {'volume': {'name': 'new-name'}}
485        self.assert_called('PUT', '/volumes/1234', body=expected)
486        # change description only
487        self.run_command('rename 1234 --description=new-description')
488        expected = {'volume': {'description': 'new-description'}}
489        self.assert_called('PUT', '/volumes/1234', body=expected)
490        # rename and change description
491        self.run_command('rename 1234 new-name '
492                         '--description=new-description')
493        expected = {'volume': {
494            'name': 'new-name',
495            'description': 'new-description',
496        }}
497        self.assert_called('PUT', '/volumes/1234', body=expected)
498
499        # Call rename with no arguments
500        self.assertRaises(SystemExit, self.run_command, 'rename')
501
502    def test_rename_invalid_args(self):
503        """Ensure that error generated does not reference an HTTP code."""
504
505        self.assertRaisesRegex(exceptions.ClientException,
506                               '(?!HTTP)',
507                               self.run_command,
508                               'rename volume-1234-abcd')
509
510    def test_rename_snapshot(self):
511        # basic rename with positional arguments
512        self.run_command('snapshot-rename 1234 new-name')
513        expected = {'snapshot': {'name': 'new-name'}}
514        self.assert_called('PUT', '/snapshots/1234', body=expected)
515        # change description only
516        self.run_command('snapshot-rename 1234 '
517                         '--description=new-description')
518        expected = {'snapshot': {'description': 'new-description'}}
519        self.assert_called('PUT', '/snapshots/1234', body=expected)
520        # snapshot-rename and change description
521        self.run_command('snapshot-rename 1234 new-name '
522                         '--description=new-description')
523        expected = {'snapshot': {
524            'name': 'new-name',
525            'description': 'new-description',
526        }}
527        self.assert_called('PUT', '/snapshots/1234', body=expected)
528
529        # Call snapshot-rename with no arguments
530        self.assertRaises(SystemExit, self.run_command, 'snapshot-rename')
531
532    def test_rename_snapshot_invalid_args(self):
533        self.assertRaises(exceptions.ClientException,
534                          self.run_command,
535                          'snapshot-rename snapshot-1234')
536
537    def test_set_metadata_set(self):
538        self.run_command('metadata 1234 set key1=val1 key2=val2')
539        self.assert_called('POST', '/volumes/1234/metadata',
540                           {'metadata': {'key1': 'val1', 'key2': 'val2'}})
541
542    def test_set_metadata_delete_dict(self):
543        self.run_command('metadata 1234 unset key1=val1 key2=val2')
544        self.assert_called('DELETE', '/volumes/1234/metadata/key1')
545        self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
546
547    def test_set_metadata_delete_keys(self):
548        self.run_command('metadata 1234 unset key1 key2')
549        self.assert_called('DELETE', '/volumes/1234/metadata/key1')
550        self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
551
552    def test_reset_state(self):
553        self.run_command('reset-state 1234')
554        expected = {'os-reset_status': {'status': 'available'}}
555        self.assert_called('POST', '/volumes/1234/action', body=expected)
556
557    def test_reset_state_attach(self):
558        self.run_command('reset-state --state in-use 1234')
559        expected = {'os-reset_status': {'status': 'in-use'}}
560        self.assert_called('POST', '/volumes/1234/action', body=expected)
561
562    def test_reset_state_with_flag(self):
563        self.run_command('reset-state --state error 1234')
564        expected = {'os-reset_status': {'status': 'error'}}
565        self.assert_called('POST', '/volumes/1234/action', body=expected)
566
567    def test_reset_state_with_attach_status(self):
568        self.run_command('reset-state --attach-status detached 1234')
569        expected = {'os-reset_status': {'attach_status': 'detached'}}
570        self.assert_called('POST', '/volumes/1234/action', body=expected)
571
572    def test_reset_state_with_attach_status_with_flag(self):
573        self.run_command('reset-state --state in-use '
574                         '--attach-status attached 1234')
575        expected = {'os-reset_status': {'status': 'in-use',
576                                        'attach_status': 'attached'}}
577        self.assert_called('POST', '/volumes/1234/action', body=expected)
578
579    def test_reset_state_with_reset_migration_status(self):
580        self.run_command('reset-state --reset-migration-status 1234')
581        expected = {'os-reset_status': {'migration_status': 'none'}}
582        self.assert_called('POST', '/volumes/1234/action', body=expected)
583
584    def test_reset_state_multiple(self):
585        self.run_command('reset-state 1234 5678 --state error')
586        expected = {'os-reset_status': {'status': 'error'}}
587        self.assert_called_anytime('POST', '/volumes/1234/action',
588                                   body=expected)
589        self.assert_called_anytime('POST', '/volumes/5678/action',
590                                   body=expected)
591
592    def test_reset_state_two_with_one_nonexistent(self):
593        cmd = 'reset-state 1234 123456789'
594        self.assertRaises(exceptions.CommandError, self.run_command, cmd)
595        expected = {'os-reset_status': {'status': 'available'}}
596        self.assert_called_anytime('POST', '/volumes/1234/action',
597                                   body=expected)
598
599    def test_reset_state_one_with_one_nonexistent(self):
600        cmd = 'reset-state 123456789'
601        self.assertRaises(exceptions.CommandError, self.run_command, cmd)
602
603    def test_snapshot_reset_state(self):
604        self.run_command('snapshot-reset-state 1234')
605        expected = {'os-reset_status': {'status': 'available'}}
606        self.assert_called('POST', '/snapshots/1234/action', body=expected)
607
608    def test_snapshot_reset_state_with_flag(self):
609        self.run_command('snapshot-reset-state --state error 1234')
610        expected = {'os-reset_status': {'status': 'error'}}
611        self.assert_called('POST', '/snapshots/1234/action', body=expected)
612
613    def test_snapshot_reset_state_multiple(self):
614        self.run_command('snapshot-reset-state 1234 5678')
615        expected = {'os-reset_status': {'status': 'available'}}
616        self.assert_called_anytime('POST', '/snapshots/1234/action',
617                                   body=expected)
618        self.assert_called_anytime('POST', '/snapshots/5678/action',
619                                   body=expected)
620
621    def test_backup_reset_state(self):
622        self.run_command('backup-reset-state 1234')
623        expected = {'os-reset_status': {'status': 'available'}}
624        self.assert_called('POST', '/backups/1234/action', body=expected)
625
626    def test_backup_reset_state_with_flag(self):
627        self.run_command('backup-reset-state --state error 1234')
628        expected = {'os-reset_status': {'status': 'error'}}
629        self.assert_called('POST', '/backups/1234/action', body=expected)
630
631    def test_backup_reset_state_multiple(self):
632        self.run_command('backup-reset-state 1234 5678')
633        expected = {'os-reset_status': {'status': 'available'}}
634        self.assert_called_anytime('POST', '/backups/1234/action',
635                                   body=expected)
636        self.assert_called_anytime('POST', '/backups/5678/action',
637                                   body=expected)
638
639    def test_type_list(self):
640        self.run_command('type-list')
641        self.assert_called_anytime('GET', '/types?is_public=None')
642
643    def test_type_show(self):
644        self.run_command('type-show 1')
645        self.assert_called('GET', '/types/1')
646
647    def test_type_create(self):
648        self.run_command('type-create test-type-1')
649        self.assert_called('POST', '/types')
650
651    def test_type_create_public(self):
652        expected = {'volume_type': {'name': 'test-type-1',
653                                    'description': 'test_type-1-desc',
654                                    'os-volume-type-access:is_public': True}}
655        self.run_command('type-create test-type-1 '
656                         '--description=test_type-1-desc '
657                         '--is-public=True')
658        self.assert_called('POST', '/types', body=expected)
659
660    def test_type_create_private(self):
661        expected = {'volume_type': {'name': 'test-type-3',
662                                    'description': 'test_type-3-desc',
663                                    'os-volume-type-access:is_public': False}}
664        self.run_command('type-create test-type-3 '
665                         '--description=test_type-3-desc '
666                         '--is-public=False')
667        self.assert_called('POST', '/types', body=expected)
668
669    def test_type_create_with_invalid_bool(self):
670        self.assertRaises(ValueError,
671                          self.run_command,
672                          ('type-create test-type-3 '
673                          '--description=test_type-3-desc '
674                          '--is-public=invalid_bool'))
675
676    def test_type_update(self):
677        expected = {'volume_type': {'name': 'test-type-1',
678                                    'description': 'test_type-1-desc',
679                                    'is_public': False}}
680        self.run_command('type-update --name test-type-1 '
681                         '--description=test_type-1-desc '
682                         '--is-public=False 1')
683        self.assert_called('PUT', '/types/1', body=expected)
684
685    def test_type_update_with_invalid_bool(self):
686        self.assertRaises(ValueError,
687                          self.run_command,
688                          'type-update --name test-type-1 '
689                          '--description=test_type-1-desc '
690                          '--is-public=invalid_bool 1')
691
692    def test_type_update_without_args(self):
693        self.assertRaises(exceptions.CommandError, self.run_command,
694                          'type-update 1')
695
696    def test_type_access_list(self):
697        self.run_command('type-access-list --volume-type 3')
698        self.assert_called('GET', '/types/3/os-volume-type-access')
699
700    def test_type_access_add_project(self):
701        expected = {'addProjectAccess': {'project': '101'}}
702        self.run_command('type-access-add --volume-type 3 --project-id 101')
703        self.assert_called_anytime('GET', '/types/3')
704        self.assert_called('POST', '/types/3/action',
705                           body=expected)
706
707    def test_type_access_add_project_by_name(self):
708        expected = {'addProjectAccess': {'project': '101'}}
709        with mock.patch('cinderclient.utils.find_resource') as mock_find:
710            mock_find.return_value = '3'
711            self.run_command('type-access-add --volume-type type_name \
712                              --project-id 101')
713            mock_find.assert_called_once_with(mock.ANY, 'type_name')
714        self.assert_called('POST', '/types/3/action',
715                           body=expected)
716
717    def test_type_access_remove_project(self):
718        expected = {'removeProjectAccess': {'project': '101'}}
719        self.run_command('type-access-remove '
720                         '--volume-type 3 --project-id 101')
721        self.assert_called_anytime('GET', '/types/3')
722        self.assert_called('POST', '/types/3/action',
723                           body=expected)
724
725    def test_type_delete(self):
726        self.run_command('type-delete 1')
727        self.assert_called('DELETE', '/types/1')
728
729    def test_type_delete_multiple(self):
730        self.run_command('type-delete 1 3')
731        self.assert_called_anytime('DELETE', '/types/1')
732        self.assert_called('DELETE', '/types/3')
733
734    def test_type_delete_by_name(self):
735        self.run_command('type-delete test-type-1')
736        self.assert_called_anytime('GET', '/types?is_public=None')
737        self.assert_called('DELETE', '/types/1')
738
739    def test_encryption_type_list(self):
740        """
741        Test encryption-type-list shell command.
742
743        Verify a series of GET requests are made:
744        - one to get the volume type list information
745        - one per volume type to retrieve the encryption type information
746        """
747        self.run_command('encryption-type-list')
748        self.assert_called_anytime('GET', '/types?is_public=None')
749        self.assert_called_anytime('GET', '/types/1/encryption')
750        self.assert_called_anytime('GET', '/types/2/encryption')
751
752    def test_encryption_type_show(self):
753        """
754        Test encryption-type-show shell command.
755
756        Verify two GET requests are made per command invocation:
757        - one to get the volume type information
758        - one to get the encryption type information
759        """
760        self.run_command('encryption-type-show 1')
761        self.assert_called('GET', '/types/1/encryption')
762        self.assert_called_anytime('GET', '/types/1')
763
764    def test_encryption_type_create(self):
765        """
766        Test encryption-type-create shell command.
767
768        Verify GET and POST requests are made per command invocation:
769        - one GET request to retrieve the relevant volume type information
770        - one POST request to create the new encryption type
771        """
772
773        expected = {'encryption': {'cipher': None, 'key_size': None,
774                                   'provider': 'TestProvider',
775                                   'control_location': 'front-end'}}
776        self.run_command('encryption-type-create 2 TestProvider')
777        self.assert_called('POST', '/types/2/encryption', body=expected)
778        self.assert_called_anytime('GET', '/types/2')
779
780    @ddt.data('--key-size 512 --control-location front-end',
781              '--key_size 512 --control_location front-end')  # old style
782    def test_encryption_type_create_with_args(self, arg):
783        expected = {'encryption': {'cipher': None,
784                                   'key_size': 512,
785                                   'provider': 'TestProvider',
786                                   'control_location': 'front-end'}}
787        self.run_command('encryption-type-create 2 TestProvider ' + arg)
788        self.assert_called('POST', '/types/2/encryption', body=expected)
789        self.assert_called_anytime('GET', '/types/2')
790
791    def test_encryption_type_update(self):
792        """
793        Test encryption-type-update shell command.
794
795        Verify two GETs/one PUT requests are made per command invocation:
796        - one GET request to retrieve the relevant volume type information
797        - one GET request to retrieve the relevant encryption type information
798        - one PUT request to update the encryption type information
799        Verify that the PUT request correctly parses encryption-type-update
800        parameters from sys.argv
801        """
802        parameters = {'--provider': 'EncryptionProvider', '--cipher': 'des',
803                      '--key-size': 1024, '--control-location': 'back-end'}
804
805        # Construct the argument string for the update call and the
806        # expected encryption-type body that should be produced by it
807        args = ' '.join(['%s %s' % (k, v) for k, v in parameters.items()])
808        expected = {'encryption': {'provider': 'EncryptionProvider',
809                                   'cipher': 'des',
810                                   'key_size': 1024,
811                                   'control_location': 'back-end'}}
812
813        self.run_command('encryption-type-update 1 %s' % args)
814        self.assert_called('GET', '/types/1/encryption')
815        self.assert_called_anytime('GET', '/types/1')
816        self.assert_called_anytime('PUT', '/types/1/encryption/provider',
817                                   body=expected)
818
819    def test_encryption_type_update_no_attributes(self):
820        """
821        Test encryption-type-update shell command.
822
823        Verify two GETs/one PUT requests are made per command invocation:
824        - one GET request to retrieve the relevant volume type information
825        - one GET request to retrieve the relevant encryption type information
826        - one PUT request to update the encryption type information
827        """
828        expected = {'encryption': {}}
829        self.run_command('encryption-type-update 1')
830        self.assert_called('GET', '/types/1/encryption')
831        self.assert_called_anytime('GET', '/types/1')
832        self.assert_called_anytime('PUT', '/types/1/encryption/provider',
833                                   body=expected)
834
835    def test_encryption_type_update_default_attributes(self):
836        """
837        Test encryption-type-update shell command.
838
839        Verify two GETs/one PUT requests are made per command invocation:
840        - one GET request to retrieve the relevant volume type information
841        - one GET request to retrieve the relevant encryption type information
842        - one PUT request to update the encryption type information
843        Verify that the encryption-type body produced contains default None
844        values for all specified parameters.
845        """
846        parameters = ['--cipher', '--key-size']
847
848        # Construct the argument string for the update call and the
849        # expected encryption-type body that should be produced by it
850        args = ' '.join(['%s' % (p) for p in parameters])
851        expected_pairs = [(k.strip('-').replace('-', '_'), None) for k in
852                          parameters]
853        expected = {'encryption': dict(expected_pairs)}
854
855        self.run_command('encryption-type-update 1 %s' % args)
856        self.assert_called('GET', '/types/1/encryption')
857        self.assert_called_anytime('GET', '/types/1')
858        self.assert_called_anytime('PUT', '/types/1/encryption/provider',
859                                   body=expected)
860
861    def test_encryption_type_delete(self):
862        """
863        Test encryption-type-delete shell command.
864
865        Verify one GET/one DELETE requests are made per command invocation:
866        - one GET request to retrieve the relevant volume type information
867        - one DELETE request to delete the encryption type information
868        """
869        self.run_command('encryption-type-delete 1')
870        self.assert_called('DELETE', '/types/1/encryption/provider')
871        self.assert_called_anytime('GET', '/types/1')
872
873    def test_migrate_volume(self):
874        self.run_command('migrate 1234 fakehost --force-host-copy=True '
875                         '--lock-volume=True')
876        expected = {'os-migrate_volume': {'force_host_copy': 'True',
877                                          'lock_volume': 'True',
878                                          'host': 'fakehost'}}
879        self.assert_called('POST', '/volumes/1234/action', body=expected)
880
881    def test_migrate_volume_bool_force(self):
882        self.run_command('migrate 1234 fakehost --force-host-copy '
883                         '--lock-volume')
884        expected = {'os-migrate_volume': {'force_host_copy': True,
885                                          'lock_volume': True,
886                                          'host': 'fakehost'}}
887        self.assert_called('POST', '/volumes/1234/action', body=expected)
888
889    def test_migrate_volume_bool_force_false(self):
890        # Set both --force-host-copy and --lock-volume to False.
891        self.run_command('migrate 1234 fakehost --force-host-copy=False '
892                         '--lock-volume=False')
893        expected = {'os-migrate_volume': {'force_host_copy': 'False',
894                                          'lock_volume': 'False',
895                                          'host': 'fakehost'}}
896        self.assert_called('POST', '/volumes/1234/action', body=expected)
897
898        # Do not set the values to --force-host-copy and --lock-volume.
899        self.run_command('migrate 1234 fakehost')
900        expected = {'os-migrate_volume': {'force_host_copy': False,
901                                          'lock_volume': False,
902                                          'host': 'fakehost'}}
903        self.assert_called('POST', '/volumes/1234/action',
904                           body=expected)
905
906    def test_snapshot_metadata_set(self):
907        self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
908        self.assert_called('POST', '/snapshots/1234/metadata',
909                           {'metadata': {'key1': 'val1', 'key2': 'val2'}})
910
911    def test_snapshot_metadata_unset_dict(self):
912        self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2')
913        self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
914        self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
915
916    def test_snapshot_metadata_unset_keys(self):
917        self.run_command('snapshot-metadata 1234 unset key1 key2')
918        self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
919        self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
920
921    def test_volume_metadata_update_all(self):
922        self.run_command('metadata-update-all 1234  key1=val1 key2=val2')
923        self.assert_called('PUT', '/volumes/1234/metadata',
924                           {'metadata': {'key1': 'val1', 'key2': 'val2'}})
925
926    def test_snapshot_metadata_update_all(self):
927        self.run_command('snapshot-metadata-update-all\
928                         1234 key1=val1 key2=val2')
929        self.assert_called('PUT', '/snapshots/1234/metadata',
930                           {'metadata': {'key1': 'val1', 'key2': 'val2'}})
931
932    def test_readonly_mode_update(self):
933        self.run_command('readonly-mode-update 1234 True')
934        expected = {'os-update_readonly_flag': {'readonly': True}}
935        self.assert_called('POST', '/volumes/1234/action', body=expected)
936
937        self.run_command('readonly-mode-update 1234 False')
938        expected = {'os-update_readonly_flag': {'readonly': False}}
939        self.assert_called('POST', '/volumes/1234/action', body=expected)
940
941    def test_service_disable(self):
942        self.run_command('service-disable host cinder-volume')
943        self.assert_called('PUT', '/os-services/disable',
944                           {"binary": "cinder-volume", "host": "host"})
945
946    def test_services_disable_with_reason(self):
947        cmd = 'service-disable host cinder-volume --reason no_reason'
948        self.run_command(cmd)
949        body = {'host': 'host', 'binary': 'cinder-volume',
950                'disabled_reason': 'no_reason'}
951        self.assert_called('PUT', '/os-services/disable-log-reason', body)
952
953    def test_service_enable(self):
954        self.run_command('service-enable host cinder-volume')
955        self.assert_called('PUT', '/os-services/enable',
956                           {"binary": "cinder-volume", "host": "host"})
957
958    def test_retype_with_policy(self):
959        self.run_command('retype 1234 foo --migration-policy=on-demand')
960        expected = {'os-retype': {'new_type': 'foo',
961                                  'migration_policy': 'on-demand'}}
962        self.assert_called('POST', '/volumes/1234/action', body=expected)
963
964    def test_retype_default_policy(self):
965        self.run_command('retype 1234 foo')
966        expected = {'os-retype': {'new_type': 'foo',
967                                  'migration_policy': 'never'}}
968        self.assert_called('POST', '/volumes/1234/action', body=expected)
969
970    def test_snapshot_delete(self):
971        """Tests delete snapshot without force parameter"""
972        self.run_command('snapshot-delete 1234')
973        self.assert_called('DELETE', '/snapshots/1234')
974
975    def test_snapshot_delete_multiple(self):
976        """Tests delete multiple snapshots without force parameter"""
977        self.run_command('snapshot-delete 5678 1234')
978        self.assert_called_anytime('DELETE', '/snapshots/5678')
979        self.assert_called('DELETE', '/snapshots/1234')
980
981    def test_force_snapshot_delete(self):
982        """Tests delete snapshot with default force parameter value(True)"""
983        self.run_command('snapshot-delete 1234 --force')
984        expected_body = {'os-force_delete': None}
985        self.assert_called('POST',
986                           '/snapshots/1234/action',
987                           expected_body)
988
989    def test_force_snapshot_delete_multiple(self):
990        """
991        Tests delete multiple snapshots with force parameter
992
993        Snapshot delete with force parameter allows deleting snapshot of a
994        volume when its status is other than "available" or "error".
995        """
996        self.run_command('snapshot-delete 5678 1234 --force')
997        expected_body = {'os-force_delete': None}
998        self.assert_called_anytime('POST',
999                                   '/snapshots/5678/action',
1000                                   expected_body)
1001        self.assert_called_anytime('POST',
1002                                   '/snapshots/1234/action',
1003                                   expected_body)
1004
1005    def test_quota_delete(self):
1006        self.run_command('quota-delete 1234')
1007        self.assert_called('DELETE', '/os-quota-sets/1234')
1008
1009    def test_volume_manage(self):
1010        self.run_command('manage host1 some_fake_name '
1011                         '--name foo --description bar '
1012                         '--volume-type baz --availability-zone az '
1013                         '--metadata k1=v1 k2=v2')
1014        expected = {'volume': {'host': 'host1',
1015                               'ref': {'source-name': 'some_fake_name'},
1016                               'name': 'foo',
1017                               'description': 'bar',
1018                               'volume_type': 'baz',
1019                               'availability_zone': 'az',
1020                               'metadata': {'k1': 'v1', 'k2': 'v2'},
1021                               'bootable': False}}
1022        self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
1023
1024    def test_volume_manage_bootable(self):
1025        """
1026        Tests the --bootable option
1027
1028        If this flag is specified, then the resulting POST should contain
1029        bootable: True.
1030        """
1031        self.run_command('manage host1 some_fake_name '
1032                         '--name foo --description bar --bootable '
1033                         '--volume-type baz --availability-zone az '
1034                         '--metadata k1=v1 k2=v2')
1035        expected = {'volume': {'host': 'host1',
1036                               'ref': {'source-name': 'some_fake_name'},
1037                               'name': 'foo',
1038                               'description': 'bar',
1039                               'volume_type': 'baz',
1040                               'availability_zone': 'az',
1041                               'metadata': {'k1': 'v1', 'k2': 'v2'},
1042                               'bootable': True}}
1043        self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
1044
1045    def test_volume_manage_source_name(self):
1046        """
1047        Tests the --source-name option.
1048
1049        Checks that the --source-name option correctly updates the
1050        ref structure that is passed in the HTTP POST
1051        """
1052        self.run_command('manage host1 VolName '
1053                         '--name foo --description bar '
1054                         '--volume-type baz --availability-zone az '
1055                         '--metadata k1=v1 k2=v2')
1056        expected = {'volume': {'host': 'host1',
1057                               'ref': {'source-name': 'VolName'},
1058                               'name': 'foo',
1059                               'description': 'bar',
1060                               'volume_type': 'baz',
1061                               'availability_zone': 'az',
1062                               'metadata': {'k1': 'v1', 'k2': 'v2'},
1063                               'bootable': False}}
1064        self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
1065
1066    def test_volume_manage_source_id(self):
1067        """
1068        Tests the --source-id option.
1069
1070        Checks that the --source-id option correctly updates the
1071        ref structure that is passed in the HTTP POST
1072        """
1073        self.run_command('manage host1 1234 '
1074                         '--id-type source-id '
1075                         '--name foo --description bar '
1076                         '--volume-type baz --availability-zone az '
1077                         '--metadata k1=v1 k2=v2')
1078        expected = {'volume': {'host': 'host1',
1079                               'ref': {'source-id': '1234'},
1080                               'name': 'foo',
1081                               'description': 'bar',
1082                               'volume_type': 'baz',
1083                               'availability_zone': 'az',
1084                               'metadata': {'k1': 'v1', 'k2': 'v2'},
1085                               'bootable': False}}
1086        self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
1087
1088    def test_volume_manageable_list(self):
1089        self.run_command('manageable-list fakehost')
1090        self.assert_called('GET', '/os-volume-manage/detail?host=fakehost')
1091
1092    def test_volume_manageable_list_details(self):
1093        self.run_command('manageable-list fakehost --detailed True')
1094        self.assert_called('GET', '/os-volume-manage/detail?host=fakehost')
1095
1096    def test_volume_manageable_list_no_details(self):
1097        self.run_command('manageable-list fakehost --detailed False')
1098        self.assert_called('GET', '/os-volume-manage?host=fakehost')
1099
1100    def test_volume_unmanage(self):
1101        self.run_command('unmanage 1234')
1102        self.assert_called('POST', '/volumes/1234/action',
1103                           body={'os-unmanage': None})
1104
1105    def test_create_snapshot_from_volume_with_metadata(self):
1106        """
1107        Tests create snapshot with --metadata parameter.
1108
1109        Checks metadata params are set during create snapshot
1110        when metadata is passed
1111        """
1112        expected = {'snapshot': {'volume_id': 1234,
1113                                 'metadata': {'k1': 'v1',
1114                                              'k2': 'v2'}}}
1115        self.run_command('snapshot-create 1234 --metadata k1=v1 k2=v2 '
1116                         '--force=True')
1117        self.assert_called_anytime('POST', '/snapshots', partial_body=expected)
1118
1119    def test_create_snapshot_from_volume_with_metadata_bool_force(self):
1120        """
1121        Tests create snapshot with --metadata parameter.
1122
1123        Checks metadata params are set during create snapshot
1124        when metadata is passed
1125        """
1126        expected = {'snapshot': {'volume_id': 1234,
1127                                 'metadata': {'k1': 'v1',
1128                                              'k2': 'v2'}}}
1129        self.run_command('snapshot-create 1234 --metadata k1=v1 k2=v2 --force')
1130        self.assert_called_anytime('POST', '/snapshots', partial_body=expected)
1131
1132    def test_get_pools(self):
1133        self.run_command('get-pools')
1134        self.assert_called('GET', '/scheduler-stats/get_pools')
1135
1136    def test_get_pools_detail(self):
1137        self.run_command('get-pools --detail')
1138        self.assert_called('GET', '/scheduler-stats/get_pools?detail=True')
1139
1140    def test_list_transfer(self):
1141        self.run_command('transfer-list')
1142        self.assert_called('GET', '/os-volume-transfer/detail?all_tenants=0')
1143
1144    def test_list_transfer_all_tenants(self):
1145        self.run_command('transfer-list --all-tenants=1')
1146        self.assert_called('GET', '/os-volume-transfer/detail?all_tenants=1')
1147
1148    def test_consistencygroup_update(self):
1149        self.run_command('consisgroup-update '
1150                         '--name cg2 --description desc2 '
1151                         '--add-volumes uuid1,uuid2 '
1152                         '--remove-volumes uuid3,uuid4 '
1153                         '1234')
1154        expected = {'consistencygroup': {'name': 'cg2',
1155                                         'description': 'desc2',
1156                                         'add_volumes': 'uuid1,uuid2',
1157                                         'remove_volumes': 'uuid3,uuid4'}}
1158        self.assert_called('PUT', '/consistencygroups/1234',
1159                           body=expected)
1160
1161    def test_consistencygroup_update_invalid_args(self):
1162        self.assertRaises(exceptions.ClientException,
1163                          self.run_command,
1164                          'consisgroup-update 1234')
1165
1166    def test_consistencygroup_create_from_src_snap(self):
1167        self.run_command('consisgroup-create-from-src '
1168                         '--name cg '
1169                         '--cgsnapshot 1234')
1170        expected = {
1171            'consistencygroup-from-src': {
1172                'name': 'cg',
1173                'cgsnapshot_id': '1234',
1174                'description': None,
1175                'user_id': None,
1176                'project_id': None,
1177                'status': 'creating',
1178                'source_cgid': None
1179            }
1180        }
1181        self.assert_called('POST', '/consistencygroups/create_from_src',
1182                           expected)
1183
1184    def test_consistencygroup_create_from_src_cg(self):
1185        self.run_command('consisgroup-create-from-src '
1186                         '--name cg '
1187                         '--source-cg 1234')
1188        expected = {
1189            'consistencygroup-from-src': {
1190                'name': 'cg',
1191                'cgsnapshot_id': None,
1192                'description': None,
1193                'user_id': None,
1194                'project_id': None,
1195                'status': 'creating',
1196                'source_cgid': '1234'
1197            }
1198        }
1199        self.assert_called('POST', '/consistencygroups/create_from_src',
1200                           expected)
1201
1202    def test_consistencygroup_create_from_src_fail_no_snap_cg(self):
1203        self.assertRaises(exceptions.ClientException,
1204                          self.run_command,
1205                          'consisgroup-create-from-src '
1206                          '--name cg')
1207
1208    def test_consistencygroup_create_from_src_fail_both_snap_cg(self):
1209        self.assertRaises(exceptions.ClientException,
1210                          self.run_command,
1211                          'consisgroup-create-from-src '
1212                          '--name cg '
1213                          '--cgsnapshot 1234 '
1214                          '--source-cg 5678')
1215
1216    def test_set_image_metadata(self):
1217        self.run_command('image-metadata 1234 set key1=val1')
1218        expected = {"os-set_image_metadata": {"metadata": {"key1": "val1"}}}
1219        self.assert_called('POST', '/volumes/1234/action',
1220                           body=expected)
1221
1222    def test_unset_image_metadata(self):
1223        self.run_command('image-metadata 1234 unset key1')
1224        expected = {"os-unset_image_metadata": {"key": "key1"}}
1225        self.assert_called('POST', '/volumes/1234/action',
1226                           body=expected)
1227
1228    def _get_params_from_stack(self, pos=-1):
1229        method, url = self.shell.cs.client.callstack[pos][0:2]
1230        path, query = parse.splitquery(url)
1231        params = parse.parse_qs(query)
1232        return path, params
1233
1234    def test_backup_list_all_tenants(self):
1235        self.run_command('backup-list --all-tenants=1 --name=bc '
1236                         '--status=available --volume-id=1234')
1237        expected = {
1238            'all_tenants': ['1'],
1239            'name': ['bc'],
1240            'status': ['available'],
1241            'volume_id': ['1234'],
1242        }
1243
1244        path, params = self._get_params_from_stack()
1245
1246        self.assertEqual('/backups/detail', path)
1247        self.assertEqual(4, len(params))
1248
1249        for k in params.keys():
1250            self.assertEqual(expected[k], params[k])
1251
1252    def test_backup_list_volume_id(self):
1253        self.run_command('backup-list --volume-id=1234')
1254        self.assert_called('GET', '/backups/detail?volume_id=1234')
1255
1256    def test_backup_list(self):
1257        self.run_command('backup-list')
1258        self.assert_called('GET', '/backups/detail')
1259
1260    @mock.patch("cinderclient.utils.print_list")
1261    def test_backup_list_sort(self, mock_print_list):
1262        self.run_command('backup-list --sort id')
1263        self.assert_called('GET', '/backups/detail?sort=id')
1264        columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count',
1265               'Container']
1266        mock_print_list.assert_called_once_with(mock.ANY, columns,
1267            sortby_index=None)
1268
1269    def test_backup_list_data_timestamp(self):
1270        self.run_command('backup-list --sort data_timestamp')
1271        self.assert_called('GET', '/backups/detail?sort=data_timestamp')
1272
1273    def test_get_capabilities(self):
1274        self.run_command('get-capabilities host')
1275        self.assert_called('GET', '/capabilities/host')
1276
1277    def test_image_metadata_show(self):
1278        # since the request is not actually sent to cinder API but is
1279        # calling the method in :class:`v2.fakes.FakeHTTPClient` instead.
1280        # Thus, ignore any exception which is false negative compare
1281        # with real API call.
1282        try:
1283            self.run_command('image-metadata-show 1234')
1284        except Exception:
1285            pass
1286        expected = {"os-show_image_metadata": None}
1287        self.assert_called('POST', '/volumes/1234/action', body=expected)
1288
1289    def test_snapshot_manage(self):
1290        self.run_command('snapshot-manage 1234 some_fake_name '
1291                         '--name foo --description bar '
1292                         '--metadata k1=v1 k2=v2')
1293        expected = {'snapshot': {'volume_id': 1234,
1294                                 'ref': {'source-name': 'some_fake_name'},
1295                                 'name': 'foo',
1296                                 'description': 'bar',
1297                                 'metadata': {'k1': 'v1', 'k2': 'v2'}
1298                                 }}
1299        self.assert_called_anytime('POST', '/os-snapshot-manage',
1300                                   body=expected)
1301
1302    def test_snapshot_manageable_list(self):
1303        self.run_command('snapshot-manageable-list fakehost')
1304        self.assert_called('GET', '/os-snapshot-manage/detail?host=fakehost')
1305
1306    def test_snapshot_manageable_list_details(self):
1307        self.run_command('snapshot-manageable-list fakehost --detailed True')
1308        self.assert_called('GET', '/os-snapshot-manage/detail?host=fakehost')
1309
1310    def test_snapshot_manageable_list_no_details(self):
1311        self.run_command('snapshot-manageable-list fakehost --detailed False')
1312        self.assert_called('GET', '/os-snapshot-manage?host=fakehost')
1313
1314    def test_snapshot_unmanage(self):
1315        self.run_command('snapshot-unmanage 1234')
1316        self.assert_called('POST', '/snapshots/1234/action',
1317                           body={'os-unmanage': None})
1318
1319    def test_extra_specs_list(self):
1320        self.run_command('extra-specs-list')
1321        self.assert_called('GET', '/types?is_public=None')
1322
1323    def test_quota_class_show(self):
1324        self.run_command('quota-class-show test')
1325        self.assert_called('GET', '/os-quota-class-sets/test')
1326
1327    def test_quota_class_update(self):
1328        expected = {'quota_class_set': {'volumes': 2,
1329                                        'snapshots': 2,
1330                                        'gigabytes': 1,
1331                                        'backups': 1,
1332                                        'backup_gigabytes': 1,
1333                                        'per_volume_gigabytes': 1}}
1334        self.run_command('quota-class-update test '
1335                         '--volumes 2 '
1336                         '--snapshots 2 '
1337                         '--gigabytes 1 '
1338                         '--backups 1 '
1339                         '--backup-gigabytes 1 '
1340                         '--per-volume-gigabytes 1')
1341        self.assert_called('PUT', '/os-quota-class-sets/test', body=expected)
1342
1343    def test_translate_attachments(self):
1344        attachment_id = 'aaaa'
1345        server_id = 'bbbb'
1346        obj_id = 'cccc'
1347        info = {
1348            'attachments': [{
1349                'attachment_id': attachment_id,
1350                'id': obj_id,
1351                'server_id': server_id}]
1352            }
1353
1354        new_info = test_shell._translate_attachments(info)
1355
1356        self.assertEqual(attachment_id, new_info['attachment_ids'][0])
1357        self.assertEqual(server_id, new_info['attached_servers'][0])
1358        self.assertNotIn('id', new_info)
1359