Reducing Snapshot Generations

Below is a script that reduces the number of Snapshot generations. Use this script for operations that use SnapOPC+ to back up generation-managed differential data. For more information, see the description of Generation Managed Differential Data Backup with SnapOPC+.

The sequence of script processing is as follows.

  1. Use GET /volume/{volume_id}/copysession API to get the snapshot information.

  2. Sort the snapshot information obtained in 1 from the oldest creation time.

  3. Remove older snapshots using DELETE /volume/{volume_id} API, starting with the oldest snapshot.

  4. Repeat 3 for the number of generations for which the reduction is desired.

snapopcplus_reduce.py


import sys

from datetime import datetime
from eternus_rest import EtdxBaseApi

# Change the following values for your environment.
storage_url = "https://192.168.0.1:5665"
storage_user_name = "username"
storage_password = "password"

# Change the following values as required.
# Volume ID of the source volume of SnapOPC+ to reduce generations.
volume_id = 100001
# Number of generations of SnapOPC+ after reducing.
generation_count = 5


class EtdxApi(EtdxBaseApi):
    def __init__(self, base_url, boxid=None, proxies=None):
        super().__init__(base_url, boxid=boxid, proxies=proxies)

    def snapopcplus_get_snapshot_list(self, volume_id):
        """Get the SnapOPC+ snapshots about the specified volume.

        Args:
            volume_id (int): Volume ID of the source volume of SnapOPC+ to get.

        Returns:
            list[obj]: A list of snapshot objects of SnapOPC+. Returns null
              list if fails to get information.
        """
        r = super().get("/api/v1/volume/{0!s}/copysession?backup_type=snapshot"
                        "&is_manual_snapshot=false".format(volume_id))
        if r.status_code != 200 or r.json()["backup_volume_list"] == []:
            print("Failed to get snapshots or the volume has no "
                  "SnapOPC+ snaphosts.")
            print(r)
            print(r.json())
            return []
        return r.json()["backup_volume_list"]

    def snapopcplus_delete_volumes(self, sorted_snapshot_volume_id_list,
                                   delete_count):
        """Delete specified number of SnapOPC+ snapshots one by one.

        Snapshot of SnapOPC+ can be deleted from the oldest ones.

        Args:
            sorted_snapshot_volume_id_list (list[int]): Sorted list of
              snapshot volume ID of SnapOPC+.
            delete_count (int): Number of snapshots to delete.

        Returns:
            list[int]: List of deleted snapshot volume IDs.
        """
        deleted_volume_id_list = []
        for i in range(delete_count):
            snapshot_volume_id = sorted_snapshot_volume_id_list[i]
            r = super().delete("/api/v1/volume/{0!s}"
                               .format(snapshot_volume_id))
            if r.status_code != 202:
                print("Failed to request the volume (ID: {0!s}) deletion."
                      .format(snapshot_volume_id))
                print(r)
                print(r.json())
                break

            # Wait for the job completion
            job_id = r.json()["job_id"]
            job_r = super().wait_job(job_id)
            if job_r.json()["status"] != "Success":
                print("Failed to delete the volume (ID: {0!s})."
                      .format(snapshot_volume_id))
                print(job_r)
                print(job_r.json())
                continue
            deleted_volume_id_list.append(snapshot_volume_id)

        return deleted_volume_id_list

    def snapopcplus_reduce_generations(self, volume_id, generation_count):
        """Reduce generations of SnapOPC+

        Args:
            volume_id (int): Volume ID of the source volume of SnapOPC+ to
              reduce generations.
            generation_count (int): Number of generations of SnapOPC+ after
              reducing.

        Returns:
            bool: True if successful, False otherwise.
        """
        # Get snapshots of SnapOPC+
        snapshot_list = self.snapopcplus_get_snapshot_list(volume_id)
        if snapshot_list == []:
            return False

        # Validate the generation_count
        if len(snapshot_list) <= generation_count:
            print("Specified generation_count is greater than or equal to the "
                  "current number of generations.")
            print("Current number of generations:", len(snapshot_list))
            print("Specified generation_count:", generation_count)
            return False

        # Sort SnapOPC+ by backup_time, since destination volume of SnapOPC+
        # can be deleted oldest one.
        sorted_snapshot_list = \
            sorted(snapshot_list,
                   key=lambda x: datetime.strptime(
                       x["backup_time"], '%Y-%m-%dT%H:%M:%SZ'))

        # Delete the snapshots
        sorted_snapshot_volume_id_list = \
            [int(i["volume_href"].split("/")[4]) for i in sorted_snapshot_list]
        deleted_volume_id_list = \
            self.snapopcplus_delete_volumes(
                    sorted_snapshot_volume_id_list,
                    len(sorted_snapshot_volume_id_list) - generation_count)
        if (len(deleted_volume_id_list) !=
                len(sorted_snapshot_volume_id_list) - generation_count):
            print("Failed to delete some SnapOPC+ snapshots.")
        else:
            print("Deleted SnapOPC+ snapshots.")
        print("Deleted volumes:", deleted_volume_id_list)
        print("Current number of generations:",
              len(sorted_snapshot_volume_id_list)
              - len(deleted_volume_id_list))
        return (len(deleted_volume_id_list) ==
                len(sorted_snapshot_volume_id_list) - generation_count)


def main():
    storage = EtdxApi(storage_url)

    # Login
    if not storage.login(storage_user_name, storage_password):
        return False

    # Initialize return value as True, since the procedure continues even if
    # error occurs.
    rtn = True

    # Reduce generations of SnapOPC+
    print("Reducing the number of generation of the SnapOPC+ of volume "
          "(ID: {0!s}) to {1!s}.".format(volume_id, generation_count))
    if not storage.snapopcplus_reduce_generations(volume_id, generation_count):
        print("Failed to reduce the number of generations of the SnapOPC+.")
        rtn = False
    else:
        print("Succeeded to reduce the number of generations of the SnapOPC+.")

    # Get information of the SnapOPC+
    r = storage.get("/api/v1/volume/{0!s}/copysession?backup_type=snapshot"
                    "&is_manual_snapshot=false".format(volume_id))
    print(r)
    print(r.json())

    # Logout
    storage.logout()
    return rtn


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