Manage and unmanage snapshot

Add APIs to support manage and unmanage share snapshots.
Also add support in the Generic driver.
This only supports for DHSS=False driver mode.

Add provider_location column to the share_snapshots table
to save data used to identify the snapshot on the backend.

Also need to bump microversion.

APIImpact
DocImpact
Change-Id: I87a066173c85d969607d132accd9f0e9bd49c235
Implements: blueprint manage-unmanage-snapshot
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index 8183d1c..3f8c08a 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -36,7 +36,7 @@
                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.11",
+               default="2.12",
                help="The maximum api microversion is configured to be the "
                     "value of the latest microversion supported by Manila."),
     cfg.StrOpt("region",
@@ -128,11 +128,6 @@
                 help="Whether to suppress errors with clean up operation "
                      "or not. There are cases when we may want to skip "
                      "such errors and catch only test errors."),
-    cfg.BoolOpt("run_manage_unmanage_tests",
-                default=False,
-                help="Defines whether to run manage/unmanage tests or not. "
-                     "These test may leave orphaned resources, so be careful "
-                     "enabling this opt."),
 
     # Switching ON/OFF test suites filtered by features
     cfg.BoolOpt("run_quota_tests",
@@ -161,6 +156,16 @@
     cfg.BoolOpt("run_migration_tests",
                 default=False,
                 help="Enable or disable migration tests."),
+    cfg.BoolOpt("run_manage_unmanage_tests",
+                default=False,
+                help="Defines whether to run manage/unmanage tests or not. "
+                     "These test may leave orphaned resources, so be careful "
+                     "enabling this opt."),
+    cfg.BoolOpt("run_manage_unmanage_snapshot_tests",
+                default=False,
+                help="Defines whether to run manage/unmanage snapshot tests "
+                     "or not. These tests may leave orphaned resources, so be "
+                     "careful enabling this opt."),
 
     cfg.StrOpt("image_with_share_tools",
                default="manila-service-image",
diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py
index 7a454fa..5187145 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -439,6 +439,113 @@
 
 ###############
 
+    def create_snapshot(self, share_id, name=None, description=None,
+                        force=False, version=LATEST_MICROVERSION):
+        if name is None:
+            name = data_utils.rand_name("tempest-created-share-snap")
+        if description is None:
+            description = data_utils.rand_name(
+                "tempest-created-share-snap-desc")
+        post_body = {
+            "snapshot": {
+                "name": name,
+                "force": force,
+                "description": description,
+                "share_id": share_id,
+            }
+        }
+        body = json.dumps(post_body)
+        resp, body = self.post("snapshots", body, version=version)
+        self.expected_success(202, resp.status)
+        return self._parse_resp(body)
+
+    def get_snapshot(self, snapshot_id, version=LATEST_MICROVERSION):
+        resp, body = self.get("snapshots/%s" % snapshot_id, version=version)
+        self.expected_success(200, resp.status)
+        return self._parse_resp(body)
+
+    def list_snapshots(self, detailed=False, params=None,
+                       version=LATEST_MICROVERSION):
+        """Get list of share snapshots w/o filters."""
+        uri = 'snapshots/detail' if detailed else 'snapshots'
+        uri += '?%s' % urlparse.urlencode(params) if params else ''
+        resp, body = self.get(uri, version=version)
+        self.expected_success(200, resp.status)
+        return self._parse_resp(body)
+
+    def list_snapshots_with_detail(self, params=None,
+                                   version=LATEST_MICROVERSION):
+        """Get detailed list of share snapshots w/o filters."""
+        return self.list_snapshots(detailed=True, params=params,
+                                   version=version)
+
+    def delete_snapshot(self, snap_id, version=LATEST_MICROVERSION):
+        resp, body = self.delete("snapshots/%s" % snap_id, version=version)
+        self.expected_success(202, resp.status)
+        return body
+
+    def wait_for_snapshot_status(self, snapshot_id, status,
+                                 version=LATEST_MICROVERSION):
+        """Waits for a snapshot to reach a given status."""
+        body = self.get_snapshot(snapshot_id, version=version)
+        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_id, version=version)
+            snapshot_status = body['status']
+            if 'error' in snapshot_status:
+                raise (share_exceptions.
+                       SnapshotBuildErrorException(snapshot_id=snapshot_id))
+
+            if int(time.time()) - start >= self.build_timeout:
+                message = ('Share Snapshot %s failed to reach %s status '
+                           'within the required time (%s s).' %
+                           (snapshot_name, status, self.build_timeout))
+                raise exceptions.TimeoutException(message)
+
+    def manage_snapshot(self, share_id, provider_location,
+                        name=None, description=None,
+                        version=LATEST_MICROVERSION,
+                        driver_options=None):
+        if name is None:
+            name = data_utils.rand_name("tempest-manage-snapshot")
+        if description is None:
+            description = data_utils.rand_name("tempest-manage-snapshot-desc")
+        post_body = {
+            "snapshot": {
+                "share_id": share_id,
+                "provider_location": provider_location,
+                "name": name,
+                "description": description,
+                "driver_options": driver_options if driver_options else {},
+            }
+        }
+        url = 'snapshots/manage'
+        body = json.dumps(post_body)
+        resp, body = self.post(url, body, version=version)
+        self.expected_success(202, resp.status)
+        return self._parse_resp(body)
+
+    def unmanage_snapshot(self, snapshot_id, version=LATEST_MICROVERSION,
+                          body=None):
+        url = 'snapshots'
+        action_name = 'action'
+        if body is None:
+            body = json.dumps({'unmanage': {}})
+        resp, body = self.post(
+            "%(url)s/%(snapshot_id)s/%(action_name)s" % {
+                'url': url, 'snapshot_id': snapshot_id,
+                'action_name': action_name},
+            body,
+            version=version)
+        self.expected_success(202, resp.status)
+        return body
+
+###############
+
     def _get_access_action_name(self, version, action):
         if utils.is_microversion_gt(version, "2.6"):
             return action.split('os-')[-1]
diff --git a/manila_tempest_tests/tests/api/admin/test_snapshot_manage.py b/manila_tempest_tests/tests/api/admin/test_snapshot_manage.py
new file mode 100644
index 0000000..4bd7649
--- /dev/null
+++ b/manila_tempest_tests/tests/api/admin/test_snapshot_manage.py
@@ -0,0 +1,143 @@
+# Copyright 2015 EMC Corporation.
+# 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 six
+from tempest import config
+from tempest import test
+from tempest_lib.common.utils import data_utils
+from tempest_lib import exceptions as lib_exc
+import testtools
+
+from manila_tempest_tests.tests.api import base
+
+CONF = config.CONF
+
+
+class ManageNFSSnapshotTest(base.BaseSharesAdminTest):
+    protocol = 'nfs'
+
+    # NOTE(vponomaryov): be careful running these tests using generic driver
+    # because cinder volume snapshots won't be deleted.
+
+    @classmethod
+    @base.skip_if_microversion_lt("2.12")
+    @testtools.skipIf(
+        CONF.share.multitenancy_enabled,
+        "Only for driver_handles_share_servers = False driver mode.")
+    @testtools.skipUnless(
+        CONF.share.run_manage_unmanage_snapshot_tests,
+        "Manage/unmanage snapshot tests are disabled.")
+    def resource_setup(cls):
+        super(ManageNFSSnapshotTest, cls).resource_setup()
+        if cls.protocol not in CONF.share.enable_protocols:
+            message = "%s tests are disabled" % cls.protocol
+            raise cls.skipException(message)
+
+        # Create share type
+        cls.st_name = data_utils.rand_name("tempest-manage-st-name")
+        cls.extra_specs = {
+            'storage_protocol': CONF.share.capability_storage_protocol,
+            'driver_handles_share_servers': False,
+            'snapshot_support': six.text_type(
+                CONF.share.capability_snapshot_support),
+        }
+
+        cls.st = cls.create_share_type(
+            name=cls.st_name,
+            cleanup_in_class=True,
+            extra_specs=cls.extra_specs)
+
+        creation_data = {'kwargs': {
+            'share_type_id': cls.st['share_type']['id'],
+            'share_protocol': cls.protocol,
+        }}
+
+        # Data for creating shares
+        data = [creation_data]
+        shares_created = cls.create_shares(data)
+
+        cls.snapshot = None
+        cls.shares = []
+        # Load all share data (host, etc.)
+        for share in shares_created:
+            cls.shares.append(cls.shares_v2_client.get_share(share['id']))
+            # Create snapshot
+            snap_name = data_utils.rand_name("tempest-snapshot-name")
+            snap_desc = data_utils.rand_name(
+                "tempest-snapshot-description")
+            snap = cls.create_snapshot_wait_for_active(
+                share['id'], snap_name, snap_desc)
+            cls.snapshot = cls.shares_v2_client.get_snapshot(snap['id'])
+            # Unmanage snapshot
+            cls.shares_v2_client.unmanage_snapshot(snap['id'])
+            cls.shares_client.wait_for_resource_deletion(
+                snapshot_id=snap['id'])
+
+    def _test_manage(self, snapshot, version=CONF.share.max_api_microversion):
+        name = ("Name for 'managed' snapshot that had ID %s" %
+                snapshot['id'])
+        description = "Description for 'managed' snapshot"
+
+        # Manage snapshot
+        share_id = snapshot['share_id']
+        snapshot = self.shares_v2_client.manage_snapshot(
+            share_id,
+            snapshot['provider_location'],
+            name=name,
+            description=description,
+            driver_options={}
+        )
+
+        # Add managed snapshot to cleanup queue
+        self.method_resources.insert(
+            0, {'type': 'snapshot', 'id': snapshot['id'],
+                'client': self.shares_v2_client})
+
+        # Wait for success
+        self.shares_v2_client.wait_for_snapshot_status(snapshot['id'],
+                                                       'available')
+
+        # Verify data of managed snapshot
+        get_snapshot = self.shares_v2_client.get_snapshot(snapshot['id'])
+        self.assertEqual(name, get_snapshot['name'])
+        self.assertEqual(description, get_snapshot['description'])
+        self.assertEqual(snapshot['share_id'], get_snapshot['share_id'])
+        self.assertEqual(snapshot['provider_location'],
+                         get_snapshot['provider_location'])
+
+        # Delete snapshot
+        self.shares_v2_client.delete_snapshot(get_snapshot['id'])
+        self.shares_client.wait_for_resource_deletion(
+            snapshot_id=get_snapshot['id'])
+        self.assertRaises(lib_exc.NotFound,
+                          self.shares_v2_client.get_snapshot,
+                          get_snapshot['id'])
+
+    @test.attr(type=["gate", "smoke"])
+    def test_manage(self):
+        # Manage snapshot
+        self._test_manage(snapshot=self.snapshot)
+
+
+class ManageCIFSSnapshotTest(ManageNFSSnapshotTest):
+    protocol = 'cifs'
+
+
+class ManageGLUSTERFSSnapshotTest(ManageNFSSnapshotTest):
+    protocol = 'glusterfs'
+
+
+class ManageHDFSSnapshotTest(ManageNFSSnapshotTest):
+    protocol = 'hdfs'
diff --git a/manila_tempest_tests/tests/api/admin/test_snapshot_manage_negative.py b/manila_tempest_tests/tests/api/admin/test_snapshot_manage_negative.py
new file mode 100644
index 0000000..c2d7804
--- /dev/null
+++ b/manila_tempest_tests/tests/api/admin/test_snapshot_manage_negative.py
@@ -0,0 +1,109 @@
+# Copyright 2015 EMC Corporation.
+# 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 six
+from tempest import config
+from tempest import test
+from tempest_lib.common.utils import data_utils
+from tempest_lib import exceptions as lib_exc
+import testtools
+
+from manila_tempest_tests.tests.api import base
+
+CONF = config.CONF
+
+
+class ManageNFSSnapshotNegativeTest(base.BaseSharesAdminTest):
+    protocol = 'nfs'
+
+    @classmethod
+    @base.skip_if_microversion_lt("2.12")
+    @testtools.skipIf(
+        CONF.share.multitenancy_enabled,
+        "Only for driver_handles_share_servers = False driver mode.")
+    @testtools.skipUnless(
+        CONF.share.run_manage_unmanage_snapshot_tests,
+        "Manage/unmanage snapshot tests are disabled.")
+    def resource_setup(cls):
+        super(ManageNFSSnapshotNegativeTest, cls).resource_setup()
+        if cls.protocol not in CONF.share.enable_protocols:
+            message = "%s tests are disabled" % cls.protocol
+            raise cls.skipException(message)
+
+        # Create share type
+        cls.st_name = data_utils.rand_name("tempest-manage-st-name")
+        cls.extra_specs = {
+            'storage_protocol': CONF.share.capability_storage_protocol,
+            'driver_handles_share_servers': False,
+            'snapshot_support': six.text_type(
+                CONF.share.capability_snapshot_support),
+        }
+
+        cls.st = cls.create_share_type(
+            name=cls.st_name,
+            cleanup_in_class=True,
+            extra_specs=cls.extra_specs)
+
+        # Create share
+        cls.share = cls.create_share(
+            share_type_id=cls.st['share_type']['id'],
+            share_protocol=cls.protocol
+        )
+
+    @test.attr(type=["gate", "smoke", "negative", ])
+    def test_manage_not_found(self):
+        # Manage snapshot fails
+        self.assertRaises(lib_exc.NotFound,
+                          self.shares_v2_client.manage_snapshot,
+                          'fake-share-id',
+                          'fake-vol-snap-id',
+                          driver_options={})
+
+    @test.attr(type=["gate", "smoke", "negative", ])
+    def test_manage_already_exists(self):
+        # Manage already existing snapshot fails
+
+        # Create snapshot
+        snap = self.create_snapshot_wait_for_active(self.share['id'])
+        get_snap = self.shares_v2_client.get_snapshot(snap['id'])
+        self.assertEqual(self.share['id'], get_snap['share_id'])
+        self.assertIsNotNone(get_snap['provider_location'])
+
+        # Manage snapshot fails
+        self.assertRaises(lib_exc.Conflict,
+                          self.shares_v2_client.manage_snapshot,
+                          self.share['id'],
+                          get_snap['provider_location'],
+                          driver_options={})
+
+        # Delete snapshot
+        self.shares_v2_client.delete_snapshot(get_snap['id'])
+        self.shares_client.wait_for_resource_deletion(
+            snapshot_id=get_snap['id'])
+        self.assertRaises(lib_exc.NotFound,
+                          self.shares_v2_client.get_snapshot,
+                          get_snap['id'])
+
+
+class ManageCIFSSnapshotNegativeTest(ManageNFSSnapshotNegativeTest):
+    protocol = 'cifs'
+
+
+class ManageGLUSTERFSSnapshotNegativeTest(ManageNFSSnapshotNegativeTest):
+    protocol = 'glusterfs'
+
+
+class ManageHDFSSnapshotNegativeTest(ManageNFSSnapshotNegativeTest):
+    protocol = 'hdfs'
diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py
index f9a678d..8ee5ddd 100644
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -79,6 +79,7 @@
 
 
 skip_if_microversion_not_supported = utils.skip_if_microversion_not_supported
+skip_if_microversion_lt = utils.skip_if_microversion_lt
 
 
 class BaseSharesTest(test.BaseTestCase):
@@ -104,6 +105,13 @@
             raise self.skipException(
                 "Microversion '%s' is not supported." % microversion)
 
+    def skip_if_microversion_lt(self, microversion):
+        if utils.is_microversion_lt(CONF.share.max_api_microversion,
+                                    microversion):
+            raise self.skipException(
+                "Microversion must be greater than or equal to '%s'." %
+                microversion)
+
     @classmethod
     def get_client_with_isolated_creds(cls,
                                        name=None,
diff --git a/manila_tempest_tests/utils.py b/manila_tempest_tests/utils.py
index 94d8cd3..dea51ab 100644
--- a/manila_tempest_tests/utils.py
+++ b/manila_tempest_tests/utils.py
@@ -81,6 +81,15 @@
     return lambda f: f
 
 
+def skip_if_microversion_lt(microversion):
+    """Decorator for tests that are microversion-specific."""
+    if is_microversion_lt(CONF.share.max_api_microversion, microversion):
+        reason = ("Skipped. Test requires microversion greater than or "
+                  "equal to '%s'." % microversion)
+        return testtools.skip(reason)
+    return lambda f: f
+
+
 def rand_ip():
     """This uses the TEST-NET-3 range of reserved IP addresses.