From 275d264b2648dc3a067f00ce43dd0a7dcbe4f21d Mon Sep 17 00:00:00 2001 From: Giovanni Torres Date: Sat, 27 Sep 2025 16:23:54 -0400 Subject: [PATCH] feat: add support for remote images --- kvm-install-vm | 118 ++++++++++++++++++++++++++------- tests/check_remote_images.bats | 111 +++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 tests/check_remote_images.bats diff --git a/kvm-install-vm b/kvm-install-vm index dcb7678..f04dc67 100755 --- a/kvm-install-vm +++ b/kvm-install-vm @@ -51,7 +51,7 @@ function usage_subcommand() { printf " -f CPU Model / Feature (default: host)\n" printf " -g Graphics type (default: vnc)\n" printf " -h Display help\n" - printf " -i Custom QCOW2 Image\n" + printf " -i Custom image (local path or HTTPS URL)\n" printf " -k SSH Public Key (default: %s/.ssh/id_rsa.pub)\n" "$HOME" printf " -l Location of Images (default: %s/virt/images)\n" "$HOME" printf " -L Location of VMs (default: %s/virt/vms)\n" "$HOME" @@ -86,6 +86,12 @@ function usage_subcommand() { printf " %s create -T UTC foo\n" "$prog" printf " Create a default VM with UTC timezone.\n" printf "\n" + printf " %s create -i /path/to/custom-image.qcow2 foo\n" "$prog" + printf " Create a VM from a local custom image file.\n" + printf "\n" + printf " %s create -i https://example.com/image.qcow2 foo\n" "$prog" + printf " Create a VM from a remote image (HTTPS download).\n" + printf "\n" ;; remove) printf "NAME\n" @@ -256,6 +262,44 @@ function delete_vm() { fi } +function download_image() { + local url="$1" + local target_dir="$2" + local filename="$3" + + # Create target directory if it doesn't exist + mkdir -p "${target_dir}" + + local target_path="${target_dir}/${filename}" + local temp_path="${target_path}.part" + + # Skip download if file already exists + if [ -f "${target_path}" ]; then + output "Image already exists: ${target_path}" + return 0 + fi + + set_wget + local continue_flag="" + + if [ -f "${temp_path}" ]; then + continue_flag="--continue" + output "Partial image found. Resuming download" + else + output "Downloading image from ${url}" + fi + + ${WGET} \ + ${continue_flag} \ + --directory-prefix="${target_dir}" \ + --output-document="${temp_path}" \ + "${url}" || + die "Could not download image from ${url}." + + mv "${temp_path}" "${target_path}" + output "Downloaded: ${target_path}" +} + function fetch_images() { # Create image directory if it doesn't already exist mkdir -p ${IMAGEDIR} @@ -283,22 +327,7 @@ function fetch_images() { IMAGE=${IMAGEDIR}/${QCOW} if [ ! -f ${IMAGEDIR}/${QCOW} ]; then - set_wget - if [ -f ${IMAGEDIR}/${QCOW}.part ]; then - CONTINUE="--continue" - output "Partial cloud image found. Resuming download" - else - CONTINUE="" - output "Cloud image not found. Downloading" - fi - ${WGET} \ - ${CONTINUE} \ - --directory-prefix ${IMAGEDIR} \ - --output-document=${IMAGEDIR}/${QCOW}.part \ - ${IMAGE_URL}/${QCOW} || - die "Could not download image." - - mv ${IMAGEDIR}/${QCOW}.part ${IMAGEDIR}/${QCOW} + download_image "${vm_url}" "${IMAGEDIR}" "${QCOW}" fi } @@ -387,6 +416,18 @@ function detect_disk_format() { esac } +function is_url() { + local input="$1" + case "$input" in + https://*) + return 0 + ;; + *) + return 1 + ;; + esac +} + function set_boot_flag() { local share_dir="" @@ -849,12 +890,43 @@ function create() { check_ssh_key if [ ! -z "${IMAGE+x}" ]; then - # Convert relative path to absolute path - IMAGE=$(realpath "${IMAGE}") - DISK_FORMAT=$(detect_disk_format "${IMAGE}") - output "Using custom image: ${IMAGE} (format: ${DISK_FORMAT})." - OS_INFO="linux2024" - LOGIN_USER="" + if is_url "${IMAGE}"; then + # Handle remote URL + QCOW="${IMAGE##*/}" # Extract filename from URL + DISK_FORMAT=$(detect_disk_format "${QCOW}") + + # Validate that it's a supported image format + case "${DISK_FORMAT}" in + qcow2|raw|vpc) + output "Using remote image: ${IMAGE} (format: ${DISK_FORMAT})" + ;; + *) + die "Unsupported remote image format. Only .qcow2, .raw, and .vhd files are supported." + ;; + esac + + # Download the image + download_image "${IMAGE}" "${IMAGEDIR}" "${QCOW}" + IMAGE="${IMAGEDIR}/${QCOW}" + OS_INFO="linux2024" + LOGIN_USER="" + else + # Handle local file path + if [[ "${IMAGE}" != /* ]]; then + # Convert relative path to absolute path + IMAGE=$(realpath "${IMAGE}") + fi + + # Verify file exists + if [ ! -f "${IMAGE}" ]; then + die "Custom image file not found: ${IMAGE}" + fi + + DISK_FORMAT=$(detect_disk_format "${IMAGE}") + output "Using custom image: ${IMAGE} (format: ${DISK_FORMAT})" + OS_INFO="linux2024" + LOGIN_USER="" + fi else fetch_images fi diff --git a/tests/check_remote_images.bats b/tests/check_remote_images.bats new file mode 100644 index 0000000..eb465ab --- /dev/null +++ b/tests/check_remote_images.bats @@ -0,0 +1,111 @@ +#!/usr/bin/env bats + +VMPREFIX=batstestvm +TESTDIR=~/virt/.tests + +setup() { + # Create test directory + mkdir -p "${TESTDIR}" +} + +teardown() { + # Clean up any created VMs + ./kvm-install-vm remove "${VMPREFIX}"-remote-rocky 2>/dev/null || true + ./kvm-install-vm remove "${VMPREFIX}"-remote-fresh 2>/dev/null || true + ./kvm-install-vm remove "${VMPREFIX}"-remote-invalid 2>/dev/null || true + + # Clean up downloaded images from tests + rm -f ~/virt/images/Rocky-9-GenericCloud.latest.x86_64.qcow2 2>/dev/null || true + rm -f ~/virt/images/invalid-file.txt 2>/dev/null || true +} + +@test "Remote Rocky Linux image download and VM creation" { + # Use the real Rocky Linux 9 image URL for testing + ROCKY_URL="https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2" + + # Test download detection and VM creation (use -n to assume no, preventing actual VM creation) + run timeout 30 ./kvm-install-vm create -n -i "${ROCKY_URL}" "${VMPREFIX}"-remote-rocky + + # Check that it recognizes it as a remote image + [[ "${output}" =~ "Using remote image" ]] + [[ "${output}" =~ "format: qcow2" ]] + + # Should either download or use existing image + [[ "${output}" =~ "Downloading image from" ]] || [[ "${output}" =~ "Image already exists" ]] +} + +@test "Remote image fresh download" { + # Use a specific test URL to ensure fresh download + ROCKY_URL="https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2" + IMAGE_FILE="~/virt/images/Rocky-9-GenericCloud.latest.x86_64.qcow2" + + # Clean up any existing image to force fresh download + rm -f "${IMAGE_FILE}" 2>/dev/null || true + rm -f "${IMAGE_FILE}.part" 2>/dev/null || true + + # Test actual download (use -n to prevent VM creation) + run timeout 60 ./kvm-install-vm create -n -i "${ROCKY_URL}" "${VMPREFIX}"-remote-fresh + + # Should show download activity + [[ "${output}" =~ "Using remote image" ]] + [[ "${output}" =~ "Downloading image from" ]] +} + +@test "Remote image URL validation - unsupported format" { + # Test with an unsupported file extension + INVALID_URL="https://example.com/invalid-file.txt" + + run timeout 5 ./kvm-install-vm create -n -i "${INVALID_URL}" "${VMPREFIX}"-remote-invalid + + # Should fail with unsupported format error + [ "$status" -eq 2 ] + [[ "${output}" =~ "Unsupported remote image format" ]] +} + +@test "Remote image format detection - qcow2" { + QCOW2_URL="https://example.com/test.qcow2" + + # This should pass format validation but fail on download (which is expected) + run timeout 5 ./kvm-install-vm create -n -i "${QCOW2_URL}" "${VMPREFIX}"-format-test + + # Should show qcow2 format detection before failing on download + [[ "${output}" =~ "format: qcow2" ]] || [[ "${output}" =~ "Could not download" ]] +} + +@test "Remote image format detection - raw" { + RAW_URL="https://example.com/test.raw" + + run timeout 5 ./kvm-install-vm create -n -i "${RAW_URL}" "${VMPREFIX}"-format-test + + # Should show raw format detection + [[ "${output}" =~ "format: raw" ]] || [[ "${output}" =~ "Could not download" ]] +} + +@test "Remote image format detection - vhd" { + VHD_URL="https://example.com/test.vhd" + + run timeout 5 ./kvm-install-vm create -n -i "${VHD_URL}" "${VMPREFIX}"-format-test + + # Should show vpc format detection (vhd maps to vpc) + [[ "${output}" =~ "format: vpc" ]] || [[ "${output}" =~ "Could not download" ]] +} + +@test "URL detection function" { + # Test HTTPS URL detection + HTTPS_URL="https://example.com/test.qcow2" + + run timeout 5 ./kvm-install-vm create -n -i "${HTTPS_URL}" "${VMPREFIX}"-url-test + + # Should be detected as remote image + [[ "${output}" =~ "Using remote image" ]] +} + +@test "Non-URL path handling" { + # Test that local paths still work as before + LOCAL_PATH="/nonexistent/local/file.qcow2" + + run timeout 5 ./kvm-install-vm create -n -i "${LOCAL_PATH}" "${VMPREFIX}"-local-test + + # Should show local file handling and fail because file doesn't exist + [[ "${output}" =~ "Custom image file not found" ]] +} \ No newline at end of file