From 449a732fde200c31643b9537caacf03e36839608 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 19 Oct 2026 18:41:48 +0200 Subject: [PATCH 01/13] setup: update the Debian 13 templates, add build-acs-image.sh - templates.sql: use the patched Debian 13 images (20261017-2634) from http://10.0.34.8/templates/debian-13-genericcloud-amd64/ for the KVM, XenServer (vhd.bz2) and VMware (ova) templates, with their SHA-512 checksums - scripts/util/build-acs-image.sh: new script to patch, shrink and convert a cloud image (qcow2) to VHD, OVA and XenServer VHD for CloudStack, and to create SHA512SUMS - schema-21to22.sql, schema-2214to30.sql: remove the inserts of the old CentOS 5 builtin templates (ids 4, 7 and 5) --- .../resources/META-INF/db/schema-21to22.sql | 5 - .../resources/META-INF/db/schema-2214to30.sql | 3 - scripts/util/build-acs-image.sh | 601 ++++++++++++++++++ setup/db/templates.sql | 6 +- 4 files changed, 604 insertions(+), 11 deletions(-) create mode 100755 scripts/util/build-acs-image.sh diff --git a/engine/schema/src/main/resources/META-INF/db/schema-21to22.sql b/engine/schema/src/main/resources/META-INF/db/schema-21to22.sql index 8da29caae53b..6905f62e84ca 100755 --- a/engine/schema/src/main/resources/META-INF/db/schema-21to22.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-21to22.sql @@ -964,11 +964,6 @@ INSERT INTO `cloud`.`vm_template` (id, unique_name, name, public, created, type, INSERT INTO `cloud`.`vm_template` (id, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type) VALUES (8, 'routing-8', 'SystemVM Template (vSphere)', 0, now(), 'SYSTEM', 0, 32, 1, 'http://download.cloudstack.org/releases/2.2.0/systemvm.ova', '3c9d4c704af44ebd1736e1bc78cec1fa', 0, 'SystemVM Template (vSphere)', 'OVA', 15, 0, 1, 'VMware'); -INSERT INTO `cloud`.`vm_template` (id, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, display_text, enable_password, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (4, 'centos55-x86_64', 'CentOS 5.5(64-bit) no GUI (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/2.2.0/eec2209b-9875-3c8d-92be-c001bd8a0faf.qcow2.bz2', 'ed0e788280ff2912ea40f7f91ca7a249', 'CentOS 5.5(64-bit) no GUI (KVM)', 0, 'QCOW2', 112, 1, 1, 'KVM', 1); - -INSERT INTO `cloud`.`vm_template` (id, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (7, 'centos53-x64', 'CentOS 5.3(64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/2.2.0/CentOS5.3-x86_64.ova', 'f6f881b7f2292948d8494db837fe0f47', 0, 'CentOS 5.3(64-bit) no GUI (vSphere)', 'OVA', 12, 1, 1, 'VMware', 1); UPDATE vm_instance SET guest_os_id=15 where vm_template_id=1; UPDATE vm_instance SET vm_template_id=(SELECT id FROM vm_template WHERE name='systemvm-xenserver-2.2.4' AND removed IS NULL) where vm_template_id=1; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-2214to30.sql b/engine/schema/src/main/resources/META-INF/db/schema-2214to30.sql index 8be481e47368..fdb260e57728 100755 --- a/engine/schema/src/main/resources/META-INF/db/schema-2214to30.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-2214to30.sql @@ -625,9 +625,6 @@ UPDATE `cloud`.`event` e set e.domain_id = (select acc.domain_id from `cloud`.`a update `cloud`.`vm_template` set removed=now() where id=2; -INSERT INTO `cloud`.`vm_template` (id, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (5, 'centos56-x86_64-xen', 'CentOS 5.6(64-bit) no GUI (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/builtin/centos56-x86_64.vhd.bz2', '905cec879afd9c9d22ecc8036131a180', 0, 'CentOS 5.6(64-bit) no GUI (XenServer)', 'VHD', 12, 1, 1, 'XenServer', 1); - DELETE from `cloud`.`configuration` where name='firewall.rule.ui.enabled'; DELETE FROM `cloud`.`resource_limit` WHERE domain_id = 1 AND account_id IS NULL; diff --git a/scripts/util/build-acs-image.sh b/scripts/util/build-acs-image.sh new file mode 100755 index 000000000000..57fb7ef24ced --- /dev/null +++ b/scripts/util/build-acs-image.sh @@ -0,0 +1,601 @@ +#!/bin/bash +# Prepare a generic cloud qcow2 image for CloudStack: patch it, shrink it and convert it to VHD/OVA. +# +# Usage: build-acs-image.sh [-a ACTIONS] [-f FORMATS] [-o OUTDIR] [-g GUESTOS] [-u] [-z] +# [-d debian|rhel|suse] [-p ROOT_PASSWORD] +# -a actions, a list separated by commas or spaces, done in this order (default: all) +# patch install the guest tools, set the root password, cloud-init and sshd config +# (OS detected from the image: Debian/Ubuntu, EL, SUSE and variants) +# shrink virt-sparsify and qemu-img convert -c +# convert convert to the formats of -f +# all patch, shrink and convert +# -f formats for the convert action, a list like "vhd,ova" or "ova, xen" (default: all) +# vhd plain VHD for Hyper-V (also hyperv) -acs.vhd (removed if xen is built too) +# ova OVA for VMware (also vmware) -acs.ova +# xen VHD for XenServer, bzip2 (also xenserver) -acs-xen.vhd.bz2 +# all vhd, ova and xen +# -o output directory (default: directory of the image) +# -g VMware guestos id for the OVA (default: detected from /etc/os-release in the image, +# e.g. debian12-64, ubuntu-64, rhel9-64, sles15-64, otherlinux-64) +# -u with patch: also run a full package upgrade inside the image +# -z with shrink: do not compress (plain qcow2 rewrite) +# -d with patch: OS family, if it cannot be detected from the image +# -p with patch: root password to set (default: password) +# +# The image is never modified. Patch and shrink work on a copy named -acs. in the +# output directory (the name is kept if it already ends with -acs), and the converted files +# are named -acs.vhd and -acs.ova. Convert alone reads the original image. +# +# SHA512SUMS with the checksums of the output files is written to the output directory when all +# is done (the entries of other files are kept). The tools needed by the actions and formats are +# checked first, and nothing is installed. +set -euo pipefail + +usage() { sed -n '2,29p' "$0"; exit 1; } + +ACTIONS="all" +FORMATS="all" +OUTDIR="" +GUESTOS="" +UPDATE="" +COMPRESS="-c" +OS_FAMILY="" +ROOT_PASSWORD="password" +while getopts "a:f:o:g:uzd:p:" opt; do + case $opt in + a) ACTIONS="$OPTARG" ;; + f) FORMATS="$OPTARG" ;; + o) OUTDIR="$OPTARG" ;; + g) GUESTOS="$OPTARG" ;; + u) UPDATE="--update" ;; + z) COMPRESS="" ;; + d) OS_FAMILY="$OPTARG" ;; + p) ROOT_PASSWORD="$OPTARG" ;; + *) usage ;; + esac +done +shift $((OPTIND - 1)) + +IMAGE="${1:-}" +[ -f "$IMAGE" ] || { echo "Image not found: '$IMAGE'" >&2; usage; } +case "$OS_FAMILY" in ""|debian|rhel|suse) ;; *) echo "Invalid OS family: $OS_FAMILY" >&2; exit 1 ;; esac + +# ACTIONS and FORMATS are lists separated by commas and/or spaces +DO_PATCH="" DO_SHRINK="" DO_CONVERT="" +for a in $(echo "$ACTIONS" | tr 'A-Z,;' 'a-z '); do + case "$a" in + patch) DO_PATCH=1 ;; + shrink) DO_SHRINK=1 ;; + convert) DO_CONVERT=1 ;; + all) DO_PATCH=1; DO_SHRINK=1; DO_CONVERT=1 ;; + *) echo "Invalid action: '$a' (supported: patch, shrink, convert, all)" >&2; exit 1 ;; + esac +done +[ -n "$DO_PATCH$DO_SHRINK$DO_CONVERT" ] || { echo "No action given" >&2; exit 1; } + +WANT_VHD="" WANT_OVA="" WANT_XEN="" +for f in $(echo "$FORMATS" | tr 'A-Z,;' 'a-z '); do + case "$f" in + vhd|hyperv) WANT_VHD=1 ;; + ova|vmware) WANT_OVA=1 ;; + xen|xenserver) WANT_XEN=1 ;; + all) WANT_VHD=1; WANT_OVA=1; WANT_XEN=1 ;; + *) echo "Invalid format: '$f' (supported: vhd, ova, xen, all, or a list like vhd,ova)" >&2; exit 1 ;; + esac +done +[ -n "$WANT_VHD$WANT_OVA$WANT_XEN" ] || { echo "No output format given" >&2; exit 1; } +# the formats are only needed by the convert action +if [ -z "$DO_CONVERT" ]; then WANT_VHD=""; WANT_OVA=""; WANT_XEN=""; fi + +# names: -acs. for the copy that is patched and shrunk, -acs. for the conversions +IMAGE=$(readlink -f "$IMAGE") +[ -n "$OUTDIR" ] || OUTDIR=$(dirname "$IMAGE") +mkdir -p "$OUTDIR" +OUTDIR=$(readlink -f "$OUTDIR") +file=$(basename "$IMAGE") +if [[ "$file" == *.* ]]; then stem="${file%.*}"; ext=".${file##*.}"; else stem="$file"; ext=""; fi +case "$stem" in *-acs) OUT_BASE="$stem" ;; *) OUT_BASE="${stem}-acs" ;; esac +WORK_IMAGE="$OUTDIR/$OUT_BASE$ext" +# patch and shrink work on the copy, convert alone reads the original image +SOURCE="$IMAGE" +if [ -n "$DO_PATCH$DO_SHRINK" ]; then SOURCE="$WORK_IMAGE"; fi + +export LIBGUESTFS_BACKEND=direct + +# Hint on the package names for the missing tools, according to the package manager of the host +pkg_hint() { + local what="$1" + if command -v apt-get >/dev/null 2>&1; then + [ "$what" = "libguestfs" ] && echo "apt install libguestfs-tools qemu-utils" || echo "apt install $what" + elif command -v zypper >/dev/null 2>&1; then + [ "$what" = "libguestfs" ] && echo "zypper install guestfs-tools libguestfs qemu-tools" || echo "zypper install $what" + elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then + [ "$what" = "libguestfs" ] && echo "dnf install guestfs-tools (libguestfs-tools-c on EL8) qemu-img" || echo "dnf install $what" + else + echo "install libguestfs tools and qemu-img" + fi +} + +# Check what the actions and formats need, before anything is created, and report all the +# problems at once. Packages are never installed here, as the script runs on Debian/Ubuntu, +# EL, SUSE, ... +check_dependencies() { + local missing=() warnings=() + local cmd + + for cmd in qemu-img cp mktemp sha512sum; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd not found ($(pkg_hint libguestfs))") + done + if [ -n "$DO_PATCH" ]; then + for cmd in virt-customize virt-cat virt-ls virt-inspector; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd not found ($(pkg_hint libguestfs))") + done + fi + if [ -n "$DO_SHRINK" ]; then + command -v virt-sparsify >/dev/null 2>&1 || missing+=("virt-sparsify not found ($(pkg_hint libguestfs))") + fi + + # the formats needed by qemu-img + if command -v qemu-img >/dev/null 2>&1 && [ -n "$DO_CONVERT" ]; then + local formats + formats=$(qemu-img --help 2>&1 || true) + if [ -n "$WANT_VHD" ]; then + echo "$formats" | grep -qw vpc || missing+=("qemu-img does not support the vpc (VHD) format") + fi + if [ -n "$WANT_OVA" ]; then + echo "$formats" | grep -qw vmdk || missing+=("qemu-img does not support the vmdk format") + fi + fi + + # the VMware guestos of the OVA is detected from the image, unless -g is given + if [ -n "$WANT_OVA" ] && [ -z "$GUESTOS" ] && [ -z "$DO_PATCH" ]; then + command -v virt-cat >/dev/null 2>&1 || missing+=("virt-cat not found, needed to detect the VMware guestos of the OVA from the image ($(pkg_hint libguestfs)), or use -g") + fi + + # ovftool must be runnable, not only be present + if [ -n "$WANT_OVA" ]; then + if ! command -v ovftool >/dev/null 2>&1; then + missing+=("ovftool not found, required for OVA (VMware OVF Tool, add it to PATH)") + elif ! ovftool --version >/dev/null 2>&1; then + missing+=("ovftool cannot be run (check its libraries and that the whole ovftool directory is installed)") + fi + fi + + # the XenServer VHD: bzip2, and the 32-bit vhd-util with faketime (as in tools/appliance/build.sh) + if [ -n "$WANT_XEN" ]; then + command -v bzip2 >/dev/null 2>&1 || missing+=("bzip2 not found, required for the XenServer VHD (install bzip2)") + command -v faketime >/dev/null 2>&1 || missing+=("faketime not found, required for the XenServer VHD (install faketime)") + if ! command -v vhd-util >/dev/null 2>&1; then + missing+=("vhd-util not found, required for the XenServer VHD (see the comments in tools/appliance/build.sh, or http://packages.shapeblue.com/systemvmtemplate/vhd-util)") + else + # vhd-util is a 32-bit binary: it needs the i386 libraries, and faketime must be preloadable into it + local vhd_out + vhd_out=$(ldd "$(command -v vhd-util)" 2>&1 || true) + if echo "$vhd_out" | grep -qE "not found|not a dynamic|No such file"; then + missing+=("vhd-util cannot be run, missing libraries (32-bit libuuid and libc, and libvhd.so.1.0 in the ldconfig path)") + elif command -v faketime >/dev/null 2>&1; then + vhd_out=$(faketime '2010-01-01' vhd-util 2>&1 || true) + echo "$vhd_out" | grep -q "cannot be preloaded" && \ + missing+=("faketime cannot be used with the 32-bit vhd-util (install the 32-bit faketime library, e.g. faketime:i386)") + fi + fi + fi + + # the image must be a readable qcow2 file + local info vsize_gb=1 + if command -v qemu-img >/dev/null 2>&1; then + if ! info=$(qemu-img info --output=json "$IMAGE" 2>&1); then + missing+=("cannot read the image: $(echo "$info" | head -1)") + else + echo "$info" | grep -q '"format": *"qcow2"' || missing+=("$IMAGE is not a qcow2 image") + vsize_gb=$(echo "$info" | sed -n 's/.*"virtual-size": *\([0-9]*\).*/\1/p' | head -1) + vsize_gb=$(( ${vsize_gb:-0} / 1024 / 1024 / 1024 + 1 )) + fi + fi + + # the architecture of the guest: commands cannot be run in a guest of another architecture + # (virt-customize --install/--run-command), and the VHD/OVA are for x86_64 hypervisors + if command -v virt-inspector >/dev/null 2>&1 && [ -z "${missing[*]:-}" ]; then + local guest_arch host_arch + guest_arch=$(virt-inspector -a "$IMAGE" 2>/dev/null | sed -n 's#.*\(.*\).*#\1#p' | head -1) + host_arch=$(uname -m) + if [ -n "$guest_arch" ] && [ "$guest_arch" != "$host_arch" ]; then + if [ -n "$DO_PATCH" ]; then + missing+=("the image is $guest_arch but the host is $host_arch: virt-customize cannot run commands (install packages) in the guest, so the patch action is not possible here; run it on a $guest_arch host, or use -a shrink,convert") + fi + fi + if [ -n "$guest_arch" ] && [ "$guest_arch" != "x86_64" ] && [ -n "$WANT_VHD$WANT_OVA$WANT_XEN" ]; then + warnings+=("the image is $guest_arch, but the VHD (Hyper-V, XenServer) and the OVA (VMware) are normally used with x86_64 images") + fi + fi + + # the output + [ -w "$OUTDIR" ] || missing+=("$OUTDIR is not writable") + if [ -n "$DO_PATCH$DO_SHRINK" ]; then + if [ "$WORK_IMAGE" = "$IMAGE" ]; then + missing+=("$IMAGE is already an -acs image, the original image is never modified (use convert only, or pass the original image)") + elif [ -e "$WORK_IMAGE" ]; then + missing+=("$WORK_IMAGE already exists, remove it or move it away first") + fi + fi + + # disk space in the output directory: the copy, the sparsified and converted copies, the VHD, the VMDK and the OVA + local need_gb=0 free_gb + [ -n "$DO_PATCH$DO_SHRINK" ] && need_gb=$(( need_gb + vsize_gb )) + [ -n "$DO_SHRINK" ] && need_gb=$(( need_gb + 2 * vsize_gb )) + [ -n "$WANT_VHD" ] && need_gb=$(( need_gb + vsize_gb )) + [ -n "$WANT_OVA" ] && need_gb=$(( need_gb + 2 * vsize_gb )) + [ -n "$WANT_XEN" ] && need_gb=$(( need_gb + 2 * vsize_gb )) + free_gb=$(df -P -BG "$OUTDIR" | awk 'NR==2 {gsub("G","",$4); print $4}') + [ -n "$free_gb" ] && [ "$free_gb" -lt "$need_gb" ] 2>/dev/null && \ + warnings+=("only ${free_gb}G free in $OUTDIR, up to ${need_gb}G may be needed") + + if [ -n "$DO_PATCH$DO_SHRINK" ]; then + # libguestfs builds its appliance in TMPDIR (or LIBGUESTFS_CACHEDIR), which needs about 2G + local cache_dir="${LIBGUESTFS_CACHEDIR:-${TMPDIR:-/var/tmp}}" + local cache_free + cache_free=$(df -P -BG "$cache_dir" 2>/dev/null | awk 'NR==2 {gsub("G","",$4); print $4}') + if [ -n "$cache_free" ] && [ "$cache_free" -lt 2 ] 2>/dev/null; then + missing+=("only ${cache_free}G free in $cache_dir for the libguestfs appliance (set LIBGUESTFS_CACHEDIR and TMPDIR to a bigger disk)") + fi + + # the libguestfs appliance is built from the kernel of the host (not of the image): on Ubuntu + # /boot/vmlinuz-* is only readable by root, so supermin fails for other users + if [ "$(id -u)" != "0" ] && ls /boot/vmlinuz-* >/dev/null 2>&1; then + local k + for k in /boot/vmlinuz-*; do [ -r "$k" ] && break; k=; done + [ -n "${k:-}" ] || warnings+=("the host kernel in /boot is not readable by $(id -un), run as root or chmod a+r /boot/vmlinuz-*") + fi + fi + + local item + for item in "${warnings[@]}"; do echo "WARN: $item" >&2; done + if [ ${#missing[@]} -gt 0 ]; then + for item in "${missing[@]}"; do echo "ERROR: $item" >&2; done + echo "ERROR: ${#missing[@]} dependency problem(s), see above" >&2 + exit 1 + fi +} + +# Read /etc/os-release from inside the image (read-only) +load_os_release() { + local osr + osr=$(virt-cat -a "$IMAGE" /etc/os-release 2>/dev/null || virt-cat -a "$IMAGE" /usr/lib/os-release 2>/dev/null || true) + osr_value() { echo "$osr" | sed -n "s/^$1=//p" | head -1 | tr -d "\"'" ; } + OS_ID=$(osr_value ID | tr 'A-Z' 'a-z') + OS_ID_LIKE=$(osr_value ID_LIKE | tr 'A-Z' 'a-z') + OS_VERSION_ID=$(osr_value VERSION_ID) + OS_NAME=$(osr_value PRETTY_NAME) +} + +# Map /etc/os-release to a VMware guestos id (the ids known by ESXi 7/8; newer releases of a +# distribution use the newest id, as older vSphere versions do not know the newest ids) +vmware_guestos() { + local id="$OS_ID" like="$OS_ID_LIKE" major="${OS_VERSION_ID%%.*}" + local n + case "$id" in + debian) + n=$major; [[ "$n" =~ ^[0-9]+$ ]] || n=12 + [ "$n" -gt 12 ] && n=12; [ "$n" -lt 8 ] && n=8 + echo "debian${n}-64"; return ;; + ubuntu) echo "ubuntu-64"; return ;; + rhel|centos|ol|oracle) + n=$major; [[ "$n" =~ ^[0-9]+$ ]] || n=9 + [ "$n" -gt 9 ] && n=9; [ "$n" -lt 6 ] && n=6 + case "$id" in + rhel) echo "rhel${n}-64" ;; + centos) echo "centos${n}-64" ;; + *) echo "oraclelinux${n}-64" ;; + esac + return ;; + rocky) echo "rockylinux-64"; return ;; + almalinux|alma) echo "almalinux-64"; return ;; + fedora) echo "fedora-64"; return ;; + sles|sled|sle-micro) + n=$major; [[ "$n" =~ ^[0-9]+$ ]] || n=15 + [ "$id" = "sle-micro" ] && n=15 + [ "$n" -gt 15 ] && n=15; [ "$n" -lt 11 ] && n=11 + echo "sles${n}-64"; return ;; + opensuse*|suse) echo "opensuse-64"; return ;; + esac + # variants: use the family of ID_LIKE + case " $like " in + *" ubuntu "*) echo "ubuntu-64"; return ;; + *" debian "*) echo "debian12-64"; return ;; + *" rhel "*|*" fedora "*) + n=$major; [[ "$n" =~ ^[0-9]+$ ]] || n=9 + [ "$n" -gt 9 ] && n=9; [ "$n" -lt 6 ] && n=6 + echo "rhel${n}-64"; return ;; + *" suse "*) echo "sles15-64"; return ;; + esac + echo "otherlinux-64" +} + +# Detect the OS family from the os-release of the image, to decide the packages and the cleanup +detect_os() { + if [ -z "$OS_FAMILY" ]; then + local id + for id in $OS_ID $OS_ID_LIKE; do + case "$id" in + debian|ubuntu|linuxmint|raspbian|kali) OS_FAMILY=debian ;; + rhel|centos|fedora|rocky|almalinux|alma|ol|oracle|amzn|scientific|eurolinux|cloudlinux|anolis|openeuler) OS_FAMILY=rhel ;; + suse|sles|sled|sle-micro|opensuse|opensuse-leap|opensuse-tumbleweed|opensuse-microos) OS_FAMILY=suse ;; + esac + [ -n "$OS_FAMILY" ] && break + done + fi + if [ -z "$OS_FAMILY" ]; then + echo "ERROR: unsupported OS in the image (ID='${OS_ID}', ID_LIKE='${OS_ID_LIKE}'), use -d debian|rhel|suse to force a family" >&2 + exit 1 + fi + echo "Detected OS: ${OS_NAME:-$OS_ID} (family: $OS_FAMILY)" + + local base_cleanup='journalctl --vacuum-time=1s; rm -rf /var/log/*; rm -f /root/.bash_history' + case "$OS_FAMILY" in + debian) + PACKAGES="qemu-guest-agent,open-vm-tools,xenstore-utils,libxenstore4" + CLEANUP="apt clean; apt autoclean; apt autoremove --purge -y; $base_cleanup" ;; + rhel) + # there is no xenstore package in the EL repositories + PACKAGES="qemu-guest-agent,open-vm-tools" + CLEANUP="(dnf clean all || yum clean all); rm -rf /var/cache/dnf /var/cache/yum; $base_cleanup" ;; + suse) + PACKAGES="qemu-guest-agent,open-vm-tools,xen-tools-domU" + CLEANUP="zypper --non-interactive clean --all; $base_cleanup" ;; + esac +} + +do_patch() { + echo "== Patching $WORK_IMAGE ==" + local tmp + tmp=$(mktemp -d) + CLEAN_PATHS+=("$tmp") + echo "policy: enabled" > "$tmp/ds-identify.cfg" + cat > "$tmp/cloudstack.cfg" <<'EOF' +datasource_list: [ ConfigDrive, CloudStack, None ] +datasource: + CloudStack: {} + None: {} +EOF + # sshd uses the first value it reads, so 40- loads before cloud-init's 50-cloud-init.conf + printf 'PermitRootLogin yes\nPasswordAuthentication yes\n' > "$tmp/40-password-login.conf" + + # the drop-in only works if sshd_config includes sshd_config.d (not in EL8, Ubuntu 20.04, ...); + # otherwise put the settings at the top of sshd_config, as the first value wins + local sshd_fix='f=/etc/ssh/sshd_config; d=/etc/ssh/sshd_config.d/40-password-login.conf; +if [ -f "$f" ] && ! grep -qiE "^[[:space:]]*Include[[:space:]].*sshd_config\.d" "$f"; then + { cat "$d"; cat "$f"; } > "$f.new" && cat "$f.new" > "$f" && rm -f "$f.new" +fi' + + virt-customize -a "$WORK_IMAGE" \ + $UPDATE \ + --install "$PACKAGES" \ + --root-password "password:$ROOT_PASSWORD" \ + --upload "$tmp/ds-identify.cfg:/etc/cloud/ds-identify.cfg" \ + --upload "$tmp/cloudstack.cfg:/etc/cloud/cloud.cfg.d/cloudstack.cfg" \ + --mkdir /etc/ssh/sshd_config.d \ + --upload "$tmp/40-password-login.conf:/etc/ssh/sshd_config.d/40-password-login.conf" \ + --run-command "$sshd_fix" \ + --run-command "$CLEANUP" + rm -rf "$tmp" + + echo "--- verification ---" + virt-cat -a "$WORK_IMAGE" /etc/cloud/ds-identify.cfg /etc/cloud/cloud.cfg.d/cloudstack.cfg /etc/ssh/sshd_config.d/40-password-login.conf + local d + for d in /usr/sbin /usr/bin; do + virt-ls -a "$WORK_IMAGE" $d | grep -E '^(qemu-ga|vmtoolsd|xenstore)' | sed "s#^#$d/#" || true + done +} + +do_shrink() { + echo "== Shrinking $WORK_IMAGE ==" + local sparse out + sparse=$(mktemp "$OUTDIR/.sparse.XXXXXX") + out=$(mktemp "$OUTDIR/.shrink.XXXXXX") + CLEAN_PATHS+=("$sparse" "$out") + rm -f "$sparse" + virt-sparsify --convert qcow2 "$WORK_IMAGE" "$sparse" + qemu-img convert $COMPRESS -f qcow2 -O qcow2 "$sparse" "$out" + qemu-img check "$out" >/dev/null + chmod --reference="$WORK_IMAGE" "$out" + mv -f "$out" "$WORK_IMAGE" + rm -f "$sparse" + qemu-img info "$WORK_IMAGE" | grep -E 'virtual size|disk size' +} + +do_convert_vhd() { + echo "== Converting to VHD ==" + qemu-img convert -f qcow2 -O vpc "$SOURCE" "$OUTDIR/$OUT_BASE.vhd" + chmod a+r "$OUTDIR/$OUT_BASE.vhd" + echo "Created $OUTDIR/$OUT_BASE.vhd" +} + +do_convert_xen() { + echo "== Converting to the XenServer VHD ==" + local work + work=$(mktemp -d "$OUTDIR/.xen-work.XXXXXX") + CLEAN_PATHS+=("$work") + qemu-img convert -f qcow2 -O raw "$SOURCE" "$work/img.raw" + vhd-util convert -s 0 -t 1 -i "$work/img.raw" -o "$work/stagefixed.vhd" + faketime '2010-01-01' vhd-util convert -s 1 -t 2 -i "$work/stagefixed.vhd" -o "$work/$OUT_BASE-xen.vhd" + bzip2 -f "$work/$OUT_BASE-xen.vhd" + mv -f "$work/$OUT_BASE-xen.vhd.bz2" "$OUTDIR/$OUT_BASE-xen.vhd.bz2" + chmod a+r "$OUTDIR/$OUT_BASE-xen.vhd.bz2" + rm -rf "$work" + echo "Created $OUTDIR/$OUT_BASE-xen.vhd.bz2" +} + +do_convert_ova() { + echo "== Converting to OVA ==" + local work + work=$(mktemp -d "$OUTDIR/.ova-work.XXXXXX") + CLEAN_PATHS+=("$work") + + qemu-img convert -f qcow2 -O vmdk "$SOURCE" "$work/$OUT_BASE.vmdk" + + cat > "$work/$OUT_BASE.vmx" <&2 + fi +} +trap cleanup EXIT + +check_dependencies +# detect the OS before anything is created, so that an unsupported OS stops here +if [ -n "$DO_PATCH" ] || { [ -n "$WANT_OVA" ] && [ -z "$GUESTOS" ]; }; then + load_os_release +fi +[ -n "$DO_PATCH" ] && detect_os +if [ -n "$WANT_OVA" ] && [ -z "$GUESTOS" ]; then + GUESTOS=$(vmware_guestos) + echo "VMware guestos: $GUESTOS (from ID='${OS_ID}', ID_LIKE='${OS_ID_LIKE}', VERSION_ID='${OS_VERSION_ID}')" +fi + +if [ -n "$DO_PATCH$DO_SHRINK" ]; then + cp --reflink=auto "$IMAGE" "$WORK_IMAGE" + WORK_BUSY=1 + echo "Created $WORK_IMAGE from $IMAGE (the original is not modified)" +fi +[ -n "$DO_PATCH" ] && do_patch +[ -n "$DO_SHRINK" ] && do_shrink +WORK_BUSY="" +if [ -n "$DO_CONVERT" ]; then + [ -n "$WANT_VHD" ] && do_convert_vhd + [ -n "$WANT_OVA" ] && do_convert_ova + if [ -n "$WANT_XEN" ]; then + do_convert_xen + # the plain .vhd is not kept when the XenServer .vhd.bz2 is built too + if [ -n "$WANT_VHD" ]; then + rm -f "$OUTDIR/$OUT_BASE.vhd" + echo "Removed $OUTDIR/$OUT_BASE.vhd, as $OUT_BASE-xen.vhd.bz2 was created" + fi + fi +fi + +# SHA512SUMS of the output files, when all is done; the entries of other files are kept +make_sha512sums() { + local f n h + local -A mine=() + local -a names=() + for f in "$(basename "$WORK_IMAGE")" "$OUT_BASE.vhd" "$OUT_BASE.ova" "$OUT_BASE-xen.vhd.bz2"; do + [ -f "$OUTDIR/$f" ] || continue + mine[$f]=1 + names+=("$f") + done + [ ${#names[@]} -gt 0 ] || return 0 + local tmp + tmp=$(mktemp "$OUTDIR/.sums.XXXXXX") + CLEAN_PATHS+=("$tmp") + if [ -f "$OUTDIR/SHA512SUMS" ]; then + while read -r h n; do + [ -n "$n" ] && [ -z "${mine[${n#\*}]:-}" ] && echo "$h $n" >> "$tmp" + done < "$OUTDIR/SHA512SUMS" + fi + (cd "$OUTDIR" && sha512sum "${names[@]}") >> "$tmp" + chmod a+r "$tmp" + mv -f "$tmp" "$OUTDIR/SHA512SUMS" + echo "Created $OUTDIR/SHA512SUMS" +} +make_sha512sums + +echo "== Done ==" +ls -lh "$WORK_IMAGE" "$OUTDIR/$OUT_BASE".vhd "$OUTDIR/$OUT_BASE".ova "$OUTDIR/$OUT_BASE"-xen.vhd.bz2 "$OUTDIR/SHA512SUMS" 2>/dev/null || true +exit 0 diff --git a/setup/db/templates.sql b/setup/db/templates.sql index 8e8f7b7f804d..a6dee71c8480 100755 --- a/setup/db/templates.sql +++ b/setup/db/templates.sql @@ -34,16 +34,16 @@ INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, VALUES (2, UUID(), 'centos53-x86_64', 'CentOS 5.3(64-bit) no GUI (XenServer)', 1, now(), now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/builtin/f59f18fb-ae94-4f97-afd2-f84755767aca.vhd.bz2', 'b63d854a9560c013142567bbae8d98cf', 0, 'CentOS 5.3(64-bit) no GUI (XenServer)', 'VHD', 12, 1, 1, 'XenServer', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, display_text, enable_password, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (4, UUID(), 'centos55-x86_64', 'CentOS 5.5(64-bit) no GUI (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/2.2.0/eec2209b-9875-3c8d-92be-c001bd8a0faf.qcow2.bz2', 'ed0e788280ff2912ea40f7f91ca7a249', 'CentOS 5.5(64-bit) no GUI (KVM)', 0, 'QCOW2', 112, 1, 1, 'KVM', 1); + VALUES (4, UUID(), 'debian13-x86_64-kvm', 'Debian 13 (64-bit) no GUI (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.qcow2', '{SHA-512}38389fb9e29448e6d7da460c0342217bd1a8aa6e32e6192fa1c6dbc6f7078f6c8210dde110d8631cc823b2762fb1738f8ae94b3e5a3774cf30a0cf11af69dfb8', 'Debian 13 (64-bit) no GUI (KVM)', 0, 'QCOW2', 184, 1, 1, 'KVM', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (5, UUID(), 'centos56-x86_64-xen', 'CentOS 5.6(64-bit) no GUI (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/builtin/centos56-x86_64.vhd.bz2', '905cec879afd9c9d22ecc8036131a180', 0, 'CentOS 5.6(64-bit) no GUI (XenServer)', 'VHD', 142, 1, 1, 'XenServer', 1); + VALUES (5, UUID(), 'debian13-x86_64-xen', 'Debian 13 (64-bit) no GUI (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs-xen.vhd.bz2', '{SHA-512}b2687bf7f739f49ed36b266e69d44ed49283b398497d85bda17f0eabc447934ba571629c01d94d8ac985f8133b218c8558cac22a88f78a3efd5f81bcf0689ab7', 0, 'Debian 13 (64-bit) no GUI (XenServer)', 'VHD', 184, 1, 1, 'XenServer', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) VALUES (6, UUID(), 'centos64-x64', 'CentOS 6.4(64-bit) GUI (Hyperv)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/4.3/centos6_4_64bit.vhd.bz2', 'eef6b9940ea3ed01221d963d4a012d0a', 0, 'CentOS 6.4 (64-bit) GUI (Hyperv)', 'VHD', 182, 1, 1, 'Hyperv', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (7, UUID(), 'centos53-x64', 'CentOS 5.3(64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/2.2.0/CentOS5.3-x86_64.ova', 'f6f881b7f2292948d8494db837fe0f47', 0, 'CentOS 5.3(64-bit) no GUI (vSphere)', 'OVA', 12, 1, 1, 'VMware', 1); + VALUES (7, UUID(), 'debian13-x64-vmware', 'Debian 13 (64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.ova', '{SHA-512}a090fafc53cbcf375937a8328500689543d9716a527f8a6059daee49712c56cc0e1553f386af8df1a650fa92819b235f4066df1076f242bf9a9e225f18a10b2b', 0, 'Debian 13 (64-bit) no GUI (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (1, UUID(), 'CentOS'); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (2, UUID(), 'Debian'); From 63eea5008110309eb4026b59096a48f37cace9c9 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 20 Oct 2026 09:54:22 +0200 Subject: [PATCH 02/13] schema: set the guest OS of the built-in Debian 13 templates to Debian 12 (64-bit) templates.sql cannot reference the 'Debian GNU/Linux 12 (64-bit)' guest OS because it is only added by a later upgrade script with an auto-generated id. Update the guest_os_id of the built-in Debian 13 templates (KVM, XenServer, VMware) in the 4.23.0.0 to 4.24.0.0 upgrade, looking the guest OS up by name. Also rename the VMware built-in template to debian13-x86_64-vmware to match the KVM and XenServer names, and drop the 4.3 upgrade statement that forced the XenServer built-in template back to guest OS 142. --- .../src/main/resources/META-INF/db/schema-421to430.sql | 1 - .../src/main/resources/META-INF/db/schema-42300to2400.sql | 8 ++++++++ setup/db/templates.sql | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-421to430.sql b/engine/schema/src/main/resources/META-INF/db/schema-421to430.sql index 80d32dab3fb3..838263c99915 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-421to430.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-421to430.sql @@ -129,7 +129,6 @@ ALTER TABLE `cloud`.`volumes` ADD COLUMN `hv_ss_reserve` int(32) unsigned DEFAUL UPDATE `cloud`.`disk_offering` SET `state`='Inactive' WHERE `removed` IS NOT NULL; UPDATE `cloud`.`disk_offering` SET `removed`=NULL; -UPDATE `cloud`.`vm_template` SET `guest_os_id`= "142" WHERE `id` = "5"; UPDATE `cloud`.`vm_template` SET `state`='Inactive' WHERE `removed` IS NOT NULL; UPDATE `cloud`.`vm_template` SET `state`='Active' WHERE `removed` IS NULL; UPDATE `cloud`.`vm_template` SET `removed`=NULL; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql index 3365f1f855ba..7cf85f3058c7 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -239,3 +239,11 @@ CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_details` ( PRIMARY KEY (`id`), CONSTRAINT `fk_instance_boot_group_details__group_id` FOREIGN KEY (`boot_group_id`) REFERENCES `instance_boot_group` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Set the guest OS of the built-in Debian 13 templates to 'Debian GNU/Linux 12 (64-bit)', which is only added by later upgrade scripts and is not available in templates.sql +UPDATE `cloud`.`vm_template` SET `guest_os_id` = (SELECT `id` FROM `cloud`.`guest_os` WHERE `display_name` = 'Debian GNU/Linux 12 (64-bit)' AND `removed` IS NULL LIMIT 1) + WHERE `id` = 4 AND `unique_name` = 'debian13-x86_64-kvm' AND `type` = 'BUILTIN'; +UPDATE `cloud`.`vm_template` SET `guest_os_id` = (SELECT `id` FROM `cloud`.`guest_os` WHERE `display_name` = 'Debian GNU/Linux 12 (64-bit)' AND `removed` IS NULL LIMIT 1) + WHERE `id` = 5 AND `unique_name` = 'debian13-x86_64-xen' AND `type` = 'BUILTIN'; +UPDATE `cloud`.`vm_template` SET `guest_os_id` = (SELECT `id` FROM `cloud`.`guest_os` WHERE `display_name` = 'Debian GNU/Linux 12 (64-bit)' AND `removed` IS NULL LIMIT 1) + WHERE `id` = 7 AND `unique_name` = 'debian13-x86_64-vmware' AND `type` = 'BUILTIN'; diff --git a/setup/db/templates.sql b/setup/db/templates.sql index a6dee71c8480..f4a65e1461a7 100755 --- a/setup/db/templates.sql +++ b/setup/db/templates.sql @@ -43,7 +43,7 @@ INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, VALUES (6, UUID(), 'centos64-x64', 'CentOS 6.4(64-bit) GUI (Hyperv)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/4.3/centos6_4_64bit.vhd.bz2', 'eef6b9940ea3ed01221d963d4a012d0a', 0, 'CentOS 6.4 (64-bit) GUI (Hyperv)', 'VHD', 182, 1, 1, 'Hyperv', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (7, UUID(), 'debian13-x64-vmware', 'Debian 13 (64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.ova', '{SHA-512}a090fafc53cbcf375937a8328500689543d9716a527f8a6059daee49712c56cc0e1553f386af8df1a650fa92819b235f4066df1076f242bf9a9e225f18a10b2b', 0, 'Debian 13 (64-bit) no GUI (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); + VALUES (7, UUID(), 'debian13-x86_64-vmware', 'Debian 13 (64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.ova', '{SHA-512}a090fafc53cbcf375937a8328500689543d9716a527f8a6059daee49712c56cc0e1553f386af8df1a650fa92819b235f4066df1076f242bf9a9e225f18a10b2b', 0, 'Debian 13 (64-bit) no GUI (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (1, UUID(), 'CentOS'); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (2, UUID(), 'Debian'); From caf3ef0805b192f3dcfb3ce1fabede3d4d61679e Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 21 Oct 2026 08:30:23 +0200 Subject: [PATCH 03/13] marvin: use the shared public tiny template in the smoke tests get_suitable_test_template now returns the shared public test template (tiny-kvm on KVM), registered by the admin only when it is missing, and falls back to the built-in template. It was already doing this for XenServer. The hypervisor argument is optional and is detected from the hosts when not given. Tests that only need a small guest now use it instead of the built-in template, and test_ipv4_routing, test_snapshots and test_restore_vm no longer register their own copy of the tiny template. Tests that depend on the built-in template are not changed, for example test_deploy_vm_root_resize (needs a template of at least 1 GiB), test_templates, test_sharedfs_lifecycle and test_vm_autoscaling. --- test/integration/smoke/test_affinity_groups.py | 8 ++++---- .../smoke/test_backup_recovery_dummy.py | 6 +++--- .../smoke/test_backup_recovery_kboss.py | 6 +++--- .../smoke/test_backup_recovery_nas.py | 8 ++++---- .../smoke/test_backup_recovery_veeam.py | 6 +++--- test/integration/smoke/test_console_endpoint.py | 8 ++++---- .../smoke/test_deploy_vm_extra_config_data.py | 4 ++-- .../smoke/test_gateway_on_shared_networks.py | 4 ++-- .../smoke/test_host_control_state.py | 6 +++--- .../smoke/test_instance_boot_group.py | 8 ++++---- test/integration/smoke/test_ipv4_routing.py | 14 +++++++------- test/integration/smoke/test_kms_lifecycle.py | 4 ++-- test/integration/smoke/test_migration.py | 4 ++-- test/integration/smoke/test_network.py | 6 +++--- .../smoke/test_persistent_network.py | 4 ++-- .../smoke/test_purge_expunged_vms.py | 6 +++--- test/integration/smoke/test_restore_vm.py | 9 +++------ test/integration/smoke/test_snapshots.py | 11 +++-------- test/integration/smoke/test_usage_events.py | 4 ++-- .../smoke/test_vm_deployment_planner.py | 12 ++++++------ ...test_vm_lifecycle_with_snapshot_or_volume.py | 8 +++----- test/integration/smoke/test_vm_schedule.py | 4 ++-- test/integration/smoke/test_vnf_templates.py | 4 ++-- tools/marvin/marvin/lib/common.py | 17 +++++++++++------ 24 files changed, 83 insertions(+), 88 deletions(-) diff --git a/test/integration/smoke/test_affinity_groups.py b/test/integration/smoke/test_affinity_groups.py index 26271f69e3fd..d6c37d107224 100644 --- a/test/integration/smoke/test_affinity_groups.py +++ b/test/integration/smoke/test_affinity_groups.py @@ -23,7 +23,7 @@ from marvin.lib.base import * from marvin.lib.common import (get_domain, get_zone, - get_template, + get_suitable_test_template, list_virtual_machines) from nose.plugins.attrib import attr @@ -44,14 +44,14 @@ def setUpClass(cls): cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) cls.hypervisor = cls.testClient.getHypervisorInfo() - cls.template = get_template( + cls.template = get_suitable_test_template( cls.apiclient, cls.zone.id, - cls.hypervisor + hypervisor=cls.hypervisor ) if cls.template == FAILED: - assert False, "get_template() failed to return template" + assert False, "get_suitable_test_template() failed to return template" cls.services["virtual_machine"]["zoneid"] = cls.zone.id diff --git a/test/integration/smoke/test_backup_recovery_dummy.py b/test/integration/smoke/test_backup_recovery_dummy.py index 3e2f16f8c686..ab57a797ea89 100644 --- a/test/integration/smoke/test_backup_recovery_dummy.py +++ b/test/integration/smoke/test_backup_recovery_dummy.py @@ -19,7 +19,7 @@ from marvin.cloudstackTestCase import cloudstackTestCase from marvin.lib.utils import (cleanup_resources) from marvin.lib.base import (Account, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering, Configurations, Backup, Volume) -from marvin.lib.common import (get_domain, get_zone, get_template) +from marvin.lib.common import (get_domain, get_zone, get_suitable_test_template) from nose.plugins.attrib import attr from marvin.codes import FAILED import time @@ -37,9 +37,9 @@ def setUpClass(cls): cls.services["mode"] = cls.zone.networktype cls.hypervisor = cls.testClient.getHypervisorInfo() cls.domain = get_domain(cls.api_client) - cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + cls.template = get_suitable_test_template(cls.api_client, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: - assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] cls.services["small"]["zoneid"] = cls.zone.id cls.services["small"]["template"] = cls.template.id cls._cleanup = [] diff --git a/test/integration/smoke/test_backup_recovery_kboss.py b/test/integration/smoke/test_backup_recovery_kboss.py index 10d823da195c..ee2c0bb9fb28 100644 --- a/test/integration/smoke/test_backup_recovery_kboss.py +++ b/test/integration/smoke/test_backup_recovery_kboss.py @@ -20,7 +20,7 @@ from marvin.lib import utils from marvin.lib.base import (Account, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering, Backup, Configurations, Volume, StoragePool) -from marvin.lib.common import (get_domain, get_zone, get_template) +from marvin.lib.common import (get_domain, get_zone, get_suitable_test_template) from nose.plugins.attrib import attr from marvin.codes import FAILED import time @@ -43,9 +43,9 @@ def setUpClass(cls): cls.services["mode"] = cls.zone.networktype cls.hypervisor = cls.testClient.getHypervisorInfo() cls.domain = get_domain(cls.api_client) - cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + cls.template = get_suitable_test_template(cls.api_client, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: - assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] cls.services["small"]["zoneid"] = cls.zone.id cls.services["small"]["template"] = cls.template.id cls._cleanup = [] diff --git a/test/integration/smoke/test_backup_recovery_nas.py b/test/integration/smoke/test_backup_recovery_nas.py index 96021b9edff8..ccee60a3806e 100644 --- a/test/integration/smoke/test_backup_recovery_nas.py +++ b/test/integration/smoke/test_backup_recovery_nas.py @@ -21,7 +21,7 @@ from marvin.lib.utils import (cleanup_resources) from marvin.lib.base import (Account, Network, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering, NetworkOffering, BackupRepository, Backup, Configurations, Volume, StoragePool) -from marvin.lib.common import (get_domain, get_zone, get_template) +from marvin.lib.common import (get_domain, get_zone, get_suitable_test_template) from nose.plugins.attrib import attr from marvin.codes import FAILED import time @@ -39,9 +39,9 @@ def setUpClass(cls): cls.services["mode"] = cls.zone.networktype cls.hypervisor = cls.testClient.getHypervisorInfo() cls.domain = get_domain(cls.api_client) - cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + cls.template = get_suitable_test_template(cls.api_client, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: - assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] cls.services["small"]["zoneid"] = cls.zone.id cls.services["small"]["template"] = cls.template.id cls._cleanup = [] @@ -245,7 +245,7 @@ def test_vm_backup_create_vm_from_backup_in_another_zone(self): return self.destZone = zones[1] - template = get_template(self.api_client, self.destZone.id, self.services["ostype"]) + template = get_suitable_test_template(self.api_client, self.destZone.id, self.services["ostype"]) list_isolated_network_offerings_response = NetworkOffering.list( self.apiclient, diff --git a/test/integration/smoke/test_backup_recovery_veeam.py b/test/integration/smoke/test_backup_recovery_veeam.py index f99b3db17d44..b36f311b60d0 100644 --- a/test/integration/smoke/test_backup_recovery_veeam.py +++ b/test/integration/smoke/test_backup_recovery_veeam.py @@ -20,7 +20,7 @@ from marvin.lib.utils import wait_until from marvin.lib.base import (Account, ServiceOffering, DiskOffering, Volume, VirtualMachine, BackupOffering, Configurations, Backup, BackupSchedule) -from marvin.lib.common import (get_domain, get_zone, get_template) +from marvin.lib.common import (get_domain, get_zone, get_suitable_test_template) from nose.plugins.attrib import attr from marvin.codes import FAILED @@ -39,9 +39,9 @@ def setUpClass(cls): cls.services["mode"] = cls.zone.networktype cls.hypervisor = cls.testClient.getHypervisorInfo() cls.domain = get_domain(cls.apiclient) - cls.template = get_template(cls.apiclient, cls.zone.id, cls.services["ostype"]) + cls.template = get_suitable_test_template(cls.apiclient, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: - assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] cls.services["small"]["zoneid"] = cls.zone.id cls.services["small"]["template"] = cls.template.id cls._cleanup = [] diff --git a/test/integration/smoke/test_console_endpoint.py b/test/integration/smoke/test_console_endpoint.py index 84b893e57012..afd4631d87bc 100644 --- a/test/integration/smoke/test_console_endpoint.py +++ b/test/integration/smoke/test_console_endpoint.py @@ -21,7 +21,7 @@ from marvin.lib.base import * from marvin.lib.common import (get_domain, get_zone, - get_template) + get_suitable_test_template) from nose.plugins.attrib import attr class TestConsoleEndpoint(cloudstackTestCase): @@ -36,14 +36,14 @@ def setUpClass(cls): cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) cls.hypervisor = cls.testClient.getHypervisorInfo() - cls.template = get_template( + cls.template = get_suitable_test_template( cls.apiclient, cls.zone.id, - cls.hypervisor + hypervisor=cls.hypervisor ) if cls.template == FAILED: - assert False, "get_template() failed to return template" + assert False, "get_suitable_test_template() failed to return template" cls.services["virtual_machine"]["zoneid"] = cls.zone.id diff --git a/test/integration/smoke/test_deploy_vm_extra_config_data.py b/test/integration/smoke/test_deploy_vm_extra_config_data.py index 98e60f9d287c..c224e151d194 100644 --- a/test/integration/smoke/test_deploy_vm_extra_config_data.py +++ b/test/integration/smoke/test_deploy_vm_extra_config_data.py @@ -35,7 +35,7 @@ Host) from marvin.lib.common import (get_domain, get_zone, - get_template, + get_suitable_test_template, list_hosts) from marvin.lib.utils import * from nose.plugins.attrib import attr @@ -155,7 +155,7 @@ def deploy_vm(self, hypervisor, extra_config=None): if extra_config is not None: cmd.extraconfig = extra_config - template = get_template( + template = get_suitable_test_template( self.apiclient, self.zone.id, hypervisor=hypervisor diff --git a/test/integration/smoke/test_gateway_on_shared_networks.py b/test/integration/smoke/test_gateway_on_shared_networks.py index dd2982f569db..bcf59a4bf093 100644 --- a/test/integration/smoke/test_gateway_on_shared_networks.py +++ b/test/integration/smoke/test_gateway_on_shared_networks.py @@ -29,7 +29,7 @@ ) from marvin.lib.common import (get_domain, get_zone, - get_template, + get_suitable_test_template, find_storage_pool_type, get_builtin_template_info) from marvin.codes import ( @@ -69,7 +69,7 @@ def setUpClass(cls): cls.services["template"]["hypervisor"] = builtin_info[1] cls.services["template"]["format"] = builtin_info[2] - cls.template = get_template( + cls.template = get_suitable_test_template( cls.apiclient, cls.zone.id ) diff --git a/test/integration/smoke/test_host_control_state.py b/test/integration/smoke/test_host_control_state.py index 4b8409ecc276..7edb39322660 100644 --- a/test/integration/smoke/test_host_control_state.py +++ b/test/integration/smoke/test_host_control_state.py @@ -25,7 +25,7 @@ from marvin.cloudstackTestCase import cloudstackTestCase from marvin.lib.common import (get_domain, get_zone, - get_template, + get_suitable_test_template, list_hosts, list_routers, list_ssvms, @@ -55,10 +55,10 @@ def setUpClass(cls): cls.hypervisor = cls.testClient.getHypervisorInfo() cls.hostConfig = cls.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0].__dict__["hosts"][0].__dict__ - cls.template = get_template( + cls.template = get_suitable_test_template( cls.apiclient, cls.zone.id, - cls.hypervisor + hypervisor=cls.hypervisor ) cls.services["virtual_machine"]["zoneid"] = cls.zone.id diff --git a/test/integration/smoke/test_instance_boot_group.py b/test/integration/smoke/test_instance_boot_group.py index 69c0e333637e..e0c9b9beb7dd 100644 --- a/test/integration/smoke/test_instance_boot_group.py +++ b/test/integration/smoke/test_instance_boot_group.py @@ -26,7 +26,7 @@ InstanceBootGroupReadinessRule) from marvin.lib.common import (get_domain, get_zone, - get_template) + get_suitable_test_template) from marvin.lib.utils import (random_gen) from marvin.cloudstackException import CloudstackAPIException from marvin.codes import FAILED @@ -51,13 +51,13 @@ def setUpClass(cls): cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests()) cls.hypervisor = testClient.getHypervisorInfo() - cls.template = get_template( + cls.template = get_suitable_test_template( cls.apiclient, - zone_id=cls.zone.id, + zoneid=cls.zone.id, hypervisor=cls.hypervisor ) if cls.template == FAILED: - assert False, "get_template() failed to return template" + assert False, "get_suitable_test_template() failed to return template" cls._cleanup = [] cls.account = Account.create( diff --git a/test/integration/smoke/test_ipv4_routing.py b/test/integration/smoke/test_ipv4_routing.py index 124be678965c..d6461610b0bd 100644 --- a/test/integration/smoke/test_ipv4_routing.py +++ b/test/integration/smoke/test_ipv4_routing.py @@ -22,10 +22,11 @@ import time from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.codes import FAILED from marvin.lib.base import ZoneIpv4Subnet, Domain, Account, ServiceOffering, NetworkOffering, VpcOffering, Network, \ - Ipv4SubnetForGuestNetwork, VirtualMachine, VPC, NetworkACLList, NetworkACL, RoutingFirewallRule, Template, ASNRange, \ + Ipv4SubnetForGuestNetwork, VirtualMachine, VPC, NetworkACLList, NetworkACL, RoutingFirewallRule, ASNRange, \ BgpPeer, Router -from marvin.lib.common import get_domain, get_zone, list_routers, list_hosts +from marvin.lib.common import get_domain, get_zone, get_test_template, list_routers, list_hosts from marvin.lib.utils import get_host_credentials, get_process_status from nose.plugins.attrib import attr @@ -167,11 +168,10 @@ def setUpClass(cls): cls.logger.setLevel(logging.DEBUG) cls.logger.addHandler(cls.stream_handler) - # 0. register template - cls.template = Template.register(cls.apiclient, cls.services["test_templates"][cls.hypervisor.lower()], - zoneid=cls.zone.id, hypervisor=cls.hypervisor.lower()) - cls.template.download(cls.apiclient) - cls._cleanup.append(cls.template) + # 0. get the shared public test template + cls.template = get_test_template(cls.apiclient, cls.zone.id, cls.hypervisor) + if cls.template == FAILED: + assert False, "get_test_template() failed to return template" # 1.1 create subnet for zone cls.subnet_1 = ZoneIpv4Subnet.create( diff --git a/test/integration/smoke/test_kms_lifecycle.py b/test/integration/smoke/test_kms_lifecycle.py index 783a2af72e54..ede5dede7524 100644 --- a/test/integration/smoke/test_kms_lifecycle.py +++ b/test/integration/smoke/test_kms_lifecycle.py @@ -42,7 +42,7 @@ ServiceOffering, Volume, ) -from marvin.lib.common import get_zone, get_domain, get_template +from marvin.lib.common import get_zone, get_domain, get_suitable_test_template from marvin.lib.utils import cleanup_resources from nose.plugins.attrib import attr @@ -433,7 +433,7 @@ def test_12_deploy_vm_with_root_disk_encryption(self): key = self._create_kms_key(name=_random_name("vm-root-key"), profile_id=self.default_profile.id, apiclient=self.user_apiclient) # 2. Get a template and a service offering - template = get_template( + template = get_suitable_test_template( self.apiclient, self.zone.id, self.test_client.getParsedTestDataConfig().get("ostype", "CentOS 7.0 (64-bit)") diff --git a/test/integration/smoke/test_migration.py b/test/integration/smoke/test_migration.py index cadb50692a9b..24b74d73f526 100644 --- a/test/integration/smoke/test_migration.py +++ b/test/integration/smoke/test_migration.py @@ -31,7 +31,7 @@ ) from marvin.lib.common import (get_domain, get_zone, - get_template, + get_suitable_test_template, list_virtual_machines) from marvin.lib.utils import (get_hypervisor_type, @@ -90,7 +90,7 @@ def setUpClass(cls): cls.zone = get_zone( cls.api_client, cls.testClient.getZoneForTests()) - cls.template = get_template( + cls.template = get_suitable_test_template( cls.api_client, cls.zone.id, cls.test_data["ostype"] diff --git a/test/integration/smoke/test_network.py b/test/integration/smoke/test_network.py index 3a0db0feafee..a47a5c37e776 100644 --- a/test/integration/smoke/test_network.py +++ b/test/integration/smoke/test_network.py @@ -45,7 +45,7 @@ from marvin.lib.common import (get_domain, get_free_vlan, get_zone, - get_template, + get_suitable_test_template, get_test_template, list_hosts, list_publicIP, @@ -1925,8 +1925,8 @@ def setUpClass(cls): # Get Zone, Domain and templates cls.domain = get_domain(cls.apiclient) cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) - cls.template = get_template(cls.apiclient, cls.zone.id, - cls.services["ostype"]) + cls.template = get_suitable_test_template(cls.apiclient, cls.zone.id, + cls.services["ostype"]) cls.services["virtual_machine"]["zoneid"] = cls.zone.id cls.services["virtual_machine"]["template"] = cls.template.id diff --git a/test/integration/smoke/test_persistent_network.py b/test/integration/smoke/test_persistent_network.py index 5ca3d6a506aa..d8ceaad6edc2 100644 --- a/test/integration/smoke/test_persistent_network.py +++ b/test/integration/smoke/test_persistent_network.py @@ -30,7 +30,7 @@ ServiceOffering, NetworkOffering) from marvin.lib.common import (get_zone, - get_template, + get_suitable_test_template, verifyNetworkState, wait_for_cleanup, list_routers, list_hosts) from nose.plugins.attrib import attr @@ -74,7 +74,7 @@ def setUpClass(cls): 0].__dict__ # Get Zone and templates cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) - cls.template = get_template( + cls.template = get_suitable_test_template( cls.api_client, cls.zone.id, cls.services["ostype"] diff --git a/test/integration/smoke/test_purge_expunged_vms.py b/test/integration/smoke/test_purge_expunged_vms.py index 4a5da9d1c3ac..3939a30284b8 100644 --- a/test/integration/smoke/test_purge_expunged_vms.py +++ b/test/integration/smoke/test_purge_expunged_vms.py @@ -33,7 +33,7 @@ Configurations) from marvin.lib.common import (get_domain, get_zone, - get_template) + get_suitable_test_template) from marvin.lib.utils import (random_gen) from marvin.lib.decoratorGenerators import skipTestIf from marvin.sshClient import SshClient @@ -75,12 +75,12 @@ def setUpClass(cls): cls.logger = logging.getLogger('TestPurgeExpungedVms') cls.logger.setLevel(logging.DEBUG) - template = get_template( + template = get_suitable_test_template( cls.apiclient, cls.zone.id, cls.services["ostype"]) if template == FAILED: - assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] # Set Zones and disk offerings cls.services["small"]["zoneid"] = cls.zone.id diff --git a/test/integration/smoke/test_restore_vm.py b/test/integration/smoke/test_restore_vm.py index b961bee39f28..a97395d7b55a 100644 --- a/test/integration/smoke/test_restore_vm.py +++ b/test/integration/smoke/test_restore_vm.py @@ -22,7 +22,7 @@ # Import Local Modules from marvin.cloudstackTestCase import cloudstackTestCase from marvin.lib.base import (VirtualMachine, Volume, DiskOffering, ServiceOffering, Template) -from marvin.lib.common import (get_zone, get_domain, list_storage_pools) +from marvin.lib.common import (get_zone, get_domain, get_test_template, list_storage_pools) from nose.plugins.attrib import attr _multiprocess_shared_ = True @@ -51,11 +51,8 @@ def setUpClass(cls): cls.disk_offering = DiskOffering.create(cls.apiclient, cls.services["disk_offering"], disksize='8') cls._cleanup.append(cls.disk_offering) - template_t1 = Template.register(cls.apiclient, cls.services["test_templates"][ - cls.hypervisor.lower() if cls.hypervisor.lower() != 'simulator' else 'xenserver'], - zoneid=cls.zone.id, hypervisor=cls.hypervisor.lower()) - cls._cleanup.append(template_t1) - template_t1.download(cls.apiclient) + # the first template is the shared public test template, the restore needs another one + template_t1 = get_test_template(cls.apiclient, cls.zone.id, cls.hypervisor) cls.template_t1 = Template.list(cls.apiclient, templatefilter='all', id=template_t1.id)[0] template_t2 = Template.register(cls.apiclient, cls.services["test_templates"][ diff --git a/test/integration/smoke/test_snapshots.py b/test/integration/smoke/test_snapshots.py index b1a2569d9694..b7ea437285b4 100644 --- a/test/integration/smoke/test_snapshots.py +++ b/test/integration/smoke/test_snapshots.py @@ -434,14 +434,9 @@ def setUpClass(cls): UserName=cls.account.name, DomainName=cls.account.domain ) - cls.template = Template.register( - cls.userapiclient, - cls.services["test_templates"][cls.hypervisor.lower()], - zoneid=cls.zone.id, - hypervisor=cls.hypervisor - ) - cls._cleanup.append(cls.template) - cls.template.download(cls.apiclient) + cls.template = get_test_template(cls.apiclient, cls.zone.id, cls.hypervisor) + if cls.template == FAILED: + assert False, "get_test_template() failed to return template" cls.ostypeid = cls.template.ostypeid cls.virtual_machine = VirtualMachine.create( cls.userapiclient, diff --git a/test/integration/smoke/test_usage_events.py b/test/integration/smoke/test_usage_events.py index 94bf75ac3e31..890c93163b72 100644 --- a/test/integration/smoke/test_usage_events.py +++ b/test/integration/smoke/test_usage_events.py @@ -24,7 +24,7 @@ VirtualMachine) from marvin.lib.common import (get_domain, get_zone, - get_template) + get_suitable_test_template) from marvin.codes import (PASS) class TestUsageEvents(cloudstackTestCase): @@ -39,7 +39,7 @@ def setUpClass(cls): cls.domain = get_domain(cls.apiclient) cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests()) - cls.template = get_template( + cls.template = get_suitable_test_template( cls.apiclient, cls.zone.id, cls.testdata["ostype"]) diff --git a/test/integration/smoke/test_vm_deployment_planner.py b/test/integration/smoke/test_vm_deployment_planner.py index e8d24cbf31e5..418264f5533b 100644 --- a/test/integration/smoke/test_vm_deployment_planner.py +++ b/test/integration/smoke/test_vm_deployment_planner.py @@ -24,7 +24,7 @@ Host, Pod, Cluster) from marvin.lib.common import (get_domain, get_zone, - get_template) + get_suitable_test_template) from marvin.lib.utils import cleanup_resources from nose.plugins.attrib import attr @@ -67,7 +67,7 @@ def tearDownClass(cls): def deploy_vm(self, destination_id): cmd = deployVirtualMachine.deployVirtualMachineCmd() - template = get_template( + template = get_suitable_test_template( self.apiclient, self.zone.id ) @@ -112,7 +112,7 @@ def test_02_deploy_vm_on_specific_cluster(self): target_id = target_cluster.id cluster_hypervisor = target_cluster.hypervisortype - template = get_template( + template = get_suitable_test_template( self.apiclient, hypervisor=cluster_hypervisor ) @@ -153,7 +153,7 @@ def test_03_deploy_vm_on_specific_pod(self): cmd.zoneid = target_pod.zoneid cmd.serviceofferingid = self.service_offering.id - template = get_template( + template = get_suitable_test_template( self.apiclient, hypervisor=host[0].hypervisortype ) @@ -193,7 +193,7 @@ def test_04_deploy_vm_on_host_override_pod_and_cluster(self): # Required parameters cmd.zoneid = self.zone.id cmd.serviceofferingid = self.service_offering.id - template = get_template(self.apiclient, zone_id=self.zone.id, hypervisor=host.hypervisor) + template = get_suitable_test_template(self.apiclient, zoneid=self.zone.id, hypervisor=host.hypervisor) cmd.templateid = template.id # Add optional deployment params @@ -229,7 +229,7 @@ def test_05_deploy_vm_on_cluster_override_pod(self): # Required parameters cmd.zoneid = self.zone.id cmd.serviceofferingid = self.service_offering.id - template = get_template(self.apiclient, zone_id=self.zone.id, hypervisor=clusters[0].hypervisortype) + template = get_suitable_test_template(self.apiclient, zoneid=self.zone.id, hypervisor=clusters[0].hypervisortype) cmd.templateid = template.id # Add optional deployment params diff --git a/test/integration/smoke/test_vm_lifecycle_with_snapshot_or_volume.py b/test/integration/smoke/test_vm_lifecycle_with_snapshot_or_volume.py index 871cf25a4021..c67914b583f5 100644 --- a/test/integration/smoke/test_vm_lifecycle_with_snapshot_or_volume.py +++ b/test/integration/smoke/test_vm_lifecycle_with_snapshot_or_volume.py @@ -33,7 +33,7 @@ Template) from marvin.lib.common import (get_domain, get_zone, - get_template, + get_suitable_test_template, list_hosts, list_volumes, list_storage_pools) @@ -60,11 +60,9 @@ def setUpClass(cls): if cls.hypervisor.lower() != "kvm": raise unittest.SkipTest("Only KVM hypervisor is supported for deployment of a VM with volume/snapshot") - cls.template = get_template( + cls.template = get_suitable_test_template( cls.apiclient, - cls.zone.id, - account="system" - ) + cls.zone.id) if cls.template == FAILED: assert False, "get_template failed to return template with description [system]" diff --git a/test/integration/smoke/test_vm_schedule.py b/test/integration/smoke/test_vm_schedule.py index 1e476c55fc87..99f590e9d3f8 100644 --- a/test/integration/smoke/test_vm_schedule.py +++ b/test/integration/smoke/test_vm_schedule.py @@ -18,7 +18,7 @@ """ from marvin.cloudstackTestCase import cloudstackTestCase from marvin.lib.base import Account, ServiceOffering, VirtualMachine, ResourceSchedule -from marvin.lib.common import get_domain, get_zone, get_template +from marvin.lib.common import get_domain, get_zone, get_suitable_test_template from marvin.lib.utils import cleanup_resources # Import Local Modules @@ -96,7 +96,7 @@ def setUpClass(cls): cls.domain = get_domain(cls.api_client) cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) cls.services["mode"] = cls.zone.networktype - template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + template = get_suitable_test_template(cls.api_client, cls.zone.id, cls.services["ostype"]) cls.services["domainid"] = cls.domain.id cls.services["server"]["zoneid"] = cls.zone.id diff --git a/test/integration/smoke/test_vnf_templates.py b/test/integration/smoke/test_vnf_templates.py index f963ce41b384..757f128ff182 100644 --- a/test/integration/smoke/test_vnf_templates.py +++ b/test/integration/smoke/test_vnf_templates.py @@ -28,7 +28,7 @@ VnfAppliance, VnfTemplate, Zone) -from marvin.lib.common import get_zone, get_template +from marvin.lib.common import get_zone, get_suitable_test_template from nose.plugins.attrib import attr import time @@ -55,7 +55,7 @@ def setUpClass(cls): cls.hypervisor = cls.testClient.getHypervisorInfo() zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) cls.zone = Zone(zone.__dict__) - cls.template = get_template(cls.apiclient, cls.zone.id) + cls.template = get_suitable_test_template(cls.apiclient, cls.zone.id) cls.domain = Domain.create( cls.apiclient, diff --git a/tools/marvin/marvin/lib/common.py b/tools/marvin/marvin/lib/common.py index cc773654faa0..5e574b742c1b 100644 --- a/tools/marvin/marvin/lib/common.py +++ b/tools/marvin/marvin/lib/common.py @@ -74,7 +74,8 @@ xsplit, get_process_status, random_gen, - format_volume_to_ext3) + format_volume_to_ext3, + get_hypervisor_type) from marvin.lib.base import (PhysicalNetwork, PublicIPAddress, NetworkOffering, @@ -516,19 +517,23 @@ def get_windows_template( return FAILED -def get_suitable_test_template(apiclient, zoneid, ostypeid, hypervisor, deploy_as_is=False): +def get_suitable_test_template(apiclient, zoneid=None, ostypeid=None, hypervisor=None, deploy_as_is=False): ''' @Name : get_suitable_test_template @Desc : Retrieves the test template information based upon inputs provided - For Xenserver, get_test_template is used for retrieving the template - while get_template is used for other hypervisors or when - get_test_template fails + For XenServer and KVM, get_test_template is used for retrieving the + shared public test template (tiny), which is registered by the + admin only when it is missing. get_template is used for other + hypervisors or when get_test_template fails + The hypervisor is detected from the hosts when it is not given @Input : returns a template" @Output : FAILED in case of any failure template Information matching the inputs ''' template = FAILED - if hypervisor.lower() in ["xenserver"] or (hypervisor.lower() in ["vmware"] and deploy_as_is): + if hypervisor is None: + hypervisor = get_hypervisor_type(apiclient) + if hypervisor.lower() in ["xenserver", "kvm"] or (hypervisor.lower() in ["vmware"] and deploy_as_is): template = get_test_template( apiclient, zoneid, From e097aac507d45ec9f098753fc98efe1bdef87be2 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 21 Oct 2026 14:28:38 +0200 Subject: [PATCH 04/13] test_snapshots: register the template in the standalone backup class again TestSnapshotStandaloneBackup takes a snapshot, expunges the VM and then deletes its template, to test the snapshot without its template. It needs its own template, so it cannot use the shared public test template. --- test/integration/smoke/test_snapshots.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/integration/smoke/test_snapshots.py b/test/integration/smoke/test_snapshots.py index b7ea437285b4..b1a2569d9694 100644 --- a/test/integration/smoke/test_snapshots.py +++ b/test/integration/smoke/test_snapshots.py @@ -434,9 +434,14 @@ def setUpClass(cls): UserName=cls.account.name, DomainName=cls.account.domain ) - cls.template = get_test_template(cls.apiclient, cls.zone.id, cls.hypervisor) - if cls.template == FAILED: - assert False, "get_test_template() failed to return template" + cls.template = Template.register( + cls.userapiclient, + cls.services["test_templates"][cls.hypervisor.lower()], + zoneid=cls.zone.id, + hypervisor=cls.hypervisor + ) + cls._cleanup.append(cls.template) + cls.template.download(cls.apiclient) cls.ostypeid = cls.template.ostypeid cls.virtual_machine = VirtualMachine.create( cls.userapiclient, From 9013c16f17f37d8746acb3d26c445286badbeefc Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 21 Oct 2026 21:13:21 +0200 Subject: [PATCH 05/13] build-acs-image.sh: install nfs-common in the Debian and Ubuntu images The Debian 13 cloud image has no NFS client, so mounting an NFS share in the guest fails with "NFS: mount program didn't pass remote address". The shared file system smoke test mounts the share in the guest. Install nfs-common with the other guest packages. --- scripts/util/build-acs-image.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/util/build-acs-image.sh b/scripts/util/build-acs-image.sh index 57fb7ef24ced..15ad42d03d5b 100755 --- a/scripts/util/build-acs-image.sh +++ b/scripts/util/build-acs-image.sh @@ -333,7 +333,8 @@ detect_os() { local base_cleanup='journalctl --vacuum-time=1s; rm -rf /var/log/*; rm -f /root/.bash_history' case "$OS_FAMILY" in debian) - PACKAGES="qemu-guest-agent,open-vm-tools,xenstore-utils,libxenstore4" + # nfs-common is the NFS client, the cloud images do not have it and the tests mount NFS in the guest + PACKAGES="qemu-guest-agent,open-vm-tools,xenstore-utils,libxenstore4,nfs-common" CLEANUP="apt clean; apt autoclean; apt autoremove --purge -y; $base_cleanup" ;; rhel) # there is no xenstore package in the EL repositories From a0b958c5b44dd79737fb96800f88954e90094722 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 22 Oct 2026 11:10:37 +0200 Subject: [PATCH 06/13] smoke tests: use the Debian 13 built-in template on all hypervisors Moving the smoke tests to the shared tiny-kvm template broke the tests that need more from the guest. Run these on the Debian 13 built-in template, taken with get_template on every hypervisor. - test_attach_multiple_volumes, test_volumes: they detach data disks from a running VM, which the tiny-kvm guest never releases. - test_backup_recovery_nas, test_backup_recovery_kboss: they create a VM from a backup and expect the files written in the guest to be in it. - test_primary_storage: it puts a primary storage with running VMs in maintenance, which needs a guest that shuts down on request. - test_volumes: use the 512 MiB big offering, as the Debian 13 guest does not boot with the 128 MiB tiny or the 256 MiB small offering. - test_vm_autoscaling: the scale up condition needs at least 1 percent CPU, and an idle Debian 13 VM uses about 0.4 percent. Start a CPU loop from the template user data. --- .../smoke/test_attach_multiple_volumes.py | 11 ++--- .../smoke/test_backup_recovery_kboss.py | 6 +-- .../smoke/test_backup_recovery_nas.py | 8 ++-- .../integration/smoke/test_primary_storage.py | 18 ++------ test/integration/smoke/test_vm_autoscaling.py | 5 ++- test/integration/smoke/test_volumes.py | 41 ++++++------------- 6 files changed, 31 insertions(+), 58 deletions(-) diff --git a/test/integration/smoke/test_attach_multiple_volumes.py b/test/integration/smoke/test_attach_multiple_volumes.py index 9357e4d65090..89bf780140f0 100644 --- a/test/integration/smoke/test_attach_multiple_volumes.py +++ b/test/integration/smoke/test_attach_multiple_volumes.py @@ -27,9 +27,8 @@ Volume, DiskOffering, ) -from marvin.lib.common import (get_domain, +from marvin.lib.common import (get_template, get_domain, get_zone, - get_suitable_test_template, find_storage_pool_type) from marvin.codes import ( PASS, @@ -69,13 +68,9 @@ def setUpClass(cls): ) cls._cleanup.append(cls.disk_offering) - template = get_suitable_test_template( - cls.apiclient, - cls.zone.id, - cls.services["ostype"], - cls.hypervisor) + template = get_template(cls.apiclient, cls.zone.id, cls.services["ostype"]) if template == FAILED: - assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] cls.services["domainid"] = cls.domain.id cls.services["zoneid"] = cls.zone.id diff --git a/test/integration/smoke/test_backup_recovery_kboss.py b/test/integration/smoke/test_backup_recovery_kboss.py index ee2c0bb9fb28..971f379f6db5 100644 --- a/test/integration/smoke/test_backup_recovery_kboss.py +++ b/test/integration/smoke/test_backup_recovery_kboss.py @@ -20,7 +20,7 @@ from marvin.lib import utils from marvin.lib.base import (Account, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering, Backup, Configurations, Volume, StoragePool) -from marvin.lib.common import (get_domain, get_zone, get_suitable_test_template) +from marvin.lib.common import (get_template, get_domain, get_zone) from nose.plugins.attrib import attr from marvin.codes import FAILED import time @@ -43,9 +43,9 @@ def setUpClass(cls): cls.services["mode"] = cls.zone.networktype cls.hypervisor = cls.testClient.getHypervisorInfo() cls.domain = get_domain(cls.api_client) - cls.template = get_suitable_test_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: - assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] cls.services["small"]["zoneid"] = cls.zone.id cls.services["small"]["template"] = cls.template.id cls._cleanup = [] diff --git a/test/integration/smoke/test_backup_recovery_nas.py b/test/integration/smoke/test_backup_recovery_nas.py index ccee60a3806e..31169e188739 100644 --- a/test/integration/smoke/test_backup_recovery_nas.py +++ b/test/integration/smoke/test_backup_recovery_nas.py @@ -21,7 +21,7 @@ from marvin.lib.utils import (cleanup_resources) from marvin.lib.base import (Account, Network, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering, NetworkOffering, BackupRepository, Backup, Configurations, Volume, StoragePool) -from marvin.lib.common import (get_domain, get_zone, get_suitable_test_template) +from marvin.lib.common import (get_template, get_domain, get_zone) from nose.plugins.attrib import attr from marvin.codes import FAILED import time @@ -39,9 +39,9 @@ def setUpClass(cls): cls.services["mode"] = cls.zone.networktype cls.hypervisor = cls.testClient.getHypervisorInfo() cls.domain = get_domain(cls.api_client) - cls.template = get_suitable_test_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: - assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] cls.services["small"]["zoneid"] = cls.zone.id cls.services["small"]["template"] = cls.template.id cls._cleanup = [] @@ -245,7 +245,7 @@ def test_vm_backup_create_vm_from_backup_in_another_zone(self): return self.destZone = zones[1] - template = get_suitable_test_template(self.api_client, self.destZone.id, self.services["ostype"]) + template = get_template(self.api_client, self.destZone.id, self.services["ostype"]) list_isolated_network_offerings_response = NetworkOffering.list( self.apiclient, diff --git a/test/integration/smoke/test_primary_storage.py b/test/integration/smoke/test_primary_storage.py index 477d3317ad69..569335e7e77e 100644 --- a/test/integration/smoke/test_primary_storage.py +++ b/test/integration/smoke/test_primary_storage.py @@ -42,14 +42,9 @@ def setUp(self): self.pod = get_pod(self.apiclient, self.zone.id) self.hypervisor = self.testClient.getHypervisorInfo() self.domain = get_domain(self.apiclient) - self.template = get_suitable_test_template( - self.apiclient, - self.zone.id, - self.services["ostype"], - self.hypervisor - ) + self.template = get_template(self.apiclient, self.zone.id, self.services["ostype"]) if self.template == FAILED: - assert False, "get_suitable_test_template() failed to return template with description %s" % self.services["ostype"] + assert False, "get_template() failed to return template with description %s" % self.services["ostype"] self.excluded_pools = [] storage_pool_list = StoragePool.list(self.apiclient, zoneid=self.zone.id) @@ -392,14 +387,9 @@ def setUpClass(cls): cls.pod = get_pod(cls.apiclient, cls.zone.id) cls.hypervisor = testClient.getHypervisorInfo() cls.domain = get_domain(cls.apiclient) - cls.template = get_suitable_test_template( - cls.apiclient, - cls.zone.id, - cls.services["ostype"], - cls.hypervisor - ) + cls.template = get_template(cls.apiclient, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: - assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] cls.services["virtual_machine"]["zoneid"] = cls.zone.id cls.services["virtual_machine"]["template"] = cls.template.id cls.services["storage_tags"] = StorageTagsServices().storage_tags diff --git a/test/integration/smoke/test_vm_autoscaling.py b/test/integration/smoke/test_vm_autoscaling.py index dd507ece0391..1b41e8b3b3b4 100644 --- a/test/integration/smoke/test_vm_autoscaling.py +++ b/test/integration/smoke/test_vm_autoscaling.py @@ -220,11 +220,14 @@ def setUpClass(cls): cls.templateUserdata = UserData.register( cls.apiclient, name="TemplateUserdata", - userdata="IyMgdGVtcGxhdGU6IGppbmphCiNjbG91ZC1jb25maWcKcnVuY21kOgogICAgLSBlY2hvICdrZXkge3sgZHMubWV0YV9kYXRhLmtleTEgfX0nID4+IC9yb290L2luc3RhbmNlX21ldGFkYXRhCgo=", + userdata="IyMgdGVtcGxhdGU6IGppbmphCiNjbG91ZC1jb25maWcKcnVuY21kOgogICAgLSBlY2hvICdrZXkge3sgZHMubWV0YV9kYXRhLmtleTEgfX0nID4+IC9yb290L2luc3RhbmNlX21ldGFkYXRhCiAgICAtIG5vaHVwIHNoIC1jICd3aGlsZSB0cnVlOyBkbyA6OyBkb25lJyA+IC9kZXYvbnVsbCAyPiYxICYKCg==", # ## template: jinja # #cloud-config # runcmd: # - echo 'key {{ ds.meta_data.key1 }}' >> /root/instance_metadata + # - nohup sh -c 'while true; do :; done' > /dev/null 2>&1 & + # (the CPU loop keeps the VM above the 1 percent CPU threshold of the scale up condition, + # an idle Debian 13 VM only uses about 0.4 percent) # account=cls.regular_user.name, domainid=cls.regular_user.domainid diff --git a/test/integration/smoke/test_volumes.py b/test/integration/smoke/test_volumes.py index ad1c359c27b5..441f10c2c034 100644 --- a/test/integration/smoke/test_volumes.py +++ b/test/integration/smoke/test_volumes.py @@ -38,8 +38,7 @@ Host, DiskOffering, StoragePool) -from marvin.lib.common import (get_domain, - get_suitable_test_template, +from marvin.lib.common import (get_template, get_domain, get_zone, find_storage_pool_type, get_pod, @@ -94,14 +93,9 @@ def setUpClass(cls): ) cls._cleanup.append(cls.custom_disk_offering) - template = get_suitable_test_template( - cls.apiclient, - cls.zone.id, - cls.services["ostype"], - cls.hypervisor - ) + template = get_template(cls.apiclient, cls.zone.id, cls.services["ostype"]) if template == FAILED: - assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] cls.services["domainid"] = cls.domain.id cls.services["zoneid"] = cls.zone.id @@ -115,9 +109,10 @@ def setUpClass(cls): domainid=cls.domain.id ) cls._cleanup.append(cls.account) + # the Debian 13 built-in template does not boot with the 128 MiB of the tiny offering cls.service_offering = ServiceOffering.create( cls.apiclient, - cls.services["service_offerings"]["tiny"] + cls.services["service_offerings"]["big"] ) cls.virtual_machine = VirtualMachine.create( cls.apiclient, @@ -307,15 +302,9 @@ def setUpClass(cls): ) cls._cleanup.append(cls.custom_resized_disk_offering) - cls.template = get_suitable_test_template( - cls.apiclient, - cls.zone.id, - cls.services["ostype"], - cls.hypervisor, - deploy_as_is=cls.hypervisor.lower() == "vmware" - ) + cls.template = get_template(cls.apiclient, cls.zone.id, cls.services["ostype"]) if cls.template == FAILED: - assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] cls.services["domainid"] = cls.domain.id cls.services["zoneid"] = cls.zone.id @@ -331,9 +320,10 @@ def setUpClass(cls): domainid=cls.domain.id ) cls._cleanup.append(cls.account) + # the Debian 13 built-in template does not boot with the 128 MiB of the tiny offering cls.service_offering = ServiceOffering.create( cls.apiclient, - cls.services["service_offerings"]["tiny"] + cls.services["service_offerings"]["big"] ) cls._cleanup.append(cls.service_offering) cls.virtual_machine = VirtualMachine.create( @@ -1121,14 +1111,9 @@ def setUpClass(cls): cls.services["zoneid"] = cls.zone.id # Get template - template = get_suitable_test_template( - cls.apiclient, - cls.zone.id, - cls.services["ostype"], - cls.hypervisor - ) + template = get_template(cls.apiclient, cls.zone.id, cls.services["ostype"]) if template == FAILED: - assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] cls.services["template"] = template.id cls.services["diskname"] = cls.services["volume"]["diskname"] @@ -1146,14 +1131,14 @@ def setUpClass(cls): # Create Service Offering cls.service_offering = ServiceOffering.create( cls.apiclient, - cls.services["service_offerings"]["small"] + cls.services["service_offerings"]["big"] ) cls._cleanup.append(cls.service_offering) # Create Service Offering with encryptRoot true cls.service_offering_encrypt = ServiceOffering.create( cls.apiclient, - cls.services["service_offerings"]["small"], + cls.services["service_offerings"]["big"], name="Small Encrypted Instance", encryptroot=True ) From b3840bcd86480d05e5da1c3f2a6bd2fe0b48cdb0 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 22 Oct 2026 11:14:30 +0200 Subject: [PATCH 07/13] templates.sql: update the SHA-512 checksums of the Debian 13 built-in templates The images were rebuilt with the nfs-common package installed. The URLs are unchanged, so the KVM, XenServer and VMware templates get the checksums of the new files. --- setup/db/templates.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/db/templates.sql b/setup/db/templates.sql index f4a65e1461a7..e2ba6cc7625e 100755 --- a/setup/db/templates.sql +++ b/setup/db/templates.sql @@ -34,16 +34,16 @@ INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, VALUES (2, UUID(), 'centos53-x86_64', 'CentOS 5.3(64-bit) no GUI (XenServer)', 1, now(), now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/builtin/f59f18fb-ae94-4f97-afd2-f84755767aca.vhd.bz2', 'b63d854a9560c013142567bbae8d98cf', 0, 'CentOS 5.3(64-bit) no GUI (XenServer)', 'VHD', 12, 1, 1, 'XenServer', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, display_text, enable_password, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (4, UUID(), 'debian13-x86_64-kvm', 'Debian 13 (64-bit) no GUI (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.qcow2', '{SHA-512}38389fb9e29448e6d7da460c0342217bd1a8aa6e32e6192fa1c6dbc6f7078f6c8210dde110d8631cc823b2762fb1738f8ae94b3e5a3774cf30a0cf11af69dfb8', 'Debian 13 (64-bit) no GUI (KVM)', 0, 'QCOW2', 184, 1, 1, 'KVM', 1); + VALUES (4, UUID(), 'debian13-x86_64-kvm', 'Debian 13 (64-bit) no GUI (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.qcow2', '{SHA-512}b2e89ab21b6c11f183178da2cb1a04c89cb6b4c43e0968503d050710b4eb37426f78d71b4a503b282bca8c6ddd680a84f2ed9992752ef20d4288b7487d7c228b', 'Debian 13 (64-bit) no GUI (KVM)', 0, 'QCOW2', 184, 1, 1, 'KVM', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (5, UUID(), 'debian13-x86_64-xen', 'Debian 13 (64-bit) no GUI (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs-xen.vhd.bz2', '{SHA-512}b2687bf7f739f49ed36b266e69d44ed49283b398497d85bda17f0eabc447934ba571629c01d94d8ac985f8133b218c8558cac22a88f78a3efd5f81bcf0689ab7', 0, 'Debian 13 (64-bit) no GUI (XenServer)', 'VHD', 184, 1, 1, 'XenServer', 1); + VALUES (5, UUID(), 'debian13-x86_64-xen', 'Debian 13 (64-bit) no GUI (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs-xen.vhd.bz2', '{SHA-512}ba1805e0a4f73725cc463215020dc77c9501b121c112f3c8205c4bce4e40fe4fc75381018a4863dd9dce0a6d2fe5c47480793283aa1df88c2a0217dd44c2117f', 0, 'Debian 13 (64-bit) no GUI (XenServer)', 'VHD', 184, 1, 1, 'XenServer', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) VALUES (6, UUID(), 'centos64-x64', 'CentOS 6.4(64-bit) GUI (Hyperv)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/4.3/centos6_4_64bit.vhd.bz2', 'eef6b9940ea3ed01221d963d4a012d0a', 0, 'CentOS 6.4 (64-bit) GUI (Hyperv)', 'VHD', 182, 1, 1, 'Hyperv', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (7, UUID(), 'debian13-x86_64-vmware', 'Debian 13 (64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.ova', '{SHA-512}a090fafc53cbcf375937a8328500689543d9716a527f8a6059daee49712c56cc0e1553f386af8df1a650fa92819b235f4066df1076f242bf9a9e225f18a10b2b', 0, 'Debian 13 (64-bit) no GUI (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); + VALUES (7, UUID(), 'debian13-x86_64-vmware', 'Debian 13 (64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.ova', '{SHA-512}2e8f81e9ea62820cdf936a73d3c02e1a840a6b96f18163b1f4617fd48ccae792e08ef2b3ccf0be1c7c3d86dff65200b655b54a7468775806ca78c1adf50c9935', 0, 'Debian 13 (64-bit) no GUI (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (1, UUID(), 'CentOS'); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (2, UUID(), 'Debian'); From cc33a11155cee873bb66a446f54e5e79add075dd Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 22 Oct 2026 11:44:04 +0200 Subject: [PATCH 08/13] templates.sql: use the 20261021-2638 Debian 13 images for the built-in templates The images are built from the Debian 13 genericcloud image of 20261021-2638 with the nfs-common package installed. Update the URLs and the SHA-512 checksums of the KVM, XenServer and VMware templates. --- setup/db/templates.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/db/templates.sql b/setup/db/templates.sql index e2ba6cc7625e..8d7a613724e1 100755 --- a/setup/db/templates.sql +++ b/setup/db/templates.sql @@ -34,16 +34,16 @@ INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, VALUES (2, UUID(), 'centos53-x86_64', 'CentOS 5.3(64-bit) no GUI (XenServer)', 1, now(), now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/builtin/f59f18fb-ae94-4f97-afd2-f84755767aca.vhd.bz2', 'b63d854a9560c013142567bbae8d98cf', 0, 'CentOS 5.3(64-bit) no GUI (XenServer)', 'VHD', 12, 1, 1, 'XenServer', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, display_text, enable_password, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (4, UUID(), 'debian13-x86_64-kvm', 'Debian 13 (64-bit) no GUI (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.qcow2', '{SHA-512}b2e89ab21b6c11f183178da2cb1a04c89cb6b4c43e0968503d050710b4eb37426f78d71b4a503b282bca8c6ddd680a84f2ed9992752ef20d4288b7487d7c228b', 'Debian 13 (64-bit) no GUI (KVM)', 0, 'QCOW2', 184, 1, 1, 'KVM', 1); + VALUES (4, UUID(), 'debian13-x86_64-kvm', 'Debian 13 (64-bit) no GUI (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs.qcow2', '{SHA-512}732c2b760089386ac8017ed64568c2441923e0107b9b6262586096d5fcf78393b83b561d289a7eb3f008187c17258d0be53a42651040168fc11468197619adcc', 'Debian 13 (64-bit) no GUI (KVM)', 0, 'QCOW2', 184, 1, 1, 'KVM', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (5, UUID(), 'debian13-x86_64-xen', 'Debian 13 (64-bit) no GUI (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs-xen.vhd.bz2', '{SHA-512}ba1805e0a4f73725cc463215020dc77c9501b121c112f3c8205c4bce4e40fe4fc75381018a4863dd9dce0a6d2fe5c47480793283aa1df88c2a0217dd44c2117f', 0, 'Debian 13 (64-bit) no GUI (XenServer)', 'VHD', 184, 1, 1, 'XenServer', 1); + VALUES (5, UUID(), 'debian13-x86_64-xen', 'Debian 13 (64-bit) no GUI (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs-xen.vhd.bz2', '{SHA-512}454ad1a131f05bb50938ef1b4b28068181015c8afb492ad2e8f83d4cd2bf16cda55a142eae6f550ebb936a37a58b60bf7bd1e98ff965db338544db84f5fd85bd', 0, 'Debian 13 (64-bit) no GUI (XenServer)', 'VHD', 184, 1, 1, 'XenServer', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) VALUES (6, UUID(), 'centos64-x64', 'CentOS 6.4(64-bit) GUI (Hyperv)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/4.3/centos6_4_64bit.vhd.bz2', 'eef6b9940ea3ed01221d963d4a012d0a', 0, 'CentOS 6.4 (64-bit) GUI (Hyperv)', 'VHD', 182, 1, 1, 'Hyperv', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (7, UUID(), 'debian13-x86_64-vmware', 'Debian 13 (64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261017-2634-acs.ova', '{SHA-512}2e8f81e9ea62820cdf936a73d3c02e1a840a6b96f18163b1f4617fd48ccae792e08ef2b3ccf0be1c7c3d86dff65200b655b54a7468775806ca78c1adf50c9935', 0, 'Debian 13 (64-bit) no GUI (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); + VALUES (7, UUID(), 'debian13-x86_64-vmware', 'Debian 13 (64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs.ova', '{SHA-512}d199ef7728c73b6f172429aacd7f5942737d8bde5742be8a8b0d01b9330621eb55c5a521251cbb44ef091023af02ece38d5b7120d81f6b7dbeac98f4e4ec94e0', 0, 'Debian 13 (64-bit) no GUI (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (1, UUID(), 'CentOS'); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (2, UUID(), 'Debian'); From 39d7d37faa98905a1911d9b04ad32246659e8da8 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 23 Oct 2026 14:22:29 +0200 Subject: [PATCH 09/13] test_backup_recovery_nas: sync the guest after writing the marker files The tests write a file in the guest and take a live backup a few seconds later. The backup is not quiesced, so a file that was not flushed yet can be missing from the backup. The guest root filesystem commits its journal about every 5 seconds, which is also the wait before the backup. Run sync after each touch, so that the file is on the disk before the backup starts. --- .../smoke/test_backup_recovery_nas.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/integration/smoke/test_backup_recovery_nas.py b/test/integration/smoke/test_backup_recovery_nas.py index 31169e188739..9240432c7b90 100644 --- a/test/integration/smoke/test_backup_recovery_nas.py +++ b/test/integration/smoke/test_backup_recovery_nas.py @@ -122,7 +122,7 @@ def vm_backup_create_vm_from_backup_int(self, templateid=None, networkids=None): # Create a file and take backup try: ssh_client_vm = self.vm.get_ssh_client(reconnect=True) - ssh_client_vm.execute("touch test_backup_and_recovery.txt") + ssh_client_vm.execute("touch test_backup_and_recovery.txt; sync") except Exception as err: self.fail("SSH failed for Virtual machine: %s due to %s" % (self.vm.ipaddress, err)) @@ -314,7 +314,7 @@ def test_incremental_chain_cadence(self): self._set_full_every(3) try: ssh_client_vm = self.vm.get_ssh_client(reconnect=True) - ssh_client_vm.execute("touch /root/incremental_marker_1.txt") + ssh_client_vm.execute("touch /root/incremental_marker_1.txt; sync") created = [] for i in range(5): @@ -435,15 +435,15 @@ def test_restore_from_incremental(self): self._set_full_every(5) try: ssh_client_vm = self.vm.get_ssh_client(reconnect=True) - ssh_client_vm.execute("touch /root/marker_full.txt") + ssh_client_vm.execute("touch /root/marker_full.txt; sync") Backup.create(self.apiclient, self.vm.id, "rfi_full") time.sleep(3) - ssh_client_vm.execute("touch /root/marker_inc1.txt") + ssh_client_vm.execute("touch /root/marker_inc1.txt; sync") Backup.create(self.apiclient, self.vm.id, "rfi_inc1") time.sleep(3) - ssh_client_vm.execute("touch /root/marker_inc2.txt") + ssh_client_vm.execute("touch /root/marker_inc2.txt; sync") Backup.create(self.apiclient, self.vm.id, "rfi_inc2") time.sleep(3) @@ -484,13 +484,13 @@ def test_delete_middle_incremental_repairs_chain(self): self._set_full_every(5) try: ssh_client_vm = self.vm.get_ssh_client(reconnect=True) - ssh_client_vm.execute("touch /root/dmi_full.txt") + ssh_client_vm.execute("touch /root/dmi_full.txt; sync") Backup.create(self.apiclient, self.vm.id, "dmi_full") time.sleep(3) - ssh_client_vm.execute("touch /root/dmi_inc1.txt") + ssh_client_vm.execute("touch /root/dmi_inc1.txt; sync") Backup.create(self.apiclient, self.vm.id, "dmi_inc1") time.sleep(3) - ssh_client_vm.execute("touch /root/dmi_inc2.txt") + ssh_client_vm.execute("touch /root/dmi_inc2.txt; sync") Backup.create(self.apiclient, self.vm.id, "dmi_inc2") time.sleep(3) From f715416f49e6de3c216c66963b7a57fb8508f9ed Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 26 Oct 2026 09:09:47 +0100 Subject: [PATCH 10/13] build-acs-image.sh: add the Apache license header The usage text is printed from the header comment, so the line range it prints is moved down by the length of the license. --- scripts/util/build-acs-image.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/scripts/util/build-acs-image.sh b/scripts/util/build-acs-image.sh index 15ad42d03d5b..06ce920abcd5 100755 --- a/scripts/util/build-acs-image.sh +++ b/scripts/util/build-acs-image.sh @@ -1,4 +1,21 @@ #!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + # Prepare a generic cloud qcow2 image for CloudStack: patch it, shrink it and convert it to VHD/OVA. # # Usage: build-acs-image.sh [-a ACTIONS] [-f FORMATS] [-o OUTDIR] [-g GUESTOS] [-u] [-z] @@ -31,7 +48,7 @@ # checked first, and nothing is installed. set -euo pipefail -usage() { sed -n '2,29p' "$0"; exit 1; } +usage() { sed -n '19,46p' "$0"; exit 1; } ACTIONS="all" FORMATS="all" From 245e6623c5903d6c161ad8a90ee8befb5bb3d285 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 26 Oct 2026 09:09:54 +0100 Subject: [PATCH 11/13] templates.sql: download the Debian 13 built-in templates from download.cloudstack.org The images are the same 20261021-2638 files with the same checksums, now served from download.cloudstack.org instead of the build server. --- setup/db/templates.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/db/templates.sql b/setup/db/templates.sql index 8d7a613724e1..8b60d9cf3607 100755 --- a/setup/db/templates.sql +++ b/setup/db/templates.sql @@ -34,16 +34,16 @@ INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, VALUES (2, UUID(), 'centos53-x86_64', 'CentOS 5.3(64-bit) no GUI (XenServer)', 1, now(), now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/builtin/f59f18fb-ae94-4f97-afd2-f84755767aca.vhd.bz2', 'b63d854a9560c013142567bbae8d98cf', 0, 'CentOS 5.3(64-bit) no GUI (XenServer)', 'VHD', 12, 1, 1, 'XenServer', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, display_text, enable_password, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (4, UUID(), 'debian13-x86_64-kvm', 'Debian 13 (64-bit) no GUI (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs.qcow2', '{SHA-512}732c2b760089386ac8017ed64568c2441923e0107b9b6262586096d5fcf78393b83b561d289a7eb3f008187c17258d0be53a42651040168fc11468197619adcc', 'Debian 13 (64-bit) no GUI (KVM)', 0, 'QCOW2', 184, 1, 1, 'KVM', 1); + VALUES (4, UUID(), 'debian13-x86_64-kvm', 'Debian 13 (64-bit) genericcloud (KVM)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs.qcow2', '{SHA-512}732c2b760089386ac8017ed64568c2441923e0107b9b6262586096d5fcf78393b83b561d289a7eb3f008187c17258d0be53a42651040168fc11468197619adcc', 'Debian 13 (64-bit) genericcloud (KVM)', 0, 'QCOW2', 184, 1, 1, 'KVM', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (5, UUID(), 'debian13-x86_64-xen', 'Debian 13 (64-bit) no GUI (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs-xen.vhd.bz2', '{SHA-512}454ad1a131f05bb50938ef1b4b28068181015c8afb492ad2e8f83d4cd2bf16cda55a142eae6f550ebb936a37a58b60bf7bd1e98ff965db338544db84f5fd85bd', 0, 'Debian 13 (64-bit) no GUI (XenServer)', 'VHD', 184, 1, 1, 'XenServer', 1); + VALUES (5, UUID(), 'debian13-x86_64-xen', 'Debian 13 (64-bit) genericcloud (XenServer)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs-xen.vhd.bz2', '{SHA-512}454ad1a131f05bb50938ef1b4b28068181015c8afb492ad2e8f83d4cd2bf16cda55a142eae6f550ebb936a37a58b60bf7bd1e98ff965db338544db84f5fd85bd', 0, 'Debian 13 (64-bit) genericcloud (XenServer)', 'VHD', 184, 1, 1, 'XenServer', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) VALUES (6, UUID(), 'centos64-x64', 'CentOS 6.4(64-bit) GUI (Hyperv)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/releases/4.3/centos6_4_64bit.vhd.bz2', 'eef6b9940ea3ed01221d963d4a012d0a', 0, 'CentOS 6.4 (64-bit) GUI (Hyperv)', 'VHD', 182, 1, 1, 'Hyperv', 1); INSERT INTO `cloud`.`vm_template` (id, uuid, unique_name, name, public, created, type, hvm, bits, account_id, url, checksum, enable_password, display_text, format, guest_os_id, featured, cross_zones, hypervisor_type, extractable) - VALUES (7, UUID(), 'debian13-x86_64-vmware', 'Debian 13 (64-bit) no GUI (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://10.0.34.8/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs.ova', '{SHA-512}d199ef7728c73b6f172429aacd7f5942737d8bde5742be8a8b0d01b9330621eb55c5a521251cbb44ef091023af02ece38d5b7120d81f6b7dbeac98f4e4ec94e0', 0, 'Debian 13 (64-bit) no GUI (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); + VALUES (7, UUID(), 'debian13-x86_64-vmware', 'Debian 13 (64-bit) genericcloud (vSphere)', 1, now(), 'BUILTIN', 0, 64, 1, 'http://download.cloudstack.org/templates/debian-13-genericcloud-amd64/debian-13-genericcloud-amd64-20261021-2638-acs.ova', '{SHA-512}d199ef7728c73b6f172429aacd7f5942737d8bde5742be8a8b0d01b9330621eb55c5a521251cbb44ef091023af02ece38d5b7120d81f6b7dbeac98f4e4ec94e0', 0, 'Debian 13 (64-bit) genericcloud (vSphere)', 'OVA', 184, 1, 1, 'VMware', 1); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (1, UUID(), 'CentOS'); INSERT INTO `cloud`.`guest_os_category` (id, uuid, name) VALUES (2, UUID(), 'Debian'); From 2a2cbc54733a8189e52f069039e30ed2ba2c3d42 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 26 Oct 2026 09:10:43 +0100 Subject: [PATCH 12/13] Keep the template install path when a download fails without one An error answer, like the timeout when the secondary storage VM disconnects during a download, has no install path. The download callback wrote it to the template_store_ref entry anyway, so the install path became null and the next download of the template went to a directory named "null". Only update the install path when the answer has one. --- .../image/BaseImageStoreDriverImpl.java | 7 +- .../image/BaseImageStoreDriverImplTest.java | 123 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 engine/storage/src/test/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImplTest.java diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java index 26b39e30776f..d3c4750de07c 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java @@ -52,6 +52,7 @@ import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; import org.apache.cloudstack.storage.endpoint.DefaultEndPointSelector; import org.apache.cloudstack.storage.image.deployasis.DeployAsIsHelper; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -229,7 +230,11 @@ protected Void createTemplateAsyncCallback(AsyncCallbackDispatcher parentCallback; + @Mock + private AsyncCallbackDispatcher callback; + @Mock + private TemplateDataStoreVO existingRef; + @Mock + private VMTemplateZoneVO templateZone; + + private BaseImageStoreDriverImpl driver; + private TemplateDataStoreVO updateBuilder; + + @Before + public void setUp() { + driver = Mockito.mock(BaseImageStoreDriverImpl.class, Mockito.CALLS_REAL_METHODS); + ReflectionTestUtils.setField(driver, "logger", org.apache.logging.log4j.LogManager.getLogger(BaseImageStoreDriverImpl.class)); + ReflectionTestUtils.setField(driver, "_templateDao", templateDao); + ReflectionTestUtils.setField(driver, "_templateStoreDao", templateStoreDao); + ReflectionTestUtils.setField(driver, "_vmTemplateZoneDao", vmTemplateZoneDao); + ReflectionTestUtils.setField(driver, "_alertMgr", alertManager); + + Mockito.when(data.getId()).thenReturn(7L); + Mockito.when(data.getDataStore()).thenReturn(store); + Mockito.when(store.getId()).thenReturn(1L); + Mockito.when(templateDao.findById(7L)).thenReturn(Mockito.mock(VMTemplateVO.class)); + Mockito.when(templateDao.createForUpdate()).thenReturn(Mockito.mock(VMTemplateVO.class)); + Mockito.when(existingRef.getId()).thenReturn(3L); + Mockito.when(existingRef.getDownloadState()).thenReturn(Status.DOWNLOAD_IN_PROGRESS); + Mockito.when(templateStoreDao.findByStoreTemplate(1L, 7L)).thenReturn(existingRef); + updateBuilder = Mockito.mock(TemplateDataStoreVO.class); + Mockito.when(templateStoreDao.createForUpdate()).thenReturn(updateBuilder); + Mockito.when(templateZone.getZoneId()).thenReturn(2L); + Mockito.when(vmTemplateZoneDao.listByTemplateId(7L)).thenReturn(Collections.singletonList(templateZone)); + } + + private void callWith(DownloadAnswer answer) { + Mockito.when(callback.getResult()).thenReturn(answer); + BaseImageStoreDriverImpl.CreateContext context = driver.new CreateContext<>(parentCallback, data); + driver.createTemplateAsyncCallback(callback, context); + } + + @Test + public void testErrorAnswerWithoutInstallPathKeepsInstallPath() { + DownloadAnswer answer = new DownloadAnswer("Timeout waiting for response from storage host", Status.DOWNLOAD_ERROR); + + callWith(answer); + + Mockito.verify(updateBuilder, Mockito.never()).setInstallPath(Mockito.any()); + Mockito.verify(updateBuilder).setDownloadState(Status.DOWNLOAD_ERROR); + Mockito.verify(templateStoreDao).update(3L, updateBuilder); + } + + @Test + public void testAnswerWithInstallPathSetsInstallPath() { + DownloadAnswer answer = new DownloadAnswer("job", 100, null, Status.DOWNLOADED, "/dl/path", "template/tmpl/2/7", 10L, 10L, "checksum"); + + callWith(answer); + + Mockito.verify(updateBuilder).setInstallPath("template/tmpl/2/7"); + Mockito.verify(templateStoreDao).update(3L, updateBuilder); + } +} From 6031babf7dfecc90d570df3184c894c79e3aad0b Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 26 Oct 2026 10:03:55 +0100 Subject: [PATCH 13/13] schema: deploy the built-in Debian 13 VMware template as is Set deploy_as_is on the built-in Debian 13 VMware template in the 4.23.0.0 to 24.0.0 upgrade, so that VMs take the NIC and disk controller of the OVA (vmxnet3, pvscsi). Without it CloudStack uses an e1000 NIC, which the Debian cloud kernel has no driver for, and the VM gets no network. --- .../src/main/resources/META-INF/db/schema-42300to2400.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql index 7cf85f3058c7..2b07bb1c0575 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -247,3 +247,6 @@ UPDATE `cloud`.`vm_template` SET `guest_os_id` = (SELECT `id` FROM `cloud`.`gues WHERE `id` = 5 AND `unique_name` = 'debian13-x86_64-xen' AND `type` = 'BUILTIN'; UPDATE `cloud`.`vm_template` SET `guest_os_id` = (SELECT `id` FROM `cloud`.`guest_os` WHERE `display_name` = 'Debian GNU/Linux 12 (64-bit)' AND `removed` IS NULL LIMIT 1) WHERE `id` = 7 AND `unique_name` = 'debian13-x86_64-vmware' AND `type` = 'BUILTIN'; + +-- Deploy the built-in Debian 13 VMware template as is, with the NIC and disk controller of the OVA (vmxnet3, pvscsi). +UPDATE `cloud`.`vm_template` SET `deploy_as_is` = 1 WHERE `id` = 7 AND `unique_name` = 'debian13-x86_64-vmware' AND `type` = 'BUILTIN';