diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index 3fa8997..e37088a 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -37,6 +37,7 @@ on: - "tests/stale-silent-record.sh" - "tests/exit-code-hints.sh" - "tests/telemetry-trace.sh" + - "tests/host-probes.sh" - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" - "tests/console-hangup.sh" @@ -73,6 +74,7 @@ on: - "tests/stale-silent-record.sh" - "tests/exit-code-hints.sh" - "tests/telemetry-trace.sh" + - "tests/host-probes.sh" - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" - "tests/console-hangup.sh" @@ -146,6 +148,9 @@ jobs: - name: Telemetry trace tests run: bash tests/telemetry-trace.sh + - name: Host probe tests + run: bash tests/host-probes.sh + - name: Forge release lookup tests run: bash tests/forge-release-json.sh diff --git a/api/API.txt b/api/API.txt index d708af0..951e9eb 100644 --- a/api/API.txt +++ b/api/API.txt @@ -14,6 +14,7 @@ _tm_post_progress _tm_post_run _tm_post_update _tm_quiet +_tm_requirement_code _tm_send _tm_step_output build_error_string diff --git a/api/sysinfo.func b/api/sysinfo.func index beaadd8..32b7fc2 100644 --- a/api/sysinfo.func +++ b/api/sysinfo.func @@ -235,7 +235,16 @@ telemetry_collect_sysinfo() { TM_PLATFORM="${TELEMETRY_PLATFORM:-}" TM_PVE_VERSION="" if command -v pveversion &>/dev/null; then - TM_PVE_VERSION=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true + # Every payload is built in $( ), so _TM_SYSINFO_DONE never reaches the + # caller and this ran once per send; _pve_version caches in a file, and + # the VM engine, which lacks it, keeps what its pve_check read. + if declare -f _pve_version >/dev/null 2>&1; then + TM_PVE_VERSION="$(_pve_version)" + elif [[ "${PVEVERSION:-}" =~ ^[0-9] ]]; then + TM_PVE_VERSION="$PVEVERSION" + else + TM_PVE_VERSION=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true + fi TM_PLATFORM="${TM_PLATFORM:-pve}" elif command -v incus &>/dev/null; then TM_PVE_VERSION="incus-$(incus version 2>/dev/null | head -1 | awk '{print $NF}')" || true diff --git a/api/telemetry.func b/api/telemetry.func index d27ab91..7652c48 100644 --- a/api/telemetry.func +++ b/api/telemetry.func @@ -259,16 +259,10 @@ _tm_post_update() { ;; esac - # Unset: no install was reported yet. A host run that failed in its checks - # (storage, ID, template) reports one now; an update run inside a container - # or a cancelled dialog does not. "false" means that send failed. - if [[ -z "${POST_TO_API_DONE:-}" ]]; then - if [[ "$pb_status" == "failed" && -n "${NSAPP:-}" ]] && - ! { declare -f _is_container_context >/dev/null 2>&1 && _is_container_context; }; then - post_to_api - fi - [[ -z "${POST_TO_API_DONE:-}" ]] && return 0 - fi + # Unset: the run ended before its build reported the install, in the checks + # or a dialog. Those report nothing; a host that is not ready is no failed + # install. "false" means the install was due and its send failed. + [[ -z "${POST_TO_API_DONE:-}" ]] && return 0 if [[ "$pb_status" == "failed" || "$pb_status" == "unknown" ]]; then _tm_error_context "$exit_code" @@ -302,6 +296,17 @@ _tm_post_update() { return 0 } +# Tools and add-ons have no build step that starts their telemetry, so their +# checks are known by the code they exit with: wrong host or OS, nothing to work +# on, a prerequisite missing. Like the checks before a container build, those +# report nothing. +_tm_requirement_code() { + case "$1" in + 10 | 103 | 104 | 106 | 232 | 233 | 234 | 236 | 238) return 0 ;; + esac + return 1 +} + # ------------------------------------------------------------------------------ # _tm_post_run - tool and add-on runs # @@ -318,6 +323,7 @@ _tm_post_run() { uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "${type}-$(date +%s)") [[ "$status" == "done" ]] && status="success" [[ "$exit_code" =~ ^[0-9]+$ ]] || exit_code=199 + [[ "$status" == "failed" ]] && _tm_requirement_code "$exit_code" && return 0 if [[ "$status" == "failed" ]]; then _tm_error_context "$exit_code" diff --git a/core/build.func b/core/build.func index bf748a4..458a4bd 100644 --- a/core/build.func +++ b/core/build.func @@ -268,7 +268,7 @@ _cs_prefetch_engine() { [[ -d "$base" && -w "$base" ]] || continue # An EXIT trap would be the obvious cleanup, but core.func and api.func # install their own later and the last one wins. Sweep instead. - find "$base" -maxdepth 1 \( -name 'cs-engine.*' -o -name 'cs-pveam.*' -o -name 'cs-pveversion.*' \) -mmin +60 -exec rm -rf {} + 2>/dev/null || true + find "$base" -maxdepth 1 \( -name 'cs-engine.*' -o -name 'cs-pveam.*' -o -name 'cs-pveversion.*' -o -name 'cs-probe.*' \) -mmin +60 -exec rm -rf {} + 2>/dev/null || true dir="$(mktemp -d "${base}/cs-engine.XXXXXX" 2>/dev/null)" && break dir="" done @@ -305,6 +305,52 @@ _cs_prefetch_engine() { return 0 } +# The checks, the settings and the storage dialogs wait on pveversion, pvesm, +# pveam and pvesh, each a Perl start of up to a second. Start them now, while the +# engine downloads and the header loads; _cs_probe_read hands over what they printed. +# _cs_probe_run - run it, keep its output and exit code +_cs_probe_run() { + local dir="$1" name="$2" + shift 2 + "$@" >"${dir}/${name}.out" 2>/dev/null + echo "$?" >"${dir}/${name}.tmp" && mv -f "${dir}/${name}.tmp" "${dir}/${name}.rc" +} + +# _cs_probe - the same in the background. The empty +# .out tells readers a result is coming before the subshell has started. +_cs_probe() { + : >"${1}/${2}.out" + _cs_probe_run "$@" /dev/null 2>&1 || return 0 + [[ "$(id -u)" == "0" ]] || return 0 + local base dir="" name names=(pveversion pvesm vztmpl rootdir pveam nextid) + for base in /dev/shm "${TMPDIR:-/tmp}" /tmp; do + [[ -d "$base" && -w "$base" ]] || continue + dir="$(mktemp -d "${base}/cs-probe.XXXXXX" 2>/dev/null)" && break + dir="" + done + [[ -n "$dir" ]] || return 0 + [[ -f /etc/pve/corosync.conf ]] && names+=(pvecm) + for name in "${names[@]}"; do : >"${dir}/${name}.out"; done + # One fork from this shell; the probes branch off inside it. + ( + _cs_probe_run "$dir" pveversion pveversion & + _cs_probe_run "$dir" pvesm pvesm status & + _cs_probe_run "$dir" vztmpl pvesm status -content vztmpl & + _cs_probe_run "$dir" rootdir pvesm status -content rootdir & + _cs_probe_run "$dir" pveam pveam available -section system & + _cs_probe_run "$dir" nextid pvesh get /cluster/nextid & + [[ -f "${dir}/pvecm.out" ]] && _cs_probe_run "$dir" pvecm pvecm status & + wait + ) - output of a host probe that core/build.func started at +# load, once it has finished; fails when there is none or it failed, and the +# caller runs the command itself. Polls a file, so it works inside $( ) too. +_cs_probe_read() { + local rc + rc="$(_cs_probe_rc "$1")" && [[ "$rc" == "0" ]] || return 1 + cat "${CS_PROBE_DIR}/$1.out" +} + +# _cs_probe_rc - a finished probe's exit code, for checks that answer +# with one; fails when there is no such probe +_cs_probe_rc() { + local dir="${CS_PROBE_DIR:-}" n=0 + [[ -n "$dir" && -f "${dir}/$1.out" ]] || return 1 + while [[ ! -f "${dir}/$1.rc" ]]; do + ((n++ < 1200)) || return 1 + sleep 0.05 + done + cat "${dir}/$1.rc" +} + +# _cs_probe_drop ... - forget probes whose answer a change made stale +_cs_probe_drop() { + local name + for name in "$@"; do + rm -f "${CS_PROBE_DIR:-/nonexistent}/${name}.out" "${CS_PROBE_DIR:-/nonexistent}/${name}.rc" 2>/dev/null + done + return 0 +} + +_cs_pveversion_out() { + _cs_probe_read pveversion || pveversion 2>/dev/null +} + _pve_version() { [[ -n "${PVEVERSION:-}" ]] && { printf '%s' "$PVEVERSION"; return 0; } local dir cache dir="$(_cs_runtime_cache_dir)" || { - pveversion 2>/dev/null | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' - return + _cs_pveversion_out | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' + return 0 } cache="${dir}/cs-pveversion.$$" [[ -s "$cache" ]] || - pveversion 2>/dev/null | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' >"$cache" + _cs_pveversion_out | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' >"$cache" cat "$cache" 2>/dev/null } @@ -566,7 +600,9 @@ _pve_version() { # reports it verbatim as pve_version. A fixed "PVE Version" prefix therefore # printed "PVE Version Incus 7.3" on Incus hosts. _cs_host_version_line() { - local v="${PVEVERSION:-unknown}" + local v="${PVEVERSION:-}" + [[ -z "$v" ]] && command -v pveversion >/dev/null 2>&1 && v="$(_pve_version)" + v="${v:-unknown}" if [[ "$v" == Incus* ]]; then v="${v#Incus}" v="${v# }" @@ -643,11 +679,11 @@ _cs_scripts_ref_line() { _pveam_available() { local dir cache dir="$(_cs_runtime_cache_dir)" || { - pveam available -section system 2>/dev/null - return + _cs_probe_read pveam || pveam available -section system 2>/dev/null + return 0 } cache="${dir}/cs-pveam.$$" - [[ -s "$cache" ]] || pveam available -section system 2>/dev/null >"$cache" + [[ -s "$cache" ]] || { _cs_probe_read pveam || pveam available -section system 2>/dev/null; } >"$cache" cat "$cache" 2>/dev/null } @@ -656,6 +692,7 @@ _pveam_available() { _pveam_update() { local dir dir="$(_cs_runtime_cache_dir)" && rm -f "${dir}/cs-pveam.$$" + _cs_probe_drop pveam if command -v timeout &>/dev/null; then timeout 30 pveam update >/dev/null 2>&1 || return 1 else @@ -735,7 +772,7 @@ pmxcfs_check() { exit 228 fi - if [[ -f /etc/pve/corosync.conf ]] && ! pvecm status 2>/dev/null | grep -qE '^Quorate:[[:space:]]+Yes'; then + if [[ -f /etc/pve/corosync.conf ]] && ! { _cs_probe_read pvecm || pvecm status 2>/dev/null; } | grep -qE '^Quorate:[[:space:]]+Yes'; then msg_error "This cluster node has no quorum, so /etc/pve is read-only and nothing can be created." msg_custom "🔧" "${YW}" "Bring enough cluster nodes online, then check: pvecm status" exit 210 @@ -899,8 +936,8 @@ log_msg() { local logfile logfile="$(get_active_logfile)" - [[ -z "$msg" ]] && return - [[ -z "$logfile" ]] && return + [[ -z "$msg" ]] && return 0 + [[ -z "$logfile" ]] && return 0 # Ensure log directory exists mkdir -p "$(dirname "$logfile")" 2>/dev/null || true @@ -908,7 +945,8 @@ log_msg() { # Strip ANSI codes and write with timestamp local clean_msg clean_msg=$(strip_ansi "$msg") - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $clean_msg" >>"$logfile" + # A log that cannot be written (full disk, removed directory) must not end the run. + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $clean_msg" >>"$logfile" 2>/dev/null || true } # ------------------------------------------------------------------------------ @@ -923,7 +961,7 @@ log_section() { local logfile logfile="$(get_active_logfile)" - [[ -z "$logfile" ]] && return + [[ -z "$logfile" ]] && return 0 mkdir -p "$(dirname "$logfile")" 2>/dev/null || true { @@ -931,7 +969,7 @@ log_section() { echo "================================================================================" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $section" echo "================================================================================" - } >>"$logfile" + } >>"$logfile" 2>/dev/null || true } # ------------------------------------------------------------------------------ diff --git a/docs/api.md b/docs/api.md index c1fef0a..ecb53ed 100644 --- a/docs/api.md +++ b/docs/api.md @@ -15,9 +15,21 @@ terminal status: it writes `/root/.install-.failed` and an `.errinfo` capture, which the host picks up after `lxc-attach` returns. See [`core/error_handler.func`](../core/error_handler.func). -A host run that fails before its install record went out (storage, ID or -template checks) sends that record first, then the failure. Cancelled dialogs -and update runs inside a container send nothing. +Telemetry starts with the install record (`post_to_api`, `post_to_api_vm`), +which the build sends once the checks and dialogs are behind it. A run that +ends before that sends nothing: a host that is not ready is not a failed +install. Tools and add-ons have no such point, so a failure with a +"requirement not met" code (10, 103, 104, 106, 232, 233, 234, 236, 238) sends +nothing either. Update runs inside a container send nothing. + +The checks, the settings and the storage dialogs wait on `pveversion`, +`pvesm status` (all, `vztmpl`, `rootdir`), `pveam available` and `pvesh`. +`core/build.func` starts these in the background before it downloads the +engine, `variables` adds pve-container's verdict on the release, and the +readers take them through `_cs_probe_read` / `_cs_probe_rc` (`_pvesm_status` +for storage). `_offer_storage_content` and `_pveam_update` drop what they +change with `_cs_probe_drop`. Guest IDs are checked against the configs under +`/etc/pve/nodes`, which pmxcfs keeps for every node. ## What a failure carries (payload version 3) diff --git a/pve/backend.func b/pve/backend.func index 390579b..009326b 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -313,7 +313,11 @@ lxc_os_supported() { lxc_os_support_check() { local os="${var_os:-}" ver="${var_version:-}" rc=0 [[ -n "$os" && -n "$ver" ]] || return 0 - lxc_os_supported "$os" "$ver" || rc=$? + # variables() asked in the background; a release changed since is asked now. + rc="$(_cs_probe_rc "lxcos-${os}-${ver}")" || { + rc=0 + lxc_os_supported "$os" "$ver" || rc=$? + } [[ $rc -eq 1 ]] || return 0 local installed candidate="" older="" v @@ -2084,13 +2088,13 @@ resolve_storage_preselect() { *) return 1 ;; esac [[ -z "$preselect" ]] && return 1 - if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then + if ! _pvesm_status "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)" return 1 fi local line total used free - line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')" + line="$(_pvesm_status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')" if [[ -z "$line" ]]; then STORAGE_INFO="n/a" else @@ -2172,13 +2176,24 @@ fix_gpu_gids() { fi } +# _pvesm_status [content] - `pvesm status [-content ]`, from the probe +# core/build.func started at load (all, vztmpl, rootdir) while it holds. +# _offer_storage_content drops the probes when it changes a storage. +_pvesm_status() { + if [[ -n "${1:-}" ]]; then + _cs_probe_read "$1" || pvesm status -content "$1" + else + _cs_probe_read pvesm || pvesm status + fi +} + check_storage_support() { local CONTENT="$1" VALID=0 while IFS= read -r line; do local STORAGE_NAME STORAGE_NAME=$(awk '{print $1}' <<<"$line") [[ -n "$STORAGE_NAME" ]] && VALID=1 - done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') + done < <(_pvesm_status "$CONTENT" 2>/dev/null | awk 'NR>1') [[ $VALID -eq 1 ]] } @@ -2228,7 +2243,8 @@ _offer_storage_content() { current="$(_storage_content_of "$choice")" [[ "$current" == "none" ]] && current="" - if pvesm set "$choice" --content "${current:+${current},}${content}" >/dev/null 2>&1 && check_storage_support "$content"; then + if pvesm set "$choice" --content "${current:+${current},}${content}" >/dev/null 2>&1 && + _cs_probe_drop "$content" pvesm && check_storage_support "$content"; then msg_ok "Enabled ${label} on storage '${choice}'" return 0 fi @@ -2295,7 +2311,7 @@ select_storage() { STORAGE_MAP["$DISPLAY"]="$TAG" MENU+=("$DISPLAY" "$INFO" "OFF") ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} - done < <(pvesm status -content "$CONTENT" | awk 'NR>1') + done < <(_pvesm_status "$CONTENT" | awk 'NR>1') if [[ ${#MENU[@]} -eq 0 ]]; then msg_error "No storage found for content type '$CONTENT'." @@ -2439,7 +2455,7 @@ validate_storage_space() { # Get full storage line from pvesm status local storage_line - storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') + storage_line=$(_pvesm_status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') if [[ -z "$storage_line" ]]; then [[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' not found! @@ -2495,7 +2511,7 @@ check_storage_health() { [[ -z "$storage" ]] && return 0 local storage_line storage_type total_kb avail_kb used_pct free_gb_fmt - storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') + storage_line=$(_pvesm_status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') [[ -z "$storage_line" ]] && return 0 storage_type=$(awk '{print $2}' <<<"$storage_line") @@ -3069,7 +3085,7 @@ create_lxc_container() { msg_error "ID cannot be less than 100." exit 205 } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then + if _vmid_config_exists "$CTID"; then msg_warn "Container/VM ID $CTID is already in use (detected late). Reassigning..." CTID=$(get_valid_container_id "$((CTID + 1))") export CTID @@ -3167,7 +3183,7 @@ create_lxc_container() { ;; esac - if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then + if ! _pvesm_status rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support 'rootdir' content." msg_custom "💡" "${YW}" "Enable 'Disk image' (rootdir) for storage '${CONTAINER_STORAGE}' in:" msg_custom " " "${YW}" "Datacenter → Storage → ${CONTAINER_STORAGE} → Edit → Content" @@ -3183,7 +3199,7 @@ create_lxc_container() { msg_info "Validating template storage '$TEMPLATE_STORAGE'" TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 || true) - if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then + if ! _pvesm_status vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'" fi msg_ok "Template storage '$TEMPLATE_STORAGE' validated${TEMPLATE_STORAGE_INFO:+ - ${TEMPLATE_STORAGE_INFO}}" @@ -3969,6 +3985,10 @@ ensure_log_on_host() { # Try pulling from container and creating combined log if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]] && command -v pct &>/dev/null; then + # A run that failed before pct create has no container to pull from, and + # each pull below waited up to 8 s to find that out. + local has_ct=0 + [[ -f "/etc/pve/lxc/${CTID}.conf" ]] && has_ct=1 local host_combined="" if [[ -n "${combined_log:-}" && -s "${combined_log}" ]]; then host_combined="$combined_log" @@ -4000,7 +4020,7 @@ ensure_log_on_host() { fi # Pull INSTALL_LOG from container (with timeout to prevent hangs on dead containers) local temp_log="/tmp/.install-temp-${SESSION_ID}.log" - if timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then + if ((has_ct)) && timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then { echo "================================================================================" echo "PHASE 2: APPLICATION INSTALLATION (Container)" @@ -4014,7 +4034,7 @@ ensure_log_on_host() { # Also pull the structured error capture for the telemetry error trace if [[ -z "${TELEMETRY_ERRINFO:-}" || ! -s "${TELEMETRY_ERRINFO:-}" ]]; then local host_errinfo="/tmp/.errinfo-${SESSION_ID}" - if timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log.errinfo" "$host_errinfo" 2>/dev/null && [[ -s "$host_errinfo" ]]; then + if ((has_ct)) && timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log.errinfo" "$host_errinfo" 2>/dev/null && [[ -s "$host_errinfo" ]]; then TELEMETRY_ERRINFO="$host_errinfo" export TELEMETRY_ERRINFO fi diff --git a/pve/vm-core.func b/pve/vm-core.func index 011af99..9041aea 100644 --- a/pve/vm-core.func +++ b/pve/vm-core.func @@ -787,12 +787,12 @@ cleanup() { if [[ -n "${TEMP_DIR:-}" && -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" fi - # The final status nobody sent yet; post_update_to_api decides whether an early failure counts. + # The final status nobody sent yet; post_update_to_api drops it when the run never reached post_to_api_vm. if [[ "${POST_UPDATE_DONE:-}" != "true" ]] && declare -f post_update_to_api >/dev/null 2>&1; then if [[ $exit_code -ne 0 ]]; then post_update_to_api "failed" "$exit_code" elif [[ -n "${POST_TO_API_DONE:-}" ]]; then - if [[ -n "${VMID:-}" ]] && qm config "$VMID" &>/dev/null; then + if [[ -n "${VMID:-}" && -f "/etc/pve/qemu-server/${VMID}.conf" ]]; then # Clean exit with the VM in place: a script that never said so itself. post_update_to_api "done" "none" else @@ -816,6 +816,8 @@ check_root() { pve_check() { local pve_ver pve_ver="$(declare -f _pve_version >/dev/null 2>&1 && _pve_version || pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" + # Telemetry builds each payload in a subshell and would start pveversion again. + PVEVERSION="${PVEVERSION:-$pve_ver}" if [[ "$pve_ver" =~ ^8\.([0-9]+) ]]; then local minor="${BASH_REMATCH[1]}" diff --git a/tests/host-probes.sh b/tests/host-probes.sh new file mode 100644 index 0000000..14f5ba3 --- /dev/null +++ b/tests/host-probes.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# The checks, settings and storage dialogs read pveversion, pvesm, pveam and +# pvesh output that core/build.func collects in the background while the engine +# loads. A reader must wait for an unfinished probe, and fall back to the real +# command when a probe failed, never ran or was dropped after a change. +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^_cs_probe(_run)?[(][)] [{]/,/^}/' "$CORE_DIR/core/build.func")" +eval "$(awk '/^_cs_probe_(read|drop|rc)[(][)] [{]/,/^}/' "$CORE_DIR/core/core.func")" +eval "$(awk '/^(_pvesm_status|check_storage_support)[(][)] [{]/,/^}/' "$CORE_DIR/pve/backend.func")" + +CS_PROBE_DIR="$(mktemp -d)" +trap 'rm -rf -- "$CS_PROBE_DIR"' EXIT +passed=0 failed=0 + +expect() { + if [[ "$2" == "$3" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} + +# Stands in for the live command, so a fallback is visible. +pvesm() { printf 'Name Type Status Total Used Available %%\nlive dir active 1 1 1 1%%\n'; } + +_cs_probe "$CS_PROBE_DIR" pveversion sh -c 'sleep 0.4; echo pve-manager/9.1.4/5ac30304265fbd8e' +_cs_probe "$CS_PROBE_DIR" broken sh -c 'echo partial; exit 2' +_cs_probe "$CS_PROBE_DIR" osfail sh -c 'exit 1' +_cs_probe "$CS_PROBE_DIR" rootdir printf 'Name Type Status Total Used Available %%\nlocal-lvm lvmthin active 1 1 1 1%%\n' +_cs_probe "$CS_PROBE_DIR" vztmpl printf 'Name Type Status Total Used Available %%\n' + +expect "a reader waits for a probe still running" "pve-manager/9.1.4/5ac30304265fbd8e" "$(_cs_probe_read pveversion)" +expect "a failed probe gives nothing" "fail" "$(_cs_probe_read broken >/dev/null && echo ok || echo fail)" +expect "a check probe hands over its exit code" "1" "$(_cs_probe_rc osfail)" +expect "a probe never started gives nothing" "fail" "$(_cs_probe_read nextid >/dev/null && echo ok || echo fail)" +expect "pvesm status for a content comes from its probe" "local-lvm" "$(_pvesm_status rootdir | awk 'NR == 2 {print $1}')" +expect "a storage listed by the probe counts" yes "$(check_storage_support rootdir && echo yes || echo no)" +expect "a probe header without storages does not" no "$(check_storage_support vztmpl && echo yes || echo no)" +expect "without a probe for it pvesm runs live" "live" "$(_pvesm_status | awk 'NR == 2 {print $1}')" + +_cs_probe_drop rootdir +expect "a dropped probe is asked live" "live" "$(_pvesm_status rootdir | awk 'NR == 2 {print $1}')" + +saved="$CS_PROBE_DIR" +CS_PROBE_DIR="" +expect "without a probe dir the caller runs the command" "fail" "$(_cs_probe_read pveversion >/dev/null && echo ok || echo fail)" +CS_PROBE_DIR="$saved" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0)) diff --git a/tests/telemetry-trace.sh b/tests/telemetry-trace.sh index ff78b8b..4a0739a 100644 --- a/tests/telemetry-trace.sh +++ b/tests/telemetry-trace.sh @@ -117,7 +117,12 @@ unset POST_TO_API_DONE : >"$TEST_DIR/statuses" CS_ERROR_MSG="Not enough space on 'local'. Required: 500GB, Available: 23.2GB" post_update_to_api failed 214 -expect "an early host failure is reported with its install record" "installing failed" "$(paste -sd' ' "$TEST_DIR/statuses")" +expect "a failed check sends nothing" "" "$(paste -sd' ' "$TEST_DIR/statuses")" +reset +POST_TO_API_DONE=false +: >"$TEST_DIR/statuses" +post_update_to_api failed 214 +expect "a failure after an install record that did not go out still does" failed "$(paste -sd' ' "$TEST_DIR/statuses")" reset unset POST_TO_API_DONE : >"$TEST_DIR/statuses" @@ -133,11 +138,18 @@ expect "message with quotes and backslashes round-trips" 'quote " backslash \ ta # --- tools and add-ons carry the same fields --- reset -CS_ERROR_MSG="No LXC containers found" CS_STEP="Scanning containers" -post_tool_to_api demo-tool failed 234 +CS_ERROR_MSG="Backup of CT 105 failed" CS_STEP="Backing up containers" +post_tool_to_api demo-tool failed 235 expect "tool failures stay failures" failed "$(json_field "$SENT" status)" expect "tool type" pve "$(json_field "$SENT" type)" -expect "tool message" "No LXC containers found" "$(json_field "$SENT" error_message)" +expect "tool message" "Backup of CT 105 failed" "$(json_field "$SENT" error_message)" +reset +CS_ERROR_MSG="No LXC containers found" +post_tool_to_api demo-tool failed 234 +expect "a tool without anything to work on sends nothing" no "$([[ -f "$SENT" ]] && echo yes || echo no)" +reset +post_addon_to_api demo-addon failed 10 +expect "an add-on whose prerequisite is missing sends nothing" no "$([[ -f "$SENT" ]] && echo yes || echo no)" printf '%s passed, %s failed\n' "$passed" "$failed" ((failed == 0)) diff --git a/ui/API.txt b/ui/API.txt index 7e91610..8514e9e 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -10,6 +10,10 @@ _cs_host_arch _cs_host_version_line _cs_live_base _cs_os_family +_cs_probe_drop +_cs_probe_rc +_cs_probe_read +_cs_pveversion_out _cs_reattach_output _cs_ref_line _cs_run_os_section @@ -73,9 +77,11 @@ _tm_post_progress _tm_post_run _tm_post_update _tm_quiet +_tm_requirement_code _tm_send _tm_step_output _vars_file_value +_vmid_config_exists _write_storage_to_vars advanced_settings apt_conffile_guard diff --git a/ui/advanced.func b/ui/advanced.func index f8c31ee..080ba4b 100644 --- a/ui/advanced.func +++ b/ui/advanced.func @@ -43,7 +43,8 @@ advanced_settings() { local _ct_type="${var_unprivileged:-1}" local _pw="" local _pw_display="Automatic Login" - local _ct_id="$NEXTID" + # base_settings ran first and checked it; NEXTID can be taken by a leftover disk. + local _ct_id="${CT_ID:-$NEXTID}" local _hostname="$NSAPP" local _disk_size="${var_disk:-4}" local _core_count="${var_cpu:-1}" diff --git a/ui/build-ui.func b/ui/build-ui.func index 9cb54cd..beae560 100644 --- a/ui/build-ui.func +++ b/ui/build-ui.func @@ -90,14 +90,20 @@ variables() { combined_log="/var/log/community-scripts/install-${SESSION_ID}-combined-$(date +%Y%m%d_%H%M%S).log" fi - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION="$(_pve_version)" - else - PVEVERSION="N/A" - fi + # On a PVE host the version is read where it is shown (_pve_version, cached): + # asking here held the header for the whole pveversion start. + command -v pveversion >/dev/null 2>&1 || PVEVERSION="N/A" KERNEL_VERSION=$(uname -r) + # pve-container's verdict on the release is a Perl start too; the script set + # the release before calling this, and lxc_os_support_check reads the answer. + case "${var_os:-}" in + debian | ubuntu) + [[ -n "${CS_PROBE_DIR:-}" && -n "${var_version:-}" ]] && declare -f lxc_os_supported >/dev/null 2>&1 && + _cs_probe "$CS_PROBE_DIR" "lxcos-${var_os}-${var_version}" lxc_os_supported "$var_os" "$var_version" + ;; + esac + # Capture app-declared defaults (for precedence logic) # These values are set by the app script BEFORE default.vars is loaded # If app declares higher values than default.vars, app values take precedence diff --git a/ui/defaults.func b/ui/defaults.func index d379651..d7ffc7b 100644 --- a/ui/defaults.func +++ b/ui/defaults.func @@ -172,7 +172,7 @@ resolve_os_version() { ') [[ -n "$candidate" ]] || return 0 - msg_warn "${ostype^} ${want} is not in this host's template catalog (PVE ${PVEVERSION:-unknown})" + msg_warn "${ostype^} ${want} is not in this host's template catalog (PVE ${PVEVERSION:-$(_pve_version)})" if [[ "${PHS_SILENT:-0}" != "1" ]] && command -v whiptail &>/dev/null && [ -t 0 ] && [[ "$TERM" != "dumb" ]]; then local -a menu=() diff --git a/ui/menu.func b/ui/menu.func index d33eddd..8463747 100644 --- a/ui/menu.func +++ b/ui/menu.func @@ -189,8 +189,10 @@ install_script() { # pvesh spawns a Perl client and waits on pvedaemon — the slowest thing here # by far, and it needs nothing the checks below produce. Start it now and # collect it when it is actually needed, so it runs while they do. + # core/build.func usually started it at load already (_cs_start_host_probes). local _nextid_file="" _nextid_pid="" - if ! declare -f get_next_container_identifier &>/dev/null && command -v pvesh &>/dev/null; then + if ! declare -f get_next_container_identifier &>/dev/null && [[ ! -f "${CS_PROBE_DIR:-/nonexistent}/nextid.out" ]] && + command -v pvesh &>/dev/null; then _nextid_file="$(mktemp 2>/dev/null)" || _nextid_file="" if [[ -n "$_nextid_file" ]]; then pvesh get /cluster/nextid >"$_nextid_file" 2>/dev/null & @@ -198,9 +200,11 @@ install_script() { fi fi - pve_check + # Root first: for any other user /etc/pve/storage.cfg is unreadable, and the + # cluster check in pve_check took that for a damaged cluster. shell_check root_check + pve_check ensure_whiptail arch_check arm64_notice @@ -215,6 +219,8 @@ install_script() { if declare -f get_next_container_identifier &>/dev/null; then NEXTID="$(get_next_container_identifier)" + elif NEXTID="$(_cs_probe_read nextid | tr -d '[:space:]')" && [[ -n "$NEXTID" ]]; then + : elif [[ -n "$_nextid_pid" ]]; then wait "$_nextid_pid" 2>/dev/null || true NEXTID="$(tr -d '[:space:]' <"$_nextid_file" 2>/dev/null)" diff --git a/ui/validate.func b/ui/validate.func index b2605a3..53db13a 100644 --- a/ui/validate.func +++ b/ui/validate.func @@ -182,6 +182,16 @@ install_ssh_keys_into_ct() { return 0 } +# _vmid_config_exists - a VM or container config for the ID on any node +_vmid_config_exists() { + local f + for f in /etc/pve/nodes/*/qemu-server/"$1".conf /etc/pve/nodes/*/lxc/"$1".conf \ + /etc/pve/qemu-server/"$1".conf /etc/pve/lxc/"$1".conf; do + [[ -f "$f" ]] && return 0 + done + return 1 +} + # ------------------------------------------------------------------------------ # validate_container_id() # @@ -198,11 +208,13 @@ validate_container_id() { return 1 fi - # Ask the cluster first: /etc/pve/qemu-server and /etc/pve/lxc are symlinks - # into this node's own directory, so checking them alone only sees local - # guests. On a cluster the wizard then hands out an ID already in use - # elsewhere and pct create fails long after the choice was made. - if command -v pvesh &>/dev/null; then + # /etc/pve/qemu-server and /etc/pve/lxc only point into this node's own + # directory; /etc/pve/nodes holds every node's guests. pvesh's + # /cluster/resources says the same in about a second, and the ID search + # asks once per ID it tries, so it is only the fallback without pmxcfs. + if [[ -d "/etc/pve/nodes" ]]; then + _vmid_config_exists "$ctid" && return 1 + elif command -v pvesh &>/dev/null; then local cluster_ids cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true) @@ -211,22 +223,6 @@ validate_container_id() { fi fi - # Local config, for when pvesh is unavailable or answered with nothing. - if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then - return 1 - fi - - # Every node's config directly, which also covers the window where pmxcfs has - # the file but /cluster/resources has not caught up. - if [[ -d "/etc/pve/nodes" ]]; then - local node_dir - for node_dir in /etc/pve/nodes/*/; do - if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then - return 1 - fi - done - fi - # Check if ID is used in LVM logical volumes if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then return 1