added examples that allows bulk vpg creating and re-ip using csv files

This commit is contained in:
Kosta Mushkin
2025-05-27 19:47:37 -04:00
parent 4cf11b7bc0
commit fa82ed2221
7 changed files with 923 additions and 3 deletions
+163
View File
@@ -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,148 @@
#!/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.
import json
import csv
import sys
from pathlib import Path
"""
Zerto VPG Settings JSON to CSV Converter
This script converts Zerto VPG settings from JSON format to CSV format, making it easier to
view and edit the settings in spreadsheet applications. It's designed to work with the JSON
output from export_vpg_settings_nics_to_csv.py.
Key Features:
1. JSON to CSV Conversion:
- Convert VPG settings from JSON to CSV format
- Preserve all NIC and network settings
- Maintain data structure and relationships
- Support for both DHCP and static IP configurations
2. Data Formatting:
- Format boolean values as "True"/"False"
- Preserve network identifiers
- Handle null/empty values appropriately
- Maintain consistent field ordering
3. Output Generation:
- Generate timestamped CSV files
- Include all relevant VPG settings
- Preserve data integrity
- Easy to read and edit format
Required Arguments:
--json_file: Path to the JSON file containing VPG settings
Example Usage:
python convert_export_settings_to_csv.py \
--json_file "ExportedSettings_2024-05-12.json"
Output:
- Generates a CSV file with the same base name as the input JSON
- Includes all VPG settings in a tabular format
- Preserves all network and IP configurations
- Maintains compatibility with import_vpg_settings_nics_from_csv.py
Note: This script is part of a suite of tools for managing Zerto VPG settings. It's designed
to work seamlessly with both the export and import scripts, providing a convenient way to
view and edit VPG settings in spreadsheet applications.
"""
def extract_nic_settings(json_data):
"""Extract NIC settings from VPG JSON data."""
nic_settings = []
for vpg in json_data:
vpg_name = vpg['Basic']['Name']
for vm in vpg['Vms']:
vm_id = vm['VmIdentifier']
for nic in vm['Nics']:
nic_id = nic['NicIdentifier']
# Extract failover settings
failover = nic['Failover']['Hypervisor'] if nic['Failover'] and nic['Failover']['Hypervisor'] else {}
failover_network = failover.get('NetworkIdentifier', '')
failover_ip_config = failover.get('IpConfig', {}) or {}
# Extract failover test settings
failover_test = nic['FailoverTest']['Hypervisor'] if nic['FailoverTest'] and nic['FailoverTest']['Hypervisor'] else {}
failover_test_network = failover_test.get('NetworkIdentifier', '')
failover_test_ip_config = failover_test.get('IpConfig', {}) or {}
# Create a row for each NIC
row = {
'VPG Name': vpg_name,
'VM Identifier': vm_id,
'NIC Identifier': nic_id,
'Failover Network': failover_network,
'Failover IP': failover_ip_config.get('StaticIp', ''),
'Failover Subnet': failover_ip_config.get('SubnetMask', ''),
'Failover Gateway': failover_ip_config.get('Gateway', ''),
'Failover DNS1': failover_ip_config.get('PrimaryDns', ''),
'Failover DNS2': failover_ip_config.get('SecondaryDns', ''),
'Failover DHCP': 'Yes' if failover_ip_config.get('IsDhcp', False) else 'No',
'Failover IsDhcp': failover_ip_config.get('IsDhcp', False),
'Failover Test Network': failover_test_network,
'Failover Test IP': failover_test_ip_config.get('StaticIp', ''),
'Failover Test Subnet': failover_test_ip_config.get('SubnetMask', ''),
'Failover Test Gateway': failover_test_ip_config.get('Gateway', ''),
'Failover Test DNS1': failover_test_ip_config.get('PrimaryDns', ''),
'Failover Test DNS2': failover_test_ip_config.get('SecondaryDns', ''),
'Failover Test DHCP': 'Yes' if failover_test_ip_config.get('IsDhcp', False) else 'No',
'Failover Test IsDhcp': failover_test_ip_config.get('IsDhcp', False)
}
nic_settings.append(row)
return nic_settings
def main():
if len(sys.argv) != 2:
print("Usage: python vpg_nic_settings_to_csv.py <json_file>")
sys.exit(1)
json_file = Path(sys.argv[1])
if not json_file.exists():
print(f"Error: File {json_file} does not exist")
sys.exit(1)
# Read JSON file
with open(json_file, 'r') as f:
json_data = json.load(f)
# Extract NIC settings
nic_settings = extract_nic_settings(json_data)
# Create CSV file
csv_file = json_file.with_suffix('.csv')
fieldnames = [
'VPG Name', 'VM Identifier', 'NIC Identifier',
'Failover Network', 'Failover IP', 'Failover Subnet', 'Failover Gateway',
'Failover DNS1', 'Failover DNS2', 'Failover DHCP', 'Failover IsDhcp',
'Failover Test Network', 'Failover Test IP', 'Failover Test Subnet',
'Failover Test Gateway', 'Failover Test DNS1', 'Failover Test DNS2',
'Failover Test DHCP', 'Failover Test IsDhcp'
]
with open(csv_file, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(nic_settings)
print(f"CSV file created: {csv_file}")
if __name__ == '__main__':
main()
@@ -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()
@@ -0,0 +1,250 @@
#!/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.
import argparse
import logging
import json
import csv
import sys
import os
from pathlib import Path
import urllib3
from typing import List, Dict
import codecs
# Add parent directory to path to import zvml
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)
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
"""
Zerto VPG NIC Settings Export Script
This script exports Virtual Protection Group (VPG) NIC settings to a CSV file, focusing on network
and IP configuration details. It's designed to help with bulk management of VPG NIC settings.
Key Features:
1. VPG NIC Settings Export:
- Export NIC settings for specific VPGs or all VPGs
- Save settings to both JSON and CSV formats
- Include network and IP configuration details
- Capture DHCP and static IP settings
2. CSV Format:
- Organized by VPG, VM, and NIC
- Includes network identifiers
- DHCP settings (True/False)
- Static IP configuration (IP, Subnet, Gateway, DNS)
- ShouldReplaceIpConfiguration flag
3. Settings Management:
- Export current VPG settings
- Convert to CSV format
- Save with timestamp
- Support for Windows line endings
Required Arguments:
--zvm_address: ZVM address
--client_id: Keycloak client ID
--client_secret: Keycloak client secret
--ignore_ssl: Ignore SSL certificate verification (optional)
--vpg_names: Comma-separated list of VPG names to export (optional)
Example Usage:
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 Files:
- ExportedSettings_[timestamp].json: Full VPG settings in JSON format
- ExportedSettings_[timestamp].csv: NIC settings in CSV format
Note: This script is part of a pair with import_vpg_settings_nics_from_csv.py, allowing for
export and import of VPG NIC settings in bulk. The CSV format is designed to be easily
editable in spreadsheet applications.
"""
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 extract_nic_settings(json_data):
"""Extract NIC settings from VPG JSON data."""
nic_settings = []
for vpg in json_data:
vpg_name = vpg['Basic']['Name']
for vm in vpg['Vms']:
vm_id = vm['VmIdentifier']
for nic in vm['Nics']:
nic_id = nic['NicIdentifier']
# Extract failover settings
failover = nic['Failover']['Hypervisor'] if nic['Failover'] and nic['Failover']['Hypervisor'] else {}
failover_network = failover.get('NetworkIdentifier', '')
failover_ip_config = failover.get('IpConfig', {}) or {}
# Extract failover test settings
failover_test = nic['FailoverTest']['Hypervisor'] if nic['FailoverTest'] and nic['FailoverTest']['Hypervisor'] else {}
failover_test_network = failover_test.get('NetworkIdentifier', '')
failover_test_ip_config = failover_test.get('IpConfig', {}) or {}
# Create a row for each NIC
row = {
'VPG Name': vpg_name,
'VM Identifier': vm_id,
'NIC Identifier': nic_id,
'Failover Network': failover_network,
'Failover ShouldReplaceIpConfiguration': str(failover.get('ShouldReplaceIpConfiguration', False)),
'Failover DHCP': str(failover_ip_config.get('IsDhcp', False)),
'Failover IP': failover_ip_config.get('StaticIp', ''),
'Failover Subnet': failover_ip_config.get('SubnetMask', ''),
'Failover Gateway': failover_ip_config.get('Gateway', ''),
'Failover DNS1': failover_ip_config.get('PrimaryDns', ''),
'Failover DNS2': failover_ip_config.get('SecondaryDns', ''),
'Failover Test Network': failover_test_network,
'Failover Test ShouldReplaceIpConfiguration': str(failover_test.get('ShouldReplaceIpConfiguration', False)),
'Failover Test DHCP': str(failover_test_ip_config.get('IsDhcp', False)),
'Failover Test IP': failover_test_ip_config.get('StaticIp', ''),
'Failover Test Subnet': failover_test_ip_config.get('SubnetMask', ''),
'Failover Test Gateway': failover_test_ip_config.get('Gateway', ''),
'Failover Test DNS1': failover_test_ip_config.get('PrimaryDns', ''),
'Failover Test DNS2': failover_test_ip_config.get('SecondaryDns', '')
}
nic_settings.append(row)
return nic_settings
def get_safe_filename(timestamp):
"""Convert timestamp to a URL-safe filename."""
# Replace colons with underscores and remove any other problematic characters
return timestamp.replace(':', '_').replace('/', '_').replace('\\', '_')
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)
# Process VPG names if provided
vpg_names = None
if args.vpg_names:
vpg_names = [name.strip() for name in args.vpg_names.split(',')]
logging.info(f"Exporting settings for VPGs: {vpg_names}")
else:
logging.info("No VPG names provided, exporting all VPGs")
# Export VPG settings
print("\nExporting VPG settings...")
export_result = client.vpgs.export_vpg_settings(vpg_names)
if not export_result or 'TimeStamp' not in export_result:
logging.error("Failed to export VPG settings")
sys.exit(1)
timestamp = export_result['TimeStamp']
safe_timestamp = get_safe_filename(timestamp)
print(f"Export completed successfully. Timestamp: {timestamp}")
# Get the exported settings
export_settings = client.vpgs.read_exported_vpg_settings(timestamp, vpg_names)
# Save the JSON export
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}")
# Convert to CSV
nic_settings = extract_nic_settings(export_settings['ExportedVpgSettingsApi'])
# Create CSV file with Windows line endings
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',
'Failover IP', 'Failover Subnet', 'Failover Gateway',
'Failover DNS1', 'Failover DNS2',
'Failover Test Network', 'Failover Test ShouldReplaceIpConfiguration', 'Failover Test DHCP',
'Failover Test IP', 'Failover Test Subnet',
'Failover Test Gateway', 'Failover Test DNS1', 'Failover Test DNS2'
]
# Write CSV content directly
with open(csv_file_name, 'w', newline='') as f:
writer = csv.DictWriter(
f,
fieldnames=fieldnames,
delimiter=',',
quoting=csv.QUOTE_ALL,
quotechar='"',
lineterminator='\r\n'
)
writer.writeheader()
for row in nic_settings:
# Ensure all fields are present and properly formatted
for field in fieldnames:
if field not in row:
row[field] = ''
# No need to convert boolean values since they're already strings
writer.writerow(row)
print(f"CSV file created: {csv_file_name}")
except Exception as e:
logging.exception("Error occurred:")
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,588 @@
#!/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.
import argparse
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
import json
import csv
import sys
import os
from pathlib import Path
import urllib3
from typing import List, Dict, Tuple
from datetime import datetime
# Add parent directory to path to import zvml
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)
"""
Zerto VPG NIC Settings Import Script
This script imports Virtual Protection Group (VPG) NIC settings from a CSV file, allowing for
bulk updates of network and IP configurations. It's designed to work with the exported CSV
from export_vpg_settings_nics_to_csv.py.
Key Features:
1. VPG NIC Settings Import:
- Import NIC settings from CSV file
- Update specific VPGs or all VPGs
- Validate settings before applying
- Support for both DHCP and static IP configurations
2. Settings Validation:
- Validate DHCP and static IP settings
- Check ShouldReplaceIpConfiguration flag
- Ensure no conflicting configurations
- Verify network identifiers
3. Bulk Updates:
- Process multiple VPGs in one operation
- Show changes before applying
- Require confirmation before updates
- Detailed logging of changes
Required Arguments:
--zvm_address: ZVM address
--client_id: Keycloak client ID
--client_secret: Keycloak client secret
--ignore_ssl: Ignore SSL certificate verification (optional)
--csv_file: Path to the CSV file with updated settings
--vpg_names: Comma-separated list of VPG names to update (optional)
Example Usage:
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 "ExportedSettings_2024-05-12.csv" \
--vpg_names "VpgTest1,VpgTest2" \
--ignore_ssl
CSV Format Requirements:
- Must include VPG Name, VM Identifier, and NIC Identifier
- DHCP values must be "True" or "False" (case-insensitive)
- ShouldReplaceIpConfiguration must be "True" to modify IP settings
- Static IP settings (IP, Subnet, Gateway, DNS) are optional when DHCP is True
Note: This script is part of a pair with export_vpg_settings_nics_to_csv.py. It's designed
to safely update VPG NIC settings in bulk, with validation and confirmation steps to
prevent unintended changes.
"""
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 read_csv_settings(csv_path: str) -> List[Dict]:
"""Read settings from CSV file."""
settings = []
with open(csv_path, 'r', newline='') as f:
reader = csv.DictReader(f)
for row in reader:
settings.append(row)
return settings
def get_current_settings(client: ZVMLClient, vpg_names: List[str] = None) -> Tuple[str, List[Dict]]:
"""Get current VPG settings and convert to CSV format."""
# Export current settings
export_result = client.vpgs.export_vpg_settings(vpg_names)
if not export_result or 'TimeStamp' not in export_result:
raise Exception("Failed to export VPG settings")
timestamp = export_result['TimeStamp']
export_settings = client.vpgs.read_exported_vpg_settings(timestamp, vpg_names)
# logging.info(f"get_current_settings: export_settings: {json.dumps(export_settings, indent=4)}")
# Convert to CSV format
nic_settings = []
for vpg in export_settings['ExportedVpgSettingsApi']:
vpg_name = vpg['Basic']['Name']
for vm in vpg['Vms']:
vm_id = vm['VmIdentifier']
for nic in vm['Nics']:
nic_id = nic['NicIdentifier']
# Extract failover settings
failover = nic['Failover']['Hypervisor'] if nic['Failover'] and nic['Failover']['Hypervisor'] else {}
failover_network = failover.get('NetworkIdentifier', '')
failover_ip_config = failover.get('IpConfig', {}) or {}
# Extract failover test settings
failover_test = nic['FailoverTest']['Hypervisor'] if nic['FailoverTest'] and nic['FailoverTest']['Hypervisor'] else {}
failover_test_network = failover_test.get('NetworkIdentifier', '')
failover_test_ip_config = failover_test.get('IpConfig', {}) or {}
row = {
'VPG Name': vpg_name,
'VM Identifier': vm_id,
'NIC Identifier': nic_id,
'Failover Network': failover_network,
'Failover ShouldReplaceIpConfiguration': str(failover.get('ShouldReplaceIpConfiguration', False)),
'Failover DHCP': str(failover_ip_config.get('IsDhcp', False)),
'Failover IP': failover_ip_config.get('StaticIp', ''),
'Failover Subnet': failover_ip_config.get('SubnetMask', ''),
'Failover Gateway': failover_ip_config.get('Gateway', ''),
'Failover DNS1': failover_ip_config.get('PrimaryDns', ''),
'Failover DNS2': failover_ip_config.get('SecondaryDns', ''),
'Failover Test Network': failover_test_network,
'Failover Test ShouldReplaceIpConfiguration': str(failover_test.get('ShouldReplaceIpConfiguration', False)),
'Failover Test DHCP': str(failover_test_ip_config.get('IsDhcp', False)),
'Failover Test IP': failover_test_ip_config.get('StaticIp', ''),
'Failover Test Subnet': failover_test_ip_config.get('SubnetMask', ''),
'Failover Test Gateway': failover_test_ip_config.get('Gateway', ''),
'Failover Test DNS1': failover_test_ip_config.get('PrimaryDns', ''),
'Failover Test DNS2': failover_test_ip_config.get('SecondaryDns', '')
}
nic_settings.append(row)
return timestamp, nic_settings
def normalize_value(value):
"""Normalize values for comparison."""
# Treat None, empty string, and 'None' as the same
if value in ['', None, 'None', 'null']:
return ''
if isinstance(value, bool):
return str(value).lower()
if isinstance(value, str):
value = value.lower()
if value == 'true':
return 'true'
if value == 'false':
return 'false'
return str(value)
def compare_settings(client, current: List[Dict], updated: List[Dict]) -> List[Dict]:
"""Compare current and updated settings and return changes."""
changes = []
# Create lookup dictionaries for faster comparison
current_lookup = {
(row['VPG Name'], row['VM Identifier'], row['NIC Identifier']): row
for row in current
}
def validate_dhcp_settings(client, row: Dict, vpg_name: str, vm_id: str, nic_id: str):
"""Validate that DHCP and IP settings are not conflicting."""
vm_name = client.vms.list_vms(vm_identifier=vm_id).get('VmName')
def validate_ip_settings(prefix: str):
should_replace = normalize_value(row.get(f'{prefix} ShouldReplaceIpConfiguration', '')) == 'true'
dhcp = normalize_value(row.get(f'{prefix} DHCP', '')) == 'true'
has_static_ip = any(row.get(f'{prefix} {field}') for field in ['IP', 'Subnet', 'Gateway', 'DNS1', 'DNS2'])
if not should_replace and (dhcp or has_static_ip):
raise ValueError(
f"Invalid configuration for VPG '{vpg_name}', VM Name '{vm_name}', VM ID '{vm_id}', NIC '{nic_id}': "
f"{prefix} ShouldReplaceIpConfiguration is False but IP settings are present. "
f"Set ShouldReplaceIpConfiguration to True to modify IP settings."
)
if should_replace and not dhcp and not has_static_ip:
raise ValueError(
f"Invalid configuration for VPG '{vpg_name}', VM Name '{vm_name}', VM ID '{vm_id}', NIC '{nic_id}': "
f"{prefix} ShouldReplaceIpConfiguration is True but no IP configuration is provided. "
f"Either set DHCP=True or provide IP configuration (IP, Subnet, Gateway, DNS1, DNS2)."
)
if dhcp and has_static_ip:
raise ValueError(
f"Invalid configuration for VPG '{vpg_name}', VM Name '{vm_name}', VM ID '{vm_id}', NIC '{nic_id}': "
f"Cannot have {prefix} DHCP=True and static IP settings. "
f"Please remove static IP settings or set DHCP=False."
)
# Validate both failover and failover test settings
validate_ip_settings('Failover')
validate_ip_settings('Failover Test')
for updated_row in updated:
key = (updated_row['VPG Name'], updated_row['VM Identifier'], updated_row['NIC Identifier'])
# Validate DHCP settings before processing changes
validate_dhcp_settings(
client,
updated_row,
updated_row['VPG Name'],
updated_row['VM Identifier'],
updated_row['NIC Identifier']
)
if key in current_lookup:
current_row = current_lookup[key]
row_changes = {}
# Compare each field
for field in updated_row:
if field in ['VPG Name', 'VM Identifier', 'NIC Identifier']:
continue
current_value = normalize_value(current_row.get(field, ''))
updated_value = normalize_value(updated_row.get(field, ''))
# Only include the change if the values are different after normalization
if current_value != updated_value:
row_changes[field] = {
'current': current_row.get(field, ''),
'updated': updated_row.get(field, '')
}
if row_changes:
vm_name = client.vms.list_vms(vm_identifier=updated_row['VM Identifier']).get('VmName')
logging.info(f"compare_settings: vm_name {vm_name}")
changes.append({
'VPG Name': updated_row['VPG Name'],
'VM Identifier': updated_row['VM Identifier'],
'NIC Identifier': updated_row['NIC Identifier'],
'VM Name': vm_name,
'changes': row_changes
})
return changes
def display_changes(client, changes: List[Dict]):
"""Display changes in a user-friendly format."""
if not changes:
print("\nNo changes found in the CSV file.")
return
# Group changes by VPG
vpg_changes = {}
for change in changes:
vpg_name = change['VPG Name']
if vpg_name not in vpg_changes:
vpg_changes[vpg_name] = {}
vm_id = change['VM Identifier']
if vm_id not in vpg_changes[vpg_name]:
vpg_changes[vpg_name][vm_id] = {}
nic_id = change['NIC Identifier']
vpg_changes[vpg_name][vm_id][nic_id] = change['changes']
print("\nThe following changes will be applied:")
print("=" * 80)
for vpg_name, vm_changes in vpg_changes.items():
# Skip VPGs with no actual changes
has_vpg_changes = False
for vm_id, nic_changes in vm_changes.items():
for nic_id, changes in nic_changes.items():
if any(values['current'] != values['updated'] for values in changes.values()):
has_vpg_changes = True
break
if has_vpg_changes:
break
if not has_vpg_changes:
continue
print(f"\nVPG: {vpg_name}")
print("-" * 40)
for vm_id, nic_changes in vm_changes.items():
# Skip VMs with no actual changes
has_vm_changes = False
for nic_id, changes in nic_changes.items():
if any(values['current'] != values['updated'] for values in changes.values()):
has_vm_changes = True
break
if not has_vm_changes:
continue
vm_name = client.vms.list_vms(vm_identifier=vm_id).get('VmName')
print(f" VM name: {vm_name}, VM ID: {vm_id}")
for nic_id, changes in nic_changes.items():
# Skip NICs with no actual changes
if not any(values['current'] != values['updated'] for values in changes.values()):
continue
print(f" NIC: {nic_id}")
print(" Changes:")
for field, values in changes.items():
# Only show fields that have actual changes
if values['current'] != values['updated']:
print(f" {field}:")
print(f" Current: {values['current']}")
print(f" Updated: {values['updated']}")
print()
print("=" * 80)
print(f"\nTotal changes: {len(changes)} NIC(s) across {len(vpg_changes)} VPG(s)")
def update_vpg_settings(client: ZVMLClient, changes: List[Dict]):
"""Update VPG settings based on changes."""
# Group changes by VPG
vpg_changes = {}
for change in changes:
vpg_name = change['VPG Name']
if vpg_name not in vpg_changes:
vpg_changes[vpg_name] = []
vpg_changes[vpg_name].append(change)
# Process each VPG
for vpg_name, vpg_change_list in vpg_changes.items():
logging.info(f"update_vpg_settings: Processing VPG: {vpg_name}")
logging.info(f"update_vpg_settings: VPG change list: {json.dumps(vpg_change_list, indent=4)}")
# Get VPG identifier
vpg_info = client.vpgs.list_vpgs(vpg_name=vpg_name)
if not vpg_info:
logging.error(f"update_vpg_settings: VPG {vpg_name} not found")
continue
vpg_identifier = vpg_info['VpgIdentifier']
# Create new VPG settings
vpg_settings_id = client.vpgs.create_vpg_settings(vpg_identifier=vpg_identifier)
vpg_settings = client.vpgs.get_vpg_settings_by_id(vpg_settings_id)
logging.info(f"update_vpg_settings: VPG settings: {json.dumps(vpg_settings, indent=4)}")
# Process each NIC change
for change in vpg_change_list:
vm_id = change['VM Identifier']
nic_id = change['NIC Identifier']
vm_name = change['VM Name']
logging.info(f"update_vpg_settings: Processing NIC: {nic_id} for VM: {vm_name} VM ID: {vm_id}")
# Find the VM and NIC in the settings
vm = None
for v in vpg_settings['Vms']:
if v['VmIdentifier'] == vm_id:
vm = v
# logging.info(f"update_vpg_settings: Found VM: {vm_id} in VPG {vpg_name} vm={json.dumps(vm, indent=4)}")
break
if not vm:
logging.error(f"update_vpg_settings: VM {vm_id} not found in VPG {vpg_name}")
continue
# Find the NIC
nic = None
for n in vm['Nics']:
if n['NicIdentifier'] == nic_id:
nic = n
logging.info(f"update_vpg_settings: Found NIC: {nic_id} in VM {vm_name} VPG {vpg_name} nic={json.dumps(nic, indent=4)}")
break
if not nic:
logging.error(f"update_vpg_settings: NIC {nic_id} not found in VM {vm_id}")
continue
# Initialize structures if needed
if not nic.get('Failover'):
nic['Failover'] = {'Hypervisor': {}}
if not nic.get('FailoverTest'):
nic['FailoverTest'] = {'Hypervisor': {}}
# Process each change for this NIC
for field, values in change['changes'].items():
# Handle Failover settings
if field in ['Failover Network', 'Failover ShouldReplaceIpConfiguration', 'Failover IP',
'Failover Subnet', 'Failover Gateway', 'Failover DNS1', 'Failover DNS2',
'Failover DHCP']:
if field == 'Failover ShouldReplaceIpConfiguration':
nic['Failover']['Hypervisor']['ShouldReplaceIpConfiguration'] = normalize_value(values['updated']) == 'true'
elif field == 'Failover Network':
nic['Failover']['Hypervisor']['NetworkIdentifier'] = values['updated']
elif field == 'Failover DHCP':
if not nic['Failover']['Hypervisor'].get('IpConfig'):
nic['Failover']['Hypervisor']['IpConfig'] = {
'StaticIp': None,
'SubnetMask': None,
'Gateway': None,
'PrimaryDns': None,
'SecondaryDns': None,
'IsDhcp': False
}
nic['Failover']['Hypervisor']['IpConfig']['IsDhcp'] = normalize_value(values['updated']) == 'true'
# If DHCP is enabled, clear other IP settings
if normalize_value(values['updated']) == 'true':
nic['Failover']['Hypervisor']['IpConfig'].update({
'StaticIp': None,
'SubnetMask': None,
'Gateway': None,
'PrimaryDns': None,
'SecondaryDns': None
})
elif field in ['Failover IP', 'Failover Subnet', 'Failover Gateway',
'Failover DNS1', 'Failover DNS2']:
if not nic['Failover']['Hypervisor'].get('IpConfig'):
nic['Failover']['Hypervisor']['IpConfig'] = {
'StaticIp': None,
'SubnetMask': None,
'Gateway': None,
'PrimaryDns': None,
'SecondaryDns': None,
'IsDhcp': False
}
if field == 'Failover IP':
nic['Failover']['Hypervisor']['IpConfig']['StaticIp'] = values['updated'] if values['updated'] else None
elif field == 'Failover Subnet':
nic['Failover']['Hypervisor']['IpConfig']['SubnetMask'] = values['updated'] if values['updated'] else '255.255.255.0'
elif field == 'Failover Gateway':
nic['Failover']['Hypervisor']['IpConfig']['Gateway'] = values['updated'] if values['updated'] else None
elif field == 'Failover DNS1':
nic['Failover']['Hypervisor']['IpConfig']['PrimaryDns'] = values['updated'] if values['updated'] else None
elif field == 'Failover DNS2':
nic['Failover']['Hypervisor']['IpConfig']['SecondaryDns'] = values['updated'] if values['updated'] else None
# Handle Failover Test settings
elif field in ['Failover Test Network', 'Failover Test ShouldReplaceIpConfiguration',
'Failover Test IP', 'Failover Test Subnet', 'Failover Test Gateway',
'Failover Test DNS1', 'Failover Test DNS2', 'Failover Test DHCP']:
if field == 'Failover Test ShouldReplaceIpConfiguration':
nic['FailoverTest']['Hypervisor']['ShouldReplaceIpConfiguration'] = normalize_value(values['updated']) == 'true'
elif field == 'Failover Test Network':
nic['FailoverTest']['Hypervisor']['NetworkIdentifier'] = values['updated']
elif field == 'Failover Test DHCP':
if not nic['FailoverTest']['Hypervisor'].get('IpConfig'):
nic['FailoverTest']['Hypervisor']['IpConfig'] = {
'StaticIp': None,
'SubnetMask': None,
'Gateway': None,
'PrimaryDns': None,
'SecondaryDns': None,
'IsDhcp': False
}
nic['FailoverTest']['Hypervisor']['IpConfig']['IsDhcp'] = normalize_value(values['updated']) == 'true'
# If DHCP is enabled, clear other IP settings
if normalize_value(values['updated']) == 'true':
nic['FailoverTest']['Hypervisor']['IpConfig'].update({
'StaticIp': None,
'SubnetMask': None,
'Gateway': None,
'PrimaryDns': None,
'SecondaryDns': None
})
elif field in ['Failover Test IP', 'Failover Test Subnet', 'Failover Test Gateway',
'Failover Test DNS1', 'Failover Test DNS2']:
if not nic['FailoverTest']['Hypervisor'].get('IpConfig'):
nic['FailoverTest']['Hypervisor']['IpConfig'] = {
'StaticIp': None,
'SubnetMask': None,
'Gateway': None,
'PrimaryDns': None,
'SecondaryDns': None,
'IsDhcp': False
}
if field == 'Failover Test IP':
nic['FailoverTest']['Hypervisor']['IpConfig']['StaticIp'] = values['updated'] if values['updated'] else None
elif field == 'Failover Test Subnet':
nic['FailoverTest']['Hypervisor']['IpConfig']['SubnetMask'] = values['updated'] if values['updated'] else '255.255.255.0'
elif field == 'Failover Test Gateway':
nic['FailoverTest']['Hypervisor']['IpConfig']['Gateway'] = values['updated'] if values['updated'] else None
elif field == 'Failover Test DNS1':
nic['FailoverTest']['Hypervisor']['IpConfig']['PrimaryDns'] = values['updated'] if values['updated'] else None
elif field == 'Failover Test DNS2':
nic['FailoverTest']['Hypervisor']['IpConfig']['SecondaryDns'] = values['updated'] if values['updated'] else None
logging.info(f"update_vpg_settings: Updated NIC structure: VPG {vpg_name} VM {vm_name} NIC {nic_id} nic={json.dumps(nic, indent=4)}")
# Update VPG settings with all changes
logging.info(f"update_vpg_settings: Updating VPG settings for {vpg_name}")
logging.info(f"update_vpg_settings: VPG settings: {json.dumps(vpg_settings, indent=4)}")
client.vpgs.update_vpg_settings(vpg_settings_id, vpg_settings)
# Commit changes
logging.info(f"update_vpg_settings: Committing changes for VPG: {vpg_name}")
client.vpgs.commit_vpg(vpg_settings_id, vpg_name, sync=False)
logging.info(f"update_vpg_settings: Successfully updated VPG: {vpg_name}")
def main():
parser = argparse.ArgumentParser(description="Import VPG settings from 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("--csv_file", required=True, help="Path to the CSV file with updated settings")
parser.add_argument("--vpg_names", help="Comma-separated list of VPG names to update (optional)")
args = parser.parse_args()
try:
# Setup client
client = setup_client(args)
# Process VPG names if provided
vpg_names = None
if args.vpg_names:
vpg_names = [name.strip() for name in args.vpg_names.split(',')]
logging.info(f"Updating settings for VPGs: {json.dumps(vpg_names, indent=4)}")
else:
logging.info("No VPG names provided, will update all VPGs in the CSV file")
# Read updated settings from CSV
print("\nReading updated settings from CSV...")
updated_settings = read_csv_settings(args.csv_file)
# logging.info(f"Updated settings: {updated_settings}")
# Get current settings
print("Getting current VPG settings...")
timestamp, current_settings = get_current_settings(client, vpg_names)
# Compare settings
print("Comparing settings...")
try:
changes = compare_settings(client, current_settings, updated_settings)
except ValueError as e:
print(f"\nError: {str(e)}")
print("\nPlease fix the configuration in the CSV file and try again.")
return
# Display changes
display_changes(client, changes)
if not changes:
print("\nNo changes to apply.")
return
# Ask for confirmation
while True:
response = input("\nDo you want to apply these changes? (yes/no): ").lower()
if response in ['yes', 'y']:
break
elif response in ['no', 'n']:
print("Changes cancelled.")
return
else:
print("Please answer 'yes' or 'no'.")
# Apply changes
print("\nApplying changes...")
update_vpg_settings(client, changes)
print("\nAll changes have been applied successfully.")
except Exception as e:
if isinstance(e, ValueError):
print(f"\nError: {str(e)}")
print("\nPlease fix the configuration in the CSV file and try again.")
else:
logging.exception("Error occurred:")
sys.exit(1)
if __name__ == "__main__":
main()
@@ -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
1 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
2 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
3 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
4 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