This commit is contained in:
David Schroeder
2026-07-28 22:09:13 -05:00
parent 247c4ddace
commit 3bea3baa88
12 changed files with 236 additions and 49 deletions
+12 -6
View File
@@ -177,12 +177,18 @@ a default company URL.
V2 creates a random installation UUID and 256-bit credential in
`/var/lib/ta-proxmenu/identity.env`. The directory is mode `0700` and the file
is mode `0600`. On each interactive launch, TA-ProxMenu makes best-effort HTTPS
calls to the configured broker to record the hostname, installation/upgrade
state, start and completion status, duration, TA-ProxMenu and Git versions,
PVE/OS/kernel versions, architecture, and whether the host is clustered. No
background service is installed, and broker availability never prevents the
menu from running.
is mode `0600`. The first successful launch enrolls the host with the broker.
Later launches make one best-effort HTTPS call on completion or failure to
record the hostname, duration, result, TA-ProxMenu and Git versions,
PVE/OS/kernel versions, architecture, and whether the host is clustered. A
failed event schedules re-enrollment on the next invocation instead of adding
another retry to the current run. No background service is installed, and
broker availability never prevents the menu from running.
Automatic Git update checks and the stable VirtIO release lookup are cached for
24 hours. Their explicit **Check again** and **Refresh** actions bypass the
cache. Installer, package-repository, and vendor download traffic occurs only
after a technician selects the corresponding action.
The hostname is treated as a limited operational identifier. The registry does
not send machine IDs, MAC addresses, usernames, VM/container inventory,
+1 -1
View File
@@ -3,7 +3,7 @@
action="${1:-}"
FOLDER='/opt/idssys/ta-proxmenu'
VERS='2026.7.28-8'
VERS='2026.7.28-9'
noupdate=' '
+13 -4
View File
@@ -49,6 +49,15 @@ TAPM_ISO_NFS_FAIL() {
return 1
}
TAPM_ISO_NFS_LOCAL_TEMPLATE() {
local storage="$1"
pveam list "$storage" 2>/dev/null |
awk 'NR > 1 && $1 ~ /:vztmpl\/debian-(13|12)-standard_/ { print $1 }' |
sort -V |
tail -1
}
TAPM_ISO_NFS_SELECT_STORAGE() {
local variable="$1"
local label="$2"
@@ -198,7 +207,10 @@ TAPM_DEPLOY_ISO_NFS_LXC() {
}
echo -e "\n${idsCL[LightCyan]}Locating a Debian container template...${idsCL[Default]}"
pveam update || { TAPM_ISO_NFS_FAIL "Could not refresh the template catalog."; return 1; }
template_path="$(TAPM_ISO_NFS_LOCAL_TEMPLATE "$template_storage")"
if [[ -z "$template_path" ]]; then
pveam update ||
{ TAPM_ISO_NFS_FAIL "Could not refresh the template catalog."; return 1; }
template_name="$(
pveam available --section system |
awk '$2 ~ /^debian-(13|12)-standard_/ { print $2 }' |
@@ -208,9 +220,6 @@ TAPM_DEPLOY_ISO_NFS_LXC() {
[[ -n "$template_name" ]] ||
{ TAPM_ISO_NFS_FAIL "No supported Debian 12/13 standard template was found."; return 1; }
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
pveam download "$template_storage" "$template_name" ||
{ TAPM_ISO_NFS_FAIL "The Debian template download failed."; return 1; }
fi
+37 -7
View File
@@ -60,6 +60,28 @@ TAPM_PULSE_VALID_NONNEGATIVE_INTEGER() {
[[ "${1:-}" =~ ^[0-9]+$ ]]
}
TAPM_PULSE_BACKOFF_NEXT() {
local current="${1:-1}"
local maximum="${2:-8}"
local next
[[ "$current" =~ ^[1-9][0-9]*$ &&
"$maximum" =~ ^[1-9][0-9]*$ ]] || return 1
next=$((current * 2))
(( next > maximum )) && next="$maximum"
printf '%s\n' "$next"
}
TAPM_PULSE_BACKOFF_SLEEP() {
local deadline="$1"
local delay="$2"
local remaining=$((deadline - SECONDS))
(( remaining > 0 )) || return 0
(( delay > remaining )) && delay="$remaining"
sleep "$delay"
}
TAPM_PULSE_VALID_PORT() {
[[ "${1:-}" =~ ^[0-9]+$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 ))
}
@@ -498,7 +520,8 @@ TAPM_PULSE_CONFIGURE_SECURITY() {
local token_variable="$6"
local requested_password="${7:-}"
local bootstrap_output bootstrap_token generated_username generated_password generated_api_token
local request_file curl_config response security_ready='no' attempt
local request_file curl_config response security_ready='no'
local deadline delay
generated_username='admin'
if [[ -n "$requested_password" ]]; then
@@ -577,7 +600,9 @@ raise SystemExit(0 if success is True else 1)
{
printf 'header = "X-API-Token: %s"\n' "$generated_api_token"
} >"$curl_config" || return 1
for (( attempt = 1; attempt <= 15; attempt++ )); do
deadline=$((SECONDS + 30))
delay=1
while (( SECONDS < deadline )); do
if curl --fail --silent --show-error \
--connect-timeout 3 --max-time 5 \
--config "$curl_config" \
@@ -585,7 +610,8 @@ raise SystemExit(0 if success is True else 1)
security_ready='yes'
break
fi
sleep 2
TAPM_PULSE_BACKOFF_SLEEP "$deadline" "$delay"
delay="$(TAPM_PULSE_BACKOFF_NEXT "$delay" 4)"
done
if [[ "$security_ready" != 'yes' ]]; then
unset generated_password generated_api_token
@@ -796,7 +822,8 @@ TAPM_PULSE_WAIT_AGENT_REGISTERED() {
local node="$3"
local temp_dir="$4"
local curl_config="${temp_dir}/agent-${node}-verify.curl"
local encoded_node response attempt
local encoded_node response
local deadline delay
printf 'header = "X-API-Token: %s"\n' "$token" >"$curl_config" || return 1
chmod 0600 "$curl_config" || return 1
@@ -809,8 +836,10 @@ print(urllib.parse.quote(os.environ["TAPM_PULSE_NODE_NAME"], safe=""))
# Pulse v6.1.1 completes its Proxmox setup before sending the first host
# report. Its built-in registration retries can span at least 135 seconds,
# so allow up to four minutes before treating the active agent as unconfirmed.
for (( attempt = 1; attempt <= 80; attempt++ )); do
# so allow up to four minutes while backing off repeated local lookups.
deadline=$((SECONDS + 240))
delay=1
while (( SECONDS < deadline )); do
response="$(
curl --fail --silent --show-error \
--connect-timeout 3 --max-time 5 \
@@ -821,7 +850,8 @@ print(urllib.parse.quote(os.environ["TAPM_PULSE_NODE_NAME"], safe=""))
if TAPM_PULSE_AGENT_REGISTERED_FROM_RESPONSE "$response"; then
return 0
fi
sleep 3
TAPM_PULSE_BACKOFF_SLEEP "$deadline" "$delay"
delay="$(TAPM_PULSE_BACKOFF_NEXT "$delay" 8)"
done
return 1
}
+31 -18
View File
@@ -5,6 +5,7 @@ TAPM_FLEET_IDENTITY_FILE="${TAPM_FLEET_IDENTITY_FILE:-${TAPM_FLEET_STATE_DIR}/id
TAPM_FLEET_INSTALLATION_ID=''
TAPM_FLEET_CREDENTIAL=''
TAPM_FLEET_LAST_VERSION=''
TAPM_FLEET_ENROLLED=0
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_STARTED_AT=0
TAPM_FLEET_VERSION=''
@@ -26,6 +27,8 @@ TAPM_FLEET_LOAD_IDENTITY() {
TAPM_FLEET_INSTALLATION_ID="$(TAPM_FLEET_READ_VALUE INSTALLATION_ID)"
TAPM_FLEET_CREDENTIAL="$(TAPM_FLEET_READ_VALUE CREDENTIAL)"
TAPM_FLEET_LAST_VERSION="$(TAPM_FLEET_READ_VALUE LAST_VERSION)"
TAPM_FLEET_ENROLLED="$(TAPM_FLEET_READ_VALUE ENROLLED)"
[[ "$TAPM_FLEET_ENROLLED" == 1 ]] || TAPM_FLEET_ENROLLED=0
}
TAPM_FLEET_VALID_IDENTITY() {
@@ -43,6 +46,7 @@ TAPM_FLEET_WRITE_IDENTITY() {
printf 'INSTALLATION_ID=%s\n' "$TAPM_FLEET_INSTALLATION_ID"
printf 'CREDENTIAL=%s\n' "$TAPM_FLEET_CREDENTIAL"
printf 'LAST_VERSION=%s\n' "$TAPM_FLEET_LAST_VERSION"
printf 'ENROLLED=%s\n' "$TAPM_FLEET_ENROLLED"
} >"$temporary_file" ||
! chmod 0600 "$temporary_file" ||
! mv -f "$temporary_file" "$TAPM_FLEET_IDENTITY_FILE"; then
@@ -64,6 +68,7 @@ TAPM_FLEET_ENSURE_IDENTITY() {
fi
TAPM_FLEET_CREDENTIAL="$(openssl rand -hex 32)" || return 1
TAPM_FLEET_LAST_VERSION=''
TAPM_FLEET_ENROLLED=0
TAPM_FLEET_WRITE_IDENTITY
}
@@ -155,23 +160,27 @@ TAPM_FLEET_EVENT_SEND() {
local duration="${4:-0}"
local event_payload=''
(( TAPM_FLEET_REGISTERED == 1 )) || return 0
event_payload="$(mktemp "${TMPDIR:-/tmp}/tapm-fleet-event.XXXXXX")" || return 0
(( TAPM_FLEET_REGISTERED == 1 )) || return 1
event_payload="$(mktemp "${TMPDIR:-/tmp}/tapm-fleet-event.XXXXXX")" || return 1
chmod 0600 "$event_payload" 2>/dev/null || {
rm -f "$event_payload"
return 0
return 1
}
if ! TAPM_FLEET_JSON "$event" "$result" "$error_code" "$duration" >"$event_payload"; then
rm -f "$event_payload"
return 0
return 1
fi
printf 'header = "Content-Type: application/json"\nheader = "Authorization: Bearer %s"\nurl = "%s/api/v1/hosts/events"\n' \
if printf 'header = "Content-Type: application/json"\nheader = "Authorization: Bearer %s"\nurl = "%s/api/v1/hosts/events"\n' \
"$TAPM_FLEET_CREDENTIAL" "$TAPM_BROKER_URL" |
curl --fail --silent --show-error \
--connect-timeout 3 --max-time 10 \
--config - --data-binary "@${event_payload}" \
>/dev/null 2>&1 || true
>/dev/null 2>&1; then
rm -f "$event_payload"
return 0
fi
rm -f "$event_payload"
return 1
}
TAPM_FLEET_START() {
@@ -181,34 +190,38 @@ TAPM_FLEET_START() {
command -v python3 >/dev/null 2>&1 || return 0
command -v curl >/dev/null 2>&1 || return 0
TAPM_FLEET_COLLECT_METADATA
if TAPM_FLEET_REGISTER; then
if (( TAPM_FLEET_ENROLLED == 1 )); then
TAPM_FLEET_REGISTERED=1
else
return 0
elif TAPM_FLEET_REGISTER; then
TAPM_FLEET_REGISTERED=1
TAPM_FLEET_ENROLLED=1
TAPM_FLEET_WRITE_IDENTITY || true
fi
if [[ -n "$TAPM_FLEET_LAST_VERSION" &&
"$TAPM_FLEET_LAST_VERSION" != "$TAPM_FLEET_VERSION" ]]; then
TAPM_FLEET_EVENT_SEND upgraded success
fi
TAPM_FLEET_EVENT_SEND run_started started
}
TAPM_FLEET_FINISH() {
local exit_status="${1:-0}"
local duration=0
local event_sent=0
if [[ "$TAPM_FLEET_STARTED_AT" =~ ^[0-9]+$ ]] &&
(( TAPM_FLEET_STARTED_AT > 0 )); then
duration="$(( $(date +%s) - TAPM_FLEET_STARTED_AT ))"
fi
if (( exit_status == 0 )); then
TAPM_FLEET_EVENT_SEND run_completed success '' "$duration"
TAPM_FLEET_EVENT_SEND run_completed success '' "$duration" &&
event_sent=1
else
TAPM_FLEET_EVENT_SEND run_failed failure exit_nonzero "$duration"
TAPM_FLEET_EVENT_SEND run_failed failure exit_nonzero "$duration" &&
event_sent=1
fi
if (( TAPM_FLEET_REGISTERED == 1 )) && TAPM_FLEET_VALID_IDENTITY; then
if (( event_sent == 1 )) && TAPM_FLEET_VALID_IDENTITY; then
TAPM_FLEET_LAST_VERSION="$TAPM_FLEET_VERSION"
TAPM_FLEET_WRITE_IDENTITY || true
else
# A failed event may mean the broker no longer recognizes this local
# credential. Re-enroll on the next invocation, not during this one.
TAPM_FLEET_ENROLLED=0
fi
TAPM_FLEET_VALID_IDENTITY && TAPM_FLEET_WRITE_IDENTITY || true
return 0
}
+14
View File
@@ -23,3 +23,17 @@ TAPM_VIRTIO_LABELED_FILENAME() {
printf 'virtio-win-%s%s.iso\n' "$label" "${version:+-${version}}"
}
TAPM_VIRTIO_CACHE_VALID() {
local checked_at="${1:-}"
local now="${2:-}"
local max_age="${3:-}"
local filename="${4:-}"
[[ "$checked_at" =~ ^[0-9]+$ &&
"$now" =~ ^[0-9]+$ &&
"$max_age" =~ ^[1-9][0-9]*$ &&
"$filename" =~ ^virtio-win(-[0-9]+\.[0-9]+\.[0-9]+)?\.iso$ ]] ||
return 1
(( now >= checked_at && now - checked_at < max_age ))
}
+33 -2
View File
@@ -687,6 +687,8 @@ INSTALL_OMSA() {
VIRTIO_STABLE_CHECKED=0
VIRTIO_STABLE_STATUS='unknown'
VIRTIO_STABLE_FILE=''
VIRTIO_STABLE_CACHE_FILE="${VIRTIO_STABLE_CACHE_FILE:-/var/cache/ta-proxmenu/virtio-stable}"
VIRTIO_STABLE_CACHE_SECONDS=86400
VIRTIO_LAST_FILE=''
DLDIR=''
VIRTIO_SELECTED_STORAGE=''
@@ -784,8 +786,12 @@ TAPM_REFRESH_VIRTIO_LOCAL_STATUS() {
TAPM_CHECK_VIRTIO_STABLE() {
local force="${1:-0}"
local cached_at=''
local cached_filename=''
local effective_url
local now
local source_filename
local cache_temp
if (( VIRTIO_STABLE_CHECKED == 1 && force == 0 )); then
return
@@ -794,6 +800,19 @@ TAPM_CHECK_VIRTIO_STABLE() {
VIRTIO_STABLE_CHECKED=1
VIRTIO_STABLE_STATUS='unavailable'
VIRTIO_STABLE_FILE=''
now="$(date +%s)"
if (( force == 0 )) && [[ -r "$VIRTIO_STABLE_CACHE_FILE" ]]; then
IFS='|' read -r cached_at cached_filename <"$VIRTIO_STABLE_CACHE_FILE"
if TAPM_VIRTIO_CACHE_VALID \
"$cached_at" "$now" "$VIRTIO_STABLE_CACHE_SECONDS" "$cached_filename"; then
VIRTIO_STABLE_FILE="$(
TAPM_VIRTIO_LABELED_FILENAME "$cached_filename" latest
)" || return 1
TAPM_REFRESH_VIRTIO_LOCAL_STATUS
return 0
fi
fi
effective_url="$(
curl --fail --location --silent --show-error --head \
@@ -806,6 +825,18 @@ TAPM_CHECK_VIRTIO_STABLE() {
return 1
VIRTIO_STABLE_FILE="$(TAPM_VIRTIO_LABELED_FILENAME "$source_filename" latest)" ||
return 1
if mkdir -p "$(dirname "$VIRTIO_STABLE_CACHE_FILE")" 2>/dev/null; then
cache_temp="$(mktemp "${VIRTIO_STABLE_CACHE_FILE}.tmp.XXXXXX")" || cache_temp=''
if [[ -n "$cache_temp" ]]; then
if printf '%s|%s\n' "$now" "$source_filename" >"$cache_temp" &&
chmod 0644 "$cache_temp"; then
mv -f "$cache_temp" "$VIRTIO_STABLE_CACHE_FILE" ||
rm -f "$cache_temp"
else
rm -f "$cache_temp"
fi
fi
fi
TAPM_REFRESH_VIRTIO_LOCAL_STATUS
}
@@ -1000,7 +1031,7 @@ VIRTIO_MENU() {
echo -en "\n${idsCL[LightCyan]}Checking the current stable VirtIO release...${idsCL[Default]} "
TAPM_REFRESH_ISO_STORAGES || true
TAPM_CHECK_VIRTIO_STABLE 1 || true
TAPM_CHECK_VIRTIO_STABLE 0 || true
echo
while true; do
@@ -1492,7 +1523,7 @@ INSTALL_KEEPALIVE() {
UPDATE_CACHE_DIR='/var/cache/ta-proxmenu'
UPDATE_CACHE_FILE="${UPDATE_CACHE_DIR}/update-status"
UPDATE_CACHE_SECONDS=14400
UPDATE_CACHE_SECONDS=86400
UPDATE_CHECK_PID=''
UPDATE_STATUS='unknown'
UPDATE_REMOTE_COMMIT=''
+58 -2
View File
@@ -48,6 +48,7 @@ assert payload["clustered"] is True
'
printf '0\n' >"${test_dir}/curl-count"
curl_should_fail=0
curl() {
local count data_path='' previous='' argument=''
count="$(cat "${test_dir}/curl-count")"
@@ -59,11 +60,11 @@ curl() {
fi
previous="$argument"
done
if [[ -n "$data_path" ]]; then
if [[ -n "$data_path" && "$data_path" != '-' ]]; then
cp "$data_path" "${test_dir}/curl-body-${count}.json"
fi
cat >"${test_dir}/curl-config-${count}"
return 0
(( curl_should_fail == 0 ))
}
TAPM_FLEET_REGISTERED=1
@@ -89,4 +90,59 @@ if compgen -G "${TMPDIR:-/tmp}/tapm-fleet-event.*" >/dev/null; then
exit 1
fi
printf '0\n' >"${test_dir}/curl-count"
TAPM_FLEET_ENROLLED=0
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_LAST_VERSION=''
TAPM_FLEET_COLLECT_METADATA() {
TAPM_FLEET_HOSTNAME='pve01'
TAPM_FLEET_GIT_COMMIT='0123456789abcdef0123456789abcdef01234567'
TAPM_FLEET_PVE_VERSION='pve-manager/9.2.4'
TAPM_FLEET_OS_VERSION='Debian GNU/Linux 13 (trixie)'
TAPM_FLEET_KERNEL_VERSION='6.14.11-5-pve'
TAPM_FLEET_ARCHITECTURE='amd64'
TAPM_FLEET_CLUSTERED=true
}
TAPM_FLEET_START '2026.7.28-9'
TAPM_FLEET_FINISH 0
if [[ "$(cat "${test_dir}/curl-count")" != 2 ]]; then
printf 'FAIL: initial run did not make exactly registration + completion calls\n' >&2
exit 1
fi
TAPM_FLEET_LOAD_IDENTITY
if [[ "$TAPM_FLEET_ENROLLED" != 1 ||
"$TAPM_FLEET_LAST_VERSION" != '2026.7.28-9' ]]; then
printf 'FAIL: successful fleet enrollment was not persisted\n' >&2
exit 1
fi
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_START '2026.7.28-9'
TAPM_FLEET_FINISH 0
if [[ "$(cat "${test_dir}/curl-count")" != 3 ]]; then
printf 'FAIL: normal run made more than one broker call\n' >&2
exit 1
fi
curl_should_fail=1
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_START '2026.7.28-9'
TAPM_FLEET_FINISH 0
TAPM_FLEET_LOAD_IDENTITY
if [[ "$TAPM_FLEET_ENROLLED" != 0 ]]; then
printf 'FAIL: failed fleet event did not schedule next-run re-enrollment\n' >&2
exit 1
fi
curl_should_fail=0
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_START '2026.7.28-9'
TAPM_FLEET_FINISH 0
TAPM_FLEET_LOAD_IDENTITY
if [[ "$TAPM_FLEET_ENROLLED" != 1 ]]; then
printf 'FAIL: fleet client did not recover enrollment after a failed event\n' >&2
exit 1
fi
printf 'PASS: fleet identity and event client\n'
+3
View File
@@ -5,6 +5,9 @@ TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/git-update.inc"
assert_success "automatic Git check cache is 24 hours" \
grep -q '^UPDATE_CACHE_SECONDS=86400$' "${TEST_ROOT}/proxmenu-scripts.sh"
TEST_TEMP_DIR="$(mktemp -d /tmp/tapm-git-tests.XXXXXX)"
TEST_REPOSITORY="${TEST_TEMP_DIR}/repository"
+12
View File
@@ -60,4 +60,16 @@ test_default_storage_selection() {
assert_success "default storage menu selection" test_default_storage_selection
pveam() {
[[ "${1:-}" == list && "${2:-}" == local ]] || return 1
printf '%s\n' \
'NAME VOLID FORMAT TYPE SIZE VMID' \
'local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst 0 0 0 0 0' \
'local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst 0 0 0 0 0'
}
assert_equal \
'local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst' \
"$(TAPM_ISO_NFS_LOCAL_TEMPLATE local)" \
"newest local Debian template avoids catalog refresh"
finish_tests
+4
View File
@@ -15,6 +15,10 @@ assert_equal amd64 "$(TAPM_PULSE_ARCH x86_64)" "x86 architecture mapping"
assert_equal arm64 "$(TAPM_PULSE_ARCH aarch64)" "ARM architecture mapping"
assert_failure "unsupported Pulse architecture" TAPM_PULSE_ARCH riscv64
assert_equal 2 "$(TAPM_PULSE_BACKOFF_NEXT 1 8)" "Pulse backoff doubles"
assert_equal 8 "$(TAPM_PULSE_BACKOFF_NEXT 4 8)" "Pulse backoff reaches cap"
assert_equal 8 "$(TAPM_PULSE_BACKOFF_NEXT 8 8)" "Pulse backoff remains capped"
test_bridge_selection_assignment() {
local bridge=''
+9
View File
@@ -31,4 +31,13 @@ assert_equal virtio-win-server-2008r2-0.1.172.iso \
assert_failure "unsafe local label rejected" \
TAPM_VIRTIO_LABELED_FILENAME virtio-win-0.1.285.iso '../latest'
assert_success "fresh VirtIO cache accepted" \
TAPM_VIRTIO_CACHE_VALID 1000 1500 86400 virtio-win-0.1.285.iso
assert_failure "expired VirtIO cache rejected" \
TAPM_VIRTIO_CACHE_VALID 1000 87400 86400 virtio-win-0.1.285.iso
assert_failure "future VirtIO cache rejected" \
TAPM_VIRTIO_CACHE_VALID 2000 1000 86400 virtio-win-0.1.285.iso
assert_failure "unsafe VirtIO cache filename rejected" \
TAPM_VIRTIO_CACHE_VALID 1000 1500 86400 ../../installer
finish_tests