Add validation for input parameters

This commit is contained in:
moeny-matt 2025-04-30 18:40:52 -04:00
parent b56e50dad7
commit 537edfd389
7 changed files with 663 additions and 3 deletions

View File

@ -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.

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"