From f87b77b6758a5b0189a5ffaffb045710954ef54e Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:13:49 +0200 Subject: [PATCH 1/6] Drive a remote Incus server from the client's default remote Every call in the backend already uses bare names, so a switched default remote reaches another server; what still read this machine was the architecture, the keyctl probe and the df fallback for pool space. The target is announced when it is not local. This is the way in for IncusOS. --- .github/workflows/verify-split-files.yml | 5 +++ docs/backends.md | 16 ++++++++ incus/backend.func | 7 ++-- incus/core.func | 17 +++++++++ tests/incus-target.sh | 47 ++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/incus-target.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index c2f8727..30695e9 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -36,6 +36,7 @@ on: - "tests/exit-code-hints.sh" - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" + - "tests/incus-target.sh" - ".github/workflows/verify-split-files.yml" push: branches: [main] @@ -63,6 +64,7 @@ on: - "tests/exit-code-hints.sh" - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" + - "tests/incus-target.sh" - ".github/workflows/verify-split-files.yml" workflow_dispatch: @@ -126,6 +128,9 @@ jobs: - name: Install cleanup and uv trap tests run: bash tests/install-cleanup.sh + - name: Incus target tests + run: bash tests/incus-target.sh + - name: Prefetch lists cover every engine file run: | set -euo pipefail diff --git a/docs/backends.md b/docs/backends.md index ca47423..48df071 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -71,6 +71,22 @@ from `/etc/localtime`, or from `Etc/UTC`. [`core/core.func`](../core/core.func) renders "Incus Version 6.x" on an Incus host and "PVE Version …" on Proxmox VE, from the same `PVEVERSION` value. +### Driving a remote server + +The `incus` client always talks to a remote; `local:` is only the default. +Every call in this backend uses bare names, so switching the default remote +points the whole flow at another server: + + incus remote add incusos https://:8443 --token + incus remote switch incusos + bash -c "$(curl -fsSL .../ct/.sh)" + +The machine running the script needs bash, curl and the client. The backend +takes the server's architecture from `incus info` and names the target when +it is not `local`. This is the way in for IncusOS, which has no shell of its +own. Pool free space comes from `incus storage info`; the local `df` fallback +is skipped for a remote. + ## VM scripts on the two backends `ct/` scripts are shared; `vm/` scripts are not, and the reason is worth diff --git a/incus/backend.func b/incus/backend.func index 896344d..daf8efb 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -215,7 +215,7 @@ incus_storage_pool_free_gb() { if [[ -z "${free_bytes:-}" ]]; then local src src="$(LC_ALL=C incus storage get "$pool" source 2>/dev/null || true)" - if [[ -n "$src" && -d "$src" ]]; then + if [[ "${INCUS_REMOTE:-local}" == "local" && -n "$src" && -d "$src" ]]; then free_bytes="$(df -PB1 "$src" 2>/dev/null | awk 'NR==2 {print $4}')" fi fi @@ -471,8 +471,9 @@ incus_create_lxc_container() { # plus a rollback to reach the state it would have had anyway. Privileged # only, and only when the host default is actually below what we want. local optional_sysctl_pending=0 + # The sysctl probe reads this machine; against a remote, just try the setting. if [[ "${CT_TYPE:-1}" == "0" ]] && - (($(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 2000) < 2000)); then + { [[ "${INCUS_REMOTE:-local}" != "local" ]] || (($(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 2000) < 2000)); }; then if incus config set "${CT_NAME}" linux.sysctl.kernel.keys.maxkeys=2000 >>"$LOGFILE" 2>&1; then optional_sysctl_pending=1 fi @@ -974,7 +975,7 @@ incus_set_root_password() { incus_customize_container() { msg_info "Customizing LXC Container" - ARCH="$(dpkg --print-architecture 2>/dev/null || uname -m)" + ARCH="${INCUS_SERVER_ARCH:-$(dpkg --print-architecture 2>/dev/null || uname -m)}" if [[ "${var_os:-debian}" == "alpine" ]]; then sleep 2 diff --git a/incus/core.func b/incus/core.func index da0a6d2..636d8e0 100644 --- a/incus/core.func +++ b/incus/core.func @@ -233,6 +233,21 @@ incus_root_check() { # - Checks incus daemon is running # - Exits with error message if Incus is not available # ------------------------------------------------------------------------------ +# The client's default remote is where every bare name in this backend goes; +# IncusOS has no shell, so a switched remote is the only way to drive it. +incus_target() { + INCUS_REMOTE="$(LC_ALL=C incus remote get-default 2>/dev/null || echo local)" + INCUS_SERVER_ARCH="$(LC_ALL=C incus info 2>/dev/null | awk '/^environment:/ {e=1} e && /^ architectures:/ {a=1; next} a && /^ - / {print $2; exit}')" + case "$INCUS_SERVER_ARCH" in + x86_64) INCUS_SERVER_ARCH="amd64" ;; + aarch64) INCUS_SERVER_ARCH="arm64" ;; + esac + export INCUS_REMOTE INCUS_SERVER_ARCH + if [[ "$INCUS_REMOTE" != "local" ]]; then + incus_msg_custom "ℹ️" "${YW:-}" "Target: Incus remote '${INCUS_REMOTE}' (${INCUS_SERVER_ARCH:-unknown architecture})" + fi +} + incus_check() { if ! command -v incus &>/dev/null; then incus_msg_error "Incus is not installed on this system." @@ -281,6 +296,8 @@ incus_check() { incus_msg_error "Create one with: incus storage create default dir" exit 1 fi + + incus_target } # ============================================================================== diff --git a/tests/incus-target.sh b/tests/incus-target.sh new file mode 100644 index 0000000..1a36b5c --- /dev/null +++ b/tests/incus-target.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^incus_target[(][)] [{]/,/^}/' "$CORE_DIR/incus/core.func")" + +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +LOG="$TEST_DIR/log" +passed=0 failed=0 + +incus_msg_custom() { echo "$3" >>"$LOG"; } +# The client answers for its default remote; `incus info` is the server's YAML. +incus() { + case "$1 ${2:-}" in + "remote get-default") echo "${MOCK_REMOTE:-local}" ;; + "info ") printf 'config: {}\nenvironment:\n addresses: []\n architectures:\n - %s\n - i686\n server_name: box\n' "${MOCK_ARCH:-x86_64}" ;; + esac +} + +expect() { + if [[ "$2" == "$3" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} + +: >"$LOG" +MOCK_REMOTE=incusos MOCK_ARCH=aarch64 incus_target +expect "the default remote is the target" incusos "$INCUS_REMOTE" +expect "the server architecture is read, in Debian naming" arm64 "$INCUS_SERVER_ARCH" +expect "a remote target is announced" "Target: Incus remote 'incusos' (arm64)" "$(cat "$LOG")" + +: >"$LOG" +MOCK_REMOTE=local MOCK_ARCH=x86_64 incus_target +expect "local stays local" local "$INCUS_REMOTE" +expect "x86_64 becomes amd64" amd64 "$INCUS_SERVER_ARCH" +expect "local is not announced" "" "$(cat "$LOG")" + +MOCK_REMOTE=lab MOCK_ARCH=riscv64 incus_target +expect "an unmapped architecture is passed through" riscv64 "$INCUS_SERVER_ARCH" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0)) From 854f1662523d133b6eefbd8330141afbf48b1f81 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:20:07 +0200 Subject: [PATCH 2/6] Incus remote: read the server architecture from kernel_architecture, skip the local keyctl preflight The architectures list is indented differently across server versions, so the scalar is the reliable field. The kernel key limits belong to the server; a client driving a remote cannot read them (tester feedback on #154). --- host/preflight.func | 7 +++++++ incus/core.func | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/host/preflight.func b/host/preflight.func index 28a8c22..b37a79f 100644 --- a/host/preflight.func +++ b/host/preflight.func @@ -81,6 +81,13 @@ preflight_warn() { # - https://docs.kernel.org/security/keys/core.html # ------------------------------------------------------------------------------ preflight_maxkeys() { + # The limits are the server's; a client driving a remote Incus cannot read them. + if ! command -v pveversion >/dev/null 2>&1 && command -v incus >/dev/null 2>&1 && + [[ "$(LC_ALL=C incus remote get-default 2>/dev/null || echo local)" != "local" ]]; then + preflight_pass "Kernel key limits left to the remote Incus server" + return 0 + fi + local per_user_maxkeys per_user_maxbytes per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0) diff --git a/incus/core.func b/incus/core.func index 636d8e0..13819d5 100644 --- a/incus/core.func +++ b/incus/core.func @@ -237,7 +237,9 @@ incus_root_check() { # IncusOS has no shell, so a switched remote is the only way to drive it. incus_target() { INCUS_REMOTE="$(LC_ALL=C incus remote get-default 2>/dev/null || echo local)" - INCUS_SERVER_ARCH="$(LC_ALL=C incus info 2>/dev/null | awk '/^environment:/ {e=1} e && /^ architectures:/ {a=1; next} a && /^ - / {print $2; exit}')" + # kernel_architecture is a scalar; the architectures list indents by version. + INCUS_SERVER_ARCH="$(LC_ALL=C incus info 2>/dev/null | awk '$1 == "kernel_architecture:" {print $2; exit}')" + [[ -n "$INCUS_SERVER_ARCH" ]] || INCUS_SERVER_ARCH="$(LC_ALL=C incus info 2>/dev/null | awk '$1 == "architectures:" {a=1; next} a && $1 == "-" {print $2; exit}')" case "$INCUS_SERVER_ARCH" in x86_64) INCUS_SERVER_ARCH="amd64" ;; aarch64) INCUS_SERVER_ARCH="arm64" ;; From 46f7c3fb0942cc84aa0525021d5dea9798a06621 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:20:35 +0200 Subject: [PATCH 3/6] Cover the four-space architectures list and kernel_architecture in the target test --- tests/incus-target.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/incus-target.sh b/tests/incus-target.sh index 1a36b5c..96644c4 100644 --- a/tests/incus-target.sh +++ b/tests/incus-target.sh @@ -14,7 +14,14 @@ incus_msg_custom() { echo "$3" >>"$LOG"; } incus() { case "$1 ${2:-}" in "remote get-default") echo "${MOCK_REMOTE:-local}" ;; - "info ") printf 'config: {}\nenvironment:\n addresses: []\n architectures:\n - %s\n - i686\n server_name: box\n' "${MOCK_ARCH:-x86_64}" ;; + "info ") + # Newer servers indent the list by four; kernel_architecture is the scalar we prefer. + if [[ -n "${MOCK_KERNEL_ARCH:-}" ]]; then + printf 'config: {}\nenvironment:\n addresses: []\n architectures:\n - %s\n - i686\n kernel_architecture: %s\n server_name: box\n' "${MOCK_ARCH:-x86_64}" "$MOCK_KERNEL_ARCH" + else + printf 'config: {}\nenvironment:\n addresses: []\n architectures:\n - %s\n - i686\n server_name: box\n' "${MOCK_ARCH:-x86_64}" + fi + ;; esac } @@ -43,5 +50,8 @@ expect "local is not announced" "" "$(cat "$LOG")" MOCK_REMOTE=lab MOCK_ARCH=riscv64 incus_target expect "an unmapped architecture is passed through" riscv64 "$INCUS_SERVER_ARCH" +MOCK_REMOTE=incus-001 MOCK_ARCH=i686 MOCK_KERNEL_ARCH=x86_64 incus_target +expect "kernel_architecture wins over the list" amd64 "$INCUS_SERVER_ARCH" + printf '%s passed, %s failed\n' "$passed" "$failed" ((failed == 0)) From f5031f3d58d44321dc8b399daed6dc39673bbc75 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:46:16 +0200 Subject: [PATCH 4/6] Incus remote: skip the keyring check in the wizard as well --- ui/build-ui.func | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/build-ui.func b/ui/build-ui.func index 017147c..4134b88 100644 --- a/ui/build-ui.func +++ b/ui/build-ui.func @@ -178,6 +178,12 @@ run_preflight() { # - Exits with dedicated error codes when limits cannot be read or are exceeded # ------------------------------------------------------------------------------ maxkeys_check() { + # The limits are the server's; a client driving a remote Incus cannot read them. + if ! command -v pveversion >/dev/null 2>&1 && command -v incus >/dev/null 2>&1 && + [[ "$(LC_ALL=C incus remote get-default 2>/dev/null || echo local)" != "local" ]]; then + return 0 + fi + local per_user_maxkeys per_user_maxbytes used_lxc_keys used_lxc_bytes local threshold_keys threshold_bytes new_limit_keys new_limit_bytes local failure=0 From 4b26e83ae20aa46f651d89045d30152ae2e6b561 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:36:42 +0200 Subject: [PATCH 5/6] Incus remote: survive a BusyBox client and read pool space in bytes The tester drives IncusOS from Alpine. dpkg does not exist there, so the architecture now comes from one helper that prefers the server; BusyBox sed has no -u, which killed the log tee with SIGPIPE; and the pool maths went through awk floats, which BusyBox truncates, so a 82 GiB pool read as 1 GB. incus storage info reports total and used only, and --bytes gives integers, so the subtraction stays in bash. The summary now names the server's kernel, not the client's. --- .github/workflows/verify-split-files.yml | 5 +++ core/core.func | 18 +++++++++-- docs/backends.md | 3 +- host/preflight.func | 4 +-- incus/backend.func | 15 ++++++--- incus/build.func | 4 ++- incus/core.func | 15 +++++---- tests/incus-storage-space.sh | 40 ++++++++++++++++++++++++ tests/incus-target.sh | 11 ++++--- ui/API.txt | 1 + ui/defaults.func | 6 ++-- ui/menu.func | 2 +- 12 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 tests/incus-storage-space.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index 30695e9..f443837 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -37,6 +37,7 @@ on: - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" - "tests/incus-target.sh" + - "tests/incus-storage-space.sh" - ".github/workflows/verify-split-files.yml" push: branches: [main] @@ -65,6 +66,7 @@ on: - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" - "tests/incus-target.sh" + - "tests/incus-storage-space.sh" - ".github/workflows/verify-split-files.yml" workflow_dispatch: @@ -131,6 +133,9 @@ jobs: - name: Incus target tests run: bash tests/incus-target.sh + - name: Incus storage space tests + run: bash tests/incus-storage-space.sh + - name: Prefetch lists cover every engine file run: | set -euo pipefail diff --git a/core/core.func b/core/core.func index 2b92e09..3a05c29 100644 --- a/core/core.func +++ b/core/core.func @@ -368,6 +368,18 @@ default_vars() { # - If VERBOSE=no: STD="silent" (suppress output via silent() wrapper) # - If DEV_MODE_TRACE=true: Enables bash tracing (set -x) # ------------------------------------------------------------------------------ +# Debian-style architecture of the machine that will run the container: the +# Incus server when driving a remote, otherwise this host. Needs no dpkg. +_cs_host_arch() { + local a="${INCUS_SERVER_ARCH:-}" + [[ -n "$a" ]] || a="$(dpkg --print-architecture 2>/dev/null || uname -m)" + case "$a" in + x86_64) a="amd64" ;; + aarch64) a="arm64" ;; + esac + printf '%s' "$a" +} + set_std_mode() { # Verbose shows the output live and still keeps it in the log; silent() reads VERBOSE. STD="silent" @@ -736,7 +748,7 @@ pmxcfs_check() { # ------------------------------------------------------------------------------ arch_check() { local arch - arch="$(dpkg --print-architecture 2>/dev/null || uname -m)" + arch="$(_cs_host_arch)" case "$arch" in x86_64) arch="amd64" ;; aarch64) arch="arm64" ;; @@ -2642,7 +2654,7 @@ ensure_whiptail() { # - Shows a short warning when running scripts on ARM64 systems # ------------------------------------------------------------------------------ arm64_notice() { - [[ "$(dpkg --print-architecture)" == "arm64" ]] || return 0 + [[ "$(_cs_host_arch)" == "arm64" ]] || return 0 whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "ARM64 SUPPORT" \ @@ -2675,7 +2687,7 @@ on_hup_keepalive() { arch_resolve() { local amd64_val="${1:-amd64}" local arm64_val="${2:-arm64}" - local arch="${PCT_ARCH:-$(dpkg --print-architecture 2>/dev/null || uname -m)}" + local arch="${PCT_ARCH:-$(_cs_host_arch)}" case "$arch" in amd64 | x86_64) echo "$amd64_val" ;; arm64 | aarch64) echo "$arm64_val" ;; diff --git a/docs/backends.md b/docs/backends.md index 48df071..79ccbbe 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -81,7 +81,8 @@ points the whole flow at another server: incus remote switch incusos bash -c "$(curl -fsSL .../ct/.sh)" -The machine running the script needs bash, curl and the client. The backend +The machine running the script needs bash, curl, newt (for whiptail) and the +client; a BusyBox userland such as Alpine is fine. The backend takes the server's architecture from `incus info` and names the target when it is not `local`. This is the way in for IncusOS, which has no shell of its own. Pool free space comes from `incus storage info`; the local `df` fallback diff --git a/host/preflight.func b/host/preflight.func index b37a79f..3c9aca3 100644 --- a/host/preflight.func +++ b/host/preflight.func @@ -402,7 +402,7 @@ preflight_container_id() { # ------------------------------------------------------------------------------ preflight_template_connectivity() { # ARM64 uses custom template sources (GitHub/jenkins), not the pveam catalog - if [[ "$(dpkg --print-architecture)" == "arm64" ]]; then + if [[ "$(_cs_host_arch)" == "arm64" ]]; then preflight_pass "Template connectivity check skipped (ARM64 uses custom template source)" return 0 fi @@ -442,7 +442,7 @@ preflight_template_connectivity() { # ------------------------------------------------------------------------------ preflight_template_available() { # ARM64 uses custom template sources (GitHub/jenkins), not the pveam catalog - if [[ "$(dpkg --print-architecture)" == "arm64" ]]; then + if [[ "$(_cs_host_arch)" == "arm64" ]]; then preflight_pass "Template check skipped (ARM64 uses custom template source)" return 0 fi diff --git a/incus/backend.func b/incus/backend.func index daf8efb..44ec6ea 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -187,10 +187,17 @@ incus_storage_pool_free_gb() { # LC_ALL=C: incus translates this output. A German host reports "Gesamter # Speicherplatz" and "Speicherplatz in Benutzung", so the awk below matched # nothing and a 357GB pool looked unmeasurable. + # Incus reports total and used only; with --bytes they are plain integers, so + # the subtraction stays in bash and never goes through awk floats (BusyBox awk + # truncates those to 32 bit). info=$(LC_ALL=C incus storage info --bytes "$pool" 2>/dev/null || true) free_raw=$(echo "$info" | awk -F': *' '/space free:/ {print $2; exit}') - if [[ -n "$free_raw" ]]; then - free_bytes="$(_incus_parse_size_to_bytes "$free_raw" 2>/dev/null || true)" + total_raw=$(echo "$info" | awk -F': *' '/total space:/ {print $2; exit}') + used_raw=$(echo "$info" | awk -F': *' '/space used:/ {print $2; exit}') + if [[ "$free_raw" =~ ^[0-9]+$ ]]; then + free_bytes="$free_raw" + elif [[ "$total_raw" =~ ^[0-9]+$ && "$used_raw" =~ ^[0-9]+$ && "$total_raw" -ge "$used_raw" ]]; then + free_bytes=$((total_raw - used_raw)) fi # Older/human output often has only "space used" + "total space" (no free line). @@ -975,7 +982,7 @@ incus_set_root_password() { incus_customize_container() { msg_info "Customizing LXC Container" - ARCH="${INCUS_SERVER_ARCH:-$(dpkg --print-architecture 2>/dev/null || uname -m)}" + ARCH="$(_cs_host_arch)" if [[ "${var_os:-debian}" == "alpine" ]]; then sleep 2 @@ -1094,7 +1101,7 @@ INNEREOF" >>"${LOGFILE:-$INCUS_BUILD_LOG}" 2>&1 || { msg_error "Failed to push f incus exec "${CT_NAME}" -- bash -c "${_run_env} ${_install_script} touch /root/.install-${SESSION_ID}.done" 2>&1 | - tee >(sed -u 's/.*\r//' >>"${LOGFILE:-$INCUS_BUILD_LOG}") + tee >(sed 's/.*\r//' >>"${LOGFILE:-$INCUS_BUILD_LOG}") # PIPESTATUS, not $?: the line above turns pipefail off, so $? is tee's # status - always 0. Every non-zero exit from the container was discarded # here, which is why a half-finished install reported success. diff --git a/incus/build.func b/incus/build.func index c8fa33d..30e93f8 100644 --- a/incus/build.func +++ b/incus/build.func @@ -93,7 +93,9 @@ incus_variables() { else PVEVERSION="Incus" fi - KERNEL_VERSION=$(uname -r) + # The summary names the machine that runs the container, which may be a remote. + declare -f incus_target >/dev/null 2>&1 && command -v incus >/dev/null 2>&1 && incus_target + KERNEL_VERSION="${INCUS_SERVER_KERNEL:-$(uname -r)}" if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then export APP_DEFAULT_CPU="${var_cpu}"; fi if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then export APP_DEFAULT_RAM="${var_ram}"; fi if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then export APP_DEFAULT_DISK="${var_disk}"; fi diff --git a/incus/core.func b/incus/core.func index 13819d5..9997431 100644 --- a/incus/core.func +++ b/incus/core.func @@ -237,17 +237,17 @@ incus_root_check() { # IncusOS has no shell, so a switched remote is the only way to drive it. incus_target() { INCUS_REMOTE="$(LC_ALL=C incus remote get-default 2>/dev/null || echo local)" + local info + info="$(LC_ALL=C incus info 2>/dev/null)" # kernel_architecture is a scalar; the architectures list indents by version. - INCUS_SERVER_ARCH="$(LC_ALL=C incus info 2>/dev/null | awk '$1 == "kernel_architecture:" {print $2; exit}')" - [[ -n "$INCUS_SERVER_ARCH" ]] || INCUS_SERVER_ARCH="$(LC_ALL=C incus info 2>/dev/null | awk '$1 == "architectures:" {a=1; next} a && $1 == "-" {print $2; exit}')" + INCUS_SERVER_ARCH="$(awk '$1 == "kernel_architecture:" {print $2; exit}' <<<"$info")" + [[ -n "$INCUS_SERVER_ARCH" ]] || INCUS_SERVER_ARCH="$(awk '$1 == "architectures:" {a=1; next} a && $1 == "-" {print $2; exit}' <<<"$info")" case "$INCUS_SERVER_ARCH" in x86_64) INCUS_SERVER_ARCH="amd64" ;; aarch64) INCUS_SERVER_ARCH="arm64" ;; esac - export INCUS_REMOTE INCUS_SERVER_ARCH - if [[ "$INCUS_REMOTE" != "local" ]]; then - incus_msg_custom "ℹ️" "${YW:-}" "Target: Incus remote '${INCUS_REMOTE}' (${INCUS_SERVER_ARCH:-unknown architecture})" - fi + INCUS_SERVER_KERNEL="$(awk '$1 == "kernel_version:" {print $2; exit}' <<<"$info")" + export INCUS_REMOTE INCUS_SERVER_ARCH INCUS_SERVER_KERNEL } incus_check() { @@ -300,6 +300,9 @@ incus_check() { fi incus_target + if [[ "$INCUS_REMOTE" != "local" ]]; then + incus_msg_custom "ℹ️" "${YW:-}" "Target: Incus remote '${INCUS_REMOTE}' (${INCUS_SERVER_ARCH:-unknown architecture})" + fi } # ============================================================================== diff --git a/tests/incus-storage-space.sh b/tests/incus-storage-space.sh new file mode 100644 index 0000000..dcd17ff --- /dev/null +++ b/tests/incus-storage-space.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^(_incus_parse_size_to_bytes|incus_storage_pool_free_gb)[(][)] [{]/,/^}/' "$CORE_DIR/incus/backend.func")" + +passed=0 failed=0 +export INCUS_REMOTE=incusos +# A ZFS pool as Incus reports it: total and used only, integers with --bytes. +incus() { + case "$*" in + "storage info --bytes local") + [[ "${MOCK_BYTES:-yes}" == "yes" ]] && printf 'info:\n driver: zfs\n name: local\n space used: 238747648\n total space: 88900000000\nused by: {}\n' + ;; + "storage info local") + printf 'info:\n driver: zfs\n name: local\n space used: 227.68MiB\n total space: 82.80GiB\nused by: {}\n' + ;; + "storage get local source") echo "/var/lib/incus/storage-pools/local" ;; + esac +} + +expect() { + if [[ "$2" == "$3" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} + +expect "free space comes from the byte counts" 82 "$(incus_storage_pool_free_gb local)" +expect "without --bytes the human sizes still work" 82 "$(MOCK_BYTES=no incus_storage_pool_free_gb local)" +expect "GiB parses" 88905823027 "$(_incus_parse_size_to_bytes 82.80GiB)" +expect "MiB parses" 238739783 "$(_incus_parse_size_to_bytes 227.68MiB)" +expect "a plain number is bytes" 4096 "$(_incus_parse_size_to_bytes 4096)" +expect "nonsense is rejected" 1 "$(_incus_parse_size_to_bytes bogus >/dev/null 2>&1; echo $?)" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0)) diff --git a/tests/incus-target.sh b/tests/incus-target.sh index 96644c4..614a9b4 100644 --- a/tests/incus-target.sh +++ b/tests/incus-target.sh @@ -3,6 +3,7 @@ set -uo pipefail CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" eval "$(awk '/^incus_target[(][)] [{]/,/^}/' "$CORE_DIR/incus/core.func")" +eval "$(awk '/^_cs_host_arch[(][)] [{]/,/^}/' "$CORE_DIR/core/core.func")" TEST_DIR="$(mktemp -d)" trap 'rm -rf -- "$TEST_DIR"' EXIT @@ -17,9 +18,9 @@ incus() { "info ") # Newer servers indent the list by four; kernel_architecture is the scalar we prefer. if [[ -n "${MOCK_KERNEL_ARCH:-}" ]]; then - printf 'config: {}\nenvironment:\n addresses: []\n architectures:\n - %s\n - i686\n kernel_architecture: %s\n server_name: box\n' "${MOCK_ARCH:-x86_64}" "$MOCK_KERNEL_ARCH" + printf 'config: {}\nenvironment:\n addresses: []\n architectures:\n - %s\n - i686\n kernel_architecture: %s\n kernel_version: 6.12.0-incusos\n server_name: box\n' "${MOCK_ARCH:-x86_64}" "$MOCK_KERNEL_ARCH" else - printf 'config: {}\nenvironment:\n addresses: []\n architectures:\n - %s\n - i686\n server_name: box\n' "${MOCK_ARCH:-x86_64}" + printf 'config: {}\nenvironment:\n addresses: []\n architectures:\n - %s\n - i686\n kernel_version: 6.12.0-incusos\n server_name: box\n' "${MOCK_ARCH:-x86_64}" fi ;; esac @@ -39,13 +40,15 @@ expect() { MOCK_REMOTE=incusos MOCK_ARCH=aarch64 incus_target expect "the default remote is the target" incusos "$INCUS_REMOTE" expect "the server architecture is read, in Debian naming" arm64 "$INCUS_SERVER_ARCH" -expect "a remote target is announced" "Target: Incus remote 'incusos' (arm64)" "$(cat "$LOG")" +expect "the server kernel is read" 6.12.0-incusos "$INCUS_SERVER_KERNEL" +expect "incus_target itself prints nothing" "" "$(cat "$LOG")" : >"$LOG" MOCK_REMOTE=local MOCK_ARCH=x86_64 incus_target expect "local stays local" local "$INCUS_REMOTE" expect "x86_64 becomes amd64" amd64 "$INCUS_SERVER_ARCH" -expect "local is not announced" "" "$(cat "$LOG")" +expect "_cs_host_arch follows the server" arm64 "$(INCUS_SERVER_ARCH=aarch64 _cs_host_arch)" +expect "_cs_host_arch maps this machine without dpkg" amd64 "$(INCUS_SERVER_ARCH="" _cs_host_arch)" MOCK_REMOTE=lab MOCK_ARCH=riscv64 incus_target expect "an unmapped architecture is passed through" riscv64 "$INCUS_SERVER_ARCH" diff --git a/ui/API.txt b/ui/API.txt index 795bb83..eb7aaf1 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -4,6 +4,7 @@ _container_write_failure _cs_clear _cs_conffile_list _cs_engine_ref_line +_cs_host_arch _cs_host_version_line _cs_live_base _cs_os_family diff --git a/ui/defaults.func b/ui/defaults.func index 7039615..c5a6b63 100644 --- a/ui/defaults.func +++ b/ui/defaults.func @@ -92,7 +92,7 @@ _list_os_versions() { # Templates are per-architecture. Without this the catalog offers amd64 # images on an arm64 host, which fail only after the download. local arch - arch="$(dpkg --print-architecture 2>/dev/null || echo amd64)" + arch="$(_cs_host_arch)" _pveam_available | grep -E "_${arch}\.(tar\.zst|tar\.xz|tar\.gz)([[:space:]]|$)" | awk '{print $2}' | @@ -109,7 +109,7 @@ _list_templates() { local search="${1:-}" pattern="${2:-}" [[ -n "$search" ]] || return 0 local arch - arch="$(dpkg --print-architecture 2>/dev/null || echo amd64)" + arch="$(_cs_host_arch)" _pveam_available | grep -E "_${arch}\.(tar\.zst|tar\.xz|tar\.gz)([[:space:]]|$)" | awk '{print $2}' | @@ -128,7 +128,7 @@ resolve_os_version() { [[ -n "$ostype" && -n "$want" ]] || return 0 # ARM64 pulls from linuxcontainers.org, the pveam catalog says nothing there - [[ "$(dpkg --print-architecture 2>/dev/null)" == "arm64" ]] && return 0 + [[ "$(_cs_host_arch)" == "arm64" ]] && return 0 local -a versions=() mapfile -t versions < <(_list_os_versions "$ostype") diff --git a/ui/menu.func b/ui/menu.func index b2eb5c3..9a72cfd 100644 --- a/ui/menu.func +++ b/ui/menu.func @@ -141,7 +141,7 @@ echo_default() { echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [[ "$(dpkg --print-architecture)" == "arm64" ]]; then + if [[ "$(_cs_host_arch)" == "arm64" ]]; then echo -e "${INFO}${BOLD}${DGN}Architecture: ${BGN}arm64${CL}" fi if [[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]]; then From d280bc0dcc22ae5b41fca80e8515bb7327a800bc Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:20:08 +0200 Subject: [PATCH 6/6] Incus remote: read the quoted byte counts and parse sizes without awk floats yaml.Dump quotes the --bytes values because they are strings that look like numbers, so the integer path never matched and the text path went through BusyBox awk, which truncates floats to 32 bit: an 82 GiB pool read as 1 GB. The size parser is plain bash arithmetic now. The wizard also no longer calls systemctl on a client without it. --- incus/backend.func | 40 +++++++++++++++++------------------- tests/incus-storage-space.sh | 6 +++++- ui/menu.func | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/incus/backend.func b/incus/backend.func index 44ec6ea..1471fa0 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -158,25 +158,23 @@ _incus_resolve_launch_image() { } _incus_parse_size_to_bytes() { - # Accepts raw bytes or human values like 589.51GiB / 211MiB / 1.5TB - local raw="${1:-}" val unit + # Integer arithmetic only: BusyBox awk truncates printf "%d" to 32 bit. + local raw="${1:-}" int frac unit mult scale=1 i raw="${raw// /}" - [[ -z "$raw" ]] && return 1 - if [[ "$raw" =~ ^[0-9]+$ ]]; then - echo "$raw" - return 0 - fi - val=$(echo "$raw" | sed -E 's/[^0-9.].*$//') - unit=$(echo "$raw" | sed -E 's/^[0-9.]+//' | tr '[:lower:]' '[:upper:]') - [[ -z "$val" ]] && return 1 + [[ "$raw" =~ ^([0-9]+)(\.([0-9]+))?([A-Za-z]*)$ ]] || return 1 + int="${BASH_REMATCH[1]}" + frac="${BASH_REMATCH[3]}" + unit="${BASH_REMATCH[4]^^}" case "$unit" in - B | "") echo "$(awk -v v="$val" 'BEGIN{printf "%d", v}')" ;; - KB | KIB | K) echo "$(awk -v v="$val" 'BEGIN{printf "%d", v*1024}')" ;; - MB | MIB | M) echo "$(awk -v v="$val" 'BEGIN{printf "%d", v*1024*1024}')" ;; - GB | GIB | G) echo "$(awk -v v="$val" 'BEGIN{printf "%d", v*1024*1024*1024}')" ;; - TB | TIB | T) echo "$(awk -v v="$val" 'BEGIN{printf "%d", v*1024*1024*1024*1024}')" ;; + B | "") mult=1 ;; + KB | KIB | K) mult=1024 ;; + MB | MIB | M) mult=$((1024 * 1024)) ;; + GB | GIB | G) mult=$((1024 * 1024 * 1024)) ;; + TB | TIB | T) mult=$((1024 * 1024 * 1024 * 1024)) ;; *) return 1 ;; esac + for ((i = 0; i < ${#frac}; i++)); do scale=$((scale * 10)); done + echo $((10#$int * mult + 10#${frac:-0} * mult / scale)) } incus_storage_pool_free_gb() { @@ -191,9 +189,9 @@ incus_storage_pool_free_gb() { # the subtraction stays in bash and never goes through awk floats (BusyBox awk # truncates those to 32 bit). info=$(LC_ALL=C incus storage info --bytes "$pool" 2>/dev/null || true) - free_raw=$(echo "$info" | awk -F': *' '/space free:/ {print $2; exit}') - total_raw=$(echo "$info" | awk -F': *' '/total space:/ {print $2; exit}') - used_raw=$(echo "$info" | awk -F': *' '/space used:/ {print $2; exit}') + free_raw=$(echo "$info" | awk -F': *' '/space free:/ {gsub(/"/, "", $2); print $2; exit}') + total_raw=$(echo "$info" | awk -F': *' '/total space:/ {gsub(/"/, "", $2); print $2; exit}') + used_raw=$(echo "$info" | awk -F': *' '/space used:/ {gsub(/"/, "", $2); print $2; exit}') if [[ "$free_raw" =~ ^[0-9]+$ ]]; then free_bytes="$free_raw" elif [[ "$total_raw" =~ ^[0-9]+$ && "$used_raw" =~ ^[0-9]+$ && "$total_raw" -ge "$used_raw" ]]; then @@ -203,9 +201,9 @@ incus_storage_pool_free_gb() { # Older/human output often has only "space used" + "total space" (no free line). if [[ -z "${free_bytes:-}" ]]; then info=$(LC_ALL=C incus storage info "$pool" 2>/dev/null || true) - free_raw=$(echo "$info" | awk -F': *' '/space free:/ {print $2; exit}') - total_raw=$(echo "$info" | awk -F': *' '/total space:/ {print $2; exit}') - used_raw=$(echo "$info" | awk -F': *' '/space used:/ {print $2; exit}') + free_raw=$(echo "$info" | awk -F': *' '/space free:/ {gsub(/"/, "", $2); print $2; exit}') + total_raw=$(echo "$info" | awk -F': *' '/total space:/ {gsub(/"/, "", $2); print $2; exit}') + used_raw=$(echo "$info" | awk -F': *' '/space used:/ {gsub(/"/, "", $2); print $2; exit}') if [[ -n "$free_raw" ]]; then free_bytes="$(_incus_parse_size_to_bytes "$free_raw" 2>/dev/null || true)" elif [[ -n "$total_raw" && -n "$used_raw" ]]; then diff --git a/tests/incus-storage-space.sh b/tests/incus-storage-space.sh index dcd17ff..44a7494 100644 --- a/tests/incus-storage-space.sh +++ b/tests/incus-storage-space.sh @@ -10,7 +10,8 @@ export INCUS_REMOTE=incusos incus() { case "$*" in "storage info --bytes local") - [[ "${MOCK_BYTES:-yes}" == "yes" ]] && printf 'info:\n driver: zfs\n name: local\n space used: 238747648\n total space: 88900000000\nused by: {}\n' + # yaml.Dump quotes strings that look like numbers; that is what the client prints. + [[ "${MOCK_BYTES:-yes}" == "yes" ]] && printf 'info:\n driver: zfs\n name: local\n space used: "238747648"\n total space: "88900000000"\nused by: {}\n' ;; "storage info local") printf 'info:\n driver: zfs\n name: local\n space used: 227.68MiB\n total space: 82.80GiB\nused by: {}\n' @@ -34,6 +35,9 @@ expect "without --bytes the human sizes still work" 82 "$(MOCK_BYTES=no incus_st expect "GiB parses" 88905823027 "$(_incus_parse_size_to_bytes 82.80GiB)" expect "MiB parses" 238739783 "$(_incus_parse_size_to_bytes 227.68MiB)" expect "a plain number is bytes" 4096 "$(_incus_parse_size_to_bytes 4096)" +expect "a fraction below one works" 536870912 "$(_incus_parse_size_to_bytes 0.5GiB)" +expect "a leading zero in the fraction is decimal" 1101004 "$(_incus_parse_size_to_bytes 1.05MiB)" +expect "TiB stays within 64 bit" 1649267441664 "$(_incus_parse_size_to_bytes 1.5TiB)" expect "nonsense is rejected" 1 "$(_incus_parse_size_to_bytes bogus >/dev/null 2>&1; echo $?)" printf '%s passed, %s failed\n' "$passed" "$failed" diff --git a/ui/menu.func b/ui/menu.func index 9a72cfd..3bb32e4 100644 --- a/ui/menu.func +++ b/ui/menu.func @@ -209,7 +209,7 @@ install_script() { storage_content_check diagnostics_check - if systemctl is-active -q ping-instances.service; then + if command -v systemctl >/dev/null 2>&1 && systemctl is-active -q ping-instances.service; then systemctl -q stop ping-instances.service fi