From 2795b5a55da52da92775346c3be589fbb79a0884 Mon Sep 17 00:00:00 2001
From: MickLesk <47820557+MickLesk@users.noreply.github.com>
Date: Fri, 13 Nov 2026 15:18:53 +0100
Subject: [PATCH 1/2] Take a Debian cloud image from the newest build that has
it
cloud.debian.org points latest/ at its newest build even when that
build lacks an image: 20261111-2659 has no nocloud-amd64, so docker-vm,
debian-vm and every other VM script on the nocloud image failed with
404 (VE #18702, 33 runs in three days). A mirror that has not synced
yet did the same to the generic images. vm_fetch_image now falls back
to the newest of the last five dated builds that has the file and
verifies it against that build's SHA512SUMS.
---
pve/vm-core.func | 24 ++++++++++++++++++++++++
tests/vm-helpers.sh | 32 ++++++++++++++++++++++++++++++++
2 files changed, 56 insertions(+)
diff --git a/pve/vm-core.func b/pve/vm-core.func
index 3c47d1b..39adbd1 100644
--- a/pve/vm-core.func
+++ b/pve/vm-core.func
@@ -1832,6 +1832,23 @@ vm_prompt_start_vm() {
echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}"
}
+# cloud.debian.org points latest/ at its newest build even when that build lacks
+# an image (20261111-2659 has no nocloud-amd64) or a mirror has not synced it.
+# Echoes the newest of the last five dated builds that has the file, else the URL.
+_vm_debian_build_url() {
+ local url="$1" base file dir
+ [[ "$url" =~ ^(https://cloud\.debian\.org/images/cloud/[a-z]+(/daily)?)/latest/([^/]+)$ ]] || { echo "$url"; return 0; }
+ base="${BASH_REMATCH[1]}" file="${BASH_REMATCH[3]}"
+ curl -fsIL --max-time 15 -o /dev/null "$url" 2>/dev/null && { echo "$url"; return 0; }
+ for dir in $(curl -fsSL --max-time 15 "$base/" 2>/dev/null | grep -oE 'href="[0-9]{8}-[0-9]+/"' | grep -oE '[0-9]{8}-[0-9]+' | sort -ru | head -n 5); do
+ if curl -fsIL --max-time 15 -o /dev/null "$base/$dir/${file%.*}-$dir.${file##*.}" 2>/dev/null; then
+ echo "$base/$dir/${file%.*}-$dir.${file##*.}"
+ return 0
+ fi
+ done
+ echo "$url"
+}
+
# Downloads an image and proves it is the image. A bad mirror serves HTML with
# status 200, so size decides rather than curl's exit code. Vendors also respin
# images without updating the published checksum at the same byte count, so
@@ -1939,6 +1956,13 @@ vm_fetch_image() {
esac
done
+ local resolved
+ resolved="$(_vm_debian_build_url "$url")"
+ if [[ "$resolved" != "$url" ]]; then
+ msg_warn "cloud.debian.org has no $(basename "$url") in latest/ right now; using build $(basename "$(dirname "$resolved")")"
+ url="$resolved"
+ fi
+
if [[ -n "$sha256" ]]; then
sha_tool=sha256sum sha_want="${sha256,,}"
elif [[ -n "$sha1" ]]; then
diff --git a/tests/vm-helpers.sh b/tests/vm-helpers.sh
index b0275f5..dba1eac 100644
--- a/tests/vm-helpers.sh
+++ b/tests/vm-helpers.sh
@@ -158,6 +158,38 @@ done
if vm_image_checksum "https://sourceforge.net/projects/x/files/a.iso/download" >/dev/null; then fail "unknown hosts have no checksum"; fi
ok "vendor checksum files are derived from the download URL"
+# ── Debian latest/ without the image ────────────────────────────────────────
+# 20261111-2659 shipped no nocloud-amd64, and latest/ pointed at it: docker-vm
+# and debian-vm failed with 404 until the next build (VE #18702).
+DEB=https://cloud.debian.org/images/cloud/trixie
+fixture "$DEB/" <<'EOF'
+20261026-2643/ 20261103-2651/
+20261111-2659/ daily/ latest/
+EOF
+fixture "$DEB/daily/" <<<'x x'
+HEAD_OK["$DEB/latest/debian-13-generic-amd64.qcow2"]=1
+HEAD_OK["$DEB/20261103-2651/debian-13-nocloud-amd64-20261103-2651.qcow2"]=1
+HEAD_OK["$DEB/20261026-2643/debian-13-nocloud-amd64-20261026-2643.qcow2"]=1
+HEAD_OK["$DEB/daily/20261112-2660/debian-13-generic-amd64-daily-20261112-2660.qcow2"]=1
+assert_eq "$(_vm_debian_build_url "$DEB/latest/debian-13-generic-amd64.qcow2")" "$DEB/latest/debian-13-generic-amd64.qcow2" "latest/ that has the image"
+assert_eq "$(_vm_debian_build_url "$DEB/latest/debian-13-nocloud-amd64.qcow2")" \
+ "$DEB/20261103-2651/debian-13-nocloud-amd64-20261103-2651.qcow2" "the newest build that has it"
+assert_eq "$(_vm_debian_build_url "$DEB/daily/latest/debian-13-generic-amd64-daily.qcow2")" \
+ "$DEB/daily/20261112-2660/debian-13-generic-amd64-daily-20261112-2660.qcow2" "daily builds"
+assert_eq "$(_vm_debian_build_url "$DEB/latest/debian-13-nocloud-riscv64.qcow2")" "$DEB/latest/debian-13-nocloud-riscv64.qcow2" "no build has it"
+assert_eq "$(_vm_debian_build_url https://dl.test/openwrt.img.gz)" https://dl.test/openwrt.img.gz "other hosts"
+ok "a Debian latest/ without the image falls back to the newest build that has it"
+
+reset_logs
+fixture "$DEB/20261103-2651/debian-13-nocloud-amd64-20261103-2651.qcow2" <"$IMAGE"
+fixture "$DEB/20261103-2651/SHA512SUMS" <<<"${IMAGE_SHA512} debian-13-nocloud-amd64-20261103-2651.qcow2"
+vm_fetch_image "$DEB/latest/debian-13-nocloud-amd64.qcow2" "$TEST_DIR/cache/debian-13-nocloud-amd64.qcow2" ||
+ fail "a missing latest/ image must come from the build that has it"
+cmp -s "$IMAGE" "$TEST_DIR/cache/debian-13-nocloud-amd64.qcow2" || fail "the image kept its cache name"
+assert_contains "$LOG" "$DEB/20261103-2651/SHA512SUMS"
+assert_contains "$WARNINGS" "using build 20261103-2651"
+ok "vm_fetch_image downloads and verifies it from that build"
+
# ── vm_fetch_image checksum options ─────────────────────────────────────────
fixture "https://dl.test/openwrt.img.gz" <"$IMAGE"
reset_logs
From 122c93df00b074c79e8454a579d9e889d6714cec Mon Sep 17 00:00:00 2001
From: MickLesk <47820557+MickLesk@users.noreply.github.com>
Date: Fri, 13 Nov 2026 15:24:04 +0100
Subject: [PATCH 2/2] Resolve Debian daily image names in the build fallback
too
waydroid-vm asked for daily/latest/debian-13-generic-amd64.qcow2, which
Debian publishes as debian-13-generic-amd64-daily.qcow2: 7 runs failed
with 404 independent of the latest/ change.
---
pve/vm-core.func | 19 ++++++++++++++-----
tests/vm-helpers.sh | 2 ++
2 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/pve/vm-core.func b/pve/vm-core.func
index 39adbd1..7b3e48e 100644
--- a/pve/vm-core.func
+++ b/pve/vm-core.func
@@ -1836,15 +1836,24 @@ vm_prompt_start_vm() {
# an image (20261111-2659 has no nocloud-amd64) or a mirror has not synced it.
# Echoes the newest of the last five dated builds that has the file, else the URL.
_vm_debian_build_url() {
- local url="$1" base file dir
+ local url="$1" base file stem ext dir cand
[[ "$url" =~ ^(https://cloud\.debian\.org/images/cloud/[a-z]+(/daily)?)/latest/([^/]+)$ ]] || { echo "$url"; return 0; }
base="${BASH_REMATCH[1]}" file="${BASH_REMATCH[3]}"
+ stem="${file%.*}" ext="${file##*.}"
curl -fsIL --max-time 15 -o /dev/null "$url" 2>/dev/null && { echo "$url"; return 0; }
+ # daily/latest/ names its files -daily.; a caller may have left the suffix off.
+ local -a names=("$stem")
+ [[ "$base" == */daily && "$stem" != *-daily ]] && names=("$stem-daily" "$stem")
+ for cand in "${names[@]}"; do
+ curl -fsIL --max-time 15 -o /dev/null "$base/latest/$cand.$ext" 2>/dev/null && { echo "$base/latest/$cand.$ext"; return 0; }
+ done
for dir in $(curl -fsSL --max-time 15 "$base/" 2>/dev/null | grep -oE 'href="[0-9]{8}-[0-9]+/"' | grep -oE '[0-9]{8}-[0-9]+' | sort -ru | head -n 5); do
- if curl -fsIL --max-time 15 -o /dev/null "$base/$dir/${file%.*}-$dir.${file##*.}" 2>/dev/null; then
- echo "$base/$dir/${file%.*}-$dir.${file##*.}"
- return 0
- fi
+ for cand in "${names[@]}"; do
+ if curl -fsIL --max-time 15 -o /dev/null "$base/$dir/$cand-$dir.$ext" 2>/dev/null; then
+ echo "$base/$dir/$cand-$dir.$ext"
+ return 0
+ fi
+ done
done
echo "$url"
}
diff --git a/tests/vm-helpers.sh b/tests/vm-helpers.sh
index dba1eac..ae00def 100644
--- a/tests/vm-helpers.sh
+++ b/tests/vm-helpers.sh
@@ -176,6 +176,8 @@ assert_eq "$(_vm_debian_build_url "$DEB/latest/debian-13-nocloud-amd64.qcow2")"
"$DEB/20261103-2651/debian-13-nocloud-amd64-20261103-2651.qcow2" "the newest build that has it"
assert_eq "$(_vm_debian_build_url "$DEB/daily/latest/debian-13-generic-amd64-daily.qcow2")" \
"$DEB/daily/20261112-2660/debian-13-generic-amd64-daily-20261112-2660.qcow2" "daily builds"
+HEAD_OK["$DEB/daily/latest/debian-13-generic-amd64-daily.qcow2"]=1
+assert_eq "$(_vm_debian_build_url "$DEB/daily/latest/debian-13-generic-amd64.qcow2")" "$DEB/daily/latest/debian-13-generic-amd64-daily.qcow2" "daily name without its suffix (waydroid-vm)"
assert_eq "$(_vm_debian_build_url "$DEB/latest/debian-13-nocloud-riscv64.qcow2")" "$DEB/latest/debian-13-nocloud-riscv64.qcow2" "no build has it"
assert_eq "$(_vm_debian_build_url https://dl.test/openwrt.img.gz)" https://dl.test/openwrt.img.gz "other hosts"
ok "a Debian latest/ without the image falls back to the newest build that has it"