Add snapshot instances admin CLIs

Support new API entry points for share snapshot instances:
- share-snapshot-instance-list
- share-snapshot-instance-show
- share-snapshot-instance-reset-status

Implements: blueprint snapshot-instances
Change-Id: Ica1e81012f19926e0f1ba9cd6d8eecc5fbbf40b5
This commit is contained in:
zhongjun 2016-04-12 16:19:25 +08:00 committed by Goutham Pacha Ravi
parent 947989952a
commit 5de1ef3844
13 changed files with 609 additions and 3 deletions

View File

@ -27,7 +27,7 @@ from manilaclient import utils
LOG = logging.getLogger(__name__)
MAX_VERSION = '2.17'
MAX_VERSION = '2.19'
MIN_VERSION = '2.0'
DEPRECATED_VERSION = '1.0'
_VERSIONED_METHOD_MAP = {}

View File

@ -125,6 +125,11 @@ share_opts = [
default="TESTDOMAIN\\Administrator",
help="Username, that will be used in share access tests for "
"user type of access."),
cfg.BoolOpt("run_snapshot_tests",
default=True,
help="Defines whether to run tests that use share snapshots "
"or not. Disable this feature if used driver doesn't "
"support it."),
]
# 2. Generate config

View File

@ -105,6 +105,11 @@ class BaseTestCase(base.ClientTestBase):
res_id, microversion=res["microversion"])
client.wait_for_share_deletion(
res_id, microversion=res["microversion"])
elif res["type"] is "snapshot":
client.delete_snapshot(
res_id, microversion=res["microversion"])
client.wait_for_snapshot_deletion(
res_id, microversion=res["microversion"])
else:
LOG.warning("Provided unsupported resource type for "
"cleanup '%s'. Skipping." % res["type"])
@ -277,3 +282,31 @@ class BaseTestCase(base.ClientTestBase):
else:
cls.method_resources.insert(0, resource)
return ss
@classmethod
def create_snapshot(cls, share, name=None, description=None,
force=False, client=None, wait_for_creation=True,
cleanup_in_class=False, microversion=None):
if client is None:
client = cls.get_admin_client()
data = {
'share': share,
'name': name,
'description': description,
'force': force,
'microversion': microversion,
}
snapshot = client.create_snapshot(**data)
resource = {
"type": "snapshot",
"id": snapshot["id"],
"client": client,
"microversion": microversion,
}
if cleanup_in_class:
cls.class_resources.insert(0, resource)
else:
cls.method_resources.insert(0, resource)
if wait_for_creation:
client.wait_for_snapshot_status(snapshot['id'], 'available')
return snapshot

View File

@ -125,6 +125,8 @@ class ManilaCLIClient(base.CLIClient):
func = self.is_share_network_deleted
elif res_type == SHARE:
func = self.is_share_deleted
elif res_type == SNAPSHOT:
func = self.is_snapshot_deleted
else:
raise exceptions.InvalidResource(message=res_type)
@ -692,6 +694,112 @@ class ManilaCLIClient(base.CLIClient):
metadata = output_parser.details(metadata_raw)
return metadata
def create_snapshot(self, share, name=None, description=None,
force=False, microversion=None):
"""Creates a snapshot."""
cmd = 'snapshot-create %(share)s ' % {'share': share}
if name is None:
name = data_utils.rand_name('autotest_snapshot_name')
cmd += '--name %s ' % name
if description is None:
description = data_utils.rand_name('autotest_snapshot_description')
cmd += '--description %s ' % description
if force:
cmd += '--force %s' % force
snapshot_raw = self.manila(cmd, microversion=microversion)
snapshot = output_parser.details(snapshot_raw)
return snapshot
@not_found_wrapper
def get_snapshot(self, snapshot, microversion=None):
"""Retrieves a snapshot by its Name or ID."""
snapshot_raw = self.manila('snapshot-show %s' % snapshot,
microversion=microversion)
snapshot = output_parser.details(snapshot_raw)
return snapshot
@not_found_wrapper
@forbidden_wrapper
def delete_snapshot(self, snapshot, microversion=None):
"""Deletes snapshot by Names or IDs."""
return self.manila(
"snapshot-delete %s" % snapshot, microversion=microversion)
def list_snapshot_instances(self, snapshot_id=None, columns=None,
detailed=None, microversion=None):
"""List snapshot instances."""
cmd = 'snapshot-instance-list '
if snapshot_id:
cmd += '--snapshot %s' % snapshot_id
if columns is not None:
cmd += ' --columns ' + columns
if detailed:
cmd += ' --detailed True '
snapshot_instances_raw = self.manila(cmd, microversion=microversion)
snapshot_instances = utils.listing(snapshot_instances_raw)
return snapshot_instances
def get_snapshot_instance(self, id=None, microversion=None):
"""Get snapshot instance."""
cmd = 'snapshot-instance-show %s ' % id
snapshot_instance_raw = self.manila(cmd, microversion=microversion)
snapshot_instance = output_parser.details(snapshot_instance_raw)
return snapshot_instance
def reset_snapshot_instance(self, id=None, state=None, microversion=None):
"""Reset snapshot instance status."""
cmd = 'snapshot-instance-reset-state %s ' % id
if state:
cmd += '--state %s' % state
snapshot_instance_raw = self.manila(cmd, microversion=microversion)
snapshot_instance = utils.listing(snapshot_instance_raw)
return snapshot_instance
def is_snapshot_deleted(self, snapshot, microversion=None):
"""Indicates whether snapshot is deleted or not.
:param snapshot: str -- Name or ID of snapshot
"""
try:
self.get_snapshot(snapshot, microversion=microversion)
return False
except tempest_lib_exc.NotFound:
return True
def wait_for_snapshot_deletion(self, snapshot, microversion=None):
"""Wait for snapshot deletion by its Name or ID.
:param snapshot: str -- Name or ID of snapshot
"""
self.wait_for_resource_deletion(
SNAPSHOT, res_id=snapshot, interval=5, timeout=300,
microversion=microversion)
def wait_for_snapshot_status(self, snapshot, status, microversion=None):
"""Waits for a snapshot to reach a given status."""
body = self.get_snapshot(snapshot, microversion=microversion)
snapshot_name = body['name']
snapshot_status = body['status']
start = int(time.time())
while snapshot_status != status:
time.sleep(self.build_interval)
body = self.get_snapshot(snapshot, microversion=microversion)
snapshot_status = body['status']
if snapshot_status == status:
return
elif 'error' in snapshot_status.lower():
raise exceptions.SnapshotBuildErrorException(snapshot=snapshot)
if int(time.time()) - start >= self.build_timeout:
message = (
"Snapshot %(snapshot_name)s failed to reach %(status)s "
"status within the required time (%(timeout)s s)." % {
"snapshot_name": snapshot_name, "status": status,
"timeout": self.build_timeout})
raise tempest_lib_exc.TimeoutException(message)
@not_found_wrapper
def list_access(self, share_id, columns=None, microversion=None):
"""Returns list of access rules for a share.

View File

@ -33,7 +33,7 @@ class InvalidData(exceptions.TempestException):
class ShareTypeNotFound(exceptions.NotFound):
message = "Share type '%(share_type)s' was not found"
message = "Share type '%(share_type)s' was not found."
class InvalidConfiguration(exceptions.TempestException):
@ -41,7 +41,11 @@ class InvalidConfiguration(exceptions.TempestException):
class ShareBuildErrorException(exceptions.TempestException):
message = "Share %(share)s failed to build and is in ERROR status"
message = "Share %(share)s failed to build and is in ERROR status."
class SnapshotBuildErrorException(exceptions.TempestException):
message = "Snapshot %(snapshot)s failed to build and is in ERROR status."
class AccessRuleCreateErrorException(exceptions.TempestException):

View File

@ -0,0 +1,122 @@
# Copyright 2016 Huawei inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ddt
from oslo_utils import uuidutils
import testtools
from manilaclient import config
from manilaclient.tests.functional import base
from manilaclient.tests.functional import utils
CONF = config.CONF
@ddt.ddt
@testtools.skipUnless(CONF.run_snapshot_tests,
'Snapshot tests disabled.')
@utils.skip_if_microversion_not_supported('2.19')
class SnapshotInstancesTest(base.BaseTestCase):
@classmethod
def setUpClass(cls):
super(SnapshotInstancesTest, cls).setUpClass()
cls.share = cls.create_share(
client=cls.get_user_client(),
cleanup_in_class=True)
cls.snapshot = cls.create_snapshot(share=cls.share['id'],
client=cls.get_user_client(),
cleanup_in_class=True)
def test_list_all_snapshot_instances(self):
snapshot_instances = self.admin_client.list_snapshot_instances()
self.assertTrue(len(snapshot_instances) > 0)
expected_keys = ('ID', 'Snapshot ID', 'Status')
for si in snapshot_instances:
for key in expected_keys:
self.assertIn(key, si)
self.assertTrue(uuidutils.is_uuid_like(si['ID']))
self.assertTrue(uuidutils.is_uuid_like(si['Snapshot ID']))
def test_list_all_snapshot_instances_details(self):
snapshot_instances = self.admin_client.list_snapshot_instances(
detailed=True)
self.assertTrue(len(snapshot_instances) > 0)
expected_keys = ('ID', 'Snapshot ID', 'Status', 'Created_at',
'Updated_at', 'Share_id', 'Share_instance_id',
'Progress', 'Provider_location')
for si in snapshot_instances:
for key in expected_keys:
self.assertIn(key, si)
for key in ('ID', 'Snapshot ID', 'Share_id', 'Share_instance_id'):
self.assertTrue(
uuidutils.is_uuid_like(si[key]))
def test_list_snapshot_instance_with_snapshot(self):
snapshot_instances = self.admin_client.list_snapshot_instances(
snapshot_id=self.snapshot['id'])
self.assertEqual(1, len(snapshot_instances))
expected_keys = ('ID', 'Snapshot ID', 'Status')
for si in snapshot_instances:
for key in expected_keys:
self.assertIn(key, si)
self.assertTrue(uuidutils.is_uuid_like(si['ID']))
self.assertTrue(uuidutils.is_uuid_like(si['Snapshot ID']))
def test_list_snapshot_instance_with_columns(self):
snapshot_instances = self.admin_client.list_snapshot_instances(
self.snapshot['id'], columns='id,status')
self.assertTrue(len(snapshot_instances) > 0)
expected_keys = ('Id', 'Status')
unexpected_keys = ('Snapshot ID', )
for si in snapshot_instances:
for key in expected_keys:
self.assertIn(key, si)
for key in unexpected_keys:
self.assertNotIn(key, si)
self.assertTrue(uuidutils.is_uuid_like(si['Id']))
def test_get_snapshot_instance(self):
snapshot_instances = self.admin_client.list_snapshot_instances(
self.snapshot['id'])
snapshot_instance = self.admin_client.get_snapshot_instance(
snapshot_instances[0]['ID'])
self.assertTrue(len(snapshot_instance) > 0)
expected_keys = ('id', 'snapshot_id', 'status', 'created_at',
'updated_at', 'share_id', 'share_instance_id',
'progress', 'provider_location')
for key in expected_keys:
self.assertIn(key, snapshot_instance)
for key in ('id', 'snapshot_id', 'share_id', 'share_instance_id'):
self.assertTrue(
uuidutils.is_uuid_like(snapshot_instance[key]))
def test_snapshot_instance_reset_state(self):
snapshot_instances = self.admin_client.list_snapshot_instances(
self.snapshot['id'])
self.admin_client.reset_snapshot_instance(
snapshot_instances[0]['ID'], 'error')
snapshot_instance = self.admin_client.get_snapshot_instance(
snapshot_instances[0]['ID'])
self.assertEqual('error', snapshot_instance['status'])
self.admin_client.reset_snapshot_instance(snapshot_instance['id'],
'available')

View File

@ -749,6 +749,53 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
get_types_3_share_type_access = get_types_3_os_share_type_access
fake_snapshot_instance = {
"id": "1234",
"snapshot_id": "5678",
"status": "error",
}
def get_snapshot_instances(self, **kw):
instances = {
'snapshot_instances': [
self.fake_snapshot_instance,
]
}
return (200, {}, instances)
def get_snapshot_instances_detail(self, **kw):
instances = {
'snapshot_instances': [
{
'id': '1234',
'snapshot_id': '5679',
'created_at': 'fake',
'updated_at': 'fake',
'status': 'fake',
'share_id': 'fake',
'share_instance_id': 'fake',
'progress': 'fake',
'provider_location': 'fake',
}
]
}
return (200, {}, instances)
def get_snapshot_instances_1234(self, **kw):
instances = {'snapshot_instance': self.fake_snapshot_instance}
return (200, {}, instances)
def post_snapshot_instances_1234_action(self, body, **kw):
_body = None
resp = 202
assert len(list(body)) == 1
action = list(body)[0]
if action == 'reset_status':
assert 'status' in body.get(action)
else:
raise AssertionError("Unexpected share action: %s" % action)
return (resp, {}, _body)
def fake_create(url, body, response_key):
return {'url': url, 'body': body, 'resp_key': response_key}

View File

@ -0,0 +1,95 @@
# Copyright 2016 Huawei inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ddt
import mock
from manilaclient import api_versions
from manilaclient import exceptions
from manilaclient import extension
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes
from manilaclient.v2 import share_snapshot_instances
extensions = [
extension.Extension('share_snapshot_instances', share_snapshot_instances),
]
cs = fakes.FakeClient(extensions=extensions)
@ddt.ddt
class SnapshotInstancesTest(utils.TestCase):
def setUp(self):
super(SnapshotInstancesTest, self).setUp()
microversion = api_versions.APIVersion("2.19")
mock_microversion = mock.Mock(api_version=microversion)
self.manager = share_snapshot_instances.ShareSnapshotInstanceManager(
api=mock_microversion)
@ddt.data(True, False)
def test_list(self, detailed):
if detailed:
url = '/snapshot-instances/detail'
else:
url = '/snapshot-instances'
self.mock_object(self.manager, '_list', mock.Mock())
self.manager.list(detailed=detailed)
self.manager._list.assert_called_once_with(url, 'snapshot_instances')
@ddt.data(True, False)
def test_list_with_snapshot(self, detailed):
if detailed:
url = '/snapshot-instances/detail'
else:
url = '/snapshot-instances'
self.mock_object(self.manager, '_list', mock.Mock())
self.manager.list(detailed=detailed, snapshot='snapshot_id')
self.manager._list.assert_called_once_with(
(url + '?snapshot_id=snapshot_id'), 'snapshot_instances',)
def test_get(self):
self.mock_object(self.manager, '_get', mock.Mock())
self.manager.get('fake_snapshot_instance')
self.manager._get.assert_called_once_with(
'/snapshot-instances/' + 'fake_snapshot_instance',
'snapshot_instance')
def test_reset_instance_state(self):
state = 'available'
self.mock_object(self.manager, '_action', mock.Mock())
self.manager.reset_state('fake_instance', state)
self.manager._action.assert_called_once_with(
"reset_status", 'fake_instance', {"status": state})
@ddt.data('get', 'list', 'reset_state')
def test_upsupported_microversion(self, method_name):
unsupported_microversions = ('1.0', '2.18')
arguments = {
'instance': 'FAKE_INSTANCE',
}
if method_name in ('list'):
arguments.clear()
for microversion in unsupported_microversions:
microversion = api_versions.APIVersion(microversion)
mock_microversion = mock.Mock(api_version=microversion)
manager = share_snapshot_instances.ShareSnapshotInstanceManager(
api=mock_microversion)
method = getattr(manager, method_name)
self.assertRaises(exceptions.UnsupportedVersion,
method, **arguments)

View File

@ -1826,3 +1826,39 @@ class ShellTest(test_utils.TestCase):
self.assert_called(
'POST', '/share-replicas/1234/action',
body={action_name: {attr: 'xyzzyspoon!'}})
def test_snapshot_instance_list_all(self):
self.run_command('snapshot-instance-list')
self.assert_called('GET', '/snapshot-instances')
def test_snapshot_instance_list_all_detail(self):
self.run_command('snapshot-instance-list --detail True')
self.assert_called('GET', '/snapshot-instances/detail')
@mock.patch.object(cliutils, 'print_list', mock.Mock())
def test_snapshot_instance_list_select_column(self):
self.run_command('snapshot-instance-list --columns id,status')
self.assert_called('GET', '/snapshot-instances')
cliutils.print_list.assert_called_once_with(
mock.ANY, ['Id', 'Status'])
@mock.patch.object(shell_v2, '_find_share_snapshot', mock.Mock())
def test_snapshot_instance_list_for_snapshot(self):
fsnapshot = type('FakeSansphot', (object,),
{'id': 'fake-snapshot-id'})
shell_v2._find_share_snapshot.return_value = fsnapshot
cmd = 'snapshot-instance-list --snapshot %s'
self.run_command(cmd % fsnapshot.id)
self.assert_called(
'GET', '/snapshot-instances?snapshot_id=fake-snapshot-id')
def test_snapshot_instance_show(self):
self.run_command('snapshot-instance-show 1234')
self.assert_called('GET', '/snapshot-instances/1234')
def test_snapshot_instance_reset_state(self):
self.run_command('snapshot-instance-reset-state 1234')
expected = {'reset_status': {'status': 'available'}}
self.assert_called('POST', '/snapshot-instances/1234/action',
body=expected)

View File

@ -36,6 +36,7 @@ from manilaclient.v2 import share_instances
from manilaclient.v2 import share_networks
from manilaclient.v2 import share_replicas
from manilaclient.v2 import share_servers
from manilaclient.v2 import share_snapshot_instances
from manilaclient.v2 import share_snapshots
from manilaclient.v2 import share_type_access
from manilaclient.v2 import share_types
@ -220,6 +221,8 @@ class Client(object):
share_instance_export_locations.ShareInstanceExportLocationManager(
self))
self.share_snapshots = share_snapshots.ShareSnapshotManager(self)
self.share_snapshot_instances = (
share_snapshot_instances.ShareSnapshotInstanceManager(self))
self.share_types = share_types.ShareTypeManager(self)
self.share_type_access = share_type_access.ShareTypeAccessManager(self)

View File

@ -0,0 +1,74 @@
# Copyright 2016 Huawei inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from manilaclient import api_versions
from manilaclient import base
from manilaclient.openstack.common.apiclient import base as common_base
class ShareSnapshotInstance(common_base.Resource):
"""A snapshot instance is an instance of a snapshot."""
def __repr__(self):
return "<SnapshotInstance: %s>" % self.id
def reset_state(self, state):
"""Update snapshot instance's 'status' attr."""
self.manager.reset_state(self, state)
class ShareSnapshotInstanceManager(base.ManagerWithFind):
"""Manage :class:`SnapshotInstances` resources."""
resource_class = ShareSnapshotInstance
@api_versions.wraps("2.19")
def get(self, instance):
"""Get a snapshot instance.
:param instance: either snapshot instance object or text with its ID.
:rtype: :class:`ShareSnapshotInstance`
"""
snapshot_instance_id = common_base.getid(instance)
return self._get("/snapshot-instances/%s" % snapshot_instance_id,
"snapshot_instance")
@api_versions.wraps("2.19")
def list(self, detailed=False, snapshot=None):
"""List all snapshot instances."""
if detailed:
url = '/snapshot-instances/detail'
else:
url = '/snapshot-instances'
if snapshot:
url += '?snapshot_id=%s' % common_base.getid(snapshot)
return self._list(url, 'snapshot_instances')
@api_versions.wraps("2.19")
def reset_state(self, instance, state):
"""Reset the 'status' attr of the snapshot instance.
:param instance: either snapshot instance object or its UUID.
:param state: state to set the snapshot instance's 'status' attr to.
"""
return self._action("reset_status", instance, {"status": state})
def _action(self, action, instance, info=None, **kwargs):
"""Perform a snapshot instance 'action'."""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = ('/snapshot-instances/%s/action' %
common_base.getid(instance))
return self.api.client.post(url, body=body)

View File

@ -220,6 +220,12 @@ def _print_share_snapshot(cs, snapshot):
cliutils.print_dict(info)
def _find_share_snapshot_instance(cs, snapshot_instance):
"""Get a share snapshot instance by ID."""
return apiclient_utils.find_resource(
cs.share_snapshot_instances, snapshot_instance)
def _find_share_network(cs, share_network):
"""Get a share network by ID or name."""
return apiclient_utils.find_resource(cs.share_networks, share_network)
@ -1742,6 +1748,76 @@ def do_snapshot_reset_state(cs, args):
snapshot.reset_state(args.state)
@api_versions.wraps("2.19")
@cliutils.arg(
'--snapshot',
metavar='<snapshot>',
default=None,
help='Filter results by share snapshot ID.')
@cliutils.arg(
'--columns',
metavar='<columns>',
type=str,
default=None,
help='Comma separated list of columns to be displayed '
'e.g. --columns "id"')
@cliutils.arg(
'--detailed',
metavar='<detailed>',
default=False,
help='Show detailed information about snapshot instances.'
' (Default=False)')
def do_snapshot_instance_list(cs, args):
"""List share snapshot instances."""
snapshot = (_find_share_snapshot(cs, args.snapshot)
if args.snapshot else None)
if args.columns is not None:
list_of_keys = _split_columns(columns=args.columns)
elif args.detailed:
list_of_keys = ['ID', 'Snapshot ID', 'Status', 'Created_at',
'Updated_at', 'Share_id', 'Share_instance_id',
'Progress', 'Provider_location']
else:
list_of_keys = ['ID', 'Snapshot ID', 'Status']
instances = cs.share_snapshot_instances.list(
detailed=args.detailed, snapshot=snapshot)
cliutils.print_list(instances, list_of_keys)
@api_versions.wraps("2.19")
@cliutils.arg(
'snapshot_instance',
metavar='<snapshot_instance>',
help='ID of the share snapshot instance.')
def do_snapshot_instance_show(cs, args):
"""Show details about a share snapshot instance."""
snapshot_instance = _find_share_snapshot_instance(
cs, args.snapshot_instance)
cliutils.print_dict(snapshot_instance._info)
@cliutils.arg(
'snapshot_instance',
metavar='<snapshot_instance>',
help='ID of the snapshot instance to modify.')
@cliutils.arg(
'--state',
metavar='<state>',
default='available',
help=('Indicate which state to assign the snapshot instance. '
'Options include available, error, creating, deleting, '
'error_deleting. If no state is provided, available '
'will be used.'))
@api_versions.wraps("2.19")
def do_snapshot_instance_reset_state(cs, args):
"""Explicitly update the state of a share snapshot instance."""
snapshot_instance = _find_share_snapshot_instance(
cs, args.snapshot_instance)
snapshot_instance.reset_state(args.state)
@cliutils.arg(
'share',
metavar='<share>',

View File

@ -0,0 +1,3 @@
---
features:
- Add list, show, and reset-status admin commands for snapshot instances.