From fd23ec525a459303242994b8425cdee444c9be6e Mon Sep 17 00:00:00 2001 From: David Schroeder Date: Sat, 25 Jul 2026 21:26:23 -0500 Subject: [PATCH] update --- defaults.inc | 2 +- inc/deploy-pulse-lxc.sh | 198 +++++++++++++++++++++++++++++++++------- tests/test-pulse.sh | 10 ++ 3 files changed, 178 insertions(+), 32 deletions(-) diff --git a/defaults.inc b/defaults.inc index 00ed7d8..606d485 100755 --- a/defaults.inc +++ b/defaults.inc @@ -3,7 +3,7 @@ action="${1:-}" FOLDER='/opt/idssys/ta-proxmenu' -VERS='2026.7.25-50' +VERS='2026.7.25-51' noupdate=' ' diff --git a/inc/deploy-pulse-lxc.sh b/inc/deploy-pulse-lxc.sh index eeed006..0811d5a 100644 --- a/inc/deploy-pulse-lxc.sh +++ b/inc/deploy-pulse-lxc.sh @@ -102,6 +102,24 @@ print(token.lower()) ' 2>/dev/null } +TAPM_PULSE_AGENT_REGISTERED_FROM_RESPONSE() { + local response="${1:-}" + + AGENT_RESPONSE="$response" python3 -c ' +import json, os +try: + agent = json.loads(os.environ["AGENT_RESPONSE"]).get("agent", {}) +except (AttributeError, TypeError, ValueError): + raise SystemExit(1) +if not isinstance(agent, dict) or not str(agent.get("id", "")).strip(): + raise SystemExit(1) +' 2>/dev/null +} + +TAPM_PULSE_AGENT_TOKEN_REQUEST_JSON() { + printf '%s\n' '{"type":"pve","enableCommands":false}' +} + TAPM_PULSE_ONLINE_NODES_FROM_JSON() { local nodes_json="${1:-[]}" @@ -146,7 +164,8 @@ except (TypeError, ValueError): for item in resources: tags = str(item.get("tags", "")).split(";") if item.get("type") == "lxc" and ( - "pulse" in tags or str(item.get("name", "")).lower() == "pulse" + "pulse" in tags or + str(item.get("name", "")).lower() in {"pulse", "pulse-monitor"} ): raise SystemExit(0) raise SystemExit(1) @@ -198,6 +217,7 @@ TAPM_PULSE_CONFIRM_CREDENTIALS_SAVED() { local primary_api_token="$4" local installed="$5" local failed="$6" + local cluster_status="$7" local acknowledgement while true; do @@ -210,6 +230,7 @@ TAPM_PULSE_CONFIRM_CREDENTIALS_SAVED() { echo -e " Username: ${idsCL[White]}${admin_username}${idsCL[Default]}" echo -e " Password: ${idsCL[LightGreen]}${admin_password}${idsCL[Default]}" echo -e " API token: ${idsCL[LightGreen]}${primary_api_token}${idsCL[Default]}" + echo " Cluster: ${cluster_status}" echo " Agents: ${installed} installed, ${failed} failed or offline" echo -e "${idsCL[LightYellow]}============================================================================${idsCL[Default]}" echo @@ -444,6 +465,48 @@ raise SystemExit(0 if success is True else 1) unset generated_password generated_api_token } +TAPM_PULSE_REGISTER_CLUSTER() { + local ctid="$1" + local container_ip="$2" + local pulse_port="$3" + local installer="$4" + local primary_token="$5" + local temp_dir="$6" + local pulse_url="http://${container_ip}:${pulse_port}" + local curl_config="${temp_dir}/pulse-cluster-registration.curl" + + printf 'header = "X-API-Token: %s"\n' "$primary_token" >"$curl_config" || + return 1 + chmod 0600 "$curl_config" || return 1 + + ( + 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 + # Proxmox roles, token contract, and compatibility checks stay in sync. + # shellcheck disable=SC1090 + source "$installer" + IN_CONTAINER=false + + # Pulse v6.1.1 requires authentication for setup-token creation. Add + # the primary token only to that request; never send it to Proxmox. + curl() { + local curl_argument + + for curl_argument in "$@"; do + if [[ "$curl_argument" == "${pulse_url}/api/setup-script-url" ]]; then + command curl --config "$curl_config" "$@" + return + fi + done + command curl "$@" + } + + wait_for_pulse_ready "$pulse_url" 120 1 || return 1 + auto_register_pve_node "$ctid" "$container_ip" "$pulse_port" + [[ "${AUTO_NODE_REGISTERED:-false}" == true ]] + ) +} + TAPM_PULSE_ISSUE_AGENT_TOKEN() { local pulse_url="$1" local primary_token="$2" @@ -453,14 +516,7 @@ TAPM_PULSE_ISSUE_AGENT_TOKEN() { local curl_config="${temp_dir}/agent-${node}.curl" local response - TAPM_PULSE_AGENT_NAME="tapm-pve-${node}" python3 -c ' -import json, os, sys -json.dump({ - "type": "host", - "name": os.environ["TAPM_PULSE_AGENT_NAME"], - "enableCommands": False, -}, sys.stdout) -' >"$request_file" || return 1 + TAPM_PULSE_AGENT_TOKEN_REQUEST_JSON >"$request_file" || return 1 chmod 0600 "$request_file" || return 1 { printf 'header = "Content-Type: application/json"\n' @@ -478,10 +534,44 @@ json.dump({ TAPM_PULSE_TOKEN_FROM_RESPONSE "$response" } -TAPM_PULSE_INSTALL_AGENT_LOCAL() { +TAPM_PULSE_WAIT_AGENT_REGISTERED() { local pulse_url="$1" local token="$2" - local token_file installer_file artifact + local node="$3" + local temp_dir="$4" + local curl_config="${temp_dir}/agent-${node}-verify.curl" + local encoded_node response attempt + + printf 'header = "X-API-Token: %s"\n' "$token" >"$curl_config" || return 1 + chmod 0600 "$curl_config" || return 1 + encoded_node="$( + TAPM_PULSE_NODE_NAME="$node" python3 -c ' +import os, urllib.parse +print(urllib.parse.quote(os.environ["TAPM_PULSE_NODE_NAME"], safe="")) +' + )" || return 1 + + for (( attempt = 1; attempt <= 20; attempt++ )); do + response="$( + curl --fail --silent --show-error \ + --connect-timeout 3 --max-time 5 \ + --config "$curl_config" \ + "${pulse_url}/api/agents/agent/lookup?hostname=${encoded_node}" \ + 2>/dev/null + )" || response='' + if TAPM_PULSE_AGENT_REGISTERED_FROM_RESPONSE "$response"; then + return 0 + fi + sleep 3 + done + return 1 +} + +TAPM_PULSE_INSTALL_AGENT_LOCAL() { + local node="$1" + local pulse_url="$2" + local token="$3" + local token_file installer_file cleanup_state_dir artifact local status=0 local -a old_artifacts=( /usr/local/bin/pulse-agent @@ -496,19 +586,39 @@ TAPM_PULSE_INSTALL_AGENT_LOCAL() { rm -f -- "$token_file" return 1 } + cleanup_state_dir="$(mktemp -d /tmp/tapm-pulse-agent-cleanup.XXXXXX)" || { + rm -f -- "$token_file" "$installer_file" + return 1 + } chmod 0600 "$token_file" "$installer_file" || { rm -f -- "$token_file" "$installer_file" + rm -rf -- "$cleanup_state_dir" return 1 } printf '%s' "$token" >"$token_file" || { rm -f -- "$token_file" "$installer_file" + rm -rf -- "$cleanup_state_dir" return 1 } curl --fail --silent --show-error --location \ --output "$installer_file" "${pulse_url}/install.sh" || status=$? if (( status == 0 )); then echo " Removing any previous Pulse Unified Agent installation..." - bash "$installer_file" --uninstall --non-interactive || status=$? + # Remove its connection sources before invoking the supported cleanup + # routine. This is a replacement install, so contacting the retired + # Pulse server to unregister is unnecessary. + systemctl stop pulse-agent >/dev/null 2>&1 || true + systemctl disable pulse-agent >/dev/null 2>&1 || true + pkill -x pulse-agent >/dev/null 2>&1 || true + rm -f -- \ + /etc/systemd/system/pulse-agent.service \ + /etc/systemd/system/multi-user.target.wants/pulse-agent.service + systemctl daemon-reload >/dev/null 2>&1 || true + rm -rf -- /var/lib/pulse-agent + bash "$installer_file" \ + --uninstall \ + --non-interactive \ + --state-dir "$cleanup_state_dir" || status=$? fi if (( status == 0 )) && systemctl is-active --quiet pulse-agent; then echo " The previous pulse-agent service is still active." >&2 @@ -531,12 +641,14 @@ TAPM_PULSE_INSTALL_AGENT_LOCAL() { bash "$installer_file" \ --url "$pulse_url" \ --token-file "$token_file" \ + --hostname "$node" \ --enable-proxmox \ --proxmox-type pve \ --non-interactive \ --insecure || status=$? fi rm -f -- "$token_file" "$installer_file" + rm -rf -- "$cleanup_state_dir" (( status == 0 )) || return "$status" systemctl is-active --quiet pulse-agent } @@ -562,18 +674,34 @@ TAPM_PULSE_INSTALL_AGENT_REMOTE() { [[ "$remote_token_file" =~ ^/tmp/tapm-pulse-agent-token\.[A-Za-z0-9]+$ ]] || return 1 - "${ssh_args[@]}" bash -s -- "$pulse_url" "$remote_token_file" <<'TAPM_PULSE_REMOTE_AGENT' + "${ssh_args[@]}" bash -s -- \ + "$pulse_url" "$remote_token_file" "$node" <<'TAPM_PULSE_REMOTE_AGENT' set -eu -o pipefail pulse_url="$1" token_file="$2" +node="$3" installer_file="$(mktemp /tmp/tapm-pulse-agent-installer.XXXXXX)" -trap 'rm -f -- "$token_file" "$installer_file"' EXIT +cleanup_state_dir="$(mktemp -d /tmp/tapm-pulse-agent-cleanup.XXXXXX)" +trap 'rm -f -- "$token_file" "$installer_file"; rm -rf -- "$cleanup_state_dir"' EXIT chmod 0600 "$token_file" "$installer_file" curl --fail --silent --show-error --location \ --output "$installer_file" "${pulse_url}/install.sh" echo " Removing any previous Pulse Unified Agent installation..." -bash "$installer_file" --uninstall --non-interactive +# This is a replacement install. Remove the old connection sources first so +# the supported cleanup routine cannot contact the retired Pulse server. +systemctl stop pulse-agent >/dev/null 2>&1 || true +systemctl disable pulse-agent >/dev/null 2>&1 || true +pkill -x pulse-agent >/dev/null 2>&1 || true +rm -f -- \ + /etc/systemd/system/pulse-agent.service \ + /etc/systemd/system/multi-user.target.wants/pulse-agent.service +systemctl daemon-reload >/dev/null 2>&1 || true +rm -rf -- /var/lib/pulse-agent +bash "$installer_file" \ + --uninstall \ + --non-interactive \ + --state-dir "$cleanup_state_dir" if systemctl is-active --quiet pulse-agent; then echo " The previous pulse-agent service is still active." >&2 exit 1 @@ -598,6 +726,7 @@ done bash "$installer_file" \ --url "$pulse_url" \ --token-file "$token_file" \ + --hostname "$node" \ --enable-proxmox \ --proxmox-type pve \ --non-interactive \ @@ -632,13 +761,20 @@ TAPM_PULSE_DEPLOY_CLUSTER_AGENTS() { } if [[ "$node" == "$current_node" || "$node" == "$(hostname)" ]]; then - TAPM_PULSE_INSTALL_AGENT_LOCAL "$pulse_url" "$agent_token" + TAPM_PULSE_INSTALL_AGENT_LOCAL "$node" "$pulse_url" "$agent_token" else TAPM_PULSE_INSTALL_AGENT_REMOTE "$node" "$pulse_url" "$agent_token" fi if (( $? == 0 )); then - echo -e "${idsCL[Green]}Pulse Unified Agent is active on ${node}.${idsCL[Default]}" - ((installed += 1)) + echo " Waiting for Pulse to confirm registration from ${node}..." + if TAPM_PULSE_WAIT_AGENT_REGISTERED \ + "$pulse_url" "$agent_token" "$node" "$temp_dir"; then + echo -e "${idsCL[Green]}Pulse confirmed Unified Agent registration for ${node}.${idsCL[Default]}" + ((installed += 1)) + else + echo -e "${idsCL[LightRed]}pulse-agent is active on ${node}, but Pulse did not confirm its registration.${idsCL[Default]}" + ((failed += 1)) + fi else echo -e "${idsCL[LightRed]}Pulse Unified Agent installation failed on ${node}.${idsCL[Default]}" ((failed += 1)) @@ -665,6 +801,7 @@ TAPM_DEPLOY_PULSE_LXC() { 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 pulse_url admin_username admin_password primary_api_token + local cluster_status='Registration failed' local container_created=0 local -a create_args=() @@ -898,7 +1035,6 @@ TAPM_DEPLOY_PULSE_LXC() { --net0 "$network_config" --onboot "$onboot" --startup "order=${startup}" - --tags 'tapm;pulse' ) [[ "$cpulimit" != 0 ]] && create_args+=(--cpulimit "$cpulimit") [[ -n "$nameserver" ]] && create_args+=(--nameserver "$nameserver") @@ -945,17 +1081,6 @@ TAPM_DEPLOY_PULSE_LXC() { if [[ -n "$container_ip" ]]; then pulse_url="http://${container_ip}:${pulse_port}" - 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 "$pulse_url" 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' ]] && @@ -984,6 +1109,16 @@ TAPM_DEPLOY_PULSE_LXC() { return 1 fi + echo -e "\n${idsCL[LightCyan]}Registering the Proxmox cluster with authenticated Pulse access...${idsCL[Default]}" + if TAPM_PULSE_REGISTER_CLUSTER \ + "$ctid" "$container_ip" "$pulse_port" "$installer" \ + "$primary_api_token" "$temp_dir"; then + cluster_status='Registered' + echo -e "${idsCL[Green]}Pulse confirmed Proxmox cluster registration.${idsCL[Default]}" + else + echo -e "${idsCL[LightRed]}Pulse is secured, but Proxmox cluster registration did not complete.${idsCL[Default]}" + fi + echo -e "\n${idsCL[LightCyan]}Deploying clean Pulse Unified Agents to cluster nodes...${idsCL[Default]}" TAPM_PULSE_AGENTS_INSTALLED=0 TAPM_PULSE_AGENTS_FAILED=0 @@ -999,5 +1134,6 @@ TAPM_DEPLOY_PULSE_LXC() { echo -e "${idsCL[Green]}Pulse ${release} was installed and its service is active.${idsCL[Default]}" TAPM_PULSE_CONFIRM_CREDENTIALS_SAVED \ "$pulse_url" "$admin_username" "$admin_password" "$primary_api_token" \ - "$TAPM_PULSE_AGENTS_INSTALLED" "$TAPM_PULSE_AGENTS_FAILED" + "$TAPM_PULSE_AGENTS_INSTALLED" "$TAPM_PULSE_AGENTS_FAILED" \ + "$cluster_status" } diff --git a/tests/test-pulse.sh b/tests/test-pulse.sh index 5c7213b..a4b75c0 100644 --- a/tests/test-pulse.sh +++ b/tests/test-pulse.sh @@ -44,6 +44,14 @@ assert_failure "Pulse agent token containing whitespace rejected" \ TAPM_PULSE_TOKEN_FROM_RESPONSE '{"token":"invalid token"}' assert_failure "non-hex Pulse agent token rejected" \ TAPM_PULSE_TOKEN_FROM_RESPONSE '{"token":"not-a-token"}' +assert_success "registered Pulse agent response accepted" \ + TAPM_PULSE_AGENT_REGISTERED_FROM_RESPONSE \ + '{"agent":{"id":"agent-123","hostname":"pve1"}}' +assert_failure "missing Pulse agent ID rejected" \ + TAPM_PULSE_AGENT_REGISTERED_FROM_RESPONSE '{"agent":{"hostname":"pve1"}}' +assert_equal '{"type":"pve","enableCommands":false}' \ + "$(TAPM_PULSE_AGENT_TOKEN_REQUEST_JSON)" \ + "Proxmox-specific agent enrollment requested with commands disabled" nodes_json='[ {"node":"pve3","status":"offline"}, @@ -85,6 +93,8 @@ resources='[ assert_success "tagged Pulse LXC detected" TAPM_PULSE_RESOURCE_INSTALLED "$resources" assert_success "legacy Pulse hostname detected" \ TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"lxc","name":"Pulse"}]' +assert_success "untagged default Pulse hostname detected" \ + TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"lxc","name":"Pulse-Monitor"}]' assert_failure "unrelated resource not detected" \ TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"qemu","name":"pulse"}]'