diff --git a/cinderclient/tests/unit/v3/fakes.py b/cinderclient/tests/unit/v3/fakes.py index e53aac7e6..6a6247745 100644 --- a/cinderclient/tests/unit/v3/fakes.py +++ b/cinderclient/tests/unit/v3/fakes.py @@ -56,6 +56,20 @@ def _stub_group_snapshot(detailed=True, **kwargs): return group_snapshot +def _stub_snapshot(**kwargs): + snapshot = { + "created_at": "2012-08-28T16:30:31.000000", + "display_description": None, + "display_name": None, + "id": '11111111-1111-1111-1111-111111111111', + "size": 1, + "status": "available", + "volume_id": '00000000-0000-0000-0000-000000000000', + } + snapshot.update(kwargs) + return snapshot + + class FakeClient(fakes.FakeClient, client.Client): def __init__(self, api_version=None, *args, **kwargs): @@ -399,6 +413,37 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient): def put_group_snapshots_1234(self, **kw): return (200, {}, {'group_snapshot': {}}) + def post_groups_1234_action(self, **kw): + return (202, {}, {}) + + def get_groups_5678(self, **kw): + return (200, {}, {'group': + _stub_group(id='5678')}) + + def post_groups_5678_action(self, **kw): + return (202, {}, {}) + + def post_snapshots_1234_action(self, **kw): + return (202, {}, {}) + + def get_snapshots_1234(self, **kw): + return (200, {}, {'snapshot': _stub_snapshot(id='1234')}) + + def post_snapshots_5678_action(self, **kw): + return (202, {}, {}) + + def get_snapshots_5678(self, **kw): + return (200, {}, {'snapshot': _stub_snapshot(id='5678')}) + + def post_group_snapshots_1234_action(self, **kw): + return (202, {}, {}) + + def post_group_snapshots_5678_action(self, **kw): + return (202, {}, {}) + + def get_group_snapshots_5678(self, **kw): + return (200, {}, {'group_snapshot': _stub_group_snapshot(id='5678')}) + def delete_group_snapshots_1234(self, **kw): return (202, {}, {}) diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py index 70dc72602..08bd2c25f 100644 --- a/cinderclient/tests/unit/v3/test_shell.py +++ b/cinderclient/tests/unit/v3/test_shell.py @@ -454,6 +454,80 @@ class ShellTest(utils.TestCase): self.run_command('--os-volume-api-version 3.3 message-list') self.assert_called('GET', '/messages') + @ddt.data('volume', 'backup', 'snapshot', None) + def test_reset_state_entity_not_found(self, entity_type): + cmd = 'reset-state 999999' + if entity_type is not None: + cmd += ' --type %s' % entity_type + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + @ddt.data({'entity_types': [{'name': 'volume', 'version': '3.0', + 'command': 'os-reset_status'}, + {'name': 'backup', 'version': '3.0', + 'command': 'os-reset_status'}, + {'name': 'snapshot', 'version': '3.0', + 'command': 'os-reset_status'}, + {'name': None, 'version': '3.0', + 'command': 'os-reset_status'}, + {'name': 'group', 'version': '3.20', + 'command': 'reset_status'}, + {'name': 'group-snapshot', 'version': '3.19', + 'command': 'reset_status'}], + 'r_id': ['1234'], + 'states': ['available', 'error', None]}, + {'entity_types': [{'name': 'volume', 'version': '3.0', + 'command': 'os-reset_status'}, + {'name': 'backup', 'version': '3.0', + 'command': 'os-reset_status'}, + {'name': 'snapshot', 'version': '3.0', + 'command': 'os-reset_status'}, + {'name': None, 'version': '3.0', + 'command': 'os-reset_status'}, + {'name': 'group', 'version': '3.20', + 'command': 'reset_status'}, + {'name': 'group-snapshot', 'version': '3.19', + 'command': 'reset_status'}], + 'r_id': ['1234', '5678'], + 'states': ['available', 'error', None]}) + @ddt.unpack + def test_reset_state_normal(self, entity_types, r_id, states): + for state in states: + for t in entity_types: + if state is None: + expected = {t['command']: {}} + cmd = ('--os-volume-api-version ' + '%s reset-state %s') % (t['version'], + ' '.join(r_id)) + else: + expected = {t['command']: {'status': state}} + cmd = ('--os-volume-api-version ' + '%s reset-state ' + '--state %s %s') % (t['version'], + state, ' '.join(r_id)) + if t['name'] is not None: + cmd += ' --type %s' % t['name'] + + self.run_command(cmd) + + name = t['name'] if t['name'] else 'volume' + for re in r_id: + self.assert_called_anytime('POST', '/%ss/%s/action' + % (name.replace('-', '_'), re), + body=expected) + + @ddt.data({'command': '--attach-status detached', + 'expected': {'attach_status': 'detached'}}, + {'command': '--state in-use --attach-status attached', + 'expected': {'status': 'in-use', + 'attach_status': 'attached'}}, + {'command': '--reset-migration-status', + 'expected': {'migration_status': 'none'}}) + @ddt.unpack + def test_reset_state_volume_additional_status(self, command, expected): + self.run_command('reset-state %s 1234' % command) + expected = {'os-reset_status': expected} + self.assert_called('POST', '/volumes/1234/action', body=expected) + def test_snapshot_list_with_metadata(self): self.run_command('--os-volume-api-version 3.22 ' 'snapshot-list --metadata key1=val1') diff --git a/cinderclient/v2/volume_backups.py b/cinderclient/v2/volume_backups.py index 4088ea1bf..7a0229275 100644 --- a/cinderclient/v2/volume_backups.py +++ b/cinderclient/v2/volume_backups.py @@ -99,7 +99,8 @@ class VolumeBackupManager(base.ManagerWithFind): def reset_state(self, backup, state): """Update the specified volume backup with the provided state.""" - return self._action('os-reset_status', backup, {'status': state}) + return self._action('os-reset_status', backup, + {'status': state} if state else {}) def _action(self, action, backup, info=None, **kwargs): """Perform a volume backup action.""" diff --git a/cinderclient/v3/group_snapshots.py b/cinderclient/v3/group_snapshots.py index d2cd76476..01e70734a 100644 --- a/cinderclient/v3/group_snapshots.py +++ b/cinderclient/v3/group_snapshots.py @@ -17,6 +17,7 @@ from cinderclient.apiclient import base as common_base +from cinderclient import api_versions from cinderclient import base from cinderclient import utils @@ -34,6 +35,10 @@ class GroupSnapshot(base.Resource): """Update the name or description for this group snapshot.""" return self.manager.update(self, **kwargs) + def reset_state(self, state): + """Reset the group snapshot's state with specified one.""" + return self.manager.reset_state(self, state) + class GroupSnapshotManager(base.ManagerWithFind): """Manage :class:`GroupSnapshot` resources.""" @@ -74,6 +79,16 @@ class GroupSnapshotManager(base.ManagerWithFind): return self._get("/group_snapshots/%s" % group_snapshot_id, "group_snapshot") + @api_versions.wraps('3.19') + def reset_state(self, group_snapshot, state): + """Update the provided group snapshot with the provided state. + + :param group_snapshot: The :class:`GroupSnapshot` to set the state. + :param state: The state of the group snapshot to be set. + """ + body = {'status': state} if state else {} + return self._action('reset_status', group_snapshot, body) + def list(self, detailed=True, search_opts=None): """Lists all group snapshots. diff --git a/cinderclient/v3/groups.py b/cinderclient/v3/groups.py index d4ac15d12..4bd5643b8 100644 --- a/cinderclient/v3/groups.py +++ b/cinderclient/v3/groups.py @@ -15,6 +15,7 @@ """Group interface (v3 extension).""" +from cinderclient import api_versions from cinderclient import base from cinderclient.apiclient import base as common_base from cinderclient import utils @@ -33,6 +34,10 @@ class Group(base.Resource): """Update the name or description for this group.""" return self.manager.update(self, **kwargs) + def reset_state(self, state): + """Reset the group's state with specified one""" + return self.manager.reset_state(self, state) + class GroupManager(base.ManagerWithFind): """Manage :class:`Group` resources.""" @@ -64,6 +69,16 @@ class GroupManager(base.ManagerWithFind): return self._create('/groups', body, 'group') + @api_versions.wraps('3.20') + def reset_state(self, group, state): + """Update the provided group with the provided state. + + :param group: The :class:`Group` to set the state. + :param state: The state of the group to be set. + """ + body = {'status': state} if state else {} + return self._action('reset_status', group, body) + def create_from_src(self, group_snapshot_id, source_group_id, name=None, description=None, user_id=None, project_id=None): diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py index f539dc59d..218b6d878 100644 --- a/cinderclient/v3/shell.py +++ b/cinderclient/v3/shell.py @@ -33,6 +33,13 @@ from cinderclient import utils from cinderclient.v2.shell import * # flake8: noqa +RESET_STATE_RESOURCES = {'volume': utils.find_volume, + 'backup': shell_utils.find_backup, + 'snapshot': shell_utils.find_volume_snapshot, + 'group': shell_utils.find_group, + 'group-snapshot': shell_utils.find_group_snapshot} + + @utils.arg('--group_id', metavar='', default=None, @@ -194,6 +201,63 @@ def do_list(cs, args): sortby_index=sortby_index) +@utils.arg('entity', metavar='', nargs='+', + help='Name or ID of entity to update.') +@utils.arg('--type', metavar='', default='volume', + choices=RESET_STATE_RESOURCES.keys(), + help="Type of entity to update. Available resources " + "are: 'volume', 'snapshot', 'backup', " + "'group' (since 3.20) and " + "'group-snapshot' (since 3.19), Default=volume.") +@utils.arg('--state', metavar='', default=None, + help=("The state to assign to the entity. " + "NOTE: This command simply changes the state of the " + "entity in the database with no regard to actual status, " + "exercise caution when using. Default=None, that means the " + "state is unchanged.")) +@utils.arg('--attach-status', metavar='', default=None, + help=('This only used in volume entity. The attach status to ' + 'assign to the volume in the DataBase, with no regard to ' + 'the actual status. Valid values are "attached" and ' + '"detached". Default=None, that means the status ' + 'is unchanged.')) +@utils.arg('--reset-migration-status', + action='store_true', + help=('This only used in volume entity. Clears the migration ' + 'status of the volume in the DataBase that indicates the ' + 'volume is source or destination of volume migration, ' + 'with no regard to the actual status.')) +def do_reset_state(cs, args): + """Explicitly updates the entity state in the Cinder database. + + Being a database change only, this has no impact on the true state of the + entity and may not match the actual state. This can render a entity + unusable in the case of changing to the 'available' state. + """ + failure_count = 0 + single = (len(args.entity) == 1) + + migration_status = 'none' if args.reset_migration_status else None + collector = RESET_STATE_RESOURCES[args.type] + argument = (args.state,) + if args.type == 'volume': + argument += (args.attach_status, migration_status) + + for entity in args.entity: + try: + collector(cs, entity).reset_state(*argument) + except Exception as e: + print(e) + failure_count += 1 + msg = "Reset state for entity %s failed: %s" % (entity, e) + if not single: + print(msg) + + if failure_count == len(args.entity): + msg = "Unable to reset the state for the specified entity(s)." + raise exceptions.CommandError(msg) + + @utils.arg('size', metavar='', nargs='?', diff --git a/cinderclient/v3/volume_snapshots.py b/cinderclient/v3/volume_snapshots.py index 2699abcd6..2fafe4bdd 100644 --- a/cinderclient/v3/volume_snapshots.py +++ b/cinderclient/v3/volume_snapshots.py @@ -150,7 +150,8 @@ class SnapshotManager(base.ManagerWithFind): def reset_state(self, snapshot, state): """Update the specified snapshot with the provided state.""" - return self._action('os-reset_status', snapshot, {'status': state}) + return self._action('os-reset_status', snapshot, + {'status': state} if state else {}) def _action(self, action, snapshot, info=None, **kwargs): """Perform a snapshot action.""" diff --git a/releasenotes/notes/add-generic-reset-state-command-d83v1f3accbf5807.yaml b/releasenotes/notes/add-generic-reset-state-command-d83v1f3accbf5807.yaml new file mode 100644 index 000000000..0a2b5ba91 --- /dev/null +++ b/releasenotes/notes/add-generic-reset-state-command-d83v1f3accbf5807.yaml @@ -0,0 +1,7 @@ +--- +features: + - Use 'cinder reset-state' as generic resource reset + state command for resource 'volume', 'snapshot', 'backup' + 'group' and 'group-snapshot'. Also change volume's + default status from 'available' to none when no + status is specified.