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,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
|
||||
|
Reference in New Issue
Block a user