update security
This commit is contained in:
@@ -16,6 +16,13 @@ interactive menu. Update availability is checked in the background and cached;
|
|||||||
updates are installed only when explicitly selected or requested with
|
updates are installed only when explicitly selected or requested with
|
||||||
`tapm update`.
|
`tapm update`.
|
||||||
|
|
||||||
|
On a clustered Proxmox host, both `tapm update` and the management-menu update
|
||||||
|
update every online cluster node over Proxmox's root SSH trust, then update the
|
||||||
|
initiating node. Each node independently performs the same clean-worktree,
|
||||||
|
branch, and fast-forward safety checks. Offline, unreachable, dirty, ahead, or
|
||||||
|
diverged nodes are never overwritten and are reported as failures. Standalone
|
||||||
|
hosts retain the local-only update behavior.
|
||||||
|
|
||||||
When testing this branch, use `tapm main` to switch the installed copy back to
|
When testing this branch, use `tapm main` to switch the installed copy back to
|
||||||
the published main branch. The command refuses to switch when local changes,
|
the published main branch. The command refuses to switch when local changes,
|
||||||
local-only commits, or diverged branch history would be at risk.
|
local-only commits, or diverged branch history would be at risk.
|
||||||
@@ -159,3 +166,20 @@ both values. Inputs are validated and written atomically with mode `0600`
|
|||||||
before update checks or menu actions continue. A non-interactive launch without
|
before update checks or menu actions continue. A non-interactive launch without
|
||||||
valid configuration stops with an explicit setup message instead of selecting
|
valid configuration stops with an explicit setup message instead of selecting
|
||||||
a default company URL.
|
a default company URL.
|
||||||
|
|
||||||
|
## Installation registry
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
|
||||||
|
The registry does not send hostnames, machine IDs, MAC addresses, usernames,
|
||||||
|
customer names, VM/container inventory, deployment codes, authorization
|
||||||
|
tokens, or command output. The broker stores only a digest of the random host
|
||||||
|
credential. A successful deployment-code exchange links the already-random
|
||||||
|
installation ID to a verified registry entry.
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
action="${1:-}"
|
action="${1:-}"
|
||||||
FOLDER='/opt/idssys/ta-proxmenu'
|
FOLDER='/opt/idssys/ta-proxmenu'
|
||||||
VERS='2026.7.26-5'
|
VERS='2026.7.26-7'
|
||||||
|
|
||||||
noupdate=' '
|
noupdate=' '
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ PULSE_PORT='7655'
|
|||||||
|
|
||||||
source "${FOLDER}/inc/runtime-config.inc"
|
source "${FOLDER}/inc/runtime-config.inc"
|
||||||
TAPM_ENSURE_RUNTIME_CONFIG || return 1 2>/dev/null || exit 1
|
TAPM_ENSURE_RUNTIME_CONFIG || return 1 2>/dev/null || exit 1
|
||||||
|
source "${FOLDER}/inc/fleet.inc"
|
||||||
|
TAPM_FLEET_ENSURE_IDENTITY || true
|
||||||
S1_BROKER_PACKAGE='sentinelone-linux'
|
S1_BROKER_PACKAGE='sentinelone-linux'
|
||||||
S1_PACKAGE='tapm-sentinelone.deb'
|
S1_PACKAGE='tapm-sentinelone.deb'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Cluster discovery and remote update helpers for TA-ProxMenu.
|
||||||
|
|
||||||
|
TAPM_COROSYNC_CONFIG="${TAPM_COROSYNC_CONFIG:-/etc/pve/corosync.conf}"
|
||||||
|
|
||||||
|
TAPM_CLUSTER_NODES_FROM_JSON() {
|
||||||
|
python3 -c '
|
||||||
|
import json, re, sys
|
||||||
|
try:
|
||||||
|
payload = json.load(sys.stdin)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
raise SystemExit(1)
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
payload = payload.get("data", [])
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
raise SystemExit(1)
|
||||||
|
valid_name = re.compile(r"^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$")
|
||||||
|
rows = []
|
||||||
|
for item in payload:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
name = str(item.get("node", "")).strip()
|
||||||
|
status = str(item.get("status", "unknown")).strip().lower()
|
||||||
|
if valid_name.fullmatch(name):
|
||||||
|
rows.append((name, status))
|
||||||
|
for name, status in sorted(rows):
|
||||||
|
print(f"{name}\t{status}")
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_CLUSTER_NODE_ROWS() {
|
||||||
|
local node_json
|
||||||
|
|
||||||
|
node_json="$(timeout 10 pvesh get /nodes --output-format json 2>/dev/null)" ||
|
||||||
|
return 1
|
||||||
|
printf '%s' "$node_json" | TAPM_CLUSTER_NODES_FROM_JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_LOCAL_CLUSTER_NODE() {
|
||||||
|
local local_link=''
|
||||||
|
|
||||||
|
local_link="$(readlink /etc/pve/local 2>/dev/null || true)"
|
||||||
|
if [[ -n "$local_link" ]]; then
|
||||||
|
basename "$local_link"
|
||||||
|
else
|
||||||
|
hostname -s
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_UPDATE_REMOTE_NODE() {
|
||||||
|
local node="$1"
|
||||||
|
|
||||||
|
[[ "$node" =~ ^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$ ]] || return 1
|
||||||
|
timeout 240 ssh \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-o ConnectTimeout=10 \
|
||||||
|
-o ServerAliveInterval=15 \
|
||||||
|
-o ServerAliveCountMax=2 \
|
||||||
|
"root@${node}" \
|
||||||
|
'/opt/idssys/ta-proxmenu/run.sh update --local-only'
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTALL_CLUSTER_UPDATES() {
|
||||||
|
local index
|
||||||
|
local local_node
|
||||||
|
local node
|
||||||
|
local required_command
|
||||||
|
local status
|
||||||
|
local node_rows=''
|
||||||
|
local cluster_failed=0
|
||||||
|
local -a cluster_nodes=()
|
||||||
|
local -a cluster_statuses=()
|
||||||
|
|
||||||
|
if [[ ! -s "$TAPM_COROSYNC_CONFIG" ]]; then
|
||||||
|
INSTALL_LOCAL_UPDATES
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
for required_command in pvesh python3 ssh timeout; do
|
||||||
|
if ! command -v "$required_command" >/dev/null 2>&1; then
|
||||||
|
echo -e "${idsCL[Red]}Cluster update requires ${required_command}; no nodes were updated.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
node_rows="$(TAPM_CLUSTER_NODE_ROWS)" || {
|
||||||
|
echo -e "${idsCL[Red]}Could not retrieve the Proxmox cluster node list; no nodes were updated.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
while IFS=$'\t' read -r node status; do
|
||||||
|
[[ -n "$node" ]] || continue
|
||||||
|
cluster_nodes+=("$node")
|
||||||
|
cluster_statuses+=("$status")
|
||||||
|
done <<<"$node_rows"
|
||||||
|
if (( ${#cluster_nodes[@]} == 0 )); then
|
||||||
|
echo -e "${idsCL[Red]}The Proxmox API returned no cluster nodes; no nodes were updated.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local_node="$(TAPM_LOCAL_CLUSTER_NODE)"
|
||||||
|
echo -e "${idsCL[LightCyan]}Updating TA-ProxMenu across ${#cluster_nodes[@]} cluster node(s)...${idsCL[Default]}"
|
||||||
|
for index in "${!cluster_nodes[@]}"; do
|
||||||
|
node="${cluster_nodes[$index]}"
|
||||||
|
status="${cluster_statuses[$index]}"
|
||||||
|
if [[ "$node" == "$local_node" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$status" != "online" ]]; then
|
||||||
|
echo -e "${idsCL[LightYellow]}Skipping ${node}: node status is ${status}.${idsCL[Default]}"
|
||||||
|
cluster_failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo -e "${idsCL[LightCyan]}Updating remote node ${node}...${idsCL[Default]}"
|
||||||
|
if TAPM_UPDATE_REMOTE_NODE "$node"; then
|
||||||
|
echo -e "${idsCL[Green]}Remote node ${node} is updated.${idsCL[Default]}"
|
||||||
|
else
|
||||||
|
echo -e "${idsCL[Red]}Remote node ${node} failed to update.${idsCL[Default]}"
|
||||||
|
cluster_failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${idsCL[LightCyan]}Updating local node ${local_node}...${idsCL[Default]}"
|
||||||
|
INSTALL_LOCAL_UPDATES || cluster_failed=1
|
||||||
|
if (( cluster_failed == 0 )); then
|
||||||
|
echo -e "${idsCL[Green]}TA-ProxMenu is updated on all ${#cluster_nodes[@]} cluster node(s).${idsCL[Default]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo -e "${idsCL[LightYellow]}Cluster update completed with failures; review the node messages above.${idsCL[Default]}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
TAPM_FLEET_STATE_DIR="${TAPM_FLEET_STATE_DIR:-/var/lib/ta-proxmenu}"
|
||||||
|
TAPM_FLEET_IDENTITY_FILE="${TAPM_FLEET_IDENTITY_FILE:-${TAPM_FLEET_STATE_DIR}/identity.env}"
|
||||||
|
TAPM_FLEET_INSTALLATION_ID=''
|
||||||
|
TAPM_FLEET_CREDENTIAL=''
|
||||||
|
TAPM_FLEET_LAST_VERSION=''
|
||||||
|
TAPM_FLEET_REGISTERED=0
|
||||||
|
TAPM_FLEET_STARTED_AT=0
|
||||||
|
TAPM_FLEET_VERSION=''
|
||||||
|
|
||||||
|
TAPM_FLEET_READ_VALUE() {
|
||||||
|
local key="$1"
|
||||||
|
local value=''
|
||||||
|
if [[ -r "$TAPM_FLEET_IDENTITY_FILE" ]]; then
|
||||||
|
value="$(
|
||||||
|
sed -n "s/^${key}=//p" "$TAPM_FLEET_IDENTITY_FILE" |
|
||||||
|
tail -n 1
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
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_VALID_IDENTITY() {
|
||||||
|
[[ "$TAPM_FLEET_INSTALLATION_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ &&
|
||||||
|
"$TAPM_FLEET_CREDENTIAL" =~ ^[0-9a-f]{64}$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_FLEET_WRITE_IDENTITY() {
|
||||||
|
local temporary_file
|
||||||
|
|
||||||
|
mkdir -p "$TAPM_FLEET_STATE_DIR" 2>/dev/null || return 1
|
||||||
|
chmod 0700 "$TAPM_FLEET_STATE_DIR" 2>/dev/null || return 1
|
||||||
|
temporary_file="$(mktemp "${TAPM_FLEET_IDENTITY_FILE}.tmp.XXXXXX")" || return 1
|
||||||
|
if ! {
|
||||||
|
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"
|
||||||
|
} >"$temporary_file" ||
|
||||||
|
! chmod 0600 "$temporary_file" ||
|
||||||
|
! mv -f "$temporary_file" "$TAPM_FLEET_IDENTITY_FILE"; then
|
||||||
|
rm -f "$temporary_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_FLEET_ENSURE_IDENTITY() {
|
||||||
|
TAPM_FLEET_LOAD_IDENTITY
|
||||||
|
TAPM_FLEET_VALID_IDENTITY && return 0
|
||||||
|
command -v openssl >/dev/null 2>&1 || return 1
|
||||||
|
if [[ -r /proc/sys/kernel/random/uuid ]]; then
|
||||||
|
IFS= read -r TAPM_FLEET_INSTALLATION_ID </proc/sys/kernel/random/uuid
|
||||||
|
elif command -v uuidgen >/dev/null 2>&1; then
|
||||||
|
TAPM_FLEET_INSTALLATION_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
TAPM_FLEET_CREDENTIAL="$(openssl rand -hex 32)" || return 1
|
||||||
|
TAPM_FLEET_LAST_VERSION=''
|
||||||
|
TAPM_FLEET_WRITE_IDENTITY
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_FLEET_COLLECT_METADATA() {
|
||||||
|
TAPM_FLEET_GIT_COMMIT="$(git -C "${FOLDER:-/opt/idssys/ta-proxmenu}" rev-parse HEAD 2>/dev/null || true)"
|
||||||
|
TAPM_FLEET_PVE_VERSION="$(pveversion 2>/dev/null | head -n 1 || true)"
|
||||||
|
TAPM_FLEET_OS_VERSION="$(
|
||||||
|
if [[ -r /etc/os-release ]]; then
|
||||||
|
(
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /etc/os-release
|
||||||
|
printf '%s' "${PRETTY_NAME:-}"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
)"
|
||||||
|
TAPM_FLEET_KERNEL_VERSION="$(uname -r 2>/dev/null || true)"
|
||||||
|
TAPM_FLEET_ARCHITECTURE="$(
|
||||||
|
dpkg --print-architecture 2>/dev/null ||
|
||||||
|
uname -m 2>/dev/null ||
|
||||||
|
true
|
||||||
|
)"
|
||||||
|
if [[ -s /etc/pve/corosync.conf ]]; then
|
||||||
|
TAPM_FLEET_CLUSTERED=true
|
||||||
|
else
|
||||||
|
TAPM_FLEET_CLUSTERED=false
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_FLEET_JSON() {
|
||||||
|
local event="$1"
|
||||||
|
local result="$2"
|
||||||
|
local error_code="${3:-}"
|
||||||
|
local duration="${4:-0}"
|
||||||
|
|
||||||
|
TAPM_FLEET_EVENT="$event" \
|
||||||
|
TAPM_FLEET_RESULT="$result" \
|
||||||
|
TAPM_FLEET_ERROR_CODE="$error_code" \
|
||||||
|
TAPM_FLEET_DURATION="$duration" \
|
||||||
|
TAPM_FLEET_INSTALLATION_ID="$TAPM_FLEET_INSTALLATION_ID" \
|
||||||
|
TAPM_FLEET_CREDENTIAL="$TAPM_FLEET_CREDENTIAL" \
|
||||||
|
TAPM_FLEET_VERSION="$TAPM_FLEET_VERSION" \
|
||||||
|
TAPM_FLEET_GIT_COMMIT="$TAPM_FLEET_GIT_COMMIT" \
|
||||||
|
TAPM_FLEET_PVE_VERSION="$TAPM_FLEET_PVE_VERSION" \
|
||||||
|
TAPM_FLEET_OS_VERSION="$TAPM_FLEET_OS_VERSION" \
|
||||||
|
TAPM_FLEET_KERNEL_VERSION="$TAPM_FLEET_KERNEL_VERSION" \
|
||||||
|
TAPM_FLEET_ARCHITECTURE="$TAPM_FLEET_ARCHITECTURE" \
|
||||||
|
TAPM_FLEET_CLUSTERED="$TAPM_FLEET_CLUSTERED" \
|
||||||
|
TAPM_FLEET_INCLUDE_CREDENTIAL="${TAPM_FLEET_INCLUDE_CREDENTIAL:-0}" \
|
||||||
|
python3 -c '
|
||||||
|
import json, os, sys
|
||||||
|
payload = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"installation_id": os.environ["TAPM_FLEET_INSTALLATION_ID"],
|
||||||
|
"event": os.environ["TAPM_FLEET_EVENT"],
|
||||||
|
"result": os.environ["TAPM_FLEET_RESULT"],
|
||||||
|
"proxmenu_version": os.environ["TAPM_FLEET_VERSION"],
|
||||||
|
"git_commit": os.environ["TAPM_FLEET_GIT_COMMIT"],
|
||||||
|
"pve_version": os.environ["TAPM_FLEET_PVE_VERSION"],
|
||||||
|
"os_version": os.environ["TAPM_FLEET_OS_VERSION"],
|
||||||
|
"kernel_version": os.environ["TAPM_FLEET_KERNEL_VERSION"],
|
||||||
|
"architecture": os.environ["TAPM_FLEET_ARCHITECTURE"],
|
||||||
|
"clustered": os.environ["TAPM_FLEET_CLUSTERED"] == "true",
|
||||||
|
"error_code": os.environ["TAPM_FLEET_ERROR_CODE"],
|
||||||
|
"duration_seconds": int(os.environ["TAPM_FLEET_DURATION"]),
|
||||||
|
}
|
||||||
|
if os.environ.get("TAPM_FLEET_INCLUDE_CREDENTIAL") == "1":
|
||||||
|
payload["credential"] = os.environ["TAPM_FLEET_CREDENTIAL"]
|
||||||
|
json.dump(payload, sys.stdout, separators=(",", ":"))
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_FLEET_REGISTER() {
|
||||||
|
TAPM_FLEET_INCLUDE_CREDENTIAL=1 TAPM_FLEET_JSON installed success |
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
--connect-timeout 3 --max-time 10 \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-binary @- \
|
||||||
|
"${TAPM_BROKER_URL}/api/v1/hosts/register" \
|
||||||
|
>/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_FLEET_EVENT_SEND() {
|
||||||
|
local event="$1"
|
||||||
|
local result="$2"
|
||||||
|
local error_code="${3:-}"
|
||||||
|
local duration="${4:-0}"
|
||||||
|
local event_payload=''
|
||||||
|
|
||||||
|
(( TAPM_FLEET_REGISTERED == 1 )) || return 0
|
||||||
|
event_payload="$(mktemp "${TMPDIR:-/tmp}/tapm-fleet-event.XXXXXX")" || return 0
|
||||||
|
chmod 0600 "$event_payload" 2>/dev/null || {
|
||||||
|
rm -f "$event_payload"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if ! TAPM_FLEET_JSON "$event" "$result" "$error_code" "$duration" >"$event_payload"; then
|
||||||
|
rm -f "$event_payload"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
rm -f "$event_payload"
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_FLEET_START() {
|
||||||
|
TAPM_FLEET_VERSION="$1"
|
||||||
|
TAPM_FLEET_STARTED_AT="$(date +%s)"
|
||||||
|
TAPM_FLEET_ENSURE_IDENTITY || return 0
|
||||||
|
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
|
||||||
|
TAPM_FLEET_REGISTERED=1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
|
else
|
||||||
|
TAPM_FLEET_EVENT_SEND run_failed failure exit_nonzero "$duration"
|
||||||
|
fi
|
||||||
|
if (( TAPM_FLEET_REGISTERED == 1 )) && TAPM_FLEET_VALID_IDENTITY; then
|
||||||
|
TAPM_FLEET_LAST_VERSION="$TAPM_FLEET_VERSION"
|
||||||
|
TAPM_FLEET_WRITE_IDENTITY || true
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
+2
-1
@@ -185,8 +185,9 @@ TAPM_AUTHORIZE() {
|
|||||||
exchange_response="$(
|
exchange_response="$(
|
||||||
TAPM_CODE="$deploycode" TAPM_FINGERPRINT="$host_fingerprint" TAPM_HOSTNAME="$(hostname)" \
|
TAPM_CODE="$deploycode" TAPM_FINGERPRINT="$host_fingerprint" TAPM_HOSTNAME="$(hostname)" \
|
||||||
TAPM_LAN_IP="${RNIP:-}" \
|
TAPM_LAN_IP="${RNIP:-}" \
|
||||||
|
TAPM_INSTALLATION_ID="${TAPM_FLEET_INSTALLATION_ID:-}" \
|
||||||
TAPM_REQUESTED_ACTION="$required_action" TAPM_REQUESTED_PACKAGE="$required_package" \
|
TAPM_REQUESTED_ACTION="$required_action" TAPM_REQUESTED_PACKAGE="$required_package" \
|
||||||
python3 -c 'import json, os, sys; json.dump({"code": os.environ["TAPM_CODE"], "host_fingerprint": os.environ["TAPM_FINGERPRINT"], "hostname": os.environ["TAPM_HOSTNAME"], "lan_ip": os.environ["TAPM_LAN_IP"], "requested_action": os.environ["TAPM_REQUESTED_ACTION"], "requested_package": os.environ["TAPM_REQUESTED_PACKAGE"]}, sys.stdout)' |
|
python3 -c 'import json, os, sys; json.dump({"code": os.environ["TAPM_CODE"], "host_fingerprint": os.environ["TAPM_FINGERPRINT"], "hostname": os.environ["TAPM_HOSTNAME"], "lan_ip": os.environ["TAPM_LAN_IP"], "installation_id": os.environ["TAPM_INSTALLATION_ID"], "requested_action": os.environ["TAPM_REQUESTED_ACTION"], "requested_package": os.environ["TAPM_REQUESTED_PACKAGE"]}, sys.stdout)' |
|
||||||
curl --fail --silent --show-error \
|
curl --fail --silent --show-error \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data-binary @- "${TAPM_BROKER_URL}/api/v1/exchange"
|
--data-binary @- "${TAPM_BROKER_URL}/api/v1/exchange"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ TAPM_ENSURE_RUNTIME_CONFIG || exit 1
|
|||||||
DEFAULTS_REPOSITORY_URL="${GITEA_URL}/voltron/iDS-Defaults.git"
|
DEFAULTS_REPOSITORY_URL="${GITEA_URL}/voltron/iDS-Defaults.git"
|
||||||
|
|
||||||
source /opt/idssys/ta-proxmenu/inc/git-update.inc
|
source /opt/idssys/ta-proxmenu/inc/git-update.inc
|
||||||
|
source /opt/idssys/ta-proxmenu/inc/cluster-update.inc
|
||||||
|
|
||||||
AUTO_UPDATE_DEFAULTS() {
|
AUTO_UPDATE_DEFAULTS() {
|
||||||
local checked_at=0
|
local checked_at=0
|
||||||
@@ -100,6 +101,8 @@ AUTO_UPDATE_DEFAULTS
|
|||||||
[ "${2:-}" != "q" ] && source /opt/idssys/defaults/colors.inc
|
[ "${2:-}" != "q" ] && source /opt/idssys/defaults/colors.inc
|
||||||
source /opt/idssys/defaults/default.inc
|
source /opt/idssys/defaults/default.inc
|
||||||
source /opt/idssys/ta-proxmenu/defaults.inc
|
source /opt/idssys/ta-proxmenu/defaults.inc
|
||||||
|
TAPM_FLEET_START "$VERS"
|
||||||
|
trap 'TAPM_FLEET_FINISH "$?"' EXIT
|
||||||
|
|
||||||
UPDATE_REPOSITORY() {
|
UPDATE_REPOSITORY() {
|
||||||
local repository="$1"
|
local repository="$1"
|
||||||
@@ -235,7 +238,7 @@ SWITCH_TAPM_BRANCH() {
|
|||||||
exec /opt/idssys/ta-proxmenu/run.sh
|
exec /opt/idssys/ta-proxmenu/run.sh
|
||||||
}
|
}
|
||||||
|
|
||||||
INSTALL_UPDATES() {
|
INSTALL_LOCAL_UPDATES() {
|
||||||
local current_branch
|
local current_branch
|
||||||
local defaults_warning=0
|
local defaults_warning=0
|
||||||
local update_failed=0
|
local update_failed=0
|
||||||
@@ -279,7 +282,11 @@ INSTALL_UPDATES() {
|
|||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
update|u)
|
update|u)
|
||||||
INSTALL_UPDATES
|
if [[ "${2:-}" == "--local-only" ]]; then
|
||||||
|
INSTALL_LOCAL_UPDATES
|
||||||
|
else
|
||||||
|
INSTALL_CLUSTER_UPDATES
|
||||||
|
fi
|
||||||
exit $?
|
exit $?
|
||||||
;;
|
;;
|
||||||
main)
|
main)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u -o pipefail
|
||||||
|
|
||||||
|
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${TEST_ROOT}/tests/testlib.sh"
|
||||||
|
source "${TEST_ROOT}/inc/cluster-update.inc"
|
||||||
|
|
||||||
|
nodes_json='[
|
||||||
|
{"node":"pve3","status":"offline"},
|
||||||
|
{"node":"pve1","status":"online"},
|
||||||
|
{"node":"pve2","status":"online"},
|
||||||
|
{"node":"bad node","status":"online"}
|
||||||
|
]'
|
||||||
|
expected=$'pve1\tonline\npve2\tonline\npve3\toffline'
|
||||||
|
assert_equal "$expected" \
|
||||||
|
"$(printf '%s' "$nodes_json" | TAPM_CLUSTER_NODES_FROM_JSON)" \
|
||||||
|
"cluster node discovery filters and sorts API output"
|
||||||
|
|
||||||
|
wrapped_json='{"data":[{"node":"pve2.example","status":"ONLINE"}]}'
|
||||||
|
assert_equal $'pve2.example\tonline' \
|
||||||
|
"$(printf '%s' "$wrapped_json" | TAPM_CLUSTER_NODES_FROM_JSON)" \
|
||||||
|
"wrapped API response and normalized status"
|
||||||
|
|
||||||
|
if printf '%s' 'not-json' | TAPM_CLUSTER_NODES_FROM_JSON 2>/dev/null; then
|
||||||
|
printf 'FAIL: malformed cluster JSON was accepted\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
assert_failure "remote node rejects shell characters" \
|
||||||
|
TAPM_UPDATE_REMOTE_NODE 'pve1;reboot'
|
||||||
|
|
||||||
|
test_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$test_dir"' EXIT
|
||||||
|
TAPM_COROSYNC_CONFIG="${test_dir}/corosync.conf"
|
||||||
|
printf 'totem {}\n' >"$TAPM_COROSYNC_CONFIG"
|
||||||
|
update_log="${test_dir}/updates"
|
||||||
|
LightCyan=0
|
||||||
|
LightYellow=0
|
||||||
|
Green=0
|
||||||
|
Red=0
|
||||||
|
Default=0
|
||||||
|
idsCL[0]=''
|
||||||
|
pvesh() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
TAPM_CLUSTER_NODE_ROWS() {
|
||||||
|
printf 'pve1\tonline\npve2\tonline\npve3\toffline\n'
|
||||||
|
}
|
||||||
|
TAPM_LOCAL_CLUSTER_NODE() {
|
||||||
|
printf 'pve1\n'
|
||||||
|
}
|
||||||
|
TAPM_UPDATE_REMOTE_NODE() {
|
||||||
|
printf 'remote:%s\n' "$1" >>"$update_log"
|
||||||
|
}
|
||||||
|
INSTALL_LOCAL_UPDATES() {
|
||||||
|
printf 'local:pve1\n' >>"$update_log"
|
||||||
|
}
|
||||||
|
if INSTALL_CLUSTER_UPDATES >/dev/null; then
|
||||||
|
printf 'FAIL: cluster update ignored an offline node\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
assert_equal $'remote:pve2\nlocal:pve1' \
|
||||||
|
"$(cat "$update_log")" \
|
||||||
|
"online remotes and initiating node update once"
|
||||||
|
|
||||||
|
finish_tests
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u -o pipefail
|
||||||
|
|
||||||
|
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
test_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$test_dir"' EXIT
|
||||||
|
|
||||||
|
TAPM_FLEET_STATE_DIR="${test_dir}/state"
|
||||||
|
TAPM_FLEET_IDENTITY_FILE="${TAPM_FLEET_STATE_DIR}/identity.env"
|
||||||
|
TAPM_BROKER_URL='https://tapm.example.com'
|
||||||
|
source "${TEST_ROOT}/inc/fleet.inc"
|
||||||
|
|
||||||
|
TAPM_FLEET_ENSURE_IDENTITY
|
||||||
|
if ! TAPM_FLEET_VALID_IDENTITY; then
|
||||||
|
printf 'FAIL: generated fleet identity is invalid\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if stat -c '%a' "$TAPM_FLEET_IDENTITY_FILE" >/dev/null 2>&1; then
|
||||||
|
identity_mode="$(stat -c '%a' "$TAPM_FLEET_IDENTITY_FILE")"
|
||||||
|
else
|
||||||
|
identity_mode="$(stat -f '%Lp' "$TAPM_FLEET_IDENTITY_FILE")"
|
||||||
|
fi
|
||||||
|
if [[ "$identity_mode" != 600 ]]; then
|
||||||
|
printf 'FAIL: fleet identity mode is not 600\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAPM_FLEET_VERSION='2026.7.26-7'
|
||||||
|
TAPM_FLEET_GIT_COMMIT='0123456789abcdef0123456789abcdef01234567'
|
||||||
|
TAPM_FLEET_PVE_VERSION='pve-manager/9.0.3/abc~1'
|
||||||
|
TAPM_FLEET_OS_VERSION='Debian GNU/Linux 13 (trixie)'
|
||||||
|
TAPM_FLEET_KERNEL_VERSION='6.14.11-2-pve'
|
||||||
|
TAPM_FLEET_ARCHITECTURE='amd64'
|
||||||
|
TAPM_FLEET_CLUSTERED=true
|
||||||
|
|
||||||
|
registration_json="$(
|
||||||
|
TAPM_FLEET_INCLUDE_CREDENTIAL=1 TAPM_FLEET_JSON installed success
|
||||||
|
)"
|
||||||
|
REGISTRATION_JSON="$registration_json" python3 -c '
|
||||||
|
import json, os
|
||||||
|
payload = json.loads(os.environ["REGISTRATION_JSON"])
|
||||||
|
assert payload["installation_id"]
|
||||||
|
assert len(payload["credential"]) == 64
|
||||||
|
assert payload["proxmenu_version"] == "2026.7.26-7"
|
||||||
|
assert payload["clustered"] is True
|
||||||
|
'
|
||||||
|
|
||||||
|
printf '0\n' >"${test_dir}/curl-count"
|
||||||
|
curl() {
|
||||||
|
local count data_path='' previous='' argument=''
|
||||||
|
count="$(cat "${test_dir}/curl-count")"
|
||||||
|
count="$((count + 1))"
|
||||||
|
printf '%s\n' "$count" >"${test_dir}/curl-count"
|
||||||
|
for argument in "$@"; do
|
||||||
|
if [[ "$previous" == '--data-binary' ]]; then
|
||||||
|
data_path="${argument#@}"
|
||||||
|
fi
|
||||||
|
previous="$argument"
|
||||||
|
done
|
||||||
|
if [[ -n "$data_path" ]]; then
|
||||||
|
cp "$data_path" "${test_dir}/curl-body-${count}.json"
|
||||||
|
fi
|
||||||
|
cat >"${test_dir}/curl-config-${count}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
TAPM_FLEET_REGISTERED=1
|
||||||
|
TAPM_FLEET_EVENT_SEND run_completed success '' 12
|
||||||
|
EVENT_BODY="$(cat "${test_dir}/curl-body-1.json")" \
|
||||||
|
EVENT_CONFIG="$(cat "${test_dir}/curl-config-1")" \
|
||||||
|
EXPECTED_CREDENTIAL="$TAPM_FLEET_CREDENTIAL" \
|
||||||
|
python3 -c '
|
||||||
|
import json, os
|
||||||
|
payload = json.loads(os.environ["EVENT_BODY"])
|
||||||
|
assert payload["event"] == "run_completed"
|
||||||
|
assert payload["result"] == "success"
|
||||||
|
assert payload["duration_seconds"] == 12
|
||||||
|
assert "credential" not in payload
|
||||||
|
config = os.environ["EVENT_CONFIG"]
|
||||||
|
assert "Authorization: Bearer " + os.environ["EXPECTED_CREDENTIAL"] in config
|
||||||
|
assert "https://tapm.example.com/api/v1/hosts/events" in config
|
||||||
|
'
|
||||||
|
|
||||||
|
if compgen -G "${TMPDIR:-/tmp}/tapm-fleet-event.*" >/dev/null; then
|
||||||
|
printf 'FAIL: fleet event left a temporary payload file\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'PASS: fleet identity and event client\n'
|
||||||
Reference in New Issue
Block a user