From 747e984da68d86a320cef3e73f68dfb60b870e42 Mon Sep 17 00:00:00 2001 From: David Schroeder Date: Sat, 25 Jul 2026 19:56:08 -0500 Subject: [PATCH] update --- defaults.inc | 2 +- inc/deploy-pulse-lxc.sh | 210 ++++++++++++++++++++++++++++++---------- tests/test-pulse.sh | 9 ++ 3 files changed, 171 insertions(+), 50 deletions(-) diff --git a/defaults.inc b/defaults.inc index 9e50dfd..f15bf34 100755 --- a/defaults.inc +++ b/defaults.inc @@ -3,7 +3,7 @@ action="${1:-}" FOLDER='/opt/idssys/ta-proxmenu' -VERS='2026.7.25-46' +VERS='2026.7.25-47' noupdate=' ' diff --git a/inc/deploy-pulse-lxc.sh b/inc/deploy-pulse-lxc.sh index 56034bb..133ec33 100644 --- a/inc/deploy-pulse-lxc.sh +++ b/inc/deploy-pulse-lxc.sh @@ -22,6 +22,14 @@ TAPM_PULSE_VALID_HOSTNAME() { [[ "${1:-}" =~ ^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$ ]] } +TAPM_PULSE_VALID_POSITIVE_INTEGER() { + [[ "${1:-}" =~ ^[1-9][0-9]*$ ]] +} + +TAPM_PULSE_VALID_PORT() { + [[ "${1:-}" =~ ^[0-9]+$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 )) +} + TAPM_PULSE_VALID_IPV4_CIDR() { local value="${1:-}" local address prefix octet @@ -98,9 +106,14 @@ TAPM_PULSE_SELECT_BRIDGE() { bridges+=("$bridge") [[ "$bridge" == "$default_bridge" ]] && default_found=1 done < <( - for bridge_path in /sys/class/net/*/bridge; do - [[ -d "$bridge_path" ]] && basename "$(dirname "$bridge_path")" - done | sort -V + { + for bridge_path in /sys/class/net/*/bridge; do + [[ -d "$bridge_path" ]] && basename "$(dirname "$bridge_path")" + done + if command -v ovs-vsctl >/dev/null 2>&1; then + ovs-vsctl list-br 2>/dev/null || true + fi + } | sort -Vu ) (( ${#bridges[@]} > 0 )) || @@ -138,6 +151,21 @@ raise SystemExit(0 if str(value).lower() in {"1", "true", "yes"} else 1) ' } +TAPM_PULSE_HA_STATUS_ENABLED() { + local status="${1:-}" + + grep -q '^quorum OK' <<<"$status" && + grep -Eq '^master[[:space:]].*\(active,' <<<"$status" +} + +TAPM_PULSE_CLUSTER_HA_ENABLED() { + local status + + command -v ha-manager >/dev/null 2>&1 || return 1 + status="$(ha-manager status 2>/dev/null)" || return 1 + TAPM_PULSE_HA_STATUS_ENABLED "$status" +} + TAPM_PULSE_VERIFY_SIGNATURE() { local target_path="$1" local signature_path="$2" @@ -176,12 +204,26 @@ TAPM_PULSE_REMOVE_PARTIAL_LXC() { TAPM_DEPLOY_PULSE_LXC() { local release="${PULSE_RELEASE:-v6.1.1}" - local pulse_port="${PULSE_PORT:-7655}" + local pulse_port="${PULSE_PORT:-7655}" auto_update_flag='--disable-auto-updates' local ctid default_ctid hostname bridge address_cidr gateway vlan_id local root_storage default_root_storage template_storage template_name template_path local arch archive_name base_url installer archive signature installer_signature - local network_config choice add_ha='no' container_ip timezone temp_dir + local memory disk cores cpulimit swap onboot firewall unprivileged nameserver startup + local network_config choice add_ha='no' auto_updates='yes' container_ip timezone temp_dir + local default_bridge local container_created=0 + local -a create_args=() + + memory=2048 + disk=4 + cores=2 + cpulimit=2 + swap=256 + onboot=1 + firewall=1 + unprivileged=1 + startup=99 + auto_update_flag='--enable-auto-updates' echo echo -e "${idsCL[LightCyan]}Deploy Pulse monitoring in a dedicated LXC${idsCL[Default]}" @@ -199,6 +241,8 @@ TAPM_DEPLOY_PULSE_LXC() { done TAPM_PULSE_VALID_RELEASE "$release" || { TAPM_PULSE_FAIL "Configured Pulse release '${release}' is invalid."; return 1; } + TAPM_PULSE_VALID_PORT "$pulse_port" || + { TAPM_PULSE_FAIL "Configured Pulse port '${pulse_port}' is invalid."; return 1; } default_ctid="$(pvesh get /cluster/nextid 2>/dev/null || true)" TAPM_PULSE_PROMPT ctid "Container ID" "$default_ctid" @@ -209,10 +253,36 @@ TAPM_DEPLOY_PULSE_LXC() { return 1 fi - TAPM_PULSE_PROMPT hostname "Container hostname" "pulse" + TAPM_PULSE_PROMPT hostname "Container hostname" "Pulse-Monitor" TAPM_PULSE_VALID_HOSTNAME "$hostname" || { TAPM_PULSE_FAIL "The hostname is invalid."; return 1; } - TAPM_PULSE_SELECT_BRIDGE bridge vmbr0 || return 1 + + read -r -p " Customize CPU, memory, disk, or swap? [y/N] " choice + if [[ "$choice" =~ ^[Yy]$ ]]; then + TAPM_PULSE_PROMPT memory "Memory in MiB" "$memory" + TAPM_PULSE_VALID_POSITIVE_INTEGER "$memory" || + { TAPM_PULSE_FAIL "Memory must be a positive whole number."; return 1; } + TAPM_PULSE_PROMPT disk "Root disk size in GiB" "$disk" + TAPM_PULSE_VALID_POSITIVE_INTEGER "$disk" || + { TAPM_PULSE_FAIL "Disk size must be a positive whole number."; return 1; } + TAPM_PULSE_PROMPT cores "CPU cores" "$cores" + TAPM_PULSE_VALID_POSITIVE_INTEGER "$cores" || + { TAPM_PULSE_FAIL "CPU cores must be a positive whole number."; return 1; } + TAPM_PULSE_PROMPT cpulimit "CPU limit (0 for unlimited)" "$cpulimit" + [[ "$cpulimit" =~ ^[0-9]+$ ]] || + { TAPM_PULSE_FAIL "CPU limit must be zero or a positive whole number."; return 1; } + TAPM_PULSE_PROMPT swap "Swap in MiB" "$swap" + [[ "$swap" =~ ^[0-9]+$ ]] || + { TAPM_PULSE_FAIL "Swap must be zero or a positive whole number."; return 1; } + fi + + echo + default_bridge="$( + ip route 2>/dev/null | + awk '/^default/ { print $5; exit }' + )" + [[ -n "$default_bridge" ]] || default_bridge='vmbr0' + TAPM_PULSE_SELECT_BRIDGE bridge "$default_bridge" || return 1 TAPM_PULSE_PROMPT address_cidr \ "Static IPv4 address with prefix (leave blank for DHCP)" if [[ -n "$address_cidr" ]]; then @@ -222,6 +292,8 @@ TAPM_DEPLOY_PULSE_LXC() { TAPM_PULSE_VALID_IPV4_CIDR "${gateway}/32" || { TAPM_PULSE_FAIL "The IPv4 gateway is invalid."; return 1; } fi + TAPM_PULSE_PROMPT nameserver \ + "DNS servers, space-separated (leave blank to inherit host settings)" TAPM_PULSE_PROMPT vlan_id "VLAN ID (leave blank for untagged)" if [[ -n "$vlan_id" ]] && { [[ ! "$vlan_id" =~ ^[0-9]+$ ]] || (( vlan_id < 1 || vlan_id > 4094 )); }; then @@ -229,12 +301,26 @@ TAPM_DEPLOY_PULSE_LXC() { return 1 fi + TAPM_PULSE_CLUSTER_HA_ENABLED && add_ha='yes' + default_root_storage="$( pvesm status --content rootdir 2>/dev/null | awk 'NR > 1 && $3 == "active" { print $1; exit }' )" [[ -n "$default_root_storage" ]] || { TAPM_PULSE_FAIL "No active storage supports LXC volumes."; return 1; } + if [[ "$add_ha" == 'yes' ]]; then + while read -r choice; do + [[ -n "$choice" ]] || continue + if TAPM_PULSE_STORAGE_IS_SHARED "$choice"; then + default_root_storage="$choice" + break + fi + done < <( + pvesm status --content rootdir 2>/dev/null | + awk 'NR > 1 && $3 == "active" { print $1 }' + ) + fi TAPM_ISO_NFS_SELECT_STORAGE root_storage \ "Pulse root filesystem storage" "$default_root_storage" || return 1 @@ -246,10 +332,11 @@ TAPM_DEPLOY_PULSE_LXC() { { TAPM_PULSE_FAIL "No active storage supports container templates."; return 1; } template_storage="$default_root_storage" - if TAPM_PULSE_STORAGE_IS_SHARED "$root_storage" && - command -v ha-manager >/dev/null 2>&1; then - read -r -p " Add the Pulse LXC to Proxmox HA after installation? [Y/n] " choice - [[ ! "$choice" =~ ^[Nn]$ ]] && add_ha='yes' + if [[ "$add_ha" == 'yes' ]] && + ! TAPM_PULSE_STORAGE_IS_SHARED "$root_storage"; then + echo -e "${idsCL[LightYellow]}Warning: HA is active, but '${root_storage}' is not marked shared.${idsCL[Default]}" + echo " The LXC will still be added to HA as requested, but automatic failover" + echo " requires shared storage or separately configured storage replication." fi echo @@ -262,11 +349,13 @@ TAPM_DEPLOY_PULSE_LXC() { echo " Network: DHCP on ${bridge}" fi [[ -n "$vlan_id" ]] && echo " VLAN: ${vlan_id}" - echo " Resources: 2 vCPU, 2048 MiB RAM, 512 MiB swap, 100 CPU units" - echo " Root volume: ${root_storage}:8 GiB" + [[ -n "$nameserver" ]] && echo " DNS servers: ${nameserver}" + echo " Resources: ${cores} vCPU (limit ${cpulimit}), ${memory} MiB RAM, ${swap} MiB swap" + echo " Root volume: ${root_storage}:${disk} GiB" echo " Pulse port: ${pulse_port}" - echo " Start at boot: yes" - echo " Automatic update: enabled" + echo " Start at boot: yes, order ${startup}" + echo " Firewall: enabled" + echo " Automatic update: ${auto_updates}" echo " Proxmox HA: ${add_ha}" echo read -r -p " Create this Pulse container (type yes to continue)? " choice @@ -303,26 +392,30 @@ TAPM_DEPLOY_PULSE_LXC() { fi echo -e "\n${idsCL[LightCyan]}Locating a Debian container template...${idsCL[Default]}" - if ! pveam update; then - TAPM_CLEAN_TEMP_DIR "$temp_dir" - TAPM_PULSE_FAIL "Could not refresh the container template catalog." - return 1 - fi - template_name="$( - pveam available --section system | - awk '$2 ~ /^debian-(13|12)-standard_/ { print $2 }' | + template_path="$( + pveam list "$template_storage" 2>/dev/null | + awk 'NR > 1 && $1 ~ /:vztmpl\/debian-(13|12)-standard_/ { print $1 }' | sort -V | tail -1 )" - if [[ -z "$template_name" ]]; then - TAPM_CLEAN_TEMP_DIR "$temp_dir" - TAPM_PULSE_FAIL "No supported Debian 12/13 standard template was found." - return 1 - fi - template_path="${template_storage}:vztmpl/${template_name}" - if ! pveam list "$template_storage" 2>/dev/null | - awk 'NR > 1 { print $1 }' | - grep -Fxq -- "$template_path"; then + if [[ -z "$template_path" ]]; then + if ! pveam update; then + TAPM_CLEAN_TEMP_DIR "$temp_dir" + TAPM_PULSE_FAIL "Could not refresh the container template catalog." + return 1 + fi + template_name="$( + pveam available --section system | + awk '$2 ~ /^debian-(13|12)-standard_/ { print $2 }' | + sort -V | + tail -1 + )" + if [[ -z "$template_name" ]]; then + TAPM_CLEAN_TEMP_DIR "$temp_dir" + TAPM_PULSE_FAIL "No supported Debian 12/13 standard template was found." + return 1 + fi + template_path="${template_storage}:vztmpl/${template_name}" if ! pveam download "$template_storage" "$template_name"; then TAPM_CLEAN_TEMP_DIR "$temp_dir" TAPM_PULSE_FAIL "The Debian template download failed." @@ -331,27 +424,31 @@ TAPM_DEPLOY_PULSE_LXC() { fi if [[ -n "$address_cidr" ]]; then - network_config="name=eth0,bridge=${bridge},ip=${address_cidr},gw=${gateway},firewall=1,type=veth" + network_config="name=eth0,bridge=${bridge},ip=${address_cidr},gw=${gateway},firewall=${firewall},type=veth" else - network_config="name=eth0,bridge=${bridge},ip=dhcp,firewall=1,type=veth" + network_config="name=eth0,bridge=${bridge},ip=dhcp,firewall=${firewall},type=veth" fi [[ -n "$vlan_id" ]] && network_config+=",tag=${vlan_id}" echo -e "\n${idsCL[LightCyan]}Creating and starting LXC ${ctid}...${idsCL[Default]}" - if ! pct create "$ctid" "$template_path" \ - --hostname "$hostname" \ - --ostype debian \ - --unprivileged 1 \ - --features nesting=1 \ - --cores 2 \ - --cpunits 100 \ - --memory 2048 \ - --swap 512 \ - --rootfs "${root_storage}:8" \ - --net0 "$network_config" \ - --onboot 1 \ - --startup order=10 \ - --tags 'tapm;pulse'; then + create_args=( + pct create "$ctid" "$template_path" + --hostname "$hostname" + --ostype debian + --unprivileged "$unprivileged" + --features nesting=1 + --cores "$cores" + --memory "$memory" + --swap "$swap" + --rootfs "${root_storage}:${disk}" + --net0 "$network_config" + --onboot "$onboot" + --startup "order=${startup}" + --tags 'tapm;pulse' + ) + [[ "$cpulimit" != 0 ]] && create_args+=(--cpulimit "$cpulimit") + [[ -n "$nameserver" ]] && create_args+=(--nameserver "$nameserver") + if ! "${create_args[@]}"; then TAPM_CLEAN_TEMP_DIR "$temp_dir" TAPM_PULSE_FAIL "Container creation failed." return 1 @@ -379,7 +476,7 @@ TAPM_DEPLOY_PULSE_LXC() { --in-container \ --version "$release" \ --archive "/tmp/${archive_name}" \ - --enable-auto-updates || + "$auto_update_flag" || ! pct exec "$ctid" -- systemctl is-active --quiet pulse; then (( container_created == 1 )) && TAPM_PULSE_REMOVE_PARTIAL_LXC "$ctid" TAPM_CLEAN_TEMP_DIR "$temp_dir" @@ -391,6 +488,21 @@ TAPM_DEPLOY_PULSE_LXC() { pct exec "$ctid" -- hostname -I 2>/dev/null | awk '{ print $1; exit }' )" + + if [[ -n "$container_ip" ]]; then + echo -e "\n${idsCL[LightCyan]}Registering the Proxmox cluster with Pulse...${idsCL[Default]}" + ( + trap 'rm -f /tmp/pulse-auto-register-request.json /tmp/pulse-auto-register-response.json' EXIT + # Reuse the verified release's registration implementation so its + # API contract and least-privilege role handling stay in sync. + # shellcheck disable=SC1090 + source "$installer" + IN_CONTAINER=false + wait_for_pulse_ready "http://${container_ip}:${pulse_port}" 120 1 || true + auto_register_pve_node "$ctid" "$container_ip" "$pulse_port" + ) || echo -e "${idsCL[LightYellow]}Pulse is installed, but automatic Proxmox registration did not complete.${idsCL[Default]}" + fi + if [[ "$add_ha" == 'yes' ]] && ! ha-manager add "ct:${ctid}" --state started; then echo -e "${idsCL[LightYellow]}Pulse is running, but it could not be added to HA.${idsCL[Default]}" diff --git a/tests/test-pulse.sh b/tests/test-pulse.sh index adf1ece..6f44214 100644 --- a/tests/test-pulse.sh +++ b/tests/test-pulse.sh @@ -19,6 +19,10 @@ assert_success "valid Pulse CTID" TAPM_PULSE_VALID_CTID 210 assert_failure "invalid Pulse CTID" TAPM_PULSE_VALID_CTID 99 assert_success "valid Pulse hostname" TAPM_PULSE_VALID_HOSTNAME pulse-monitor assert_failure "invalid Pulse hostname" TAPM_PULSE_VALID_HOSTNAME 'pulse monitor' +assert_success "valid positive integer" TAPM_PULSE_VALID_POSITIVE_INTEGER 1024 +assert_failure "zero is not positive" TAPM_PULSE_VALID_POSITIVE_INTEGER 0 +assert_success "valid Pulse port" TAPM_PULSE_VALID_PORT 7655 +assert_failure "Pulse port too high" TAPM_PULSE_VALID_PORT 65536 assert_success "valid Pulse IPv4 CIDR" TAPM_PULSE_VALID_IPV4_CIDR 10.10.1.50/24 assert_failure "invalid Pulse IPv4 CIDR" TAPM_PULSE_VALID_IPV4_CIDR 10.10.1.500/24 @@ -32,4 +36,9 @@ assert_success "legacy Pulse hostname detected" \ assert_failure "unrelated resource not detected" \ TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"qemu","name":"pulse"}]' +ha_status=$'quorum OK\nmaster pve1 (active, Sat Jul 25 12:00:00 2026)\nlrm pve1 (active, Sat Jul 25 12:00:00 2026)' +assert_success "active Proxmox HA detected" TAPM_PULSE_HA_STATUS_ENABLED "$ha_status" +assert_failure "inactive Proxmox HA rejected" \ + TAPM_PULSE_HA_STATUS_ENABLED $'quorum OK\nmaster pve1 (idle)' + finish_tests