This patch adds the skeleton of the CVM test support and modify existing scripts and test framework to enable such scenario. Split the sha1sum to support both regular and CVM guest. Add one test case for CVM. Will further add more test cases. Signed-off-by: Muminul Islam <muislam@microsoft.com>
342 lines
11 KiB
Bash
342 lines
11 KiB
Bash
#!/usr/bin/env bash
|
|
# shellcheck disable=SC1009,SC2048,SC2086,SC1073,SC1040,SC1072
|
|
# shellcheck source=/dev/null
|
|
set -x
|
|
|
|
hypervisor="kvm"
|
|
test_filter=""
|
|
build_kernel=false
|
|
|
|
# Download from a url with retries
|
|
# Args:
|
|
# $1: URL
|
|
# $2: Maximum number of retries (optional), default 3
|
|
# $3: Delays between retries in seconds (optional), default 5
|
|
download_with_retries() {
|
|
local URL="$1"
|
|
local MAX_RETRIES="${2:-3}"
|
|
local RETRY_DELAY="${3:-5}"
|
|
local i
|
|
|
|
for i in $(seq 1 "$MAX_RETRIES"); do
|
|
echo "Attempt $i/$MAX_RETRIES: downloading $url"
|
|
time wget -N --quiet "$URL" && return 0
|
|
echo "Warning: wget failed on attempt $i. Retrying in ${RETRY_DELAY}s..." >&2
|
|
sleep "$RETRY_DELAY"
|
|
done
|
|
|
|
echo "ERROR: Failed to download $url after $MAX_RETRIES attempts." >&2
|
|
return 1
|
|
}
|
|
|
|
# Checkout source code of a GIT repo with specified branch and commit
|
|
# Args:
|
|
# $1: Target directory
|
|
# $2: GIT URL of the repo
|
|
# $3: Required branch
|
|
# $4: Required commit (optional)
|
|
checkout_repo() {
|
|
SRC_DIR="$1"
|
|
GIT_URL="$2"
|
|
GIT_BRANCH="$3"
|
|
GIT_COMMIT="$4"
|
|
|
|
# Check whether the local HEAD commit same as the requested commit or not.
|
|
# If commit is not specified, compare local HEAD and remote HEAD.
|
|
# Remove the folder if there is difference.
|
|
if [ -d "$SRC_DIR" ]; then
|
|
pushd "$SRC_DIR" || exit
|
|
git fetch
|
|
SRC_LOCAL_COMMIT=$(git rev-parse HEAD)
|
|
if [ -z "$GIT_COMMIT" ]; then
|
|
GIT_COMMIT=$(git rev-parse remotes/origin/"$GIT_BRANCH")
|
|
fi
|
|
popd || exit
|
|
if [ "$SRC_LOCAL_COMMIT" != "$GIT_COMMIT" ]; then
|
|
rm -rf "$SRC_DIR"
|
|
fi
|
|
fi
|
|
|
|
# Checkout the specified branch and commit (if required)
|
|
if [ ! -d "$SRC_DIR" ]; then
|
|
git clone --depth 1 "$GIT_URL" -b "$GIT_BRANCH" "$SRC_DIR"
|
|
if [ "$GIT_COMMIT" ]; then
|
|
pushd "$SRC_DIR" || exit
|
|
git fetch --depth 1 origin "$GIT_COMMIT"
|
|
git reset --hard FETCH_HEAD
|
|
popd || exit
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Not actively used by CI
|
|
build_custom_linux() {
|
|
ARCH=$(uname -m)
|
|
LINUX_CUSTOM_DIR="$WORKLOADS_DIR/linux-custom"
|
|
LINUX_CUSTOM_BRANCH="ch-6.16.9"
|
|
LINUX_CUSTOM_URL="https://github.com/cloud-hypervisor/linux.git"
|
|
|
|
checkout_repo "$LINUX_CUSTOM_DIR" "$LINUX_CUSTOM_URL" "$LINUX_CUSTOM_BRANCH"
|
|
|
|
pushd "$LINUX_CUSTOM_DIR" || exit
|
|
make ch_defconfig
|
|
make -j "$(nproc)"
|
|
if [ "${ARCH}" == "x86_64" ]; then
|
|
cp vmlinux "$WORKLOADS_DIR/vmlinux-x86_64" || exit 1
|
|
cp arch/x86/boot/bzImage "$WORKLOADS_DIR/bzImage-x86_64" || exit 1
|
|
elif [ "${ARCH}" == "aarch64" ]; then
|
|
cp arch/arm64/boot/Image "$WORKLOADS_DIR/Image-arm64" || exit 1
|
|
cp arch/arm64/boot/Image.gz "$WORKLOADS_DIR/Image-arm64.gz" || exit 1
|
|
fi
|
|
popd || exit
|
|
}
|
|
|
|
cmd_help() {
|
|
echo ""
|
|
echo "Cloud Hypervisor $(basename "$0")"
|
|
echo "Usage: $(basename "$0") [<args>]"
|
|
echo ""
|
|
echo "Available arguments:"
|
|
echo ""
|
|
echo " --hypervisor Underlying hypervisor. Options kvm, mshv"
|
|
echo " --test-filter Tests to run"
|
|
echo " --build-guest-kernel Build guest kernel from source instead of downloading pre-built"
|
|
echo ""
|
|
echo " --help Display this help message."
|
|
echo ""
|
|
}
|
|
|
|
# shellcheck disable=SC2034
|
|
process_common_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
"-h" | "--help") {
|
|
cmd_help
|
|
exit 1
|
|
} ;;
|
|
"--hypervisor")
|
|
shift
|
|
hypervisor="$1"
|
|
;;
|
|
"--test-filter")
|
|
shift
|
|
test_filter="$1"
|
|
;;
|
|
"--build-guest-kernel")
|
|
build_kernel=true
|
|
;;
|
|
"--") {
|
|
shift
|
|
break
|
|
} ;;
|
|
*)
|
|
echo "Unknown test scripts argument: $1. Please use '-- --help' for help."
|
|
exit
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
if [[ ! ("$hypervisor" = "kvm" || "$hypervisor" = "mshv") ]]; then
|
|
die "Hypervisor value must be kvm or mshv"
|
|
fi
|
|
# shellcheck disable=SC2034
|
|
test_binary_args=("$@")
|
|
}
|
|
|
|
download_hypervisor_fw() {
|
|
FW_TAG="0.5.0"
|
|
if [ -n "$AUTH_DOWNLOAD_TOKEN" ]; then
|
|
echo "Using authenticated download from GitHub"
|
|
FW_URL=$(curl --silent https://api.github.com/repos/cloud-hypervisor/rust-hypervisor-firmware/releases/tags/${FW_TAG} \
|
|
--header "Authorization: Token $AUTH_DOWNLOAD_TOKEN" \
|
|
--header "X-GitHub-Api-Version: 2022-11-28" | grep "browser_download_url" |
|
|
grep -oP '"https://[^"]*hypervisor-fw"' | sed -e 's/^"//' -e 's/"$//')
|
|
else
|
|
echo "Using anonymous download from GitHub"
|
|
FW_URL=$(curl --silent https://api.github.com/repos/cloud-hypervisor/rust-hypervisor-firmware/releases/tags/${FW_TAG} |
|
|
grep "browser_download_url" | grep -oP '"https://[^"]*hypervisor-fw"' | sed -e 's/^"//' -e 's/"$//')
|
|
fi
|
|
FW="$WORKLOADS_DIR/hypervisor-fw"
|
|
pushd "$WORKLOADS_DIR" || exit
|
|
rm -f "$FW"
|
|
download_with_retries "$FW_URL" || exit 1
|
|
popd || exit
|
|
}
|
|
|
|
download_linux() {
|
|
KERNEL_TAG="ch-release-v6.16.9-20251112"
|
|
if [ -n "$AUTH_DOWNLOAD_TOKEN" ]; then
|
|
echo "Using authenticated download from GitHub"
|
|
KERNEL_URLS=$(curl --silent https://api.github.com/repos/cloud-hypervisor/linux/releases/tags/${KERNEL_TAG} \
|
|
--header "Authorization: Token $AUTH_DOWNLOAD_TOKEN" \
|
|
--header "X-GitHub-Api-Version: 2022-11-28" | grep "browser_download_url" | grep -o 'https://.*[^ "]')
|
|
else
|
|
echo "Using anonymous download from GitHub"
|
|
KERNEL_URLS=$(curl --silent https://api.github.com/repos/cloud-hypervisor/linux/releases/tags/${KERNEL_TAG} | grep "browser_download_url" | grep -o 'https://.*[^ "]')
|
|
fi
|
|
pushd "$WORKLOADS_DIR" || exit
|
|
for url in $KERNEL_URLS; do
|
|
download_with_retries "$url" || exit 1
|
|
done
|
|
|
|
popd || exit
|
|
}
|
|
|
|
prepare_linux() {
|
|
if [ "$build_kernel" = true ]; then
|
|
echo "Building kernel from source"
|
|
build_custom_linux
|
|
echo "Using kernel built from source"
|
|
else
|
|
echo "Downloading pre-built kernel from GitHub"
|
|
download_linux
|
|
echo "Using kernel downloaded from GitHub"
|
|
fi
|
|
}
|
|
|
|
download_ovmf() {
|
|
OVMF_FW_TAG="ch-a54f262b09"
|
|
OVMF_FW_URL="https://github.com/cloud-hypervisor/edk2/releases/download/$OVMF_FW_TAG/CLOUDHV.fd"
|
|
OVMF_FW="$WORKLOADS_DIR/CLOUDHV.fd"
|
|
pushd "$WORKLOADS_DIR" || exit
|
|
rm -f "$OVMF_FW"
|
|
download_with_retries $OVMF_FW_URL || exit 1
|
|
popd || exit
|
|
}
|
|
|
|
# Function to mount image partition, execute commands, and cleanup.
|
|
# Arguments: $1: Image file path, $2: Mount directory, $3+: Commands to execute.
|
|
mount_and_exec() {
|
|
local IMG="$1"
|
|
local MOUNT_DIR="$2"
|
|
local LOOP_DEV=""
|
|
local PARTITION_DEV=""
|
|
local COMMAND_STATUS=0
|
|
|
|
# Cleanup function to unmount and detach loop device
|
|
# shellcheck disable=SC2317,SC2329
|
|
cleanup() {
|
|
if [ -n "$MOUNT_DIR" ]; then
|
|
echo "Cleanup: Unmounting $MOUNT_DIR..." >&2
|
|
sudo umount -l "$MOUNT_DIR" 2>/dev/null || true
|
|
fi
|
|
if [ -n "$LOOP_DEV" ]; then
|
|
echo "Cleanup: Detaching loop device $LOOP_DEV..." >&2
|
|
sudo losetup -d "$LOOP_DEV" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
if [ ! -f "$IMG" ] || [ -z "$MOUNT_DIR" ]; then
|
|
echo "ERROR: Image path ($IMG) or mount directory ($MOUNT_DIR) is invalid." >&2
|
|
return 1
|
|
fi
|
|
mkdir -p "$MOUNT_DIR"
|
|
|
|
# Create loop device for the entire disk image
|
|
LOOP_DEV=$(sudo losetup -f --show "$IMG")
|
|
if [ -z "$LOOP_DEV" ]; then
|
|
echo "ERROR: Failed to create loop device for $IMG." >&2
|
|
return 1
|
|
fi
|
|
|
|
# Set the trap now that LOOP_DEV is active.
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Scan for partitions and define partition device node (p1)
|
|
sudo partprobe "$LOOP_DEV" 2>/dev/null
|
|
PARTITION_DEV="${LOOP_DEV}p1"
|
|
|
|
local MAX_RETRIES=5
|
|
local RETRY_DELAY=1
|
|
|
|
# Wait for partition node with retries
|
|
for i in $(seq 1 "$MAX_RETRIES"); do
|
|
if sudo test -b "$PARTITION_DEV"; then
|
|
break
|
|
fi
|
|
if [ "$i" -eq "$MAX_RETRIES" ]; then
|
|
echo "ERROR: Partition device node $PARTITION_DEV not found after $MAX_RETRIES attempts." >&2
|
|
return 1
|
|
fi
|
|
echo "Partition node $PARTITION_DEV not found, waiting $RETRY_DELAY second(s)... (Attempt $i/$MAX_RETRIES)" >&2
|
|
sleep "$RETRY_DELAY"
|
|
done
|
|
|
|
# Mount the partition
|
|
if ! sudo mount "$PARTITION_DEV" "$MOUNT_DIR"; then
|
|
echo "ERROR: Failed to mount $PARTITION_DEV." >&2
|
|
return 1
|
|
fi
|
|
|
|
# Execute the commands
|
|
shift 2
|
|
"$@"
|
|
COMMAND_STATUS=$?
|
|
|
|
trap - EXIT INT TERM
|
|
return $COMMAND_STATUS
|
|
}
|
|
|
|
# Function to copy a file from the host into the mounted disk image.
|
|
# Arguments:
|
|
# $1: Image file path
|
|
# $2: Mount directory
|
|
# $3: Source file path
|
|
# $4: Destination file path
|
|
copy_to_image() {
|
|
local IMG="$1"
|
|
local MOUNT_DIR="$2"
|
|
local SRC_FILE="$3"
|
|
local DST_PATH="$4"
|
|
|
|
if [ ! -f "$SRC_FILE" ]; then
|
|
echo "ERROR: Source file not found at $SRC_FILE." >&2
|
|
return 1
|
|
elif [ -z "$DST_PATH" ]; then
|
|
echo "ERROR: Destination path cannot be empty." >&2
|
|
return 1
|
|
fi
|
|
|
|
# Define the command to copy the file
|
|
local COPY_COMMAND="sudo cp \"$SRC_FILE\" \"$MOUNT_DIR/$DST_PATH\""
|
|
|
|
mount_and_exec "$IMG" "$MOUNT_DIR" /bin/bash -c "$COPY_COMMAND"
|
|
return $?
|
|
}
|
|
|
|
# Download x86 guest images (Focal and Jammy)
|
|
download_x86_guest_images() {
|
|
FOCAL_OS_IMAGE_NAME="focal-server-cloudimg-amd64-custom-20210609-0.qcow2"
|
|
FOCAL_OS_IMAGE_URL="https://ch-images.azureedge.net/$FOCAL_OS_IMAGE_NAME"
|
|
FOCAL_OS_IMAGE="$WORKLOADS_DIR/$FOCAL_OS_IMAGE_NAME"
|
|
if [ ! -f "$FOCAL_OS_IMAGE" ]; then
|
|
pushd "$WORKLOADS_DIR" || exit
|
|
time wget --quiet $FOCAL_OS_IMAGE_URL || exit 1
|
|
popd || exit
|
|
fi
|
|
|
|
FOCAL_OS_RAW_IMAGE_NAME="focal-server-cloudimg-amd64-custom-20210609-0.raw"
|
|
FOCAL_OS_RAW_IMAGE="$WORKLOADS_DIR/$FOCAL_OS_RAW_IMAGE_NAME"
|
|
if [ ! -f "$FOCAL_OS_RAW_IMAGE" ]; then
|
|
pushd "$WORKLOADS_DIR" || exit
|
|
time qemu-img convert -p -f qcow2 -O raw $FOCAL_OS_IMAGE_NAME $FOCAL_OS_RAW_IMAGE_NAME || exit 1
|
|
popd || exit
|
|
fi
|
|
|
|
JAMMY_OS_IMAGE_NAME="jammy-server-cloudimg-amd64-custom-20241017-0.qcow2"
|
|
JAMMY_OS_IMAGE_URL="https://ch-images.azureedge.net/$JAMMY_OS_IMAGE_NAME"
|
|
JAMMY_OS_IMAGE="$WORKLOADS_DIR/$JAMMY_OS_IMAGE_NAME"
|
|
if [ ! -f "$JAMMY_OS_IMAGE" ]; then
|
|
pushd "$WORKLOADS_DIR" || exit
|
|
time wget --quiet $JAMMY_OS_IMAGE_URL || exit 1
|
|
popd || exit
|
|
fi
|
|
|
|
JAMMY_OS_RAW_IMAGE_NAME="jammy-server-cloudimg-amd64-custom-20241017-0.raw"
|
|
JAMMY_OS_RAW_IMAGE="$WORKLOADS_DIR/$JAMMY_OS_RAW_IMAGE_NAME"
|
|
if [ ! -f "$JAMMY_OS_RAW_IMAGE" ]; then
|
|
pushd "$WORKLOADS_DIR" || exit
|
|
time qemu-img convert -p -f qcow2 -O raw $JAMMY_OS_IMAGE_NAME $JAMMY_OS_RAW_IMAGE_NAME || exit 1
|
|
popd || exit
|
|
fi
|
|
}
|