diff --git a/zvml/vpgs.py b/zvml/vpgs.py index 4cc1605..239bd5d 100644 --- a/zvml/vpgs.py +++ b/zvml/vpgs.py @@ -14,7 +14,7 @@ import logging import time import json from .tasks import Tasks -from .common import ZertoVPGStatus, ZertoVPGSubstatus, ZertoProtectedSiteType, ZertoRecoverySiteType, ZertoVPGPriority +from .common import ZertoVPGStatus, ZertoVPGSubstatus, ZertoProtectedSiteType, ZertoRecoverySiteType, ZertoVPGPriority, ZertoCommitPolicy, ZertoShutdownPolicy from .localsite import LocalSite from typing import Optional, Union, Dict, List @@ -204,7 +204,7 @@ class VPGs: raise TimeoutError(f"VPG {vpg_name} did not reach the {ZertoVPGStatus.get_name_by_value(expected_status.value)} state within the allotted time. Current status: {ZertoVPGStatus.get_name_by_value(vpg_status.value)}") - def add_vm_to_vpg_by_name(self, vpg_name, vm_name): + def add_vm_to_vpg_by_name(self, vpg_name, vm_name, vm_payload=None): logging.info(f'VPGs.add_vm_to_vpg_by_name(zvm_address={self.client.zvm_address}, vpg_name={vpg_name}, vm_name={vm_name})') local_site = LocalSite(self.client.zvm_address, self.client.token) @@ -217,9 +217,10 @@ class VPGs: return vm_id = vm_dict[vm_name]['VmIdentifier'] - vm_payload = { - "VmIdentifier": vm_id - } + if vm_payload is None: + vm_payload = { + "VmIdentifier": vm_id + } return self.add_vm_to_vpg(vpg_name, vm_payload) def add_vm_to_vpg(self, vpg_name, vm_list_payload): @@ -440,6 +441,84 @@ class VPGs: logging.error(f"Unexpected error: {e}") raise + def failover(self, vpg_name, checkpoint_identifier=None, vm_name_list=None, commit_policy=ZertoCommitPolicy.Commit, time_to_wait_before_shutdown_sec=3600, shutdown_policy=ZertoShutdownPolicy.NONE, is_reverse_protection=True, sync=True): + """ + Initiate a failover for a given VPG by its name. + + :param vpg_name: The name of the VPG. + :param checkpoint_identifier: checkpoint_identifier can be received by list_checkpoints, if not provided uses the latest checkpoint. + :param vm_name_list: List of VM names to failover (optional, if not provided fails over all VMs in the VPG). + :param commit_policy: Commit policy for the failover (default: ZertoCommitPolicy.Commit). + :param time_to_wait_before_shutdown_sec: Time to wait before shutdown in seconds (default: 3600). + :param shutdown_policy: Shutdown policy for the failover (default: ZertoShutdownPolicy.NONE). + :param is_reverse_protection: Whether to enable reverse protection (default: False). + :param sync: wait until task is completed. + :return: Response from the Zerto API. + """ + logging.info(f'VPGs.failover(zvm_address={self.client.zvm_address}, vpg_name={vpg_name}, checkpoint_identifier={checkpoint_identifier}, vm_name_list={vm_name_list}, sync={sync})') + + # Retrieve the VPG identifier using the VPG name + vpg_info = self.list_vpgs(vpg_name=vpg_name) + vpg_identifier = vpg_info['VpgIdentifier'] + logging.debug(f"Found VPG '{vpg_name}' with Identifier: {vpg_identifier}") + + url = f"https://{self.client.zvm_address}/v1/vpgs/{vpg_identifier}/Failover" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + payload = { + "commitPolicy": commit_policy.value, + "timeToWaitBeforeShutdownInSec": time_to_wait_before_shutdown_sec, + "shutdownPolicy": shutdown_policy.value, + "isReverseProtection": is_reverse_protection + } + + if checkpoint_identifier: + payload['checkpointIdentifier'] = checkpoint_identifier + + vm_identifier_list = [] + if vm_name_list: + for vm in vm_name_list: + vm_info = self.list_vms(vm_name=vm) + if not vm_info: + logging.error(f'failover vm={vm} not found') + return + vm_identifier_list.append(vm_info[0]['VmIdentifier']) + + if vm_identifier_list: + payload['vmIdentifiers'] = vm_identifier_list + + try: + logging.info(f"Initiating failover for VPG '{vpg_name}', payload={payload}") + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + + logging.info(f"Failover initiated for VPG {vpg_name}, task_id = {task_id}") + + if sync: + # Wait for task completion + self.tasks.wait_for_task_completion(task_id, timeout=30, interval=5) + return response.json() + + except requests.exceptions.RequestException as e: + if e.response is not None: + logging.error(f"HTTPError: {e.response.status_code} - {e.response.reason}") + try: + error_details = e.response.json() + logging.error(f"Error Message: {error_details.get('Message', 'No detailed error message available')}") + except ValueError: + logging.error(f"Response content: {e.response.text}") + else: + logging.error("HTTPError occurred with no response attached.") + raise + + except Exception as e: + logging.error(f"Unexpected error: {e}") + raise + def rollback_failover(self, vpg_name, sync=True): """ Rollback failover for a given VPG by its name. @@ -490,6 +569,60 @@ class VPGs: logging.error(f"Unexpected error: {e}") raise + def commit_failover(self, vpg_name, is_reverse_protection=True, sync=True): + """ + Commit failover for a given VPG by its name. + + :param vpg_name: The name of the VPG. + :param sync: wait until task is completed. + + """ + logging.info(f'VPGs.comit_failover(zvm_address={self.client.zvm_address}, vpg_name={vpg_name}, sync={sync})') + + # Retrieve the VPG identifier using the VPG name + vpg_info = self.list_vpgs(vpg_name=vpg_name) + vpg_identifier = vpg_info['VpgIdentifier'] + logging.info(f"Found VPG '{vpg_name}' with Identifier: {vpg_identifier}") + + payload = { + "isReverseProtection": is_reverse_protection + } + + url = f"https://{self.client.zvm_address}/v1/vpgs/{vpg_identifier}/FailoverCommit" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info(f"Rollback failover for VPG '{vpg_name}'...") + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + + logging.info(f"Commit failover for VPG {vpg_name}, task_id = {task_id}") + + if sync: + # Wait for task completion + self.tasks.wait_for_task_completion(task_id, timeout=30, interval=5) + return response.json() + + except requests.exceptions.RequestException as e: + if e.response is not None: + logging.error(f"HTTPError: {e.response.status_code} - {e.response.reason}") + try: + error_details = e.response.json() + logging.error(f"Error Message: {error_details.get('Message', 'No detailed error message available')}") + except ValueError: + logging.error(f"Response content: {e.response.text}") + else: + logging.error("HTTPError occurred with no response attached.") + raise + + except Exception as e: + logging.error(f"Unexpected error: {e}") + raise + def delete_vpg(self, vpg_name, force=False, keep_recovery_volumes=True): """ Deletes a VPG by its name.