Add share backup tests.

Share backup tests i.e. create/delete/get share_backups.

Change-Id: I366221702b3aa2e78bff82e6b2a543b6f6784f77
diff --git a/manila_tempest_tests/common/waiters.py b/manila_tempest_tests/common/waiters.py
index 11a02d3..a11b620 100644
--- a/manila_tempest_tests/common/waiters.py
+++ b/manila_tempest_tests/common/waiters.py
@@ -60,6 +60,7 @@
         'share_group': 'get_share_group',
         'share_group_snapshot': 'get_share_group_snapshot',
         'share_replica': 'get_share_replica',
+        'share_backup': 'get_share_backup'
     }
 
     action_name = get_resource_action[resource_name]
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index 4d36944..87b53e3 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -265,6 +265,9 @@
                 default=False,
                 help="Enable or disable migration with "
                      "preserve_snapshots tests set to True."),
+    cfg.BoolOpt("run_driver_assisted_backup_tests",
+                default=False,
+                help="Enable or disable share backup tests."),
     cfg.BoolOpt("run_manage_unmanage_tests",
                 default=False,
                 help="Defines whether to run manage/unmanage tests or not. "
@@ -314,6 +317,10 @@
                default=1500,
                help="Time to wait for share migration before "
                     "timing out (seconds)."),
+    cfg.IntOpt("share_backup_timeout",
+               default=1500,
+               help="Time to wait for share backup before "
+                    "timing out (seconds)."),
     cfg.IntOpt("share_server_migration_timeout",
                default="1500",
                help="Time to wait for share server migration before "
@@ -349,4 +356,7 @@
                     "writing data from /dev/zero might not yield significant "
                     "space savings as these systems are already optimized for "
                     "efficient compression."),
+    cfg.DictOpt("driver_assisted_backup_test_driver_options",
+                default={'dummy': True},
+                help="Share backup driver options specified as dict."),
 ]
diff --git a/manila_tempest_tests/services/share/json/shares_client.py b/manila_tempest_tests/services/share/json/shares_client.py
index 6871eda..fb03afc 100644
--- a/manila_tempest_tests/services/share/json/shares_client.py
+++ b/manila_tempest_tests/services/share/json/shares_client.py
@@ -323,6 +323,9 @@
         elif "server_id" in kwargs:
             return self._is_resource_deleted(
                 self.show_share_server, kwargs.get("server_id"))
+        elif "backup_id" in kwargs:
+            return self._is_resource_deleted(
+                self.get_share_backup, kwargs.get("backup_id"))
         else:
             raise share_exceptions.InvalidResource(
                 message=str(kwargs))
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 f4538d7..d72c098 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -1836,6 +1836,109 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
+###############
+
+    def get_share_backup(self, backup_id, version=LATEST_MICROVERSION):
+        """Returns the details of a single backup."""
+        resp, body = self.get("share-backups/%s" % backup_id,
+                              headers=EXPERIMENTAL,
+                              extra_headers=True,
+                              version=version)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_share_backups(self, share_id=None, version=LATEST_MICROVERSION):
+        """Get list of backups."""
+        uri = "share-backups/detail"
+        if share_id:
+            uri += (f'?share_id={share_id}')
+        resp, body = self.get(uri, headers=EXPERIMENTAL,
+                              extra_headers=True, version=version)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def create_share_backup(self, share_id, name=None, description=None,
+                            backup_options=None, version=LATEST_MICROVERSION):
+        """Create a share backup."""
+        if name is None:
+            name = data_utils.rand_name("tempest-created-share-backup")
+        if description is None:
+            description = data_utils.rand_name(
+                "tempest-created-share-backup-desc")
+        post_body = {
+            'share_backup': {
+                'name': name,
+                'description': description,
+                'share_id': share_id,
+                'backup_options': backup_options,
+            }
+        }
+        body = json.dumps(post_body)
+        resp, body = self.post('share-backups', body,
+                               headers=EXPERIMENTAL,
+                               extra_headers=True,
+                               version=version)
+
+        self.expected_success(202, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_share_backup(self, backup_id, version=LATEST_MICROVERSION):
+        """Delete share backup."""
+        uri = "share-backups/%s" % backup_id
+        resp, body = self.delete(uri,
+                                 headers=EXPERIMENTAL,
+                                 extra_headers=True,
+                                 version=version)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def restore_share_backup(self, backup_id, version=LATEST_MICROVERSION):
+        """Restore share backup."""
+        uri = "share-backups/%s/action" % backup_id
+        body = {'restore': None}
+        resp, body = self.post(uri, json.dumps(body),
+                               headers=EXPERIMENTAL,
+                               extra_headers=True,
+                               version=version)
+        self.expected_success(202, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def update_share_backup(self, backup_id, name=None, description=None,
+                            version=LATEST_MICROVERSION):
+        """Update share backup."""
+        uri = "share-backups/%s" % backup_id
+        post_body = {}
+        if name:
+            post_body['name'] = name
+        if description:
+            post_body['description'] = description
+
+        body = json.dumps({'share_backup': post_body})
+        resp, body = self.put(uri, body,
+                              headers=EXPERIMENTAL,
+                              extra_headers=True,
+                              version=version)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def reset_state_share_backup(self, backup_id,
+                                 status=constants.STATUS_AVAILABLE,
+                                 version=LATEST_MICROVERSION):
+
+        uri = "share-backups/%s/action" % backup_id
+        body = {'reset_status': {'status': status}}
+        resp, body = self.post(uri, json.dumps(body),
+                               headers=EXPERIMENTAL,
+                               extra_headers=True,
+                               version=LATEST_MICROVERSION)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
 ################
 
     def create_snapshot_access_rule(self, snapshot_id, access_type="ip",
diff --git a/manila_tempest_tests/share_exceptions.py b/manila_tempest_tests/share_exceptions.py
index efa61b5..5b5ca18 100644
--- a/manila_tempest_tests/share_exceptions.py
+++ b/manila_tempest_tests/share_exceptions.py
@@ -86,3 +86,7 @@
 class ShareServerMigrationException(exceptions.TempestException):
     message = ("Share server %(server_id)s failed to migrate and is in ERROR "
                "status")
+
+
+class ShareBackupException(exceptions.TempestException):
+    message = ("Share backup %(backup_id)s failed and is in ERROR status")
diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py
index 18562e5..cb64ec8 100755
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -705,6 +705,31 @@
         return replica
 
     @classmethod
+    def create_backup_wait_for_active(cls, share_id, client=None,
+                                      cleanup_in_class=False, cleanup=True,
+                                      version=CONF.share.max_api_microversion):
+        client = client or cls.shares_v2_client
+        backup_name = data_utils.rand_name('Backup')
+        backup_options = CONF.share.driver_assisted_backup_test_driver_options
+        backup = client.create_share_backup(
+            share_id,
+            name=backup_name,
+            backup_options=backup_options)['share_backup']
+        resource = {
+            "type": "share_backup",
+            "id": backup["id"],
+            "client": client,
+        }
+        if cleanup:
+            if cleanup_in_class:
+                cls.class_resources.insert(0, resource)
+            else:
+                cls.method_resources.insert(0, resource)
+        waiters.wait_for_resource_status(client, backup["id"], "available",
+                                         resource_name='share_backup')
+        return client.get_share_backup(backup['id'])['share_backup']
+
+    @classmethod
     def delete_share_replica(cls, replica_id, client=None,
                              version=CONF.share.max_api_microversion):
         client = client or cls.shares_v2_client
@@ -919,6 +944,9 @@
                     elif res["type"] == "share_replica":
                         client.delete_share_replica(res_id)
                         client.wait_for_resource_deletion(replica_id=res_id)
+                    elif res["type"] == "share_backup":
+                        client.delete_share_backup(res_id)
+                        client.wait_for_resource_deletion(backup_id=res_id)
                     elif res["type"] == "share_network_subnet":
                         sn_id = res["extra_params"]["share_network_id"]
                         client.delete_subnet(sn_id, res_id)
diff --git a/manila_tempest_tests/tests/api/test_backup.py b/manila_tempest_tests/tests/api/test_backup.py
new file mode 100644
index 0000000..f443802
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_backup.py
@@ -0,0 +1,117 @@
+# Copyright 2024 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 tempest.lib import exceptions as lib_exc
+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
+
+
+CONF = config.CONF
+_MIN_SUPPORTED_MICROVERSION = '2.80'
+
+
+class ShareBackupTest(base.BaseSharesMixedTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(ShareBackupTest, cls).skip_checks()
+        if not CONF.share.run_driver_assisted_backup_tests:
+            raise cls.skipException("Share backup tests are disabled.")
+        utils.check_skip_if_microversion_not_supported(
+            _MIN_SUPPORTED_MICROVERSION)
+
+    def setUp(self):
+        super(ShareBackupTest, self).setUp()
+        extra_specs = {
+            'snapshot_support': True,
+            'mount_snapshot_support': True,
+        }
+        share_type = self.create_share_type(extra_specs=extra_specs)
+        share = self.create_share(self.shares_v2_client.share_protocol,
+                                  share_type_id=share_type['id'])
+        self.share_id = share["id"]
+
+    @decorators.idempotent_id('12c36c97-faf4-4fec-9a9b-7cff0d2035cd')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
+    def test_create_share_backup(self):
+        backup = self.create_backup_wait_for_active(self.share_id)
+
+        # Verify backup create API response
+        expected_keys = ["id", "share_id", "status",
+                         "availability_zone", "created_at", "updated_at",
+                         "size", "progress", "restore_progress",
+                         "name", "description"]
+
+        # Strict key check
+        actual_backup = self.shares_v2_client.get_share_backup(
+            backup['id'])['share_backup']
+        actual_keys = actual_backup.keys()
+        self.assertEqual(backup['id'], actual_backup['id'])
+        self.assertEqual(set(expected_keys), set(actual_keys))
+
+    @decorators.idempotent_id('34c36c97-faf4-4fec-9a9b-7cff0d2035cd')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
+    def test_delete_share_backup(self):
+        backup = self.create_backup_wait_for_active(
+            self.share_id, cleanup=False)
+
+        # Delete share backup
+        self.shares_v2_client.delete_share_backup(backup['id'])
+        self.shares_v2_client.wait_for_resource_deletion(
+            backup_id=backup['id'])
+        self.assertRaises(
+            lib_exc.NotFound,
+            self.shares_v2_client.get_share_backup,
+            backup['id'])
+
+    @decorators.idempotent_id('56c36c97-faf4-4fec-9a9b-7cff0d2035cd')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
+    def test_restore_share_backup(self):
+        backup = self.create_backup_wait_for_active(self.share_id)
+
+        # Restore share backup
+        restore = self.shares_v2_client.restore_share_backup(
+            backup['id'])['restore']
+        waiters.wait_for_resource_status(
+            self.shares_v2_client, backup['id'], 'available',
+            resource_name='share_backup')
+
+        self.assertEqual(restore['share_id'], self.share_id)
+
+    @decorators.idempotent_id('78c36c97-faf4-4fec-9a9b-7cff0d2035cd')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
+    def test_update_share_backup(self):
+        backup = self.create_backup_wait_for_active(self.share_id)
+
+        # Update share backup name
+        backup_name2 = data_utils.rand_name('Backup')
+        backup = self.shares_v2_client.update_share_backup(
+            backup['id'], name=backup_name2)['share_backup']
+        updated_backup = self.shares_v2_client.get_share_backup(
+            backup['id'])['share_backup']
+        self.assertEqual(backup_name2, updated_backup['name'])
+
+    @decorators.idempotent_id('19c36c97-faf4-4fec-9a9b-7cff0d2045af')
+    @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
+    def test_list_share_backups(self):
+        self.create_backup_wait_for_active(self.share_id)
+        backups = self.shares_v2_client.list_share_backups()
+        self.assertEqual(1, len(backups))
diff --git a/manila_tempest_tests/tests/api/test_backup_negative.py b/manila_tempest_tests/tests/api/test_backup_negative.py
new file mode 100644
index 0000000..743c195
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_backup_negative.py
@@ -0,0 +1,153 @@
+# Copyright 2024 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 tempest.lib import exceptions as lib_exc
+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
+
+
+CONF = config.CONF
+_MIN_SUPPORTED_MICROVERSION = '2.80'
+
+
+class ShareBackupNegativeTest(base.BaseSharesMixedTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(ShareBackupNegativeTest, cls).skip_checks()
+        if not CONF.share.run_driver_assisted_backup_tests:
+            raise cls.skipException("Share backup tests are disabled.")
+        utils.check_skip_if_microversion_not_supported(
+            _MIN_SUPPORTED_MICROVERSION)
+
+    def setUp(self):
+        super(ShareBackupNegativeTest, self).setUp()
+        extra_specs = {
+            'snapshot_support': True,
+            'mount_snapshot_support': True,
+        }
+        share_type = self.create_share_type(extra_specs=extra_specs)
+        share = self.create_share(self.shares_v2_client.share_protocol,
+                                  share_type_id=share_type['id'])
+        self.share_id = share["id"]
+        self.backup_options = (
+            CONF.share.driver_assisted_backup_test_driver_options)
+
+    @decorators.idempotent_id('58c36c97-faf4-4fec-9a9b-7cff0d2035ab')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_BACKEND)
+    def test_create_backup_when_share_is_in_backup_creating_state(self):
+        backup_name1 = data_utils.rand_name('Backup')
+        backup1 = self.shares_v2_client.create_share_backup(
+            self.share_id,
+            name=backup_name1,
+            backup_options=self.backup_options)['share_backup']
+
+        # try create backup when share state is busy
+        backup_name2 = data_utils.rand_name('Backup')
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.create_share_backup,
+                          self.share_id,
+                          name=backup_name2,
+                          backup_options=self.backup_options)
+        waiters.wait_for_resource_status(
+            self.shares_v2_client, backup1['id'], "available",
+            resource_name='share_backup')
+
+        # delete the share backup
+        self.shares_v2_client.delete_share_backup(backup1['id'])
+        self.shares_v2_client.wait_for_resource_deletion(
+            backup_id=backup1['id'])
+
+    @decorators.idempotent_id('58c36c97-faf4-4fec-9a9b-7cff0d2012ab')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_BACKEND)
+    def test_create_backup_when_share_is_in_error_state(self):
+        self.admin_shares_v2_client.reset_state(self.share_id,
+                                                status='error')
+
+        # try create backup when share is not available
+        backup_name = data_utils.rand_name('Backup')
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.create_share_backup,
+                          self.share_id,
+                          name=backup_name,
+                          backup_options=self.backup_options)
+
+        self.admin_shares_v2_client.reset_state(self.share_id,
+                                                status='available')
+
+    @decorators.idempotent_id('58c36c97-faf4-4fec-9a9b-7cff0d2012de')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_BACKEND)
+    def test_create_backup_when_share_has_snapshots(self):
+        self.create_snapshot_wait_for_active(self.share_id,
+                                             cleanup_in_class=False)
+
+        # try create backup when share has snapshots
+        backup_name = data_utils.rand_name('Backup')
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.create_share_backup,
+                          self.share_id,
+                          name=backup_name,
+                          backup_options=self.backup_options)
+
+    @decorators.idempotent_id('58c12c97-faf4-4fec-9a9b-7cff0d2012de')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_BACKEND)
+    def test_delete_backup_when_backup_is_not_available(self):
+        backup = self.create_backup_wait_for_active(self.share_id)
+        self.admin_shares_v2_client.reset_state_share_backup(
+            backup['id'], status='creating')
+
+        # try delete backup when share backup is not available
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.delete_share_backup,
+                          backup['id'])
+
+        self.admin_shares_v2_client.reset_state_share_backup(
+            backup['id'], status='available')
+
+    @decorators.idempotent_id('58c56c97-faf4-4fec-9a9b-7cff0d2012de')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_BACKEND)
+    def test_restore_backup_when_share_is_not_available(self):
+        backup = self.create_backup_wait_for_active(self.share_id)
+        self.admin_shares_v2_client.reset_state(self.share_id,
+                                                status='error')
+
+        # try restore backup when share is not available
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.restore_share_backup,
+                          backup['id'])
+
+        self.admin_shares_v2_client.reset_state(self.share_id,
+                                                status='available')
+
+    @decorators.idempotent_id('58c12998-faf4-4fec-9a9b-7cff0d2012de')
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_BACKEND)
+    def test_restore_backup_when_backup_is_not_available(self):
+        backup = self.create_backup_wait_for_active(self.share_id)
+        self.admin_shares_v2_client.reset_state_share_backup(
+            backup['id'], status='creating')
+
+        # try restore backup when backup is not available
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.restore_share_backup,
+                          backup['id'])
+
+        self.admin_shares_v2_client.reset_state_share_backup(
+            backup['id'], status='available')
diff --git a/zuul.d/manila-tempest-jobs.yaml b/zuul.d/manila-tempest-jobs.yaml
index c23801e..702dcf8 100644
--- a/zuul.d/manila-tempest-jobs.yaml
+++ b/zuul.d/manila-tempest-jobs.yaml
@@ -626,6 +626,8 @@
         MANILA_OPTGROUP_membernet_standalone_network_plugin_mask: 24
         MANILA_OPTGROUP_membernet_standalone_network_plugin_network_type: vlan
         MANILA_OPTGROUP_membernet_standalone_network_plugin_segmentation_id: 1010
+        MANILA_CREATE_BACKUP_CONTINUE_TASK_INTERVAL: 30
+        MANILA_RESTORE_BACKUP_CONTINUE_TASK_INTERVAL: 30
       devstack_local_conf:
         test-config:
           "$TEMPEST_CONFIG":
@@ -639,6 +641,7 @@
               enable_user_rules_for_protocols: cifs
               multi_backend: true
               multitenancy_enabled: false
+              run_driver_assisted_backup_tests: true
               run_driver_assisted_migration_tests: true
               run_manage_unmanage_snapshot_tests: true
               run_manage_unmanage_tests: true