Implement share revert to snapshot

This commit adds the ability for Manila to revert a
share to the latest available snapshot.

The feature is implemented in the LVM driver, for
testing purposes.

APIImpact
DocImpact
Co-Authored-By: Ben Swartzlander <ben@swartzlander.org>
Co-Authored-By: Andrew Kerr <andrew.kerr@netapp.com>
Implements: blueprint manila-share-revert-to-snapshot
Change-Id: Id497e13070e0003db2db951526a52de6c2182cca
This commit is contained in:
Clinton Knight 2016-06-08 13:46:51 -07:00
parent 8e0904e8c2
commit 7f16b8c0a9
9 changed files with 394 additions and 12 deletions

View File

@ -10,11 +10,13 @@
# License for the specific language governing permissions and limitations
# under the License.
# Shares
STATUS_ERROR = 'error'
STATUS_AVAILABLE = 'available'
STATUS_ERROR_DELETING = 'error_deleting'
TEMPEST_MANILA_PREFIX = 'tempest-manila'
# Replication
REPLICATION_STYLE_READABLE = 'readable'
REPLICATION_STYLE_WRITABLE = 'writable'
REPLICATION_STYLE_DR = 'dr'
@ -31,6 +33,7 @@ REPLICATION_STATE_ACTIVE = 'active'
REPLICATION_STATE_IN_SYNC = 'in_sync'
REPLICATION_STATE_OUT_OF_SYNC = 'out_of_sync'
# Access Rules
RULE_STATE_ACTIVE = 'active'
RULE_STATE_OUT_OF_SYNC = 'out_of_sync'
RULE_STATE_ERROR = 'error'
@ -50,3 +53,10 @@ TASK_STATE_DATA_COPYING_COMPLETING = 'data_copying_completing'
TASK_STATE_DATA_COPYING_COMPLETED = 'data_copying_completed'
TASK_STATE_DATA_COPYING_CANCELLED = 'data_copying_cancelled'
TASK_STATE_DATA_COPYING_ERROR = 'data_copying_error'
# Revert to snapshot
REVERT_TO_SNAPSHOT_MICROVERSION = '2.27'
REVERT_TO_SNAPSHOT_SUPPORT = 'revert_to_snapshot_support'
STATUS_RESTORING = 'restoring'
STATUS_REVERTING = 'reverting'
STATUS_REVERTING_ERROR = 'reverting_error'

View File

@ -30,7 +30,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion",
default="2.26",
default="2.27",
help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."),
cfg.StrOpt("region",
@ -103,7 +103,11 @@ ShareGroup = [
"Defaults to the value of run_snapshot_tests. Set it to "
"False if the driver being tested does not support "
"creating shares from snapshots."),
cfg.BoolOpt("capability_revert_to_snapshot_support",
help="Defines extra spec that satisfies specific back end "
"capability called 'revert_to_snapshot_support' "
"and will be used for setting up custom share type. "
"Defaults to the value of run_revert_to_snapshot_tests."),
cfg.StrOpt("share_network_id",
default="",
help="Some backend drivers requires share network "
@ -161,6 +165,11 @@ ShareGroup = [
help="Defines whether to run tests that use share snapshots "
"or not. Disable this feature if used driver doesn't "
"support it."),
cfg.BoolOpt("run_revert_to_snapshot_tests",
default=False,
help="Defines whether to run tests that revert shares "
"to snapshots or not. Enable this feature if used "
"driver supports it."),
cfg.BoolOpt("run_consistency_group_tests",
default=True,
help="Defines whether to run consistency group tests or not. "

View File

@ -50,6 +50,12 @@ class ManilaTempestPlugin(plugins.TempestPlugin):
conf.share.run_snapshot_tests,
group="share",
)
if conf.share.capability_revert_to_snapshot_support is None:
conf.set_default(
"capability_revert_to_snapshot_support",
conf.share.run_revert_to_snapshot_tests,
group="share",
)
def get_opt_lists(self):
return [(config_share.share_group.name, config_share.ShareGroup),

View File

@ -551,6 +551,67 @@ class SharesV2Client(shares_client.SharesClient):
self.expected_success(202, resp.status)
return body
###############
def revert_to_snapshot(self, share_id, snapshot_id,
version=LATEST_MICROVERSION):
url = 'shares/%s/action' % share_id
body = json.dumps({'revert': {'snapshot_id': snapshot_id}})
resp, body = self.post(url, body, version=version)
self.expected_success(202, resp.status)
return self._parse_resp(body)
###############
def create_share_type_extra_specs(self, share_type_id, extra_specs,
version=LATEST_MICROVERSION):
url = "types/%s/extra_specs" % share_type_id
post_body = json.dumps({'extra_specs': extra_specs})
resp, body = self.post(url, post_body, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def get_share_type_extra_spec(self, share_type_id, extra_spec_name,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
resp, body = self.get(uri, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def get_share_type_extra_specs(self, share_type_id, params=None,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs" % share_type_id
if params is not None:
uri += '?%s' % urlparse.urlencode(params)
resp, body = self.get(uri, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def update_share_type_extra_spec(self, share_type_id, spec_name,
spec_value, version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs/%s" % (share_type_id, spec_name)
extra_spec = {spec_name: spec_value}
post_body = json.dumps(extra_spec)
resp, body = self.put(uri, post_body, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def update_share_type_extra_specs(self, share_type_id, extra_specs,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs" % share_type_id
extra_specs = {"extra_specs": extra_specs}
post_body = json.dumps(extra_specs)
resp, body = self.post(uri, post_body, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def delete_share_type_extra_spec(self, share_type_id, extra_spec_name,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
resp, body = self.delete(uri, version=version)
self.expected_success(202, resp.status)
return body
###############
def get_snapshot_instance(self, instance_id, version=LATEST_MICROVERSION):
@ -726,13 +787,6 @@ class SharesV2Client(shares_client.SharesClient):
self.expected_success(200, resp.status)
return self._parse_resp(body)
def delete_share_type_extra_spec(self, share_type_id, extra_spec_name,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
resp, body = self.delete(uri, version=version)
self.expected_success(202, resp.status)
return body
def list_access_to_share_type(self, share_type_id,
version=LATEST_MICROVERSION,
action_name=None):

View File

@ -14,11 +14,16 @@
# under the License.
import ddt
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions as lib_exc
from testtools import testcase as tc
from manila_tempest_tests.common import constants
from manila_tempest_tests.tests.api import base
from manila_tempest_tests import utils
CONF = config.CONF
@ddt.ddt
@ -69,8 +74,12 @@ class ExtraSpecsAdminNegativeTest(base.BaseSharesMixedTest):
share_type = self.shares_v2_client.get_share_type(
st['share_type']['id'])
# Verify a non-admin can only read the required extra-specs
expected_keys = ['driver_handles_share_servers', 'snapshot_support',
'create_share_from_snapshot_support']
expected_keys = ['driver_handles_share_servers', 'snapshot_support']
if utils.is_microversion_ge(CONF.share.max_api_microversion, '2.24'):
expected_keys.append('create_share_from_snapshot_support')
if utils.is_microversion_ge(CONF.share.max_api_microversion,
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
expected_keys.append('revert_to_snapshot_support')
actual_keys = share_type['share_type']['extra_specs'].keys()
self.assertEqual(sorted(expected_keys), sorted(actual_keys),
'Incorrect extra specs visible to non-admin user; '

View File

@ -740,6 +740,8 @@ class BaseSharesTest(test.BaseTestCase):
CONF.share.capability_snapshot_support)
create_from_snapshot_support = six.text_type(
CONF.share.capability_create_share_from_snapshot_support)
revert_to_snapshot_support = six.text_type(
CONF.share.capability_revert_to_snapshot_support)
extra_specs_dict = {
"driver_handles_share_servers": dhss,
@ -748,6 +750,7 @@ class BaseSharesTest(test.BaseTestCase):
optional = {
"snapshot_support": snapshot_support,
"create_share_from_snapshot_support": create_from_snapshot_support,
"revert_to_snapshot_support": revert_to_snapshot_support,
}
# NOTE(gouthamr): In micro-versions < 2.24, snapshot_support is a
# required extra-spec

View File

@ -0,0 +1,109 @@
# Copyright 2016 Andrew Kerr
# 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 tempest import config
from tempest.lib.common.utils import data_utils
from testtools import testcase as tc
from manila_tempest_tests.common import constants
from manila_tempest_tests.tests.api import base
CONF = config.CONF
@base.skip_if_microversion_not_supported(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
@ddt.ddt
class RevertToSnapshotTest(base.BaseSharesMixedTest):
@classmethod
def skip_checks(cls):
super(RevertToSnapshotTest, cls).skip_checks()
if not CONF.share.run_revert_to_snapshot_tests:
msg = "Revert to snapshot tests are disabled."
raise cls.skipException(msg)
if not CONF.share.capability_revert_to_snapshot_support:
msg = "Revert to snapshot support is disabled."
raise cls.skipException(msg)
if not CONF.share.capability_snapshot_support:
msg = "Snapshot support is disabled."
raise cls.skipException(msg)
if not CONF.share.run_snapshot_tests:
msg = "Snapshot tests are disabled."
raise cls.skipException(msg)
@classmethod
def resource_setup(cls):
super(RevertToSnapshotTest, cls).resource_setup()
cls.admin_client = cls.admin_shares_v2_client
pools = cls.admin_client.list_pools(detail=True)['pools']
revert_support = [
pool['capabilities'][constants.REVERT_TO_SNAPSHOT_SUPPORT]
for pool in pools]
if not any(revert_support):
msg = "Revert to snapshot not supported."
raise cls.skipException(msg)
cls.share_type_name = data_utils.rand_name("share-type")
extra_specs = {constants.REVERT_TO_SNAPSHOT_SUPPORT: True}
cls.revert_enabled_extra_specs = cls.add_extra_specs_to_dict(
extra_specs=extra_specs)
cls.share_type = cls.create_share_type(
cls.share_type_name,
extra_specs=cls.revert_enabled_extra_specs,
client=cls.admin_client)
cls.st_id = cls.share_type['share_type']['id']
cls.share = cls.create_share(share_type_id=cls.st_id)
@tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_latest_snapshot(self, version):
snapshot = self.create_snapshot_wait_for_active(self.share['id'],
cleanup_in_class=False)
self.shares_v2_client.revert_to_snapshot(
self.share['id'],
snapshot['id'],
version=version)
self.shares_v2_client.wait_for_share_status(self.share['id'],
constants.STATUS_AVAILABLE)
@tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_previous_snapshot(self, version):
snapshot1 = self.create_snapshot_wait_for_active(
self.share['id'], cleanup_in_class=False)
snapshot2 = self.create_snapshot_wait_for_active(
self.share['id'], cleanup_in_class=False)
self.shares_v2_client.delete_snapshot(snapshot2['id'])
self.shares_v2_client.wait_for_resource_deletion(
snapshot_id=snapshot2['id'])
self.shares_v2_client.revert_to_snapshot(self.share['id'],
snapshot1['id'],
version=version)
self.shares_v2_client.wait_for_share_status(self.share['id'],
constants.STATUS_AVAILABLE)

View File

@ -0,0 +1,162 @@
# Copyright 2016 Andrew Kerr
# 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 tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions
from testtools import testcase as tc
from manila_tempest_tests.common import constants
from manila_tempest_tests.tests.api import base
CONF = config.CONF
@base.skip_if_microversion_not_supported(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
@ddt.ddt
class RevertToSnapshotNegativeTest(base.BaseSharesMixedTest):
@classmethod
def skip_checks(cls):
super(RevertToSnapshotNegativeTest, cls).skip_checks()
if not CONF.share.run_revert_to_snapshot_tests:
msg = "Revert to snapshot tests are disabled."
raise cls.skipException(msg)
if not CONF.share.capability_revert_to_snapshot_support:
msg = "Revert to snapshot support is disabled."
raise cls.skipException(msg)
if not CONF.share.capability_snapshot_support:
msg = "Snapshot support is disabled."
raise cls.skipException(msg)
if not CONF.share.run_snapshot_tests:
msg = "Snapshot tests are disabled."
raise cls.skipException(msg)
@classmethod
def resource_setup(cls):
super(RevertToSnapshotNegativeTest, cls).resource_setup()
cls.admin_client = cls.admin_shares_v2_client
pools = cls.admin_client.list_pools(detail=True)['pools']
revert_support = [
pool['capabilities'][constants.REVERT_TO_SNAPSHOT_SUPPORT]
for pool in pools]
if not any(revert_support):
msg = "Revert to snapshot not supported."
raise cls.skipException(msg)
cls.share_type_name = data_utils.rand_name("share-type")
extra_specs = {constants.REVERT_TO_SNAPSHOT_SUPPORT: True}
cls.revert_enabled_extra_specs = cls.add_extra_specs_to_dict(
extra_specs=extra_specs)
cls.share_type = cls.create_share_type(
cls.share_type_name,
extra_specs=cls.revert_enabled_extra_specs,
client=cls.admin_client)
cls.st_id = cls.share_type['share_type']['id']
cls.share = cls.create_share(share_type_id=cls.st_id)
cls.share2 = cls.create_share(share_type_id=cls.st_id)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_second_latest_snapshot(self, version):
snapshot1 = self.create_snapshot_wait_for_active(
self.share['id'], cleanup_in_class=False)
self.create_snapshot_wait_for_active(self.share['id'],
cleanup_in_class=False)
self.assertRaises(exceptions.Conflict,
self.shares_v2_client.revert_to_snapshot,
self.share['id'],
snapshot1['id'],
version=version)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_error_snapshot(self, version):
snapshot = self.create_snapshot_wait_for_active(self.share['id'],
cleanup_in_class=False)
self.admin_client.reset_state(snapshot['id'],
status=constants.STATUS_ERROR,
s_type='snapshots')
self.assertRaises(exceptions.Conflict,
self.shares_v2_client.revert_to_snapshot,
self.share['id'],
snapshot['id'],
version=version)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_error_share_to_snapshot(self, version):
snapshot = self.create_snapshot_wait_for_active(self.share['id'],
cleanup_in_class=False)
self.admin_client.reset_state(self.share['id'],
status=constants.STATUS_ERROR,
s_type='shares')
self.addCleanup(self.admin_client.reset_state,
self.share['id'],
status=constants.STATUS_AVAILABLE,
s_type='shares')
self.assertRaises(exceptions.Conflict,
self.shares_v2_client.revert_to_snapshot,
self.share['id'],
snapshot['id'],
version=version)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_missing_snapshot(self, version):
self.assertRaises(exceptions.BadRequest,
self.shares_v2_client.revert_to_snapshot,
self.share['id'],
self.share['id'],
version=version)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_invalid_snapshot(self, version):
snapshot = self.create_snapshot_wait_for_active(
self.share['id'], cleanup_in_class=False)
self.assertRaises(exceptions.BadRequest,
self.shares_v2_client.revert_to_snapshot,
self.share2['id'],
snapshot['id'],
version=version)

View File

@ -20,6 +20,7 @@ from tempest.lib.common.utils import data_utils
import testtools
from testtools import testcase as tc
from manila_tempest_tests.common import constants
from manila_tempest_tests.tests.api import base
from manila_tempest_tests import utils
@ -106,6 +107,9 @@ class SharesActionsTest(base.BaseSharesTest):
expected_keys.append("user_id")
if utils.is_microversion_ge(version, '2.24'):
expected_keys.append("create_share_from_snapshot_support")
if utils.is_microversion_ge(version,
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
expected_keys.append("revert_to_snapshot_support")
actual_keys = list(share.keys())
[self.assertIn(key, actual_keys) for key in expected_keys]
@ -167,6 +171,12 @@ class SharesActionsTest(base.BaseSharesTest):
def test_get_share_with_create_share_from_snapshot_support(self):
self._get_share('2.24')
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
@utils.skip_if_microversion_not_supported(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
def test_get_share_with_revert_to_snapshot_support(self):
self._get_share(constants.REVERT_TO_SNAPSHOT_MICROVERSION)
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
def test_list_shares(self):
@ -213,6 +223,9 @@ class SharesActionsTest(base.BaseSharesTest):
keys.append("user_id")
if utils.is_microversion_ge(version, '2.24'):
keys.append("create_share_from_snapshot_support")
if utils.is_microversion_ge(version,
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
keys.append("revert_to_snapshot_support")
[self.assertIn(key, sh.keys()) for sh in shares for key in keys]
# our shares in list and have no duplicates
@ -264,6 +277,13 @@ class SharesActionsTest(base.BaseSharesTest):
self):
self._list_shares_with_detail('2.24')
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
@utils.skip_if_microversion_not_supported(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
def test_list_shares_with_detail_with_revert_to_snapshot_support(self):
self._list_shares_with_detail(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
def test_list_shares_with_detail_filter_by_metadata(self):
filters = {'metadata': self.metadata}