Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/verify-split-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ on:
- "tests/exit-code-hints.sh"
- "tests/forge-release-json.sh"
- "tests/install-cleanup.sh"
- "tests/incus-target.sh"
- "tests/incus-storage-space.sh"
- "tests/release-stamp-pending.sh"
- ".github/workflows/verify-split-files.yml"
push:
Expand Down Expand Up @@ -64,6 +66,8 @@ on:
- "tests/exit-code-hints.sh"
- "tests/forge-release-json.sh"
- "tests/install-cleanup.sh"
- "tests/incus-target.sh"
- "tests/incus-storage-space.sh"
- "tests/release-stamp-pending.sh"
- ".github/workflows/verify-split-files.yml"
workflow_dispatch:
Expand Down Expand Up @@ -128,6 +132,11 @@ 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: Incus storage space tests
run: bash tests/incus-storage-space.sh
- name: Release stamp pending marker tests
run: bash tests/release-stamp-pending.sh

Expand Down
18 changes: 15 additions & 3 deletions core/core.func
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" ;;
Expand Down Expand Up @@ -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" \
Expand Down Expand Up @@ -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" ;;
Expand Down
17 changes: 17 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ 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://<server>:8443 --token <token>
incus remote switch incusos
bash -c "$(curl -fsSL .../ct/<app>.sh)"

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
is skipped for a remote.

## VM scripts on the two backends

`ct/` scripts are shared; `vm/` scripts are not, and the reason is worth
Expand Down
11 changes: 9 additions & 2 deletions host/preflight.func
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -395,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
Expand Down Expand Up @@ -435,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
Expand Down
56 changes: 31 additions & 25 deletions incus/backend.func
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -187,18 +185,25 @@ 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)"
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
free_bytes=$((total_raw - used_raw))
fi

# 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
Expand All @@ -215,7 +220,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
Expand Down Expand Up @@ -471,8 +476,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
Expand Down Expand Up @@ -974,7 +980,7 @@ incus_set_root_password() {

incus_customize_container() {
msg_info "Customizing LXC Container"
ARCH="$(dpkg --print-architecture 2>/dev/null || uname -m)"
ARCH="$(_cs_host_arch)"

if [[ "${var_os:-debian}" == "alpine" ]]; then
sleep 2
Expand Down Expand Up @@ -1093,7 +1099,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.
Expand Down
4 changes: 3 additions & 1 deletion incus/build.func
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions incus/core.func
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,23 @@ 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)"
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="$(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
INCUS_SERVER_KERNEL="$(awk '$1 == "kernel_version:" {print $2; exit}' <<<"$info")"
export INCUS_REMOTE INCUS_SERVER_ARCH INCUS_SERVER_KERNEL
}

incus_check() {
if ! command -v incus &>/dev/null; then
incus_msg_error "Incus is not installed on this system."
Expand Down Expand Up @@ -281,6 +298,11 @@ incus_check() {
incus_msg_error "Create one with: incus storage create default dir"
exit 1
fi

incus_target
if [[ "$INCUS_REMOTE" != "local" ]]; then
incus_msg_custom "ℹ️" "${YW:-}" "Target: Incus remote '${INCUS_REMOTE}' (${INCUS_SERVER_ARCH:-unknown architecture})"
fi
}

# ==============================================================================
Expand Down
44 changes: 44 additions & 0 deletions tests/incus-storage-space.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/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")
# 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'
;;
"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 "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"
((failed == 0))
60 changes: 60 additions & 0 deletions tests/incus-target.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
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
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 ")
# 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 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 kernel_version: 6.12.0-incusos\n server_name: box\n' "${MOCK_ARCH:-x86_64}"
fi
;;
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 "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 "_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"

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))
Loading
Loading