Merge "Add unstable tag to test with intermittent failures"
diff --git a/manila_tempest_tests/common/waiters.py b/manila_tempest_tests/common/waiters.py
index 056dab6..24b30de 100644
--- a/manila_tempest_tests/common/waiters.py
+++ b/manila_tempest_tests/common/waiters.py
@@ -179,3 +179,41 @@
                        ' the required time (%s s).' %
                        (resource_id, client.build_timeout))
             raise exceptions.TimeoutException(message)
+
+
+def wait_for_soft_delete(client, share_id, version=LATEST_MICROVERSION):
+    """Wait for a share soft delete to recycle bin."""
+    share = client.get_share(share_id, version=version)['share']
+    start = int(time.time())
+    while not share['is_soft_deleted']:
+        time.sleep(client.build_interval)
+        share = client.get_share(share_id, version=version)['share']
+        if share['is_soft_deleted']:
+            break
+        elif int(time.time()) - start >= client.build_timeout:
+            message = ('Share %(share_id)s failed to be soft deleted to '
+                       'recycle bin within the required time '
+                       '%(timeout)s.' % {
+                           'share_id': share['id'],
+                           'timeout': client.build_timeout,
+                       })
+            raise exceptions.TimeoutException(message)
+
+
+def wait_for_restore(client, share_id, version=LATEST_MICROVERSION):
+    """Wait for a share restore from recycle bin."""
+    share = client.get_share(share_id, version=version)['share']
+    start = int(time.time())
+    while share['is_soft_deleted']:
+        time.sleep(client.build_interval)
+        share = client.get_share(share_id, version=version)['share']
+        if not share['is_soft_deleted']:
+            break
+        elif int(time.time()) - start >= client.build_timeout:
+            message = ('Share %(share_id)s failed to restore from '
+                       'recycle bin within the required time '
+                       '%(timeout)s.' % {
+                           'share_id': share['id'],
+                           'timeout': client.build_timeout,
+                       })
+            raise exceptions.TimeoutException(message)
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index 92cf7ba..f5022af 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -40,7 +40,7 @@
                     "This value is only used to validate the versions "
                     "response from Manila."),
     cfg.StrOpt("max_api_microversion",
-               default="2.65",
+               default="2.69",
                help="The maximum api microversion is configured to be the "
                     "value of the latest microversion supported by Manila."),
     cfg.StrOpt("region",
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 293c24a..6db5393 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -299,6 +299,20 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
+    def list_shares_in_recycle_bin(self, detailed=False,
+                                   params=None, version=LATEST_MICROVERSION,
+                                   experimental=False):
+        """Get list of shares in recycle bin with w/o filters."""
+        headers = EXPERIMENTAL if experimental else None
+        uri = 'shares/detail' if detailed else 'shares'
+        uri += '?is_soft_deleted=true'
+        uri += '&%s' % parse.urlencode(params) if params else ''
+        resp, body = self.get(uri, headers=headers, extra_headers=experimental,
+                              version=version)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
     def list_shares_with_detail(self, params=None,
                                 version=LATEST_MICROVERSION,
                                 experimental=False):
@@ -342,6 +356,22 @@
         self.expected_success(202, resp.status)
         return rest_client.ResponseBody(resp, body)
 
+    def soft_delete_share(self, share_id, version=LATEST_MICROVERSION):
+        post_body = {"soft_delete": None}
+        body = json.dumps(post_body)
+        resp, body = self.post(
+            "shares/%s/action" % share_id, body, version=version)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def restore_share(self, share_id, version=LATEST_MICROVERSION):
+        post_body = {"restore": None}
+        body = json.dumps(post_body)
+        resp, body = self.post(
+            "shares/%s/action" % share_id, body, version=version)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
 ###############
 
     def get_instances_of_share(self, share_id, version=LATEST_MICROVERSION):
@@ -1529,13 +1559,17 @@
         return rest_client.ResponseBody(resp, body)
 
     def create_share_replica(self, share_id, availability_zone=None,
-                             version=LATEST_MICROVERSION):
+                             version=LATEST_MICROVERSION,
+                             scheduler_hints=None):
         """Add a share replica of an existing share."""
         uri = "share-replicas"
         post_body = {
             'share_id': share_id,
             'availability_zone': availability_zone,
         }
+
+        if scheduler_hints:
+            post_body["scheduler_hints"] = scheduler_hints
         headers, extra_headers = utils.get_extra_headers(
             version, constants.SHARE_REPLICA_GRADUATION_VERSION)
         body = json.dumps({'share_replica': post_body})
diff --git a/manila_tempest_tests/tests/api/admin/test_replication.py b/manila_tempest_tests/tests/api/admin/test_replication.py
index 853471c..242e786 100644
--- a/manila_tempest_tests/tests/api/admin/test_replication.py
+++ b/manila_tempest_tests/tests/api/admin/test_replication.py
@@ -45,7 +45,6 @@
     def resource_setup(cls):
         super(ReplicationAdminTest, cls).resource_setup()
         cls.admin_client = cls.admin_shares_v2_client
-        cls.member_client = cls.shares_v2_client
         cls.replication_type = CONF.share.backend_replication_type
         cls.multitenancy_enabled = (
             utils.replication_with_multitenancy_support())
@@ -138,7 +137,8 @@
                                    version=version)
         # Original replica will need to be cleaned up before the promoted
         # replica can be deleted.
-        self.addCleanup(self.delete_share_replica, original_replica['id'])
+        self.addCleanup(self.delete_share_replica, original_replica['id'],
+                        client=self.admin_client)
 
         # Check if there is still only 1 'active' replica after promotion.
         replica_list = self.admin_client.list_share_replicas(
diff --git a/manila_tempest_tests/tests/api/admin/test_scheduler_hints.py b/manila_tempest_tests/tests/api/admin/test_scheduler_hints.py
new file mode 100644
index 0000000..55c9ca7
--- /dev/null
+++ b/manila_tempest_tests/tests/api/admin/test_scheduler_hints.py
@@ -0,0 +1,113 @@
+# Copyright 2022 Cloudification GmbH
+# 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 tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from testtools import testcase as tc
+
+from manila_tempest_tests.common import constants
+from manila_tempest_tests import share_exceptions
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+LATEST_MICROVERSION = CONF.share.max_api_microversion
+
+
+class SharesSchedulerHintsAdminTest(base.BaseSharesAdminTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(SharesSchedulerHintsAdminTest, cls).skip_checks()
+        if not CONF.share.multi_backend:
+            raise cls.skipException("Manila multi-backend tests are disabled.")
+        elif len(CONF.share.backend_names) < 2:
+            raise cls.skipException("For running multi-backend tests, two or "
+                                    "more backend names must be configured.")
+        elif any(not name for name in CONF.share.backend_names):
+            raise cls.skipException("Share backend names can not be empty.")
+        utils.check_skip_if_microversion_not_supported('2.67')
+
+    @classmethod
+    def resource_setup(cls):
+        super(SharesSchedulerHintsAdminTest, cls).resource_setup()
+        # Need for requesting pools
+        cls.admin_client = cls.admin_shares_v2_client
+        # create share type
+        share_type = cls.create_share_type()
+        cls.share_type_id = share_type['id']
+
+    @decorators.idempotent_id('54f4dea7-890e-443b-aea5-f6108da893f0')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    def test_only_host_scheduler_hint_in_share_creation(self):
+        share_a = self.create_share(share_type_id=self.share_type_id)
+        share_a = self.admin_shares_v2_client.get_share(share_a['id'])['share']
+        backend_a = share_a['host']
+        scheduler_hint = {"only_host": "%s" % backend_a}
+
+        # create share with hint
+        share_b = self.create_share(share_type_id=self.share_type_id,
+                                    scheduler_hints=scheduler_hint,
+                                    cleanup_in_class=False)
+        share_b = self.admin_shares_v2_client.get_share(share_b['id'])['share']
+        backend_b = share_b['host']
+
+        # verify same backends
+        self.assertEqual(backend_a, backend_b)
+
+    @decorators.idempotent_id('1dec3306-61f4-41b9-ba4a-572a9e6f5f57')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @tc.skipUnless(CONF.share.run_replication_tests,
+                   'Replication tests are disabled.')
+    def test_only_host_scheduler_hint_in_share_replica_creation(self):
+        replication_type = CONF.share.backend_replication_type
+        if replication_type not in constants.REPLICATION_TYPE_CHOICES:
+            raise share_exceptions.ShareReplicationTypeException(
+                replication_type=replication_type
+            )
+        extra_specs = self.add_extra_specs_to_dict({
+            "replication_type": replication_type
+        })
+        replicated_share_type = self.create_share_type(
+            data_utils.rand_name("replicated-shares"),
+            extra_specs=extra_specs)
+        share = self.create_share(
+            share_type_id=replicated_share_type['id'],
+            cleanup_in_class=False)
+        share = self.admin_shares_v2_client.get_share(share['id'])['share']
+        share_host = share['host']
+        rep_domain, pools = self.get_pools_for_replication_domain(share=share)
+        if len(pools) < 2:
+            msg = ("Can not create valid hint due to insufficient pools.")
+            raise self.skipException(msg)
+
+        for p in pools:
+            if p['name'] != share_host:
+                expected_replica_host = p['name']
+                scheduler_hint = {"only_host": "%s" % expected_replica_host}
+                break
+
+        # create share replica with hint
+        replica = self.create_share_replica(share['id'],
+                                            cleanup_in_class=False,
+                                            version=LATEST_MICROVERSION,
+                                            scheduler_hints=scheduler_hint)
+        replica = self.admin_shares_v2_client.get_share_replica(
+            replica['id'])['share_replica']
+        replica_host = replica['host']
+
+        # verify same backends
+        self.assertEqual(expected_replica_host, replica_host)
diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py
index 648c9f8..d5cc439 100755
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -656,11 +656,15 @@
         azs = cls.get_availability_zones(backends=backends_matching_share_type)
         return azs
 
-    def get_pools_for_replication_domain(self):
+    def get_pools_for_replication_domain(self, share=None):
         # Get the list of pools for the replication domain
         pools = self.admin_client.list_pools(detail=True)['pools']
-        instance_host = self.admin_client.get_share(
-            self.shares[0]['id'])['share']['host']
+        if share:
+            instance_host = self.admin_client.get_share(
+                share['id'])['share']['host']
+        else:
+            instance_host = self.admin_client.get_share(
+                self.shares[0]['id'])['share']['host']
         host_pool = [p for p in pools if p['name'] == instance_host][0]
         rep_domain = host_pool['capabilities']['replication_domain']
         pools_in_rep_domain = [p for p in pools if p['capabilities'][
@@ -671,11 +675,12 @@
     def create_share_replica(cls, share_id, availability_zone=None,
                              client=None, cleanup_in_class=False,
                              cleanup=True,
-                             version=CONF.share.max_api_microversion):
+                             version=CONF.share.max_api_microversion,
+                             scheduler_hints=None):
         client = client or cls.shares_v2_client
         replica = client.create_share_replica(
             share_id, availability_zone=availability_zone,
-            version=version)['share_replica']
+            version=version, scheduler_hints=scheduler_hints)['share_replica']
         resource = {
             "type": "share_replica",
             "id": replica["id"],
diff --git a/manila_tempest_tests/tests/api/test_metadata_negative.py b/manila_tempest_tests/tests/api/test_metadata_negative.py
index a34d1a6..93a3628 100644
--- a/manila_tempest_tests/tests/api/test_metadata_negative.py
+++ b/manila_tempest_tests/tests/api/test_metadata_negative.py
@@ -113,20 +113,3 @@
         self.assertRaises(lib_exc.NotFound,
                           self.shares_client.delete_metadata,
                           self.share["id"], "wrong_key")
-
-    @decorators.idempotent_id('c6c70d55-7ed0-439f-ae34-f19af55361f6')
-    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
-    @ddt.data(("foo.xml", False), ("foo.json", False),
-              ("foo.xml", True), ("foo.json", True))
-    @ddt.unpack
-    def test_try_delete_metadata_with_unsupport_format_key(
-            self, key, is_v2_client):
-        md = {key: u"value.test"}
-
-        client = self.shares_v2_client if is_v2_client else self.shares_client
-        # set metadata
-        client.set_metadata(self.share["id"], md)
-
-        self.assertRaises(lib_exc.NotFound,
-                          client.delete_metadata,
-                          self.share["id"], key)
diff --git a/manila_tempest_tests/tests/api/test_shares_actions.py b/manila_tempest_tests/tests/api/test_shares_actions.py
index 3232d50..779bfb6 100644
--- a/manila_tempest_tests/tests/api/test_shares_actions.py
+++ b/manila_tempest_tests/tests/api/test_shares_actions.py
@@ -687,6 +687,51 @@
         )
         self.assertEqual(new_size, share_get['size'], msg)
 
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('7a19fb58-b645-44cc-a6d7-b3508ff8754d')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    def test_soft_delete_and_restore_share(self):
+        share = self.create_share(share_type_id=self.share_type_id)
+
+        # list shares
+        shares = self.shares_v2_client.list_shares()['shares']
+
+        # check the share in share list
+        share_ids = [sh['id'] for sh in shares]
+        self.assertIn(share['id'], share_ids)
+
+        # soft delete the share
+        self.shares_v2_client.soft_delete_share(share['id'])
+        waiters.wait_for_soft_delete(self.shares_v2_client, share['id'])
+
+        # list shares again
+        shares1 = self.shares_v2_client.list_shares()['shares']
+        share_ids1 = [sh['id'] for sh in shares1]
+
+        # list shares in recycle bin
+        shares2 = self.shares_v2_client.list_shares_in_recycle_bin()['shares']
+        share_ids2 = [sh['id'] for sh in shares2]
+
+        # check share has been soft delete to recycle bin
+        self.assertNotIn(share['id'], share_ids1)
+        self.assertIn(share['id'], share_ids2)
+
+        # restore share from recycle bin
+        self.shares_v2_client.restore_share(share['id'])
+        waiters.wait_for_restore(self.shares_v2_client, share['id'])
+
+        # list shares again
+        shares3 = self.shares_v2_client.list_shares()['shares']
+        share_ids3 = [sh['id'] for sh in shares3]
+
+        # list shares in recycle bin again
+        shares4 = self.shares_v2_client.list_shares_in_recycle_bin()['shares']
+        share_ids4 = [sh['id'] for sh in shares4]
+
+        # check share has restored from recycle bin
+        self.assertNotIn(share['id'], share_ids4)
+        self.assertIn(share['id'], share_ids3)
+
 
 class SharesRenameTest(base.BaseSharesMixedTest):
 
diff --git a/manila_tempest_tests/tests/api/test_shares_actions_negative.py b/manila_tempest_tests/tests/api/test_shares_actions_negative.py
index 64818fd..a600b8e 100644
--- a/manila_tempest_tests/tests/api/test_shares_actions_negative.py
+++ b/manila_tempest_tests/tests/api/test_shares_actions_negative.py
@@ -21,6 +21,7 @@
 import testtools
 from testtools import testcase as tc
 
+from manila_tempest_tests.common import waiters
 from manila_tempest_tests.tests.api import base
 from manila_tempest_tests import utils
 
@@ -294,3 +295,68 @@
         self.assertRaises(lib_exc.NotFound,
                           self.alt_shares_v2_client.get_share,
                           self.share['id'])
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('36cbe23b-08d2-49d9-bb42-f9eb2a804cb1')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_soft_delete_share_has_been_soft_deleted(self):
+        share = self.create_share(share_type_id=self.share_type_id,
+                                  cleanup_in_class=False)
+
+        # soft delete the share
+        self.shares_v2_client.soft_delete_share(share['id'])
+
+        # try soft delete the share again
+        self.assertRaises(lib_exc.Forbidden,
+                          self.shares_v2_client.soft_delete_share,
+                          share['id'])
+
+        # restore the share for resource_cleanup
+        self.shares_v2_client.restore_share(share['id'])
+        waiters.wait_for_restore(self.shares_v2_client, share['id'])
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('cf675ac9-0970-49fc-a051-8a94555c73b5')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_soft_delete_share_with_invalid_share_state(self):
+        share = self.create_share(share_type_id=self.share_type_id,
+                                  cleanup_in_class=False)
+
+        # set "error_deleting" state
+        self.admin_client.reset_state(share['id'], status="error_deleting")
+
+        # try soft delete the share
+        self.assertRaises(lib_exc.Forbidden,
+                          self.shares_v2_client.soft_delete_share,
+                          share['id'])
+
+        # rollback to available status
+        self.admin_client.reset_state(share['id'], status="available")
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('f6106ee4-1a01-444f-b623-912a5e751d49')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_soft_delete_share_from_other_project(self):
+        share = self.create_share(share_type_id=self.share_type_id,
+                                  cleanup_in_class=False)
+
+        # try soft delete the share
+        self.assertRaises(lib_exc.Forbidden,
+                          self.alt_shares_v2_client.soft_delete_share,
+                          share['id'])
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('0ccd44dd-2fda-403e-bc23-7ce428550f36')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_soft_delete_share_with_wrong_id(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.shares_v2_client.soft_delete_share,
+                          "wrong_share_id")
+
+    @utils.skip_if_microversion_not_supported("2.69")
+    @decorators.idempotent_id('87345725-f187-4d7d-86b1-62284e8c75ae')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+    def test_restore_share_with_wrong_id(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.shares_v2_client.restore_share,
+                          "wrong_share_id")