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
diff --git a/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py b/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py
index 85cbc5d..63fdf23 100644
--- a/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py
+++ b/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py
@@ -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 @@
         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; '
diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py
index 4459f59..398108d 100644
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -740,6 +740,8 @@
             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 @@
         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
diff --git a/manila_tempest_tests/tests/api/test_revert_to_snapshot.py b/manila_tempest_tests/tests/api/test_revert_to_snapshot.py
new file mode 100644
index 0000000..686b185
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_revert_to_snapshot.py
@@ -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)
diff --git a/manila_tempest_tests/tests/api/test_revert_to_snapshot_negative.py b/manila_tempest_tests/tests/api/test_revert_to_snapshot_negative.py
new file mode 100644
index 0000000..ee403af
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_revert_to_snapshot_negative.py
@@ -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)
diff --git a/manila_tempest_tests/tests/api/test_shares_actions.py b/manila_tempest_tests/tests/api/test_shares_actions.py
index d03df33..0f904fc 100644
--- a/manila_tempest_tests/tests/api/test_shares_actions.py
+++ b/manila_tempest_tests/tests/api/test_shares_actions.py
@@ -20,6 +20,7 @@
 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 @@
             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]
 
@@ -168,6 +172,12 @@
         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):
 
         # list shares
@@ -213,6 +223,9 @@
             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
@@ -265,6 +278,13 @@
         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}