added examples that allows bulk vpg creating and re-ip using csv files
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
|
||||
# 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 Management Scripts
|
||||
|
||||
This collection of scripts helps manage Virtual Protection Groups (VPGs) in Zerto, including resource discovery, VPG creation, and NIC configuration management using csv files. All scripts are located in the `examples/bulk_vpg_actions` directory.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.x
|
||||
- Zerto Python SDK installed
|
||||
- Access to ZVM with API credentials
|
||||
- Required Python packages (install via pip):
|
||||
```bash
|
||||
pip install urllib3
|
||||
```
|
||||
|
||||
## Step 1: Export Site Resources
|
||||
|
||||
First, export all available resources from your Zerto sites to understand what can be used in VPG creation.
|
||||
|
||||
```bash
|
||||
cd examples/bulk_vpg_actions
|
||||
python export_site_resources_to_csv.py \
|
||||
--zvm_address "192.168.111.20" \
|
||||
--client_id "zerto-api" \
|
||||
--client_secret "your-secret-here" \
|
||||
--ignore_ssl \
|
||||
--output_dir
|
||||
```
|
||||
|
||||
This will create several CSV files in the output_dir directory:
|
||||
- `zerto_sites_[timestamp].csv` - Contains site information
|
||||
- `[SiteName]_datastores_[timestamp].csv` - Available datastores at a target site
|
||||
- `[SiteName]_networks_[timestamp].csv` - Available networks at a targer site
|
||||
- `[SiteName]_hosts_[timestamp].csv` - Available hosts at a target site
|
||||
- `[SiteName]_folders_[timestamp].csv` - Available folders at a target site
|
||||
- `[SiteName]_vms_[timestamp].csv` - Available VMs at the local site to be protected at a target site
|
||||
|
||||
## Step 2: Update VPG Template
|
||||
|
||||
1. In the `examples/bulk_vpg_actions` directory, open `vpg_template.csv` and update it with the information from the exported resource files:
|
||||
- Use site names and IDs from `zerto_sites_[timestamp].csv`
|
||||
- Use datastore names and IDs from `[SiteName]_datastores_[timestamp].csv`
|
||||
- Use network names and IDs from `[SiteName]_networks_[timestamp].csv`
|
||||
- Use host/host cluster names and IDs from `[SiteName]_hosts_[timestamp].csv`
|
||||
- Use folder names and IDs from `[SiteName]_folders_[timestamp].csv`
|
||||
- Use VM names and IDs from `[SiteName]_vms_[timestamp].csv`
|
||||
* RECOMMENDATION: Create an excel file that references the resources above as "Data Validation with a List from a Table" feature.
|
||||
|
||||
2. The template includes fields for both host and host cluster configuration:
|
||||
- If using a specific host, fill in "Recovery Host Name" and "Recovery Host ID"
|
||||
- If using a host cluster, fill in "Recovery Host Cluster Name" and "Recovery Host Cluster ID"
|
||||
|
||||
## Step 3: Export Current VPG NIC Settings into a csv file
|
||||
|
||||
Export the current NIC settings of your VPGs to review and modify them:
|
||||
|
||||
```bash
|
||||
cd examples/bulk_vpg_actions
|
||||
python export_vpg_settings_nics_to_csv.py \
|
||||
--zvm_address "192.168.111.20" \
|
||||
--client_id "zerto-api" \
|
||||
--client_secret "your-secret-here" \
|
||||
--vpg_names "VpgTest1,VpgTest2" \
|
||||
--ignore_ssl \
|
||||
--output_dir "output_dir name/path"
|
||||
```
|
||||
|
||||
This creates two files in the output_dir directory:
|
||||
- `ExportedSettings_[timestamp].json` - Full VPG settings in JSON format
|
||||
- `ExportedSettings_[timestamp].csv` - NIC settings in CSV format
|
||||
|
||||
## Step 4: Modify NIC Settings
|
||||
|
||||
1. In the `examples/bulk_vpg_actions` directory, open the exported CSV file (`ExportedSettings_[timestamp].csv`)
|
||||
2. Modify the NIC settings as needed:
|
||||
- Set "Failover ShouldReplaceIpConfiguration" to "True" to modify IP settings
|
||||
- For DHCP:
|
||||
- Set "Failover DHCP" to "True"
|
||||
- Leave IP, Subnet, Gateway, and DNS fields empty
|
||||
- For Static IP:
|
||||
- Set "Failover DHCP" to "False"
|
||||
- Fill in IP, Subnet, Gateway, and DNS fields
|
||||
- Repeat for "Failover Test" settings if needed
|
||||
|
||||
Important Notes:
|
||||
- You cannot have both DHCP and static IP settings enabled
|
||||
- Must set "ShouldReplaceIpConfiguration" to "True" to modify IP settings
|
||||
- Changes will be validated before applying
|
||||
|
||||
## Step 5: Import Updated NIC Settings
|
||||
|
||||
Apply the modified NIC settings to your VPGs:
|
||||
|
||||
```bash
|
||||
cd examples/bulk_vpg_actions
|
||||
python import_vpg_settings_nics_from_csv.py \
|
||||
--zvm_address "192.168.111.20" \
|
||||
--client_id "zerto-api" \
|
||||
--client_secret "your-secret-here" \
|
||||
--csv_file "output_dir/ExportedSettings_[timestamp].csv" \
|
||||
--vpg_names "VpgTest1,VpgTest2" \
|
||||
--ignore_ssl
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Validate all settings
|
||||
2. Show proposed changes
|
||||
3. Ask for confirmation before applying
|
||||
4. Apply changes if confirmed
|
||||
|
||||
## File Structure
|
||||
|
||||
All scripts and templates are located in the `examples/bulk_vpg_actions` directory:
|
||||
```
|
||||
examples/bulk_vpg_actions/
|
||||
├── README.md
|
||||
├── export_site_resources_to_csv.py
|
||||
├── export_vpg_settings_nics_to_csv.py
|
||||
├── import_vpg_settings_nics_from_csv.py
|
||||
├── vpg_template.csv
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. SSL Certificate Issues:
|
||||
- Use `--ignore_ssl` flag if you have SSL certificate validation issues
|
||||
|
||||
2. API Authentication:
|
||||
- Refer to main README file in order to create your client_id and client_secret
|
||||
- Ensure your client_id and client_secret are correct
|
||||
- Verify you have proper permissions in Zerto
|
||||
|
||||
3. CSV Format Issues:
|
||||
- Ensure CSV files are saved with UTF-8 encoding
|
||||
- Don't modify the column headers
|
||||
- Use proper boolean values ("True"/"False")
|
||||
|
||||
4. Common Errors:
|
||||
- "VPG not found" - Check VPG names in the CSV
|
||||
- "Invalid configuration" - Review DHCP and IP settings
|
||||
- "Permission denied" - Check API credentials and permissions
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Never commit API credentials to version control
|
||||
- Use secure methods to store and transmit credentials
|
||||
- Consider using environment variables for sensitive data
|
||||
- Review and validate all changes before applying them
|
||||
|
||||
## Support
|
||||
|
||||
These scripts are provided as examples and are not supported under any Zerto support program or service. Use at your own risk.
|
||||
@@ -0,0 +1,384 @@
|
||||
#!/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 VPG Creation from CSV Script
|
||||
|
||||
This script reads VPG settings from a CSV file and creates VPGs with the specified settings.
|
||||
It supports creating multiple VPGs with different VMs using a single CSV file.
|
||||
|
||||
Key Features:
|
||||
1. CSV-based VPG Creation:
|
||||
- Read VPG settings from CSV template
|
||||
- Create VPGs with specified settings
|
||||
- Add VMs to VPGs based on CSV entries
|
||||
|
||||
2. Resource Management:
|
||||
- Uses existing site resources (datastores, networks, etc.)
|
||||
- Validates resource IDs before VPG creation
|
||||
|
||||
Required Arguments:
|
||||
--zvm_address: ZVM IP address
|
||||
--client_id: API client ID
|
||||
--client_secret: API client secret
|
||||
--csv_file: Path to CSV file with VPG settings
|
||||
--ignore_ssl: Ignore SSL certificate validation (optional)
|
||||
|
||||
Example Usage:
|
||||
python create_vpgs_from_csv.py \
|
||||
--zvm_address "192.168.111.20" \
|
||||
--client_id "zerto-api" \
|
||||
--client_secret "your-secret-here" \
|
||||
--csv_file "vpg_template.csv" \
|
||||
--ignore_ssl
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
import sys
|
||||
import os
|
||||
import urllib3
|
||||
import json
|
||||
from typing import Dict, List
|
||||
from collections import defaultdict
|
||||
|
||||
# Add parent directory to Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from zvml import ZVMLClient
|
||||
|
||||
# Disable SSL warnings
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def setup_argparse() -> argparse.ArgumentParser:
|
||||
"""Set up command line argument parsing."""
|
||||
parser = argparse.ArgumentParser(description='Create VPGs from CSV settings')
|
||||
parser.add_argument('--zvm_address', required=True, help='ZVM IP address')
|
||||
parser.add_argument('--client_id', required=True, help='API client ID')
|
||||
parser.add_argument('--client_secret', required=True, help='API client secret')
|
||||
parser.add_argument('--csv_file', required=True, help='Path to CSV file with VPG settings')
|
||||
parser.add_argument('--ignore_ssl', action='store_true', help='Ignore SSL certificate validation')
|
||||
|
||||
return parser
|
||||
|
||||
def read_vpg_settings(csv_file: str) -> Dict[str, List[Dict]]:
|
||||
"""Read VPG settings from CSV file and group by VPG name."""
|
||||
vpg_settings = defaultdict(list)
|
||||
|
||||
with open(csv_file, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
vpg_name = row['VPG Name']
|
||||
vpg_settings[vpg_name].append(row)
|
||||
|
||||
return vpg_settings
|
||||
|
||||
def create_vpg_payload(vpg_row: Dict) -> Dict:
|
||||
"""Create VPG payload from CSV row."""
|
||||
# Basic settings
|
||||
basic = {
|
||||
"Name": vpg_row['VPG Name'],
|
||||
"VpgType": vpg_row['VPG Type'],
|
||||
"RpoInSeconds": int(vpg_row['RPO (seconds)']),
|
||||
"TestIntervalInMinutes": int(vpg_row['Test Interval (minutes)']),
|
||||
"JournalHistoryInHours": int(vpg_row['Journal History (hours)']),
|
||||
"Priority": vpg_row['Priority'],
|
||||
"UseWanCompression": vpg_row['Use WAN Compression'].lower() == 'true',
|
||||
"ProtectedSiteIdentifier": vpg_row['Protected Site ID'],
|
||||
"RecoverySiteIdentifier": vpg_row['Recovery Site ID']
|
||||
}
|
||||
|
||||
# Journal settings
|
||||
journal = {
|
||||
"DatastoreIdentifier": vpg_row['Journal Datastore ID'],
|
||||
"Limitation": {
|
||||
"HardLimitInMB": int(vpg_row['Journal Hard Limit (MB)']),
|
||||
"WarningThresholdInMB": int(vpg_row['Journal Warning Threshold (MB)'])
|
||||
}
|
||||
}
|
||||
|
||||
# Recovery settings
|
||||
recovery = {
|
||||
"DefaultHostClusterIdentifier": vpg_row['Recovery Host Cluster ID'],
|
||||
"DefaultDatastoreIdentifier": vpg_row['Recovery Datastore ID'],
|
||||
"DefaultFolderIdentifier": vpg_row['Recovery Folder ID']
|
||||
}
|
||||
|
||||
# Network settings
|
||||
networks = {
|
||||
"Failover": {
|
||||
"Hypervisor": {
|
||||
"DefaultNetworkIdentifier": vpg_row['Failover Network ID']
|
||||
}
|
||||
},
|
||||
"FailoverTest": {
|
||||
"Hypervisor": {
|
||||
"DefaultNetworkIdentifier": vpg_row['Failover Test Network ID']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"basic": basic,
|
||||
"journal": journal,
|
||||
"recovery": recovery,
|
||||
"networks": networks
|
||||
}
|
||||
|
||||
def validate_vpg_and_vms(client: ZVMLClient, vpg_settings: Dict[str, List[Dict]]) -> Dict[str, List[Dict]]:
|
||||
"""
|
||||
Validate VPGs and VMs before creation.
|
||||
Returns a dictionary of valid VPGs and VMs to create.
|
||||
"""
|
||||
try:
|
||||
# Get existing VPGs and VMs
|
||||
existing_vpgs = {vpg['VpgName']: vpg for vpg in client.vpgs.list_vpgs()}
|
||||
existing_vms = {vm['VmIdentifier']: vm for vm in client.vms.list_vms()}
|
||||
local_site_id = client.localsite.get_local_site()['Link']['identifier']
|
||||
eligible_vms = {vm['VmIdentifier']: vm for vm in client.virtualization_sites.get_virtualization_site_vms(local_site_id)}
|
||||
logger.debug(f"validate_vpg_and_vms: existing_vms: {json.dumps(existing_vms, indent=4)}")
|
||||
logger.debug(f"validate_vpg_and_vms: eligible_vms: {json.dumps(eligible_vms, indent=4)}")
|
||||
|
||||
# Track VMs that appear in multiple VPGs
|
||||
vm_id_occurrences = defaultdict(list)
|
||||
vm_name_occurrences = defaultdict(list)
|
||||
valid_vpg_settings = defaultdict(list)
|
||||
vm_warnings = [] # Track warnings for VMs that are already protected
|
||||
|
||||
# First pass: collect all VM occurrences
|
||||
for vpg_name, vpg_rows in vpg_settings.items():
|
||||
for row in vpg_rows:
|
||||
vm_id = row['VM ID']
|
||||
vm_name = row.get('VM Name', '')
|
||||
vm_id_occurrences[vm_id].append(vpg_name)
|
||||
if vm_name:
|
||||
vm_name_occurrences[vm_name].append(vpg_name)
|
||||
|
||||
# Check for duplicates
|
||||
for vm_id, vpgs in vm_id_occurrences.items():
|
||||
if len(vpgs) > 1:
|
||||
logger.error(f"ERROR: VM ID '{vm_id}' found in multiple VPGs: {', '.join(vpgs)}. Exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
for vm_name, vpgs in vm_name_occurrences.items():
|
||||
if len(vpgs) > 1:
|
||||
logger.error(f"ERROR: VM Name '{vm_name}' found in multiple VPGs: {', '.join(vpgs)}. Exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
# Second pass: validate VPGs and VMs
|
||||
for vpg_name, vpg_rows in vpg_settings.items():
|
||||
if vpg_name in existing_vpgs:
|
||||
logger.error(f"ERROR: VPG '{vpg_name}' already exists. Skipping VPG creation...")
|
||||
sys.exit(1)
|
||||
|
||||
valid_vms = []
|
||||
for row in vpg_rows:
|
||||
vm_id = row['VM ID']
|
||||
vm_name = row.get('VM Name', '')
|
||||
|
||||
# Check if VM exists in eligible and existing VMs list
|
||||
if vm_id not in eligible_vms and vm_id not in existing_vms:
|
||||
logger.error(f"ERROR: VM '{vm_name}' with ID '{vm_id}' is not found in the eligible and existing VMs list. Exiting")
|
||||
sys.exit(1)
|
||||
|
||||
# Check if VM is already in a VPG
|
||||
if vm_id in existing_vms:
|
||||
existing_vm = existing_vms[vm_id]
|
||||
warning_msg = f"WARNING: VM '{vm_name}' with ID '{vm_id}' already exists in VPG '{existing_vm['VpgName']}' on site '{existing_vm.get('RecoverySiteName', 'N/A')}'.\n\
|
||||
Creating a new VPG {vpg_name} will only succeed if the target site is different than {existing_vm.get('RecoverySiteName', 'N/A')}"
|
||||
vm_warnings.append(warning_msg)
|
||||
logger.warning(warning_msg)
|
||||
|
||||
# Add VM to valid list
|
||||
valid_vms.append(row)
|
||||
|
||||
# Add VPG to valid settings if it has any valid VMs
|
||||
if valid_vms:
|
||||
valid_vpg_settings[vpg_name] = valid_vms
|
||||
|
||||
# Print validation summary
|
||||
print("\nValidation Summary:")
|
||||
print("------------------")
|
||||
|
||||
if vm_warnings:
|
||||
print("\nWarnings:")
|
||||
print("---------")
|
||||
for warning in vm_warnings:
|
||||
print(f" - {warning}")
|
||||
print("\nNote: Some VMs are already protected but will be added to new VPGs.")
|
||||
|
||||
if valid_vpg_settings:
|
||||
print(f"\nVPGs to be created ({len(valid_vpg_settings)}):")
|
||||
for vpg_name, vms in valid_vpg_settings.items():
|
||||
print(f" - {vpg_name} with {len(vms)} VMs:")
|
||||
for vm in vms:
|
||||
vm_id = vm['VM ID']
|
||||
vm_name = vm.get('VM Name', 'N/A')
|
||||
print(f" * {vm_id} ({vm_name})")
|
||||
else:
|
||||
print("\nNo VPGs to create - all VPGs were skipped")
|
||||
|
||||
print("\nDo you want to continue with VPG creation? (y/n)")
|
||||
try:
|
||||
response = input().lower()
|
||||
if response != 'y':
|
||||
logger.info("Operation cancelled by user")
|
||||
sys.exit(0)
|
||||
except KeyboardInterrupt:
|
||||
print("\nOperation cancelled by user (Ctrl+C)")
|
||||
sys.exit(0)
|
||||
|
||||
return valid_vpg_settings
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nOperation cancelled by user (Ctrl+C)")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logger.exception("Error during validation:")
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
"""Main function to execute the script."""
|
||||
try:
|
||||
parser = setup_argparse()
|
||||
args = parser.parse_args()
|
||||
|
||||
# Initialize ZVM client
|
||||
client = ZVMLClient(
|
||||
zvm_address=args.zvm_address,
|
||||
client_id=args.client_id,
|
||||
client_secret=args.client_secret,
|
||||
verify_certificate=not args.ignore_ssl
|
||||
)
|
||||
|
||||
# Read VPG settings from CSV
|
||||
logger.info(f"Reading VPG settings from {args.csv_file}...")
|
||||
vpg_settings = read_vpg_settings(args.csv_file)
|
||||
|
||||
# Validate VPGs and VMs
|
||||
logger.info("Validating VPGs and VMs...")
|
||||
valid_vpg_settings = validate_vpg_and_vms(client, vpg_settings)
|
||||
|
||||
if not valid_vpg_settings:
|
||||
logger.error("No valid VPGs to create. Exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
# Process each valid VPG
|
||||
for vpg_name, vpg_rows in valid_vpg_settings.items():
|
||||
vpg_id = None
|
||||
try:
|
||||
logger.info(f"Processing VPG: {vpg_name}")
|
||||
|
||||
# Use first row for VPG settings (they should be the same for all VMs in a VPG)
|
||||
vpg_payload = create_vpg_payload(vpg_rows[0])
|
||||
|
||||
# Create VPG
|
||||
logger.info(f"Creating VPG {vpg_name}...")
|
||||
vpg_id = client.vpgs.create_vpg(
|
||||
basic=vpg_payload['basic'],
|
||||
journal=vpg_payload['journal'],
|
||||
recovery=vpg_payload['recovery'],
|
||||
networks=vpg_payload['networks']
|
||||
)
|
||||
logger.info(f"VPG {vpg_name} created successfully with ID: {vpg_id}")
|
||||
|
||||
# Track if all VMs were added successfully
|
||||
all_vms_added = True
|
||||
failed_vms = []
|
||||
|
||||
# Add VMs to VPG
|
||||
for row in vpg_rows:
|
||||
vm_id = row['VM ID']
|
||||
vm_name = row.get('VM Name', 'N/A')
|
||||
try:
|
||||
logger.info(f"Adding VM {vm_id} ({vm_name}) to VPG {vpg_name}...")
|
||||
vm_payload = {
|
||||
"VmIdentifier": vm_id,
|
||||
"Recovery": {
|
||||
"HostClusterIdentifier": vpg_payload['recovery']['DefaultHostClusterIdentifier'],
|
||||
"DatastoreIdentifier": vpg_payload['recovery']['DefaultDatastoreIdentifier'],
|
||||
"FolderIdentifier": vpg_payload['recovery']['DefaultFolderIdentifier']
|
||||
}
|
||||
}
|
||||
task_id = client.vpgs.add_vm_to_vpg(vpg_name, vm_list_payload=vm_payload)
|
||||
logger.info(f"Task ID: {task_id} to add VM {vm_id} to VPG {vpg_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add VM {vm_id} ({vm_name}) to VPG {vpg_name}: {str(e)}")
|
||||
all_vms_added = False
|
||||
failed_vms.append((vm_id, vm_name, str(e)))
|
||||
|
||||
# If any VM addition failed, delete the VPG
|
||||
if not all_vms_added:
|
||||
logger.error(f"Failed to add some VMs to VPG {vpg_name}. Deleting VPG...")
|
||||
if vpg_id:
|
||||
try:
|
||||
client.vpgs.delete_vpg(vpg_name)
|
||||
logger.info(f"Successfully deleted VPG {vpg_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete VPG {vpg_name}: {str(e)}")
|
||||
|
||||
# Print detailed error summary
|
||||
print("\nFailed VM Additions:")
|
||||
print("-------------------")
|
||||
for vm_id, vm_name, error in failed_vms:
|
||||
print(f" - VM: {vm_name} (ID: {vm_id})")
|
||||
print(f" Error: {error}")
|
||||
print(f"\nVPG {vpg_name} was deleted due to VM addition failures.")
|
||||
continue
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\nOperation cancelled by user (Ctrl+C) while processing VPG {vpg_name}")
|
||||
# Try to clean up the VPG if it was created
|
||||
if vpg_id:
|
||||
try:
|
||||
logger.info(f"Cleaning up - deleting VPG {vpg_name}...")
|
||||
client.vpgs.delete_vpg(vpg_name)
|
||||
logger.info(f"Successfully deleted VPG {vpg_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete VPG {vpg_name}: {str(e)}")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing VPG {vpg_name}: {str(e)}")
|
||||
# Try to clean up the VPG if it was created
|
||||
if vpg_id:
|
||||
try:
|
||||
logger.info(f"Cleaning up - deleting VPG {vpg_name}...")
|
||||
client.vpgs.delete_vpg(vpg_name)
|
||||
logger.info(f"Successfully deleted VPG {vpg_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete VPG {vpg_name}: {str(e)}")
|
||||
continue
|
||||
|
||||
logger.info("VPG creation completed successfully")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nOperation cancelled by user (Ctrl+C)")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logger.exception("Error occurred:")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,354 @@
|
||||
#!/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 Site Resources Export Script
|
||||
|
||||
This script exports Zerto site resources (datastores, networks, VMs) to CSV files for both local and peer sites.
|
||||
It helps in documenting and analyzing the available resources for VPG creation and management.
|
||||
|
||||
Key Features:
|
||||
1. Site Resource Export:
|
||||
- Export peer site datastores with names and IDs
|
||||
- Export peer site networks with names and IDs
|
||||
- Export local site VMs with names and IDs
|
||||
|
||||
2. CSV File Generation:
|
||||
- Creates timestamped CSV files for each resource type
|
||||
- Includes detailed resource information
|
||||
- Uses site names in filenames for easy identification
|
||||
|
||||
Required Arguments:
|
||||
--zvm_address: ZVM IP address
|
||||
--client_id: API client ID
|
||||
--client_secret: API client secret
|
||||
--ignore_ssl: Ignore SSL certificate validation (optional)
|
||||
--output_dir: Directory to save CSV files (default: current directory)
|
||||
|
||||
Example Usage:
|
||||
python export_site_resources.py \
|
||||
--zvm_address "192.168.111.20" \
|
||||
--client_id "zerto-api" \
|
||||
--client_secret "your-secret-here" \
|
||||
--ignore_ssl
|
||||
--output_dir "output_dir"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib3
|
||||
from typing import Dict, List, Optional
|
||||
import json
|
||||
|
||||
# Add parent directory to Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from zvml import ZVMLClient
|
||||
|
||||
# Disable SSL warnings
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def setup_argparse() -> argparse.ArgumentParser:
|
||||
"""Set up command line argument parsing."""
|
||||
parser = argparse.ArgumentParser(description='Export Zerto site resources to CSV files')
|
||||
parser.add_argument('--zvm_address', required=True, help='ZVM IP address')
|
||||
parser.add_argument('--client_id', required=True, help='API client ID')
|
||||
parser.add_argument('--client_secret', required=True, help='API client secret')
|
||||
parser.add_argument('--ignore_ssl', action='store_true', help='Ignore SSL certificate validation')
|
||||
parser.add_argument('--output_dir', default='.', help='Directory to save CSV files (default: current directory)')
|
||||
return parser
|
||||
|
||||
def ensure_output_dir(output_dir: str) -> None:
|
||||
"""Ensure the output directory exists."""
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
logger.info(f"Created output directory: {output_dir}")
|
||||
|
||||
def get_site_info(client: ZVMLClient) -> Dict[str, str]:
|
||||
"""Get local and peer site information."""
|
||||
virtualization_sites = client.virtualization_sites.get_virtualization_sites()
|
||||
logger.info(f"Virtualization sites: {json.dumps(virtualization_sites, indent=4)}")
|
||||
if not virtualization_sites:
|
||||
raise ValueError("No sites found in ZVM")
|
||||
|
||||
# Get local site id and name
|
||||
local_site = client.localsite.get_local_site()
|
||||
logger.info(f"Local site: {json.dumps(local_site, indent=4)}")
|
||||
local_site_id = local_site.get('SiteIdentifier')
|
||||
|
||||
# Find local site in virtualization sites to get VirtualizationSiteName
|
||||
local_virtualization_site = next(
|
||||
(site for site in virtualization_sites if site['SiteIdentifier'] == local_site_id),
|
||||
None
|
||||
)
|
||||
if not local_virtualization_site:
|
||||
raise ValueError("Local site not found in virtualization sites")
|
||||
|
||||
# Update local site with VirtualizationSiteName
|
||||
local_site['VirtualizationSiteName'] = local_virtualization_site['VirtualizationSiteName']
|
||||
|
||||
# create an array of peer sites
|
||||
peer_sites = []
|
||||
for site in virtualization_sites:
|
||||
if site['SiteIdentifier'] != local_site_id:
|
||||
peer_sites.append(site)
|
||||
|
||||
return {
|
||||
'local': local_site,
|
||||
'peers': peer_sites
|
||||
}
|
||||
|
||||
def export_datastores(client: ZVMLClient, site_info: Dict, output_dir: str) -> None:
|
||||
"""Export datastores information to CSV."""
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# Get peer site datastores
|
||||
peer_datastores = client.virtualization_sites.get_virtualization_site_datastores(
|
||||
site_identifier=site_info['SiteIdentifier']
|
||||
)
|
||||
|
||||
# Create filename using VirtualizationSiteName
|
||||
filename = os.path.join(output_dir, f"{site_info['VirtualizationSiteName']}_datastores_{timestamp}.csv")
|
||||
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['Datastore Name', 'Datastore ID'])
|
||||
|
||||
for ds in peer_datastores:
|
||||
writer.writerow([
|
||||
ds.get('DatastoreName'),
|
||||
ds.get('DatastoreIdentifier')
|
||||
])
|
||||
|
||||
logger.info(f"Exported {len(peer_datastores)} datastores to {filename}")
|
||||
|
||||
def export_networks(client: ZVMLClient, site_info: Dict, output_dir: str) -> None:
|
||||
"""Export networks information to CSV."""
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# Get peer site networks
|
||||
peer_networks = client.virtualization_sites.get_virtualization_site_networks(
|
||||
site_identifier=site_info['SiteIdentifier']
|
||||
)
|
||||
logger.info(f"export_networks: peer_networks: {json.dumps(peer_networks, indent=4)}")
|
||||
|
||||
# Create filename using VirtualizationSiteName
|
||||
filename = os.path.join(output_dir, f"{site_info['VirtualizationSiteName']}_networks_{timestamp}.csv")
|
||||
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['Network VirtualizationNetworkName', 'Network ID'])
|
||||
|
||||
for net in peer_networks:
|
||||
writer.writerow([
|
||||
net.get('VirtualizationNetworkName'),
|
||||
net.get('NetworkIdentifier')
|
||||
])
|
||||
|
||||
logger.info(f"Exported {len(peer_networks)} networks to {filename}")
|
||||
|
||||
def export_vms(client: ZVMLClient, site_info: Dict, output_dir: str) -> None:
|
||||
"""Export VMs information to CSV."""
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# Get local site VMs
|
||||
local_vms = client.virtualization_sites.get_virtualization_site_vms(
|
||||
site_identifier=site_info['SiteIdentifier']
|
||||
)
|
||||
|
||||
# Create filename using VirtualizationSiteName
|
||||
filename = os.path.join(output_dir, f"{site_info['VirtualizationSiteName']}_vms_{timestamp}.csv")
|
||||
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['VM Name', 'VM ID'])
|
||||
|
||||
for vm in local_vms:
|
||||
writer.writerow([
|
||||
vm.get('VmName'),
|
||||
vm.get('VmIdentifier')
|
||||
])
|
||||
|
||||
logger.info(f"Exported {len(local_vms)} VMs to {filename}")
|
||||
|
||||
def export_hosts(client: ZVMLClient, site_info: Dict, output_dir: str) -> None:
|
||||
"""Export hosts information to CSV."""
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# Get peer site hosts
|
||||
peer_hosts = client.virtualization_sites.get_virtualization_site_hosts(
|
||||
site_identifier=site_info['SiteIdentifier']
|
||||
)
|
||||
logger.info(f"export_hosts: peer_hosts: {json.dumps(peer_hosts, indent=4)}")
|
||||
|
||||
# Create filename using VirtualizationSiteName
|
||||
filename = os.path.join(output_dir, f"{site_info['VirtualizationSiteName']}_hosts_{timestamp}.csv")
|
||||
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['Host Name', 'Host ID'])
|
||||
|
||||
for host in peer_hosts:
|
||||
writer.writerow([
|
||||
host.get('VirtualizationHostName'),
|
||||
host.get('HostIdentifier')
|
||||
])
|
||||
|
||||
logger.info(f"Exported {len(peer_hosts)} hosts to {filename}")
|
||||
|
||||
def export_folders(client: ZVMLClient, site_info: Dict, output_dir: str) -> None:
|
||||
"""Export folders information to CSV."""
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# Get peer site folders
|
||||
peer_folders = client.virtualization_sites.get_virtualization_site_folders(
|
||||
site_identifier=site_info['SiteIdentifier']
|
||||
)
|
||||
logger.info(f"export_folders: peer_folders: {json.dumps(peer_folders, indent=4)}")
|
||||
|
||||
# Create filename using VirtualizationSiteName
|
||||
filename = os.path.join(output_dir, f"{site_info['VirtualizationSiteName']}_folders_{timestamp}.csv")
|
||||
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['Folder Name', 'Folder ID'])
|
||||
|
||||
for folder in peer_folders:
|
||||
writer.writerow([
|
||||
folder.get('FolderName'),
|
||||
folder.get('FolderIdentifier')
|
||||
])
|
||||
|
||||
logger.info(f"Exported {len(peer_folders)} folders to {filename}")
|
||||
|
||||
def export_sites(client: ZVMLClient, site_info: Dict, output_dir: str) -> None:
|
||||
"""Export sites information to CSV."""
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# Create filename for sites
|
||||
filename = os.path.join(output_dir, f"zerto_sites_{timestamp}.csv")
|
||||
|
||||
# Get detailed peer site information
|
||||
peer_sites_details = client.peersites.get_peer_sites()
|
||||
# Convert to dict for easier lookup by site identifier
|
||||
peer_sites_dict = {site['SiteIdentifier']: site for site in peer_sites_details} if isinstance(peer_sites_details, list) else {}
|
||||
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow([
|
||||
'Site Name',
|
||||
'Site ID',
|
||||
'Location',
|
||||
'Version',
|
||||
'Site Type',
|
||||
'IP Address',
|
||||
'Is Local Site',
|
||||
'Host Name',
|
||||
'Region Name'
|
||||
])
|
||||
|
||||
# Write local site
|
||||
local_site = site_info['local']
|
||||
writer.writerow([
|
||||
local_site.get('SiteName'),
|
||||
local_site.get('SiteIdentifier'),
|
||||
local_site.get('Location'),
|
||||
local_site.get('Version'),
|
||||
local_site.get('SiteType'),
|
||||
local_site.get('IpAddress'),
|
||||
'Yes',
|
||||
'N/A', # Local site doesn't have host name
|
||||
local_site.get('RegionName', '')
|
||||
])
|
||||
|
||||
# Write peer sites with detailed information
|
||||
for peer in site_info['peers']:
|
||||
peer_id = peer.get('SiteIdentifier')
|
||||
peer_details = peer_sites_dict.get(peer_id, {})
|
||||
|
||||
writer.writerow([
|
||||
peer_details.get('PeerSiteName', ''),
|
||||
peer_id,
|
||||
peer_details.get('Location', ''),
|
||||
peer_details.get('Version', ''),
|
||||
peer_details.get('SiteType', ''),
|
||||
peer_details.get('HostName', ''), # Using HostName as IP Address
|
||||
'No',
|
||||
peer_details.get('HostName', ''),
|
||||
peer_details.get('RegionName', '')
|
||||
])
|
||||
|
||||
logger.info(f"Exported {len(site_info['peers']) + 1} sites to {filename}")
|
||||
logger.info(f"Peer sites details: {json.dumps(peer_sites_details, indent=4)}")
|
||||
|
||||
def main():
|
||||
"""Main function to execute the script."""
|
||||
parser = setup_argparse()
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Ensure output directory exists
|
||||
ensure_output_dir(args.output_dir)
|
||||
|
||||
# Initialize ZVM client
|
||||
client = ZVMLClient(
|
||||
zvm_address=args.zvm_address,
|
||||
client_id=args.client_id,
|
||||
client_secret=args.client_secret,
|
||||
verify_certificate=not args.ignore_ssl
|
||||
)
|
||||
|
||||
# Get site information
|
||||
logger.info("Retrieving site information...")
|
||||
site_info = get_site_info(client)
|
||||
logger.info(f"Site info: {json.dumps(site_info, indent=4)}")
|
||||
|
||||
# Log site information
|
||||
logger.info(f"Local site: {site_info['local'].get('VirtualizationSiteName')}")
|
||||
for peer in site_info['peers']:
|
||||
logger.info(f"Peer site: {peer.get('VirtualizationSiteName')}")
|
||||
|
||||
# Export sites to CSV
|
||||
logger.info("\nExporting sites information...")
|
||||
export_sites(client, site_info, args.output_dir)
|
||||
|
||||
# Export resources to CSV files
|
||||
for peer in site_info['peers']:
|
||||
logger.info(f"Exporting resources for peer site: {peer.get('VirtualizationSiteName')}")
|
||||
export_datastores(client, peer, args.output_dir)
|
||||
export_networks(client, peer, args.output_dir)
|
||||
export_hosts(client, peer, args.output_dir)
|
||||
export_folders(client, peer, args.output_dir)
|
||||
|
||||
logger.info(f"Exporting VMs for local site: {site_info['local'].get('VirtualizationSiteName')}")
|
||||
export_vms(client, site_info['local'], args.output_dir)
|
||||
|
||||
logger.info(f"Export completed successfully. Files saved to: {args.output_dir}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error occurred:") # This will log the full stack trace
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+18
-3
@@ -149,16 +149,31 @@ def get_safe_filename(timestamp):
|
||||
# Replace colons with underscores and remove any other problematic characters
|
||||
return timestamp.replace(':', '_').replace('/', '_').replace('\\', '_')
|
||||
|
||||
def main():
|
||||
def setup_argparse() -> argparse.ArgumentParser:
|
||||
"""Set up command line argument parsing."""
|
||||
parser = argparse.ArgumentParser(description="Export VPG settings to CSV")
|
||||
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("--vpg_names", help="Comma-separated list of VPG names to export (optional)")
|
||||
parser.add_argument("--output_dir", default='.', help="Directory to save exported files (default: current directory)")
|
||||
return parser
|
||||
|
||||
def ensure_output_dir(output_dir: str) -> None:
|
||||
"""Ensure the output directory exists."""
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
logging.info(f"Created output directory: {output_dir}")
|
||||
|
||||
def main():
|
||||
parser = setup_argparse()
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Ensure output directory exists
|
||||
ensure_output_dir(args.output_dir)
|
||||
|
||||
# Setup client
|
||||
client = setup_client(args)
|
||||
|
||||
@@ -186,7 +201,7 @@ def main():
|
||||
export_settings = client.vpgs.read_exported_vpg_settings(timestamp, vpg_names)
|
||||
|
||||
# Save the JSON export
|
||||
json_file_name = f"ExportedSettings_{safe_timestamp}.json"
|
||||
json_file_name = os.path.join(args.output_dir, f"ExportedSettings_{safe_timestamp}.json")
|
||||
with open(json_file_name, 'w') as f:
|
||||
json.dump(export_settings['ExportedVpgSettingsApi'], f, indent=2)
|
||||
print(f"\nJSON export saved to: {json_file_name}")
|
||||
@@ -195,7 +210,7 @@ def main():
|
||||
nic_settings = extract_nic_settings(export_settings['ExportedVpgSettingsApi'])
|
||||
|
||||
# Create CSV file with Windows line endings
|
||||
csv_file_name = f"ExportedSettings_{safe_timestamp}.csv"
|
||||
csv_file_name = os.path.join(args.output_dir, f"ExportedSettings_{safe_timestamp}.csv")
|
||||
fieldnames = [
|
||||
'VPG Name', 'VM Identifier', 'NIC Identifier',
|
||||
'Failover Network', 'Failover ShouldReplaceIpConfiguration', 'Failover DHCP',
|
||||
@@ -0,0 +1,4 @@
|
||||
VPG Name,VPG Identifier,VPG Type,RPO (seconds),Test Interval (minutes),Journal History (hours),Priority,Use WAN Compression,Protected Site Name,Protected Site ID,Recovery Site Name,Recovery Site ID,Journal Datastore Name,Journal Datastore ID,Journal Hard Limit (MB),Journal Warning Threshold (MB),Scratch Datastore Name,Scratch Datastore ID,Scratch Hard Limit (MB),Scratch Warning Threshold (MB),Recovery Host Name,Recovery Host ID,Recovery Host Cluster Name,Recovery Host Cluster ID,Recovery Datastore Name,Recovery Datastore ID,Recovery Folder Name,Recovery Folder ID,Failover Network Name,Failover Network ID,Failover Test Network Name,Failover Test Network ID,VM Name,VM ID
|
||||
seth,,Remote,300,262080,24,Medium,TRUE,Production,17d62de4-8378-4830-a859-2e739068eaa0,DR,6340a6de-ceb5-42e6-a851-e7a6afe6920b,datastore1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.datastore-3014,153600,115200,datastore2,,307200,230400,,,cluster1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.domain-c2001,datastore1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.datastore-3014,folder1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.group-v4,network1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.network-3015,network2,03cb5a97-4d85-4449-99ef-3a268bf3f60d.network-3016,nginx-light-vm1,b13df028-21ad-493c-ad48-1e6953296a4b.vm-10009
|
||||
seth1,,Remote,300,262080,24,Medium,TRUE,Production,17d62de4-8378-4830-a859-2e739068eaa0,DR,6340a6de-ceb5-42e6-a851-e7a6afe6920b,datastore1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.datastore-3014,153600,115200,datastore2,,307200,230400,,,cluster1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.domain-c2001,datastore1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.datastore-3014,folder1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.group-v4,network1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.network-3015,network2,03cb5a97-4d85-4449-99ef-3a268bf3f60d.network-3016,SpringBoot-SmallCentOS,b13df028-21ad-493c-ad48-1e6953296a4b.vm-10011
|
||||
seth2,,Remote,300,262080,24,Medium,TRUE,Production,17d62de4-8378-4830-a859-2e739068eaa0,DR,6340a6de-ceb5-42e6-a851-e7a6afe6920b,datastore1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.datastore-3014,153600,115200,datastore2,,307200,230400,,,cluster1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.domain-c2001,datastore1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.datastore-3014,folder1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.group-v4,network1,03cb5a97-4d85-4449-99ef-3a268bf3f60d.network-3015,network2,03cb5a97-4d85-4449-99ef-3a268bf3f60d.network-3016,Microsoft 2022,b13df028-21ad-493c-ad48-1e6953296a4b.vm-6020
|
||||
|
Reference in New Issue
Block a user