diff --git a/README.md b/README.md index 612ddab..4b19554 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ cd bolt bolt plan run ubuntu::create_vm \ target_host=vortex \ vm_name=moeny-bank01 \ - hostname=moeny-bank01 - ip_with_cidr=100.40.223.189/24 \ + hostname=moeny-bank01 \ + ip_with_cidr=100.40.223.189/24 -v ``` ## Alpine VMs @@ -25,7 +25,7 @@ bolt plan run alpine::create_vm \ hostname=moeny-service \ ip_with_cidr=100.40.223.189/24 \ add_a_record_bool=true \ - dns_hostname=service + dns_hostname=service -v ``` Note that `add_a_record_bool` must be set to `true` if you would like an A record for the VM to be added to the DNS server zone file, as it is `false` by default. If using this functionality, `dns_hostname` should also be provided and optionally `dns_ttl` if you do not want the default of `3600`. The ability to interact with the DNS server depends on having set up a TSIG key on your DNS server for dynamic updates and storing a copy of your `tsig.key` file in a directory called `keys` at the root of the bolt project, alongside `bolt-project.yaml`. If either of these conditions have not been met, do not attempt to use this functionality. For more information on setting up dynamic DNS with a TSIG key, see our [bind9](https://gitea.moeny.ai/moeny/bind9) repo. diff --git a/vm_automation/alpine/plans/create_vm.yaml b/vm_automation/alpine/plans/create_vm.yaml index 307f97c..2dbc1bd 100644 --- a/vm_automation/alpine/plans/create_vm.yaml +++ b/vm_automation/alpine/plans/create_vm.yaml @@ -101,6 +101,30 @@ parameters: default: "1.1.1.1" steps: + - name: validate_parameters + description: Validate all input parameters before proceeding + task: alpine::validate_vm_parameters + targets: localhost + parameters: + target_host: $target_host + vm_name: $vm_name + hostname: $hostname + network: $network + ip_with_cidr: $ip_with_cidr + gateway_ip: $gateway_ip + iso_path: $iso_path + staging_ip: $staging_ip + dns_hostname: $dns_hostname + dns_ttl: $dns_ttl + ram: $ram + vcpus: $vcpus + disk_size: $disk_size + disk_path: $disk_path + os_variant: $os_variant + nameserver1: $nameserver1 + nameserver2: $nameserver2 + nameserver3: $nameserver3 + - name: check_ip_availability description: Check if the target IP is already in use task: common::check_ip_availability diff --git a/vm_automation/alpine/tasks/validate_vm_parameters.json b/vm_automation/alpine/tasks/validate_vm_parameters.json new file mode 100644 index 0000000..91a2dec --- /dev/null +++ b/vm_automation/alpine/tasks/validate_vm_parameters.json @@ -0,0 +1,78 @@ +{ + "description": "Validates all parameters for Alpine VM creation", + "input_method": "environment", + "parameters": { + "target_host": { + "description": "Target host to create the VM on (must be vortex or astrocore)", + "type": "String" + }, + "vm_name": { + "description": "Name of the VM (alphanumeric and hyphens only)", + "type": "String" + }, + "hostname": { + "description": "Hostname of the VM (alphanumeric and hyphens only)", + "type": "String" + }, + "network": { + "description": "Network to connect the VM to (wan-verizon or moeny-internal)", + "type": "String" + }, + "ip_with_cidr": { + "description": "IP address with CIDR notation (e.g. 192.168.1.1/24)", + "type": "String" + }, + "gateway_ip": { + "description": "Gateway IP address", + "type": "String" + }, + "iso_path": { + "description": "Absolute path to the ISO file (must end in .iso)", + "type": "String" + }, + "staging_ip": { + "description": "IP address for staging", + "type": "String" + }, + "dns_hostname": { + "description": "Hostname for the DNS A record (alphanumeric and hyphens only)", + "type": "String" + }, + "dns_ttl": { + "description": "TTL for the DNS A record (positive integer)", + "type": "Integer" + }, + "ram": { + "description": "Amount of RAM in MB (positive integer)", + "type": "Integer" + }, + "vcpus": { + "description": "Number of virtual CPUs (positive integer)", + "type": "Integer" + }, + "disk_size": { + "description": "Size of the disk in GB (positive integer)", + "type": "Integer" + }, + "disk_path": { + "description": "Base path for disk images (must be absolute path)", + "type": "String" + }, + "os_variant": { + "description": "OS variant for the VM (must be in format alpinelinuxX.XX)", + "type": "String" + }, + "nameserver1": { + "description": "Primary nameserver IP address", + "type": "String" + }, + "nameserver2": { + "description": "Secondary nameserver IP address", + "type": "String" + }, + "nameserver3": { + "description": "Tertiary nameserver IP address", + "type": "String" + } + } +} \ No newline at end of file diff --git a/vm_automation/alpine/tasks/validate_vm_parameters.sh b/vm_automation/alpine/tasks/validate_vm_parameters.sh new file mode 100755 index 0000000..19681ce --- /dev/null +++ b/vm_automation/alpine/tasks/validate_vm_parameters.sh @@ -0,0 +1,224 @@ +#!/bin/bash +# Task to validate VM parameters (alpine::validate_vm_parameters) + +# Array to store warnings +WARNINGS=() + +# Function to output status messages in JSON format +output_status() { + local status=$1 + local message=$2 + local warnings_json="" + + if [ ${#WARNINGS[@]} -gt 0 ]; then + warnings_json=", \"warnings\": [" + for i in "${!WARNINGS[@]}"; do + if [ $i -gt 0 ]; then + warnings_json="${warnings_json}, " + fi + warnings_json="${warnings_json}\"${WARNINGS[$i]}\"" + done + warnings_json="${warnings_json}]" + fi + + echo "{ \"status\": \"$status\", \"message\": \"$message\"${warnings_json} }" + if [ "$status" = "error" ]; then + exit 1 + fi +} + +# Function to output warning messages +output_warning() { + local message=$1 + WARNINGS+=("$message") +} + +# Function to get network portion of IP +get_network_from_ip() { + local ip=$1 + echo "$ip" | cut -d'.' -f1-3 +} + +# Function to validate network relationships +validate_network_relationships() { + local network=$1 + local ip_with_cidr=$2 + local gateway_ip=$3 + local staging_ip=$4 + + # Extract IP from CIDR notation + local ip=$(echo "$ip_with_cidr" | cut -d'/' -f1) + local cidr=$(echo "$ip_with_cidr" | cut -d'/' -f2) + + # Get network portions + local ip_network=$(get_network_from_ip "$ip") + local gateway_network=$(get_network_from_ip "$gateway_ip") + local staging_network=$(get_network_from_ip "$staging_ip") + + # Check CIDR is /24 + if [[ "$cidr" != "24" ]]; then + output_warning "Network mask /24 is typical, found /$cidr" + fi + + # Check gateway ends in .1 + if [[ ! "$gateway_ip" =~ \.1$ ]]; then + output_warning "Gateway typically ends in .1, found $gateway_ip" + fi + + # Validate network relationships + if [[ "$ip_network" != "$gateway_network" ]]; then + output_status "error" "IP ($ip) and gateway ($gateway_ip) must be in the same network" + fi + + if [[ "$ip_network" != "$staging_network" ]]; then + output_warning "staging IP ($staging_ip) is in a different network than primary IP ($ip)" + fi + + # Network-specific validations + if [[ "$network" == "wan-verizon" ]]; then + if [[ ! "$ip_network" =~ ^100\.40\.223$ ]]; then + output_warning "wan-verizon network typically uses 100.40.223.0/24 range, but got ${ip_network}.0/${cidr}" + fi + if [[ "$gateway_ip" != "100.40.223.1" ]]; then + output_warning "wan-verizon network typically uses 100.40.223.1 as gateway, but got ${gateway_ip}" + fi + elif [[ "$network" == "moeny-internal" ]]; then + if [[ ! "$ip_network" =~ ^10\.44\.0$ ]]; then + output_warning "moeny-internal network typically uses 10.44.0.0/24 range, but got ${ip_network}.0/${cidr}" + fi + if [[ "$gateway_ip" != "10.44.0.1" ]]; then + output_warning "moeny-internal network typically uses 10.44.0.1 as gateway, but got ${gateway_ip}" + fi + fi +} + +# Function to validate target host +validate_target_host() { + local host=$1 + if [[ "$host" != "vortex" && "$host" != "astrocore" ]]; then + output_status "error" "Invalid target_host '$host'. Must be either vortex or astrocore" + fi +} + +# Function to validate hostname format +validate_hostname() { + local hostname=$1 + local param_name=$2 + if ! echo "$hostname" | grep -qE '^[a-zA-Z0-9-]+$'; then + output_status "error" "Invalid $param_name '$hostname'. Must contain only alphanumeric characters and hyphens." + fi +} + +# Function to validate network +validate_network() { + local network=$1 + if [[ "$network" != "wan-verizon" && "$network" != "moeny-internal" ]]; then + output_status "error" "Invalid network '$network'. Must be either wan-verizon or moeny-internal" + fi +} + +# Function to validate IP with CIDR +validate_ip_with_cidr() { + local ip_cidr=$1 + local ip=$(echo "$ip_cidr" | cut -d'/' -f1) + local cidr=$(echo "$ip_cidr" | cut -d'/' -f2) + + # Validate IP + if ! echo "$ip" | grep -qE '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'; then + output_status "error" "Invalid IP address format in ip_with_cidr '$ip_cidr'" + fi + + # Validate CIDR + if ! [[ "$cidr" =~ ^[0-9]+$ ]] || [ "$cidr" -lt 0 ] || [ "$cidr" -gt 32 ]; then + output_status "error" "Invalid CIDR prefix in ip_with_cidr '$ip_cidr'. Must be between 0 and 32." + fi +} + +# Function to validate IP address +validate_ip() { + local ip=$1 + local param_name=$2 + if ! echo "$ip" | grep -qE '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'; then + output_status "error" "Invalid $param_name IP address: '$ip'" + fi +} + +# Function to validate ISO path +validate_iso_path() { + local path=$1 + if [[ ! "$path" =~ ^/.+\.iso$ ]]; then + output_status "error" "Invalid iso_path '$path'. Must be absolute path ending in .iso" + fi + + # Extract just the filename from the path + local filename=$(basename "$path") + if [[ ! "$filename" =~ ^alpine ]]; then + output_warning "ISO filename ('${filename}') should typically begin with 'alpine'" + fi +} + +# Function to validate positive integer +validate_positive_integer() { + local value=$1 + local name=$2 + if ! [[ "$value" =~ ^[0-9]+$ ]] || [ "$value" -le 0 ]; then + output_status "error" "Invalid $name '$value'. Must be a positive integer." + fi +} + +# Function to validate disk path +validate_disk_path() { + local path=$1 + if [[ ! "$path" =~ ^/ ]]; then + output_status "error" "Invalid disk_path '$path'. Must be an absolute path." + fi +} + +# Function to validate OS variant +validate_os_variant() { + local variant=$1 + if ! echo "$variant" | grep -qE '^alpinelinux[0-9]+\.[0-9]+$'; then + output_status "error" "Invalid os_variant '$variant'. Must be in format 'alpinelinuxX.XX' (e.g. alpinelinux3.20)" + fi +} + +# Function to validate name relationships +validate_name_relationships() { + local vm_name=$1 + local hostname=$2 + + if [[ "$vm_name" != "$hostname" ]]; then + output_warning "vm_name ('${vm_name}') typically matches hostname ('${hostname}')" + fi +} + +# Parse JSON input from PT_* environment variables +validate_target_host "$PT_target_host" +validate_hostname "$PT_vm_name" "vm_name" +validate_hostname "$PT_hostname" "hostname" + +# Validate name relationships +validate_name_relationships "$PT_vm_name" "$PT_hostname" + +validate_network "$PT_network" +validate_ip_with_cidr "$PT_ip_with_cidr" +validate_ip "$PT_gateway_ip" "gateway_ip" +validate_iso_path "$PT_iso_path" +validate_ip "$PT_staging_ip" "staging_ip" + +# Validate network relationships after individual validations pass +validate_network_relationships "$PT_network" "$PT_ip_with_cidr" "$PT_gateway_ip" "$PT_staging_ip" + +validate_hostname "$PT_dns_hostname" "dns_hostname" +validate_positive_integer "$PT_dns_ttl" "dns_ttl" +validate_positive_integer "$PT_ram" "RAM" +validate_positive_integer "$PT_vcpus" "vCPUs" +validate_positive_integer "$PT_disk_size" "disk_size" +validate_disk_path "$PT_disk_path" +validate_os_variant "$PT_os_variant" +validate_ip "$PT_nameserver1" "primary nameserver" +validate_ip "$PT_nameserver2" "secondary nameserver" +validate_ip "$PT_nameserver3" "tertiary nameserver" + +# If we get here, all validations passed +output_status "success" "All parameters validated successfully" \ No newline at end of file diff --git a/vm_automation/ubuntu/plans/create_vm.yaml b/vm_automation/ubuntu/plans/create_vm.yaml index 4aa041b..e5ef2c9 100644 --- a/vm_automation/ubuntu/plans/create_vm.yaml +++ b/vm_automation/ubuntu/plans/create_vm.yaml @@ -101,6 +101,30 @@ parameters: default: "1.1.1.1" steps: + - name: validate_parameters + description: Validate all input parameters before proceeding + task: ubuntu::validate_vm_parameters + targets: localhost + parameters: + target_host: $target_host + vm_name: $vm_name + hostname: $hostname + network: $network + ip_with_cidr: $ip_with_cidr + gateway_ip: $gateway_ip + iso_path: $iso_path + staging_ip: $staging_ip + dns_hostname: $dns_hostname + dns_ttl: $dns_ttl + ram: $ram + vcpus: $vcpus + disk_size: $disk_size + disk_path: $disk_path + os_variant: $os_variant + nameserver1: $nameserver1 + nameserver2: $nameserver2 + nameserver3: $nameserver3 + - name: check_ip_availability description: Check if the target IP is already in use task: common::check_ip_availability diff --git a/vm_automation/ubuntu/tasks/validate_vm_parameters.json b/vm_automation/ubuntu/tasks/validate_vm_parameters.json new file mode 100644 index 0000000..b04d177 --- /dev/null +++ b/vm_automation/ubuntu/tasks/validate_vm_parameters.json @@ -0,0 +1,78 @@ +{ + "description": "Validates all parameters for VM creation", + "input_method": "environment", + "parameters": { + "target_host": { + "description": "Target host to create the VM on (must be vortex or astrocore)", + "type": "String" + }, + "vm_name": { + "description": "Name of the VM (alphanumeric and hyphens only)", + "type": "String" + }, + "hostname": { + "description": "Hostname of the VM (alphanumeric and hyphens only)", + "type": "String" + }, + "network": { + "description": "Network to connect the VM to (wan-verizon or moeny-internal)", + "type": "String" + }, + "ip_with_cidr": { + "description": "IP address with CIDR notation (e.g. 192.168.1.1/24)", + "type": "String" + }, + "gateway_ip": { + "description": "Gateway IP address", + "type": "String" + }, + "iso_path": { + "description": "Absolute path to the ISO file (must end in .iso)", + "type": "String" + }, + "staging_ip": { + "description": "Target VM for post-installation tasks (public or internal)", + "type": "String" + }, + "dns_hostname": { + "description": "Hostname for the DNS A record (alphanumeric and hyphens only)", + "type": "String" + }, + "dns_ttl": { + "description": "TTL for the DNS A record (positive integer)", + "type": "Integer" + }, + "ram": { + "description": "Amount of RAM in MB (positive integer)", + "type": "Integer" + }, + "vcpus": { + "description": "Number of virtual CPUs (positive integer)", + "type": "Integer" + }, + "disk_size": { + "description": "Size of the disk in GB (positive integer)", + "type": "Integer" + }, + "disk_path": { + "description": "Base path for disk images (must be absolute path)", + "type": "String" + }, + "os_variant": { + "description": "OS variant for the VM (must be in format ubuntuXX.XX)", + "type": "String" + }, + "nameserver1": { + "description": "Primary nameserver IP address", + "type": "String" + }, + "nameserver2": { + "description": "Secondary nameserver IP address", + "type": "String" + }, + "nameserver3": { + "description": "Tertiary nameserver IP address", + "type": "String" + } + } +} \ No newline at end of file diff --git a/vm_automation/ubuntu/tasks/validate_vm_parameters.sh b/vm_automation/ubuntu/tasks/validate_vm_parameters.sh new file mode 100755 index 0000000..0bdeaf0 --- /dev/null +++ b/vm_automation/ubuntu/tasks/validate_vm_parameters.sh @@ -0,0 +1,232 @@ +#!/bin/bash +# Task to validate VM parameters (ubuntu::validate_vm_parameters) + +# Array to store warnings +WARNINGS=() + +# Function to output status messages in JSON format +output_status() { + local status=$1 + local message=$2 + local warnings_json="" + + if [ ${#WARNINGS[@]} -gt 0 ]; then + warnings_json=", \"warnings\": [" + for i in "${!WARNINGS[@]}"; do + if [ $i -gt 0 ]; then + warnings_json="${warnings_json}, " + fi + warnings_json="${warnings_json}\"${WARNINGS[$i]}\"" + done + warnings_json="${warnings_json}]" + fi + + echo "{ \"status\": \"$status\", \"message\": \"$message\"${warnings_json} }" + if [ "$status" = "error" ]; then + exit 1 + fi +} + +# Function to output warning messages +output_warning() { + local message=$1 + WARNINGS+=("$message") +} + +# Function to get network portion of IP +get_network_from_ip() { + local ip=$1 + echo "$ip" | cut -d'.' -f1-3 +} + +# Function to validate network relationships +validate_network_relationships() { + local network=$1 + local ip_with_cidr=$2 + local gateway_ip=$3 + local staging_ip=$4 + + # Extract IP from CIDR notation + local ip=$(echo "$ip_with_cidr" | cut -d'/' -f1) + local cidr=$(echo "$ip_with_cidr" | cut -d'/' -f2) + + # Get network portions + local ip_network=$(get_network_from_ip "$ip") + local gateway_network=$(get_network_from_ip "$gateway_ip") + + # Check CIDR is /24 + if [[ "$cidr" != "24" ]]; then + output_warning "Network mask /24 is typical, found /$cidr" + fi + + # Check gateway ends in .1 + if [[ ! "$gateway_ip" =~ \.1$ ]]; then + output_warning "Gateway typically ends in .1, found $gateway_ip" + fi + + # Validate network relationships + if [[ "$ip_network" != "$gateway_network" ]]; then + output_status "error" "IP ($ip) and gateway ($gateway_ip) must be in the same network" + fi + + # Network-specific validations + if [[ "$network" == "wan-verizon" ]]; then + if [[ ! "$ip_network" =~ ^100\.40\.223$ ]]; then + output_warning "wan-verizon network typically uses 100.40.223.0/24 range, but got ${ip_network}.0/${cidr}" + fi + if [[ "$gateway_ip" != "100.40.223.1" ]]; then + output_warning "wan-verizon network typically uses 100.40.223.1 as gateway, but got ${gateway_ip}" + fi + if [[ "$staging_ip" != "public" ]]; then + output_warning "wan-verizon network typically uses 'public' for staging_ip, but got '${staging_ip}'" + fi + elif [[ "$network" == "moeny-internal" ]]; then + if [[ ! "$ip_network" =~ ^10\.44\.0$ ]]; then + output_warning "moeny-internal network typically uses 10.44.0.0/24 range, but got ${ip_network}.0/${cidr}" + fi + if [[ "$gateway_ip" != "10.44.0.1" ]]; then + output_warning "moeny-internal network typically uses 10.44.0.1 as gateway, but got ${gateway_ip}" + fi + if [[ "$staging_ip" != "internal" ]]; then + output_warning "moeny-internal network typically uses 'internal' for staging_ip, but got '${staging_ip}'" + fi + fi +} + +# Function to validate target host +validate_target_host() { + local host=$1 + if [[ "$host" != "vortex" && "$host" != "astrocore" ]]; then + output_status "error" "Invalid target_host '$host'. Must be either vortex or astrocore" + fi +} + +# Function to validate hostname format +validate_hostname() { + local hostname=$1 + local param_name=$2 + if ! echo "$hostname" | grep -qE '^[a-zA-Z0-9-]+$'; then + output_status "error" "Invalid $param_name '$hostname'. Must contain only alphanumeric characters and hyphens." + fi +} + +# Function to validate network +validate_network() { + local network=$1 + if [[ "$network" != "wan-verizon" && "$network" != "moeny-internal" ]]; then + output_status "error" "Invalid network '$network'. Must be either wan-verizon or moeny-internal" + fi +} + +# Function to validate IP with CIDR +validate_ip_with_cidr() { + local ip_cidr=$1 + local ip=$(echo "$ip_cidr" | cut -d'/' -f1) + local cidr=$(echo "$ip_cidr" | cut -d'/' -f2) + + # Validate IP + if ! echo "$ip" | grep -qE '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'; then + output_status "error" "Invalid IP address format in ip_with_cidr '$ip_cidr'" + fi + + # Validate CIDR + if ! [[ "$cidr" =~ ^[0-9]+$ ]] || [ "$cidr" -lt 0 ] || [ "$cidr" -gt 32 ]; then + output_status "error" "Invalid CIDR prefix in ip_with_cidr '$ip_cidr'. Must be between 0 and 32." + fi +} + +# Function to validate IP address +validate_ip() { + local ip=$1 + local param_name=$2 + if ! echo "$ip" | grep -qE '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'; then + output_status "error" "Invalid $param_name IP address: '$ip'" + fi +} + +# Function to validate ISO path +validate_iso_path() { + local path=$1 + if [[ ! "$path" =~ ^/.+\.iso$ ]]; then + output_status "error" "Invalid iso_path '$path'. Must be absolute path ending in .iso" + fi + + # Extract just the filename from the path + local filename=$(basename "$path") + if [[ ! "$filename" =~ ^ubuntu ]]; then + output_warning "ISO filename ('${filename}') should typically begin with 'ubuntu'" + fi +} + +# Function to validate staging IP +validate_staging_ip() { + local staging_ip=$1 + if [[ "$staging_ip" != "public" && "$staging_ip" != "internal" ]]; then + output_status "error" "Invalid staging_ip '$staging_ip'. Must be either public or internal" + fi +} + +# Function to validate positive integer +validate_positive_integer() { + local value=$1 + local name=$2 + if ! [[ "$value" =~ ^[0-9]+$ ]] || [ "$value" -le 0 ]; then + output_status "error" "Invalid $name '$value'. Must be a positive integer." + fi +} + +# Function to validate disk path +validate_disk_path() { + local path=$1 + if [[ ! "$path" =~ ^/ ]]; then + output_status "error" "Invalid disk_path '$path'. Must be an absolute path." + fi +} + +# Function to validate OS variant +validate_os_variant() { + local variant=$1 + if ! echo "$variant" | grep -qE '^ubuntu[0-9]{2}\.[0-9]{2}$'; then + output_status "error" "Invalid os_variant '$variant'. Must be in format 'ubuntuXX.XX' (e.g. ubuntu22.04)" + fi +} + +# Function to validate name relationships +validate_name_relationships() { + local vm_name=$1 + local hostname=$2 + + if [[ "$vm_name" != "$hostname" ]]; then + output_warning "vm_name ('${vm_name}') typically matches hostname ('${hostname}')" + fi +} + +# Parse JSON input from PT_* environment variables +validate_target_host "$PT_target_host" +validate_hostname "$PT_vm_name" "vm_name" +validate_hostname "$PT_hostname" "hostname" + +# Validate name relationships +validate_name_relationships "$PT_vm_name" "$PT_hostname" + +validate_network "$PT_network" +validate_ip_with_cidr "$PT_ip_with_cidr" +validate_ip "$PT_gateway_ip" "gateway_ip" +validate_iso_path "$PT_iso_path" +validate_staging_ip "$PT_staging_ip" +validate_hostname "$PT_dns_hostname" "dns_hostname" +validate_positive_integer "$PT_dns_ttl" "dns_ttl" +validate_positive_integer "$PT_ram" "RAM" +validate_positive_integer "$PT_vcpus" "vCPUs" +validate_positive_integer "$PT_disk_size" "disk_size" +validate_disk_path "$PT_disk_path" +validate_os_variant "$PT_os_variant" +validate_ip "$PT_nameserver1" "primary nameserver" +validate_ip "$PT_nameserver2" "secondary nameserver" +validate_ip "$PT_nameserver3" "tertiary nameserver" + +# Validate network relationships after individual validations pass +validate_network_relationships "$PT_network" "$PT_ip_with_cidr" "$PT_gateway_ip" "$PT_staging_ip" + +# If we get here, all validations passed +output_status "success" "All parameters validated successfully" \ No newline at end of file