This commit is contained in:
David Schroeder
2026-07-25 21:26:23 -05:00
parent 2b8b2c8f6f
commit fd23ec525a
3 changed files with 178 additions and 32 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
action="${1:-}" action="${1:-}"
FOLDER='/opt/idssys/ta-proxmenu' FOLDER='/opt/idssys/ta-proxmenu'
VERS='2026.7.25-50' VERS='2026.7.25-51'
noupdate=' ' noupdate=' '
+167 -31
View File
@@ -102,6 +102,24 @@ print(token.lower())
' 2>/dev/null ' 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() { TAPM_PULSE_ONLINE_NODES_FROM_JSON() {
local nodes_json="${1:-[]}" local nodes_json="${1:-[]}"
@@ -146,7 +164,8 @@ except (TypeError, ValueError):
for item in resources: for item in resources:
tags = str(item.get("tags", "")).split(";") tags = str(item.get("tags", "")).split(";")
if item.get("type") == "lxc" and ( 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(0)
raise SystemExit(1) raise SystemExit(1)
@@ -198,6 +217,7 @@ TAPM_PULSE_CONFIRM_CREDENTIALS_SAVED() {
local primary_api_token="$4" local primary_api_token="$4"
local installed="$5" local installed="$5"
local failed="$6" local failed="$6"
local cluster_status="$7"
local acknowledgement local acknowledgement
while true; do while true; do
@@ -210,6 +230,7 @@ TAPM_PULSE_CONFIRM_CREDENTIALS_SAVED() {
echo -e " Username: ${idsCL[White]}${admin_username}${idsCL[Default]}" echo -e " Username: ${idsCL[White]}${admin_username}${idsCL[Default]}"
echo -e " Password: ${idsCL[LightGreen]}${admin_password}${idsCL[Default]}" echo -e " Password: ${idsCL[LightGreen]}${admin_password}${idsCL[Default]}"
echo -e " API token: ${idsCL[LightGreen]}${primary_api_token}${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 " Agents: ${installed} installed, ${failed} failed or offline"
echo -e "${idsCL[LightYellow]}============================================================================${idsCL[Default]}" echo -e "${idsCL[LightYellow]}============================================================================${idsCL[Default]}"
echo echo
@@ -444,6 +465,48 @@ raise SystemExit(0 if success is True else 1)
unset generated_password generated_api_token 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() { TAPM_PULSE_ISSUE_AGENT_TOKEN() {
local pulse_url="$1" local pulse_url="$1"
local primary_token="$2" local primary_token="$2"
@@ -453,14 +516,7 @@ TAPM_PULSE_ISSUE_AGENT_TOKEN() {
local curl_config="${temp_dir}/agent-${node}.curl" local curl_config="${temp_dir}/agent-${node}.curl"
local response local response
TAPM_PULSE_AGENT_NAME="tapm-pve-${node}" python3 -c ' TAPM_PULSE_AGENT_TOKEN_REQUEST_JSON >"$request_file" || return 1
import json, os, sys
json.dump({
"type": "host",
"name": os.environ["TAPM_PULSE_AGENT_NAME"],
"enableCommands": False,
}, sys.stdout)
' >"$request_file" || return 1
chmod 0600 "$request_file" || return 1 chmod 0600 "$request_file" || return 1
{ {
printf 'header = "Content-Type: application/json"\n' printf 'header = "Content-Type: application/json"\n'
@@ -478,10 +534,44 @@ json.dump({
TAPM_PULSE_TOKEN_FROM_RESPONSE "$response" TAPM_PULSE_TOKEN_FROM_RESPONSE "$response"
} }
TAPM_PULSE_INSTALL_AGENT_LOCAL() { TAPM_PULSE_WAIT_AGENT_REGISTERED() {
local pulse_url="$1" local pulse_url="$1"
local token="$2" 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 status=0
local -a old_artifacts=( local -a old_artifacts=(
/usr/local/bin/pulse-agent /usr/local/bin/pulse-agent
@@ -496,19 +586,39 @@ TAPM_PULSE_INSTALL_AGENT_LOCAL() {
rm -f -- "$token_file" rm -f -- "$token_file"
return 1 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" || { chmod 0600 "$token_file" "$installer_file" || {
rm -f -- "$token_file" "$installer_file" rm -f -- "$token_file" "$installer_file"
rm -rf -- "$cleanup_state_dir"
return 1 return 1
} }
printf '%s' "$token" >"$token_file" || { printf '%s' "$token" >"$token_file" || {
rm -f -- "$token_file" "$installer_file" rm -f -- "$token_file" "$installer_file"
rm -rf -- "$cleanup_state_dir"
return 1 return 1
} }
curl --fail --silent --show-error --location \ curl --fail --silent --show-error --location \
--output "$installer_file" "${pulse_url}/install.sh" || status=$? --output "$installer_file" "${pulse_url}/install.sh" || status=$?
if (( status == 0 )); then if (( status == 0 )); then
echo " Removing any previous Pulse Unified Agent installation..." 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 fi
if (( status == 0 )) && systemctl is-active --quiet pulse-agent; then if (( status == 0 )) && systemctl is-active --quiet pulse-agent; then
echo " The previous pulse-agent service is still active." >&2 echo " The previous pulse-agent service is still active." >&2
@@ -531,12 +641,14 @@ TAPM_PULSE_INSTALL_AGENT_LOCAL() {
bash "$installer_file" \ bash "$installer_file" \
--url "$pulse_url" \ --url "$pulse_url" \
--token-file "$token_file" \ --token-file "$token_file" \
--hostname "$node" \
--enable-proxmox \ --enable-proxmox \
--proxmox-type pve \ --proxmox-type pve \
--non-interactive \ --non-interactive \
--insecure || status=$? --insecure || status=$?
fi fi
rm -f -- "$token_file" "$installer_file" rm -f -- "$token_file" "$installer_file"
rm -rf -- "$cleanup_state_dir"
(( status == 0 )) || return "$status" (( status == 0 )) || return "$status"
systemctl is-active --quiet pulse-agent 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]+$ ]] || [[ "$remote_token_file" =~ ^/tmp/tapm-pulse-agent-token\.[A-Za-z0-9]+$ ]] ||
return 1 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 set -eu -o pipefail
pulse_url="$1" pulse_url="$1"
token_file="$2" token_file="$2"
node="$3"
installer_file="$(mktemp /tmp/tapm-pulse-agent-installer.XXXXXX)" 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" chmod 0600 "$token_file" "$installer_file"
curl --fail --silent --show-error --location \ curl --fail --silent --show-error --location \
--output "$installer_file" "${pulse_url}/install.sh" --output "$installer_file" "${pulse_url}/install.sh"
echo " Removing any previous Pulse Unified Agent installation..." 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 if systemctl is-active --quiet pulse-agent; then
echo " The previous pulse-agent service is still active." >&2 echo " The previous pulse-agent service is still active." >&2
exit 1 exit 1
@@ -598,6 +726,7 @@ done
bash "$installer_file" \ bash "$installer_file" \
--url "$pulse_url" \ --url "$pulse_url" \
--token-file "$token_file" \ --token-file "$token_file" \
--hostname "$node" \
--enable-proxmox \ --enable-proxmox \
--proxmox-type pve \ --proxmox-type pve \
--non-interactive \ --non-interactive \
@@ -632,13 +761,20 @@ TAPM_PULSE_DEPLOY_CLUSTER_AGENTS() {
} }
if [[ "$node" == "$current_node" || "$node" == "$(hostname)" ]]; then 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 else
TAPM_PULSE_INSTALL_AGENT_REMOTE "$node" "$pulse_url" "$agent_token" TAPM_PULSE_INSTALL_AGENT_REMOTE "$node" "$pulse_url" "$agent_token"
fi fi
if (( $? == 0 )); then if (( $? == 0 )); then
echo -e "${idsCL[Green]}Pulse Unified Agent is active on ${node}.${idsCL[Default]}" echo " Waiting for Pulse to confirm registration from ${node}..."
((installed += 1)) 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 else
echo -e "${idsCL[LightRed]}Pulse Unified Agent installation failed on ${node}.${idsCL[Default]}" echo -e "${idsCL[LightRed]}Pulse Unified Agent installation failed on ${node}.${idsCL[Default]}"
((failed += 1)) ((failed += 1))
@@ -665,6 +801,7 @@ TAPM_DEPLOY_PULSE_LXC() {
local memory disk cores cpulimit swap onboot firewall unprivileged nameserver startup 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 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 default_bridge pulse_url admin_username admin_password primary_api_token
local cluster_status='Registration failed'
local container_created=0 local container_created=0
local -a create_args=() local -a create_args=()
@@ -898,7 +1035,6 @@ TAPM_DEPLOY_PULSE_LXC() {
--net0 "$network_config" --net0 "$network_config"
--onboot "$onboot" --onboot "$onboot"
--startup "order=${startup}" --startup "order=${startup}"
--tags 'tapm;pulse'
) )
[[ "$cpulimit" != 0 ]] && create_args+=(--cpulimit "$cpulimit") [[ "$cpulimit" != 0 ]] && create_args+=(--cpulimit "$cpulimit")
[[ -n "$nameserver" ]] && create_args+=(--nameserver "$nameserver") [[ -n "$nameserver" ]] && create_args+=(--nameserver "$nameserver")
@@ -945,17 +1081,6 @@ TAPM_DEPLOY_PULSE_LXC() {
if [[ -n "$container_ip" ]]; then if [[ -n "$container_ip" ]]; then
pulse_url="http://${container_ip}:${pulse_port}" 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 fi
if [[ "$add_ha" == 'yes' ]] && if [[ "$add_ha" == 'yes' ]] &&
@@ -984,6 +1109,16 @@ TAPM_DEPLOY_PULSE_LXC() {
return 1 return 1
fi 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]}" echo -e "\n${idsCL[LightCyan]}Deploying clean Pulse Unified Agents to cluster nodes...${idsCL[Default]}"
TAPM_PULSE_AGENTS_INSTALLED=0 TAPM_PULSE_AGENTS_INSTALLED=0
TAPM_PULSE_AGENTS_FAILED=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]}" echo -e "${idsCL[Green]}Pulse ${release} was installed and its service is active.${idsCL[Default]}"
TAPM_PULSE_CONFIRM_CREDENTIALS_SAVED \ TAPM_PULSE_CONFIRM_CREDENTIALS_SAVED \
"$pulse_url" "$admin_username" "$admin_password" "$primary_api_token" \ "$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"
} }
+10
View File
@@ -44,6 +44,14 @@ assert_failure "Pulse agent token containing whitespace rejected" \
TAPM_PULSE_TOKEN_FROM_RESPONSE '{"token":"invalid token"}' TAPM_PULSE_TOKEN_FROM_RESPONSE '{"token":"invalid token"}'
assert_failure "non-hex Pulse agent token rejected" \ assert_failure "non-hex Pulse agent token rejected" \
TAPM_PULSE_TOKEN_FROM_RESPONSE '{"token":"not-a-token"}' 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='[ nodes_json='[
{"node":"pve3","status":"offline"}, {"node":"pve3","status":"offline"},
@@ -85,6 +93,8 @@ resources='[
assert_success "tagged Pulse LXC detected" TAPM_PULSE_RESOURCE_INSTALLED "$resources" assert_success "tagged Pulse LXC detected" TAPM_PULSE_RESOURCE_INSTALLED "$resources"
assert_success "legacy Pulse hostname detected" \ assert_success "legacy Pulse hostname detected" \
TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"lxc","name":"Pulse"}]' 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" \ assert_failure "unrelated resource not detected" \
TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"qemu","name":"pulse"}]' TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"qemu","name":"pulse"}]'