リモート筐体のバックアップからのデータのリストア

リモート筐体のバックアップ(非同期StackモードのREC Sessionのコピー先VolumeおよびそのClone)からデータをリストアするスクリプトを記載します。このスクリプトは、非同期StackモードのRECとQuickOPCを使用して、リモート筐体に業務Volumeのバックアップを定期的に作成する運用で使用します。詳細は、RECとQuickOPCを使用したリモート筐体へのバックアップの作成を参照してください。

備考
  • このスクリプトは、REC SessionのStatusを確認して、リモート筐体のバックアップVolume(非同期StackモードのREC SessionのCopy先Volume)からリストアするか、リモート筐体のバックアップVolumeのCloneからリストアするかを自動で判定します。

  • このスクリプトは、RECとQuickOPCを使用したリモート筐体へのバックアップの作成で記載したリモート筐体にBackup Volumeを作っている環境でのみ使用できます。それ以外の環境でデータをリストアする際には利用できません。

  • このスクリプトは、REC Sessionの状態が正常の場合に使用できます。筐体が被災して物理的に破損し、RECの経路がHaltになった場合は、経路を復旧してからリモート筐体のBackup VolumeまたはCloneからREC Sessionを再作成することでデータの復旧を実施してください。

スクリプトの処理の流れは以下です。

  1. GET /copysession/{copysession_id} APIを使用してREC Sessionの情報を取得し、REC Sessionが反転可能かどうかを確認する(StatusがErrorの場合、またはすでに反転済みの場合は処理終了)。

  2. GET /copysession/{copysession_id} APIを使用してREC Sessionの情報を取得し、バックアップVolumeからリストアするか、バックアップVolumeのCloneからリストアするかを判定する(Suspend中の場合はバックアップVolumeから、Suspend中ではない場合はバックアップVolumeのCloneからリストアする)。

  3. リモート筐体のバックアップVolumeからリストアする場合、以下の処理を実施する。

    1. POST /copysession/{copysession_id}/reverse APIを使用してREC Sessionの方向を反転する。

    2. POST /copysession/{copysession_id}/resume APIを使用してREC Sessionを再開する。

    3. GET /copysession/{copysession_id} APIを使用してREC Sessionの情報を定期的に取得し、リモート筐体からローカル筐体へのデータ転送が完了するのを待つ。

    4. POST /copysession/{copysession_id}/suspend APIを使用してREC SessionをSuspendする。

    5. POST /copysession/{copysession_id}/reverse APIを使用してREC Sessionの方向を反転する(元の方向に戻す)。

  4. リモート筐体のバックアップVolumeのCloneからリストアする場合、以下の処理を実施する。

    1. POST /copysession/{copysession_id}/suspend APIを使用してREC SessionをSuspendする。

    2. POST /copysession/{copysession_id}/reverse APIを使用してREC Sessionを反転する。

    3. POST /volume/{volume_id}/restore/{backup_volume_id} APIを使用して、リモート筐体のバックアップVolumeのCloneのデータを、リモート筐体のバックアップVolumeにコピーする。

    4. POST /copysession/{copysession_id}/resume APIを使用してREC Sessionを再開させ、リモート筐体からローカル筐体へのデータ転送を開始する。

    5. GET /copysession/{copysession_id} APIを使用してREC Sessionの情報を定期的に取得し、リモート筐体からローカル筐体へのデータ転送が完了するのを待つ。

    6. POST /copysession/{copysession_id}/suspend APIを使用してREC SessionをSuspendする。

    7. GET /volume/{volume_id}/copysession APIを使用してリモート筐体のバックアップVolumeのCloneから、リモート筐体のバックアップVolumeへのデータコピーが完了しているかどうかを確認する。

    8. POST /copysession/{copysession_id}/reverse APIを使用して、REC Sessionの方向をリストア前の状態に戻す。

    9. POST /copysessoin/{copysession_id}/resume APIを使用して、REC Sessionを再開する。

rec_stack_recover.py


import sys
import time

from eternus_rest import EtdxBaseApi

# Change the following values for your environment.
# Source storage (storage that has the source volume)
storage1_url = "https://192.168.0.1:5665"
storage1_user_name = "username"
storage1_password = "password"
# Target storage (storage that has the destination volume)
storage2_url = "https://192.168.0.2:5665"
storage2_user_name = "username"
storage2_password = "password"

# This case is for a REC(Stack) session from the volume on storage1
# to storage2.
# The destination volume of REC has been cloned using QuickOPC on storage2.
# You can paste the outputs of rec_stack_create.
# REC session ID on storage1 created by the script rec_stack_create.
storage1_copysession_id = 1
# Backup volume ID created by the script rec_stack_create as the destination
# volume of REC.
storage2_volume_id = 200002
# Clone volume ID created by the script rec_stack_create as the clone for the
# REC destination volume.
storage2_clone_volume_id = 300003


class EtdxApi(EtdxBaseApi):
    def __init__(self, base_url, boxid=None, proxies=None):
        """Constructor of EtdxApi

        If storage is used as target storage of REC without calling login
        method in this script, need to specify the boxid of this storage
        as follows.
        storage2 = EtdxApi(storage2_url,
                           boxid="00ETERNUSDXHS3ET00000A####EI000001######")

        Args:
            base_url ([type]): [description]
            boxid ([type], optional): [description]. Defaults to None.
            proxies ([type], optional): [description]. Defaults to None.
        """
        super().__init__(base_url, boxid=boxid, proxies=proxies)

    def login(self, user_name="", password=""):
        """Create a session of RESTful API and get boxid.

        Args:
            user_name (str, optional): User name to login. Defaults to "".
            password (str, optional): Password for the user. Defaults to "".

        Returns:
            bool: True if successful, False otherwise.
        """
        rtn = super().login(user_name, password)
        if rtn and self.boxid is None:
            r = super().get("/api/v1/storagesystem")
            if r.status_code != 200:
                print(r)
                print(r.json())
                print("Failed to get boxid.")
                return False
            self.boxid = r.json()["boxid"]
        return rtn

    def rec_can_recover(self, copysession_id):
        """Check if the REC session can start the recovery.

        Args:
            copysession_id (int): Copy Session ID to check.

        Returns:
            bool: True if the specified REC session can start the recovery.
              False otherwise. Returns false if retrieving copy session
              information failed.
        """
        r = super().get("/api/v1/copysession/{0!s}?fields=status,"
                        "is_local_source_volume"
                        .format(copysession_id))
        data = r.json()
        if r.status_code != 200:
            print("Failed to get information about the copy session.")
            print(r)
            print(data)
            return False
        if data["status"] not in ["Active", "Suspended"]:
            print("The REC session is not in normal status.")
            print(data)
            return False
        if not data["is_local_source_volume"]:
            print("The REC session might be in recovering.")
            print(data)
            return False
        return True

    def rec_is_suspended(self, copysession_id):
        """Check if the REC session is in "Suspended" status.

        Args:
            copysession_id (int): Copy session ID to check.

        Returns:
            bool: True if the REC session is in "Suspended". False otherwise.
              Returns false if retrieving copy session information failed.
        """
        r = super().get("/api/v1/copysession/{0!s}?fields=status"
                        .format(copysession_id))
        if r.status_code != 200:
            print(r)
            print(r.json())
            raise Exception("Failed to get information about the "
                            "copy session.")
        if r.json()["status"] == "Suspended":
            return True
        return False

    def rec_wait_for_equivalent(self, copysession_id):
        """Wait for the REC session phase to be equivalent.

        Args:
            copysession_id (int): Copy session ID to wait.

        Returns:
            bool: True if the copy session phase is equivalent.
              False if error occurs.
        """
        while True:
            r = super().get("/api/v1/copysession/{0!s}".format(copysession_id))
            data = r.json()
            if r.status_code != 200 or data["status"] == "Error":
                print("Failed to get information of the copy session or some "
                      "error occurred on the copy session.")
                print(r)
                print(data)
                return False
            if data["phase"] == "Equivalent":
                break

            print("The REC session is not yet equivalent. Check again in 60 "
                  "seconds.")
            print("Total Data Size: ", data["total_data_size"])
            print("Copied Data Size:", data["copied_data_size"])
            print("Progress:        ", data["progress"])
            print("Elapsed Time:    ", data["elapsed_time"])
            time.sleep(60)

        return True

    def rec_suspend(self, copysession_id, force=False):
        """Suspend the REC session.

        Args:
            copysession_id (int): Copy session ID to suspend.
            force (bool, optional): True to force suspend. Defaults to False.

        Returns:
            bool: True if successful, False otherwise.
        """
        if force:
            r = super().post("/api/v1/copysession/{0!s}/suspend?force=true"
                             .format(copysession_id))
        else:
            r = super().post("/api/v1/copysession/{0!s}/suspend"
                             .format(copysession_id))
        if r.status_code != 202:
            print("Failed to request the REC session suspend.")
            print(r)
            print(r.json())
            return False

        # Wait for the job completion
        job_r = super().wait_job(r.json()["job_id"])
        if job_r.json()["status"] != "Success":
            print("Failed to suspend the REC session.")
            print(job_r)
            print(job_r.json())
            return False
        return True

    def rec_resume(self, copysession_id):
        """Resume the REC session

        Args:
            copysession_id (int): Copy session ID to resume.

        Returns:
            bool: True if successful, False otherwise.
        """
        r = super().post("/api/v1/copysession/{0!s}/resume"
                         .format(copysession_id))
        if r.status_code != 202:
            print("Failed to request the REC session resume.")
            print(r)
            print(r.json())
            return False

        # Wait for the job completion
        job_r = super().wait_job(r.json()["job_id"])
        if job_r.json()["status"] != "Success":
            print("Failed to resume the REC session.")
            print(job_r)
            print(job_r.json())
            return False
        return True

    def rec_reverse(self, copysession_id):
        """Reverse the REC session

        Args:
            copysession_id (int): Copy session ID to reverse.

        Returns:
            bool: True if successful, False otherwise.
        """
        r = super().post("/api/v1/copysession/{0!s}/reverse"
                         .format(copysession_id))
        if r.status_code != 202:
            print("Failed to request the REC session reverse.")
            print(r)
            print(r.json())
            return False

        # Wait for the job completion
        job_r = super().wait_job(r.json()["job_id"])
        if job_r.json()["status"] != "Success":
            print("Failed to reverse the REC session.")
            print(job_r)
            print(job_r.json())
            return False
        return True

    def quickopc_wait_for_restore(self, volume_id):
        """Wait for the restore copy session from the clone to complete.

        Args:
            volume_id (int): Volume ID of the original of the clone.

        Returns:
            bool: True if the restore copy session completed. False if
              retrieving copy session information failed or some error
              occurred on the copy session.
        """
        while True:
            r = super().get("/api/v1/volume/{0!s}/copysession"
                            .format(volume_id))
            data = r.json()
            if r.status_code != 200:
                print("Failed to get information of the copy session of volume"
                      " (ID: {0!s})."
                      .format(volume_id))
                print(r)
                print(data)
                return False

            # If the original volume of clone is not a copy destination, the
            # restore copy session is considered complete.
            if not data["is_copy_destination_volume"]:
                break

            # If the copy session is not a restore, the restore copy session
            # is considered complete.
            if data["backup_type"] != "Restored":
                break

            # If the restore copy session is in "Suspend" or "Error" status.
            if data["status"] in ["Suspended", "Error"]:
                print("Restore Copy session is not in expected status ({0!s})"
                      .format(data["status"]))
                print(data)
                return False

            time.sleep(10)

        return True

    def quickopc_restore(self, volume_id, backup_volume_id):
        """Restore the QuickOPC.

        Args:
            volume_id (int): Volume ID of the original of the clone.
            backup_volume_id (int): Volume ID of the clone.

        Returns:
            bool: True if successful, False otherwise.
        """
        r = super().post("/api/v1/volume/{0!s}/restore/{1!s}"
                         .format(volume_id, backup_volume_id))
        if r.status_code != 202:
            print("Failed to request the QuickOPC restore.")
            print(r)
            print(r.json())
            return False

        # Wait for the job completion
        job_r = super().wait_job(r.json()["job_id"])
        if job_r.json()["status"] != "Success":
            print("Failed to restore the QuickOPC.")
            print(job_r)
            print(job_r.json())
            return False
        return True

    def rec_stack_recover_under_operating(self, copysession_id):
        """Recovery process under operating.

        Args:
            copysession_id (int): Copy session ID of the REC.

        Returns:
            bool: True if successful, False otherwise.
        """
        # Reverse the REC session
        print("Reversing REC session.")
        if not self.rec_reverse(copysession_id):
            return False
        # Restore the data from the destination volume to the source volume
        # of the REC.
        print("Resuming the REC session.")
        if not self.rec_resume(copysession_id):
            return False
        print("Waiting for the REC session to be equivalent.")
        if not self.rec_wait_for_equivalent(copysession_id):
            return False
        print("The data restore Completed.")

        # Restore the REC session to its former state.
        print("Suspending the REC session.")
        if not self.rec_suspend(copysession_id):
            return False
        print("Reversing REC session.")
        if not self.rec_reverse(copysession_id):
            return False
        return True

    def rec_stack_recover_not_operating(self, copysession_id, remote_storage,
                                        volume_id, clone_volume_id):
        """Recovery process while not operating.

        Args:
            copysession_id (int): Copy session ID of the REC.
            remote_storage (obj): Object of the destination storage of the REC.
            volume_id (int): Volume ID of the destination volume of the REC.
            clone_volume_id (int): Volume ID of the clone of the REC
              destination volume.

        Returns:
            bool: True if successful, False otherwise.
        """
        # Reverse the REC session
        print("Suspending the REC session.")
        if not self.rec_suspend(copysession_id, force=True):
            return False
        print("Reversing the REC session.")
        if not self.rec_reverse(copysession_id):
            return False
        # Restore the data from clone to the destination volume of the REC.
        print("Restoring data from the clone (volume ID: {0!s}) to the REC "
              "destination volume (ID: {1!s})."
              .format(clone_volume_id, volume_id))
        if not remote_storage.quickopc_restore(volume_id, clone_volume_id):
            return False
        # Restore the data from the destination volume to the source volume
        # of the REC.
        print("Resuming the REC session.")
        if not self.rec_resume(copysession_id):
            return False
        print("Waiting for the REC session to be equivalent.")
        if not self.rec_wait_for_equivalent(copysession_id):
            return False
        print("The data restore Completed.")

        # Restore the REC session to its former state.
        print("Suspending the REC session.")
        if not self.rec_suspend(copysession_id):
            return False
        # Wait for the restore copy session from the clone to complete.
        print("Waiting for the restore copy session from the clone to "
              "complete.")
        if not remote_storage.quickopc_wait_for_restore(volume_id):
            return False
        print("Reversing REC session.")
        if not self.rec_reverse(copysession_id):
            return False
        print("Resuming REC session.")
        if not self.rec_resume(copysession_id):
            return False
        return True


def main():
    storage1 = EtdxApi(storage1_url)
    storage2 = EtdxApi(storage2_url)

    # Login
    if not storage1.login(storage1_user_name, storage1_password):
        return False
    if not storage2.login(storage2_user_name, storage2_password):
        return False

    print("Start recovery of the source volume data about the REC session"
          " (ID: {0!s}).".format(storage1_copysession_id))

    # Change recovery process according to the operation condition.
    try:
        print("Check the copy session whether the recovery process can work.")
        if not storage1.rec_can_recover(storage1_copysession_id):
            print("Could not start recovery.")
            return False
        if storage1.rec_is_suspended(storage1_copysession_id):
            print("Starting Recovery during operation.")
            if not storage1.rec_stack_recover_under_operating(
                    storage1_copysession_id):
                print("Failed to recover.")
                return False
        else:
            print("Starting Recovery during shutdown.")
            if not storage1.rec_stack_recover_not_operating(
                    storage1_copysession_id,
                    storage2,
                    storage2_volume_id,
                    storage2_clone_volume_id):
                print("Failed to recover.")
                return False
    except Exception as e:
        print(e)
        return False

    print("Recovery completed.")

    # Logout
    storage1.logout()
    storage2.logout()
    return True


if __name__ == '__main__':
    if not main():
        sys.exit(1)