diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index cf1bada..9bf2398 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -46,6 +46,7 @@ on: - "tests/install-cleanup.sh" - "tests/console-hangup.sh" - "tests/container-removal-offer.sh" + - "tests/incus-restricted-project.sh" - "tests/incus-target.sh" - "tests/incus-storage-space.sh" - "tests/release-stamp-pending.sh" @@ -92,6 +93,7 @@ on: - "tests/install-cleanup.sh" - "tests/console-hangup.sh" - "tests/container-removal-offer.sh" + - "tests/incus-restricted-project.sh" - "tests/incus-target.sh" - "tests/incus-storage-space.sh" - "tests/release-stamp-pending.sh" @@ -190,6 +192,9 @@ jobs: - name: Container removal offer tests run: bash tests/container-removal-offer.sh + - name: Incus restricted project tests + run: bash tests/incus-restricted-project.sh + - name: Incus target tests run: bash tests/incus-target.sh diff --git a/api/exitcodes.func b/api/exitcodes.func index 075c24b..266ab80 100644 --- a/api/exitcodes.func +++ b/api/exitcodes.func @@ -187,6 +187,7 @@ explain_exit_code() { 226) echo "Proxmox: VM disk import or post-creation setup failed" ;; 227) echo "Proxmox: Container created but failed to start" ;; 228) echo "Proxmox: Cluster filesystem /etc/pve not available" ;; + 229) echo "Incus: Project restrictions forbid a config the container needs" ;; 231) echo "Proxmox: LXC stack too old for the template, or its upgrade failed" ;; # --- Tools & Addon Scripts (232-238) --- @@ -251,7 +252,7 @@ categorize_error() { 28 | 124 | 211) echo "timeout" ;; 214 | 217 | 219 | 224) echo "storage" ;; 100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;; - 126 | 152) echo "permission" ;; + 126 | 152 | 229) echo "permission" ;; 128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; 200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 227 | 228 | 231) echo "proxmox" ;; 150 | 151 | 153 | 154) echo "service" ;; diff --git a/incus/backend.func b/incus/backend.func index f3f9d4a..c880d27 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -353,6 +353,22 @@ _incus_prepare_install_exports() { _HOST_LOGFILE="${BUILD_LOG:-$INCUS_BUILD_LOG}" } +# A restricted project refuses the whole launch over a single config key -- +# restricted.containers.nesting and restricted.containers.interception both +# default to block -- so retrying other images cannot help. Incus names the key +# and the project it refused; echo them as "|". +_incus_launch_refusal() { + local out="$1" rest="" key="" project="" q='"' + [[ "$out" == *"is forbidden"* && "$out" == *"for config $q"* ]] || return 1 + rest="${out##*for config $q}" + key="${rest%%$q*}" + if [[ "$rest" == *"of project $q"* ]]; then + project="${rest#*of project $q}" + project="${project%%$q*}" + fi + printf '%s|%s\n' "$key" "${project:-default}" +} + incus_create_lxc_container() { LOGFILE="${INCUS_BUILD_LOG}" @@ -451,7 +467,20 @@ incus_create_lxc_container() { echo "user: $(id -un) uid=$(id -u)" } >>"$LOGFILE" + local launch_mark + launch_mark="$(wc -l <"$LOGFILE" 2>/dev/null || echo 0)" if ! incus launch "${image_name}" "${CT_NAME}" "${launch_args[@]}" >>"$LOGFILE" 2>&1; then + local refusal="" + refusal="$(_incus_launch_refusal "$(tail -n +$((launch_mark + 1)) "$LOGFILE" 2>/dev/null)")" || refusal="" + if [[ -n "$refusal" ]]; then + local refused_project="${refusal#*|}" + msg_error "Project '${refused_project}' forbids ${refusal%|*}" + echo -e "${YW}Allow it on the host, then run the script again:${CL}" + echo -e " incus project set ${refused_project} restricted.containers.nesting allow" + echo -e " incus project set ${refused_project} restricted.containers.interception allow" + echo -e "${YW}Log: ${BL}${LOGFILE}${CL}" + exit 229 + fi # Retry alternate candidates once if primary pull failed local retry_img="" launched=0 tried=" ${image_name} " for retry_img in "images:${IMAGE_OS}/${IMAGE_VERSION}/cloud" \ diff --git a/tests/incus-restricted-project.sh b/tests/incus-restricted-project.sh new file mode 100644 index 0000000..1ab1434 --- /dev/null +++ b/tests/incus-restricted-project.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^_incus_launch_refusal[(][)] [{]/,/^}/' "$CORE_DIR/incus/backend.func")" + +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 +} + +# Verbatim from the telemetry record of 2026-11-02, a host whose default +# project carries restricted=true. +MKNOD='Launching debian-2 +Error: Failed instance creation: Failed checking if instance creation allowed: Invalid value "true" for config "security.syscalls.intercept.mknod" on container "debian-2" of project "default": Container syscall interception is forbidden' + +expect "the refused key and project are named" \ + "security.syscalls.intercept.mknod|default" "$(_incus_launch_refusal "$MKNOD")" + +NESTING='Error: Failed instance creation: Failed checking if instance creation allowed: Invalid value "true" for config "security.nesting" on container "ct-1" of project "lab": Container nesting is forbidden' +expect "nesting is caught in a non-default project" \ + "security.nesting|lab" "$(_incus_launch_refusal "$NESTING")" + +# The retry loop appends each attempt, so the helper reads the newest one. +expect "the last attempt wins" \ + "security.nesting|lab" "$(_incus_launch_refusal "$MKNOD +$NESTING")" + +# Without the project clause the key still has to come through. +NOPROJ='Error: Invalid value "true" for config "security.nesting": Container nesting is forbidden' +expect "a missing project clause falls back" \ + "security.nesting|default" "$(_incus_launch_refusal "$NOPROJ")" + +# An image problem must still reach the retry loop. +PULL='Error: Failed instance creation: Failed getting image: not found' +expect "an image failure is not a refusal" "" "$(_incus_launch_refusal "$PULL")" +expect " and it reports no refusal" 1 "$(_incus_launch_refusal "$PULL" >/dev/null 2>&1; echo $?)" + +# "is forbidden" alone is not enough; a config key has to be named. +expect "a forbidden message without a key is not a refusal" 1 \ + "$(_incus_launch_refusal 'Error: Container nesting is forbidden' >/dev/null 2>&1; echo $?)" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0))