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
5 changes: 5 additions & 0 deletions .github/workflows/verify-split-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion api/exitcodes.func
Original file line number Diff line number Diff line change
Expand Up @@ -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) ---
Expand Down Expand Up @@ -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" ;;
Expand Down
29 changes: 29 additions & 0 deletions incus/backend.func
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<key>|<project>".
_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}"

Expand Down Expand Up @@ -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" \
Expand Down
50 changes: 50 additions & 0 deletions tests/incus-restricted-project.sh
Original file line number Diff line number Diff line change
@@ -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))
Loading