arr-stack: add auto IP allocation with ping check

Third IP entry mode "auto": user supplies a starting IP, a range
(10.0.0.50-99), or comma-separated ranges; orchestrator walks the
candidates with `ping -c 1 -W 1` and grabs the first N that don't
respond. Skips the gateway and any IPs already assigned this run.
Confirms the picks via yes/no whiptail before locking them in.

Adds iputils-ping to ensure_dependencies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michel Roegl-Brunner
2026-05-21 14:49:38 +02:00
parent fa71af52ed
commit dbe0c6a37f

View File

@@ -278,19 +278,120 @@ compute_ordered_slugs() {
}
pick_ip_mode_and_ips() {
local mode
mode=$(whiptail --backtitle "$BACKTITLE" \
--title "IP Entry Mode" \
--menu "How would you like to enter IP addresses?" 14 70 2 \
"list" "Enter all IPs at once (space- or comma-separated)" \
"one_by_one" "Prompt per container" \
3>&1 1>&2 2>&3) || cancelled "IP entry mode pick"
while true; do
local mode
mode=$(whiptail --backtitle "$BACKTITLE" \
--title "IP Entry Mode" \
--menu "How would you like to enter IP addresses?" 15 75 3 \
"list" "Enter all IPs at once (space- or comma-separated)" \
"one_by_one" "Prompt per container" \
"auto" "Auto-pick free IPs from a starting IP or range(s)" \
3>&1 1>&2 2>&3) || cancelled "IP entry mode pick"
if [[ "$mode" == "list" ]]; then
_collect_ips_list_mode
else
_collect_ips_one_by_one
case "$mode" in
list) _collect_ips_list_mode; return ;;
one_by_one) _collect_ips_one_by_one; return ;;
auto) _collect_ips_auto && return ;;
esac
done
}
_parse_ip_ranges() {
local expr=$1
local -a segments
IFS=',' read -ra segments <<<"$expr"
local seg prefix start end i
for seg in "${segments[@]}"; do
seg="${seg// /}"
[[ -z "$seg" ]] && continue
if [[ "$seg" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.)([0-9]+)-([0-9]+\.[0-9]+\.[0-9]+\.)([0-9]+)$ ]]; then
if [[ "${BASH_REMATCH[1]}" != "${BASH_REMATCH[3]}" ]]; then
echo "ERR: cross-subnet range not supported: $seg" >&2; return 1
fi
prefix=${BASH_REMATCH[1]}; start=${BASH_REMATCH[2]}; end=${BASH_REMATCH[4]}
elif [[ "$seg" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.)([0-9]+)-([0-9]+)$ ]]; then
prefix=${BASH_REMATCH[1]}; start=${BASH_REMATCH[2]}; end=${BASH_REMATCH[3]}
elif [[ "$seg" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.)([0-9]+)$ ]]; then
prefix=${BASH_REMATCH[1]}; start=${BASH_REMATCH[2]}; end=254
else
echo "ERR: invalid segment: $seg" >&2; return 1
fi
if (( start > end || start < 0 || end > 255 )); then
echo "ERR: out-of-range octet: $seg" >&2; return 1
fi
for ((i=start; i<=end; i++)); do
echo "${prefix}${i}"
done
done
}
_ip_is_free() {
local ip=$1
[[ "$ip" == "$var_gateway" ]] && return 1
if ping -c 1 -W 1 "$ip" >/dev/null 2>&1; then
return 1
fi
return 0
}
_collect_ips_auto() {
local expected_n=${#ORDERED_SLUGS[@]}
local hint="Examples:"$'\n'" 10.0.0.50 (start, scans upward to .254)"$'\n'" 10.0.0.50-99 (single range)"$'\n'" 10.0.0.50-60,10.0.0.80-99 (multiple ranges)"
while true; do
local expr
expr=$(whiptail --backtitle "$BACKTITLE" \
--title "Auto IP allocation" \
--inputbox "Need ${expected_n} free IPs. Enter a starting IP or range expression:"$'\n\n'"${hint}" \
16 78 "" 3>&1 1>&2 2>&3) || return 1
local -a parsed=()
while IFS= read -r ip; do
[[ -n "$ip" ]] && parsed+=("$ip")
done < <(_parse_ip_ranges "$expr" 2>/dev/null)
if (( ${#parsed[@]} == 0 )); then
whiptail --backtitle "$BACKTITLE" --title "Invalid" \
--msgbox "Could not parse any IPs from: ${expr}"$'\n\n'"Try a starting IP, a range like 10.0.0.50-99, or comma-separated ranges." 12 70
continue
fi
msg_info "Pinging ${#parsed[@]} candidate(s) for ${expected_n} free IP(s)..."
local -a found=()
local ip already used
for ip in "${parsed[@]}"; do
(( ${#found[@]} >= expected_n )) && break
already=0
for used in "${IP_BY_SLUG[@]}"; do
[[ "$used" == "$ip" ]] && { already=1; break; }
done
(( already )) && continue
if _ip_is_free "$ip"; then
found+=("$ip")
echo " free: ${ip}"
fi
done
if (( ${#found[@]} < expected_n )); then
whiptail --backtitle "$BACKTITLE" --title "Not enough free IPs" \
--msgbox "Found ${#found[@]}/${expected_n} free IPs in the range. Widen the range and try again." 10 70
continue
fi
local lines="" i
for i in "${!ORDERED_SLUGS[@]}"; do
lines+=" $(printf '%-12s -> %s' "${ORDERED_SLUGS[$i]}" "${found[$i]}")"$'\n'
done
if ! whiptail --backtitle "$BACKTITLE" --title "Confirm auto-assigned IPs" \
--yesno "Free IPs found:"$'\n\n'"${lines}"$'\n'"Use these?" 22 70; then
continue
fi
for i in "${!ORDERED_SLUGS[@]}"; do
IP_BY_SLUG[${ORDERED_SLUGS[$i]}]=${found[$i]}
done
return 0
done
}
_collect_ips_list_mode() {
@@ -860,7 +961,7 @@ main() {
header_info
check_root
check_pve_tools
ensure_dependencies curl whiptail jq
ensure_dependencies curl whiptail jq iputils-ping
seed_catalog
pick_storage
pick_network_defaults