diff --git a/pve/vm-core.func b/pve/vm-core.func index 3c47d1b..7b3e48e 100644 --- a/pve/vm-core.func +++ b/pve/vm-core.func @@ -1832,6 +1832,32 @@ 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 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 + 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" +} + # 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 +1965,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..ae00def 100644 --- a/tests/vm-helpers.sh +++ b/tests/vm-helpers.sh @@ -158,6 +158,40 @@ 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" +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" + +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