You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

986 lines
31 KiB

#!/usr/bin/env bash
set -euo pipefail
# Set program name variable - basename without subshell
prog=${0##*/}
function usage ()
{
cat << EOF
NAME
kvm-install-vm - Install virtual guests using cloud-init on a local KVM
hypervisor.
SYNOPSIS
$prog COMMAND [OPTIONS]
DESCRIPTION
A bash wrapper around virt-install to build virtual machines on a local KVM
hypervisor. You can run it as a normal user which will use qemu:///session
to connect locally to your KVM domains.
COMMANDS
help - show this help or help for a subcommand
attach-disk - create and attach a disk device to guest domain
create - create a new guest domain
detach-disk - detach a disk device from a guest domain
list - list all domains, running and stopped
remove - delete a guest domain
EOF
exit 0
}
function usage_subcommand ()
{
case "$1" in
create)
printf "NAME\n"
printf " %s create [COMMANDS] [OPTIONS] VMNAME\n" "$prog"
printf "\n"
printf "DESCRIPTION\n"
printf " Create a new guest domain.\n"
printf "\n"
printf "COMMANDS\n"
printf " help - show this help\n"
printf "\n"
printf "OPTIONS\n"
printf " -a Autostart (default: false)\n"
printf " -b Bridge (default: virbr0)\n"
printf " -c Number of vCPUs (default: 1)\n"
printf " -d Disk Size (GB) (default: 10)\n"
printf " -D DNS Domain (default: example.local)\n"
printf " -f CPU Model / Feature (default: host)\n"
printf " -g Graphics type (default: spice)\n"
printf " -h Display help\n"
printf " -i Custom QCOW2 Image\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"
printf " -m Memory Size (MB) (default: 1024)\n"
printf " -M Mac address (default: auto-assigned)\n"
printf " -p Console port (default: auto)\n"
printf " -s Custom shell script\n"
printf " -S Enable UEFI secureboot\n"
printf " -t Linux Distribution (default: centos8)\n"
printf " -T Timezone (default: US/Eastern)\n"
printf " -u Custom user (default: %s)\n" "$USER"
printf " -y Assume yes to prompts (default: false)\n"
printf " -n Assume no to prompts (default: false)\n"
printf " -v Verbose\n"
printf " -V Virt type (kvm, xen, qemu)\n (default: kvm)\n"
printf "\n"
printf "DISTRIBUTIONS\n"
list_available_vms
printf "\n"
printf "EXAMPLES\n"
printf " %s create foo\n" "$prog"
printf " Create VM with the default parameters: CentOS 8, 1 vCPU, 1GB RAM, 10GB\n"
printf " disk capacity.\n"
printf "\n"
printf " %s create -c 2 -m 2048 -d 20 foo\n" "$prog"
printf " Create VM with custom parameters: 2 vCPUs, 2GB RAM, and 20GB disk\n"
printf " capacity.\n"
printf "\n"
printf " %s create -t debian9 foo\n" "$prog"
printf " Create a Debian 9 VM with the default parameters.\n"
printf "\n"
printf " %s create -T UTC foo\n" "$prog"
printf " Create a default VM with UTC timezone.\n"
printf "\n"
;;
remove)
printf "NAME\n"
printf " %s remove [COMMANDS] VMNAME\n" "prog"
printf "\n"
printf "DESCRIPTION\n"
printf " Destroys (stops) and undefines a guest domain. This also remove the\n"
printf " associated storage pool.\n"
printf "\n"
printf "COMMANDS\n"
printf " help - show this help\n"
printf "\n"
printf "OPTIONS\n"
printf " -l Location of Images (default: %s/virt/images)\n" "$HOME"
printf " -L Location of VMs (default: %s/virt/vms)\n" "$HOME"
printf " -v Be verbose\n"
printf "\n"
printf "EXAMPLE\n"
printf " %s remove foo\n" "$prog"
printf " Remove (destroy and undefine) a guest domain. WARNING: This will\n"
printf " delete the guest domain and any changes made inside it!\n"
;;
attach-disk)
printf "NAME\n"
printf " %s attach-disk [OPTIONS] [COMMANDS] VMNAME\n" "$prog"
printf "\n"
printf "DESCRIPTION\n"
printf " Attaches a new disk to a guest domain.\n"
printf "\n"
printf "COMMANDS\n"
printf " help - show this help\n"
printf "\n"
printf "OPTIONS\n"
printf " -d SIZE Disk size (GB)\n"
printf " -f FORMAT Disk image format (default: qcow2)\n"
printf " -s IMAGE Source of disk device\n"
printf " -t TARGET Disk device target\n"
printf "\n"
printf "EXAMPLE\n"
printf " %s attach-disk -d 10 -s example-5g.qcow2 -t vdb foo\n" "$prog"
printf " Attach a 10GB disk device named example-5g.qcow2 to the foo guest\n"
printf " domain.\n"
;;
list)
printf "NAME\n"
printf " %s list\n" "$prog"
printf "\n"
printf "DESCRIPTION\n"
printf " Lists all running and stopped guest domains.\n"
;;
*)
printf "'%s' is not a valid subcommand.\n" "$subcommand"
exit 1
;;
esac
exit 0
}
# Console output colors
bold() { echo -e "\e[1m$@\e[0m" ; }
red() { echo -e "\e[31m$@\e[0m" ; }
green() { echo -e "\e[32m$@\e[0m" ; }
yellow() { echo -e "\e[33m$@\e[0m" ; }
die() { red "ERR: $@" >&2 ; exit 2 ; }
silent() { "$@" > /dev/null 2>&1 ; }
output() { echo -e "- $@" ; }
outputn() { echo -en "- $@ ... " ; }
ok() { green "${@:-OK}" ; }
pushd() { command pushd "$@" >/dev/null ; }
popd() { command popd "$@" >/dev/null ; }
# Join zero or more strings into a delimited string.
function join ()
{
local sep="$1"
if [ $# -eq 0 ]; then
return
fi
shift
while [ $# -gt 1 ]; do
printf "%s%s" "$1" "$sep"
shift
done
printf "%s\n" "$1"
}
# Print an optional name=value[,value,..] parameter.
# Prints nothing if no values are given.
function param ()
{
if [ $# -lt 2 ]; then
return # skip empty value
fi
local name="$1"
shift
local values="$(join ',' "$@")"
printf "%s=%s\n" $name $values
}
# Output a command, one argument per line.
function output_command ()
{
local line_cont=$' \\ \n '
local command_lines=$(join "$line_cont" "$@")
printf " %s\n" "$command_lines"
}
# Command wrapper to output the command to be run in verbose
# mode and redirect stdout and stderr to the vm log file.
function run ()
{
local msg="$1"
shift
if [ "${VERBOSE}" -eq 1 ]
then
output "$msg with the following command"
output_command "$@"
else
outputn "$msg"
fi
( "$@" &>> ${VMNAME}.log && ok )
}
# Detect OS and set wget parameters
function set_wget ()
{
if [ -f /etc/fedora-release ]
then
WGET="wget --quiet --show-progress"
else
WGET="wget"
fi
}
function check_vmname_set ()
{
[ -n "${VMNAME}" ] || die "VMNAME not set."
}
function delete_vm ()
{
# Check if domain exists and set DOMAIN_EXISTS variable.
domain_exists "${VMNAME}"
# Check if storage pool exists and set STORPOOL_EXISTS variable.
storpool_exists "${VMNAME}"
check_vmname_set
if [ "${DOMAIN_EXISTS}" -eq 1 ]
then
outputn "Destroying ${VMNAME} domain"
virsh destroy --graceful ${VMNAME} > /dev/null 2>&1 \
&& ok \
|| yellow "(Domain is not running.)"
outputn "Undefining ${VMNAME} domain"
virsh undefine --managed-save --snapshots-metadata --nvram ${VMNAME} > /dev/null 2>&1 \
&& ok \
|| die "Could not undefine domain."
else
output "Domain ${VMNAME} does not exist"
fi
[[ -d ${VMDIR}/${VMNAME} ]] && DISKDIR=${VMDIR}/${VMNAME} || DISKDIR=${IMAGEDIR}/${VMNAME}
[ -d $DISKDIR ] \
&& outputn "Deleting ${VMNAME} files" \
&& rm -rf $DISKDIR \
&& ok
if [ "${STORPOOL_EXISTS}" -eq 1 ]
then
outputn "Destroying ${VMNAME} storage pool"
virsh pool-destroy ${VMNAME} > /dev/null 2>&1 && ok
else
output "Storage pool ${VMNAME} does not exist"
fi
}
function fetch_images ()
{
# Create image directory if it doesn't already exist
mkdir -p ${IMAGEDIR}
local found="false"
local vm_url=""
for vm in "${BUILTIN_VMS[@]}"; do
IFS=\| read -r vm_distro vm_desc vm_arch vm_url vm_login_user <<< "$vm"
if [[ "$vm_distro" == "$DISTRO" && "$vm_arch" == "$ARCH" ]]; then
QCOW="${vm_url##*/}" # Grab just the file from the URL to pass into virt-install
OS_INFO="$vm_distro" # Distro name should come from osinfo
IMAGE_URL="${vm_url%/*}" # Grab everything but the filename and the slash
DISK_FORMAT="qcow2"
LOGIN_USER="$vm_login_user"
found="true"
break
fi
done
if [ "$found" = "false" ]; then
die "No matching image found for ${DISTRO} (${ARCH})"
fi
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}
fi
}
function check_ssh_key ()
{
local key
if [ -z "${PUBKEY}" ]; then
# Try to find a suitable key file.
for key in ~/.ssh/id_{rsa,dsa,ed25519}.pub; do
if [ -f "$key" ]; then
PUBKEY="$key"
break
fi
done
fi
if [ ! -f "${PUBKEY}" ]
then
# Check for existence of a pubkey, or else exit with message
die "Please generate an SSH keypair using 'ssh-keygen -t rsa' or \
specify one with the "-k" flag."
else
# Place contents of $PUBKEY into $KEY
KEY=$(<${PUBKEY})
fi
}
function check_os_variant ()
{
if [[ ${OS_INFO} != auto ]]; then
osinfo-query os short-id=${OS_INFO} >/dev/null \
|| die "Unknown OS variant '${OS_INFO}'. Please update your osinfo-db. "\
"See https://libosinfo.org/download for more information."
fi
}
function domain_exists ()
{
virsh dominfo "${1}" > /dev/null 2>&1 \
&& DOMAIN_EXISTS=1 \
|| DOMAIN_EXISTS=0
}
function storpool_exists ()
{
virsh pool-info "${1}" > /dev/null 2>&1 \
&& STORPOOL_EXISTS=1 \
|| STORPOOL_EXISTS=0
}
function set_sudo_group ()
{
case "${DISTRO}" in
almalinux*|centos*|fedora*|rocky*|*-atomic|amazon*|opensuse* )
SUDOGROUP="wheel"
;;
ubuntu*|debian* )
SUDOGROUP="sudo"
;;
*)
die "OS not supported."
;;
esac
}
function check_delete_known_host ()
{
output "Checking for ${IP} in known_hosts file"
grep -q ${IP} ${HOME}/.ssh/known_hosts \
&& outputn "Found entry for ${IP}. Removing" \
&& (sed --in-place "/^${IP}/d" ~/.ssh/known_hosts && ok ) \
|| output "No entries found for ${IP}"
}
function set_boot_flag() {
local pkgmgr=""
local share_dir=""
if command -v rpm >/dev/null 2>&1 && rpm -q edk2-ovmf >/dev/null 2>&1; then
pkgmgr="rpm"
share_dir="/usr/share/edk2/ovmf"
elif command -v dpkg >/dev/null 2>&1 && dpkg -s edk2-ovmf >/dev/null 2>&1; then
pkgmgr="deb"
share_dir="/usr/share/OVMF"
else
BOOTFLAG=""
return
fi
local machine
case "$ARCH" in
x86_64) machine="--machine q35" ;;
aarch64) machine="--machine virt" ;;
*) machine="" ;;
esac
local code_fd="${share_dir}/OVMF_CODE${SECUREBOOT:+.secboot}.fd"
local vars_fd="${share_dir}/OVMF_VARS${SECUREBOOT:+.secboot}.fd"
local secure_flag=$(( SECUREBOOT ? 1 : 0 ))
BOOTFLAG="--boot uefi,loader=${code_fd},loader.readonly=yes,loader.type=pflash,nvram_template=$vars_fd,nvram=/var/tmp/$(basename "$vars_fd"),loader.secure=$( (( secure_flag )) && echo yes || echo no ) --features smm=on" "$machine"
}
function create_vm ()
{
# Create image directory if it doesn't already exist
mkdir -p ${VMDIR}
check_vmname_set
# Start clean
[ -d "${VMDIR}/${VMNAME}" ] && rm -rf ${VMDIR}/${VMNAME}
mkdir -p ${VMDIR}/${VMNAME}
pushd ${VMDIR}/${VMNAME}
# Create log file
touch ${VMNAME}.log
# cloud-init config: set hostname, remove cloud-init package,
# and add ssh-key
cat > $USER_DATA << _EOF_
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
MIME-Version: 1.0
--==BOUNDARY==
Content-Type: text/cloud-config; charset="us-ascii"
#cloud-config
# Hostname management
preserve_hostname: False
hostname: ${VMNAME}
fqdn: ${VMNAME}.${DNSDOMAIN}
# Users
users:
- default
- name: ${ADDITIONAL_USER}
groups: ['${SUDOGROUP}']
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ${KEY}
# configure interaction with ssh server
ssh_genkeytypes: ['ed25519', 'rsa']
# Install my public ssh key to the first user-defined user configured
# in cloud.cfg in the template
ssh_authorized_keys:
- ${KEY}
timezone: ${TIMEZONE}
_EOF_
if [ ! -z "${SCRIPTNAME+x}" ]
then
SCRIPT=$(< $SCRIPTNAME)
cat >> $USER_DATA << _EOF_
--==BOUNDARY==
Content-Type: text/x-shellscript; charset="us-ascii"
${SCRIPT}
--==BOUNDARY==--
_EOF_
else
cat >> $USER_DATA << _EOF_
--==BOUNDARY==--
_EOF_
fi
{ echo "instance-id: ${VMNAME}"; echo "local-hostname: ${VMNAME}"; } > $META_DATA
outputn "Copying cloud image ($(basename ${IMAGE}))"
DISK=${VMNAME}.qcow2
qemu-img create -q -f qcow2 -F qcow2 -b $IMAGE $DISK && ok
if $RESIZE_DISK
then
outputn "Resizing the disk to $DISK_SIZE"
# Workaround to prevent virt-resize from renumbering partitions and breaking grub
# See https://bugzilla.redhat.com/show_bug.cgi?id=1472039
# Ubuntu will automatically grow the partition to the new size on its first boot
case "$DISTRO" in
ubuntu*|amazon2)
qemu-img resize $DISK $DISK_SIZE &>> ${VMNAME}.log \
&& ok \
|| die "Could not resize disk."
;;
*)
qemu-img create -f qcow2 \
-o preallocation=metadata $DISK.new $DISK_SIZE &>> ${VMNAME}.log \
&& virt-resize --quiet --expand /dev/sda1 $DISK $DISK.new &>> ${VMNAME}.log \
&& (mv $DISK.new $DISK && ok) \
|| die "Could not resize disk."
;;
esac
fi
# Create new storage pool for new VM
run "Creating storage pool" \
virsh pool-create-as \
--name=${VMNAME} \
--type=dir \
--target=${VMDIR}/${VMNAME} \
|| die "Could not create storage pool."
# Add custom MAC Address if specified
NETWORK_PARAMS="$(join ',' \
$(param bridge ${BRIDGE}) \
$(param model ${NETWORK_MODEL}) \
$(param mac ${MACADDRESS}) \
${NETWORK_EXTRA})"
# Assemble disk parameters.
DISK_PARAMS="$(join ',' \
${DISK} \
$(param format ${DISK_FORMAT}) \
$(param bus ${DISK_BUS}) \
${DISK_EXTRA})"
# Omit the --graphics option to auto-detect.
if [ "${GRAPHICS}" = 'auto' ]
then
GRAPHICS_PARAMS=""
else
GRAPHICS_PARAMS="$(join ',' \
${GRAPHICS} \
$(param port ${PORT}) \
$(param listen ${GRAPHICS_LISTEN}) \
${GRAPHICS_EXTRA})"
fi
# Assemble virt-install options.
NETWORK_OPTION="$(param --network ${NETWORK_PARAMS})"
DISK_OPTION="$(param --disk ${DISK_PARAMS})"
GRAPHICS_OPTION="$(param --graphics ${GRAPHICS_PARAMS})"
CLOUD_INIT_OPTION="$(param --cloud-init user-data=${USER_DATA},meta-data=${META_DATA},disable=on)"
# Call virt-install to import the cloud image and create a new VM
run "Installing the domain" \
virt-install --import \
--name=${VMNAME} \
--memory=${MEMORY} \
--virt-type=${VIRTTYPE} \
--vcpus=${CPUS} \
--cpu=${FEATURE} \
${DISK_OPTION} \
${CLOUD_INIT_OPTION} \
${NETWORK_OPTION} \
--osinfo=${OS_INFO} \
--noautoconsole \
${GRAPHICS_OPTION} \
${BOOTFLAG} \
${VIRT_INSTALL_EXTRA} \
|| die "Could not create domain with virt-install."
virsh dominfo ${VMNAME} &>> ${VMNAME}.log
# Enable autostart if true
if $AUTOSTART
then
outputn "Enabling autostart"
virsh autostart \
--domain ${VMNAME} > /dev/null 2>&1 \
&& ok \
|| die "Could not enable autostart."
fi
# Remove the unnecessary cloud init files
outputn "Cleaning up cloud-init files"
rm -f $USER_DATA $META_DATA && ok
MAC=$(virsh dumpxml ${VMNAME} | awk -F\' '/mac address/ {print $2}')
output "MAC address: ${MAC}"
status_file="/var/lib/libvirt/dnsmasq/${BRIDGE}.status"
IP=""
timeout=60 # seconds
if [[ -f "$status_file" ]]; then
outputn "Waiting for domain to get an IP address "
for (( i=0; i<timeout; i++ )); do
IP=$(
{ grep -B1 -m1 "\"mac-address\": \"$MAC\"" "$status_file" || true; } |
awk -F'"' '/ip-address/ {print $4; exit}'
)
if [[ -n "$IP" ]]; then
ok
break
fi
sleep 1
done
[[ -n "$IP" ]] || die "Timed out waiting for DHCP lease"
printf '\n'
check_delete_known_host
else
outputn "Bridge looks like a layer‑2 bridge; get the domain’s IP from your DHCP server"
IP="<IP address>"
fi
printf "\n"
output "SSH to ${VMNAME}: 'ssh ${LOGIN_USER}@${IP}' or 'ssh ${LOGIN_USER}@${VMNAME}'"
CONSOLE=$(virsh domdisplay ${VMNAME})
# Workaround because VNC port number shown by virsh domdisplay is offset from 5900
if [ "${GRAPHICS}" = 'vnc' ]
then
CONSOLE_NO_PORT=$(echo $CONSOLE | cut -d ':' -f 1,2 -)
CONSOLE_PORT=$(expr 5900 + $(echo $CONSOLE | cut -d ':' -f 3 -))
output "Console at ${CONSOLE_NO_PORT}:${CONSOLE_PORT}"
else
output "Console at ${CONSOLE}"
fi
output "DONE"
popd
}
# Delete VM
function remove ()
{
# Parse command line arguments
while getopts ":l:L:hv" opt
do
case "$opt" in
l ) IMAGEDIR="${OPTARG}" ;;
L ) VMDIR="${OPTARG}" ;;
v ) VERBOSE=1 ;;
h ) usage ;;
* ) die "Unsupported option. Run 'kvm-install-vm help remove'." ;;
esac
done
shift $((OPTIND - 1))
if [ "$#" != 1 ]
then
printf "Please specify a single host to remove.\n"
printf "Run 'kvm-install-vm help remove' for usage.\n"
exit 1
else
VMNAME=$1
fi
delete_vm
}
function set_defaults ()
{
# Defaults are set here. Override using command line arguments.
AUTOSTART=false # Automatically start VM at boot time
ARCH=$(uname -m) # Architecture (autodetected)
CPUS=1 # Number of virtual CPUs
FEATURE=host-model # Use host cpu features to the guest
MEMORY=1536 # Amount of RAM in MB
DISK_SIZE="" # Disk Size in GB
DNSDOMAIN=example.local # DNS domain
GRAPHICS=spice # Graphics type or "auto"
RESIZE_DISK=false # Resize disk (boolean)
IMAGEDIR=${HOME}/virt/images # Directory to store images
VMDIR=${HOME}/virt/vms # Directory to store virtual machines
BRIDGE=virbr0 # Hypervisor bridge
PUBKEY="" # SSH public key
DISTRO=rocky9 # Distribution
MACADDRESS="" # MAC Address
PORT=-1 # Console port
TIMEZONE=US/Eastern # Timezone
ADDITIONAL_USER=${USER} # User
ASSUME_YES=0 # Assume yes to prompts
ASSUME_NO=0 # Assume no to prompts
VERBOSE=0 # Verbosity
VIRTTYPE=kvm # Virt type (kvm, xen, qemu)
SECUREBOOT=0 # Enable UEFI with SecureBoot
# Reset OPTIND
OPTIND=1
# Advanced hypervisor options. Override in ~/.kivrc if needed.
NETWORK_MODEL=virtio
NETWORK_EXTRA=""
DISK_BUS=virtio
DISK_EXTRA=""
CI_ISO_EXTRA=""
GRAPHICS_LISTEN=localhost
GRAPHICS_EXTRA=""
VIRT_INSTALL_EXTRA=""
# Format: "name|description|architecture|image download url|login user"
# *Note*: The name should match what is in the osinfo list
DEFAULT_BUILTIN_VMS=(
"ubuntu24.04|Ubuntu 24.04 LTS (Noble Numbat)|x86_64|https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img|ubuntu"
"ubuntu24.04|Ubuntu 24.04 LTS (Noble Numbat)|aarch64|https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-arm64.img|ubuntu"
"ubuntu22.04|Ubuntu 22.04 LTS (Noble Numbat)|x86_64|https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img|ubuntu"
"ubuntu22.04|Ubuntu 22.04 LTS (Noble Numbat)|aarch64|https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-arm64.img|ubuntu"
"debian12|Debian 12 Stable (Bookworm)|x86_64|https://cdimage.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2|debian"
"debian12|Debian 12 Stable (Bookworm)|aarch64|https://cdimage.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-arm64.qcow2|debian"
"rocky8|Rocky Linux 8 (Green Obsidian)|x86_64|https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud.latest.x86_64.qcow2|rocky"
"rocky8|Rocky Linux 8 (Green Obsidian)|aarch64|https://dl.rockylinux.org/pub/rocky/8/images/aarch64/Rocky-8-GenericCloud.latest.aarch64.qcow2|rocky"
"rocky9|Rocky Linux 9 (Blue Onyx)|x86_64|https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2|rocky"
"rocky9|Rocky Linux 9 (Blue Onyx)|aarch64|https://dl.rockylinux.org/pub/rocky/9/images/aarch64/Rocky-9-GenericCloud.latest.aarch64.qcow2|rocky"
)
BUILTIN_VMS=("${DEFAULT_BUILTIN_VMS[@]}")
}
function set_custom_defaults ()
{
# Source custom defaults: first local .kivrc, then fallback to ~/.kivrc
if [ -f "./.kivrc" ]; then
source "./.kivrc"
elif [ -f "${HOME}/.kivrc" ]; then
source "${HOME}/.kivrc"
fi
declare -gA BUILTIN_VM_DESCRIPTIONS=()
declare -gA BUILTIN_VM_SOURCES=()
declare -gA BUILTIN_VM_ARCHS=()
for entry in "${BUILTIN_VMS[@]}"; do
IFS=":" read -r name description arch url <<< "$entry"
key="${name}_${arch}"
BUILTIN_VM_DESCRIPTIONS["$key"]=$description
BUILTIN_VM_ARCHS["$key"]=$arch
BUILTIN_VM_SOURCES["$key"]=$url
done
}
function list_available_vms() {
for key in "${!BUILTIN_VM_DESCRIPTIONS[@]}"; do
local arch="${BUILTIN_VM_ARCHS[$key]}"
local description="${BUILTIN_VM_DESCRIPTIONS[$key]}"
# Extract the base name (before the architecture part)
# Since arch can be x86_64 or aarch64, we need to be more specific
local name
if [[ "$key" == *_x86_64 ]]; then
name="${key%_x86_64}"
elif [[ "$key" == *_aarch64 ]]; then
name="${key%_aarch64}"
else
name="$key"
fi
if [[ ${#arch} -eq 6 ]]; then
printf " %-13s (%s) - %s\n" "$name" "$arch" "$description"
else
printf " %-12s (%s) - %s\n" "$name" "$arch" "$description"
fi
done | sort
}
function create ()
{
# Parse command line arguments
while getopts ":b:c:d:D:f:g:i:k:l:L:m:M:p:s:t:T:u:V:ahynSv" opt
do
case "$opt" in
a ) AUTOSTART="${OPTARG}" ;;
b ) BRIDGE="${OPTARG}" ;;
c ) CPUS="${OPTARG}" ;;
d ) DISK_SIZE="${OPTARG}" ;;
D ) DNSDOMAIN="${OPTARG}" ;;
f ) FEATURE="${OPTARG}" ;;
g ) GRAPHICS="${OPTARG}" ;;
i ) IMAGE="${OPTARG}" ;;
k ) PUBKEY="${OPTARG}" ;;
l ) IMAGEDIR="${OPTARG}" ;;
L ) VMDIR="${OPTARG}" ;;
m ) MEMORY="${OPTARG}" ;;
M ) MACADDRESS="${OPTARG}" ;;
p ) PORT="${OPTARG}" ;;
s ) SCRIPTNAME="${OPTARG}" ;;
t ) DISTRO="${OPTARG}" ;;
T ) TIMEZONE="${OPTARG}" ;;
u ) ADDITIONAL_USER="${OPTARG}" ;;
V ) VIRTTYPE="${OPTARG}" ;;
y ) ASSUME_YES=1 ;;
n ) ASSUME_NO=1 ;;
S ) SECUREBOOT=1 ;;
v ) VERBOSE=1 ;;
h ) usage ;;
* ) die "Unsupported option. Run 'kvm-install-vm help create'." ;;
esac
done
shift $((OPTIND - 1))
# Resize disk if you specify a disk size either via cmdline option or .kivrc
if [ -n "${DISK_SIZE}" ]
then
RESIZE_DISK=true
DISK_SIZE="${DISK_SIZE}G" # Append 'G' for Gigabyte
fi
# Yes (-y) and No (-n) are mutually exclusive.
if [[ "${ASSUME_YES}" -eq 1 ]] && [[ "${ASSUME_NO}" -eq 1 ]]
then
printf "Please specify only one of -y or -n flags.\n"
exit 1
fi
# After all options are processed, make sure only one variable is left (vmname)
if [ "$#" != 1 ]
then
printf "Please specify a single host to create.\n"
printf "Run 'kvm-install-vm create help' for usage.\n"
exit 1
else
VMNAME=$1
fi
# Set cloud-init variables after VMNAME is assigned
USER_DATA=user-data
META_DATA=meta-data
# Check for ssh key
check_ssh_key
if [ ! -z "${IMAGE+x}" ]
then
output "Using custom QCOW2 image: ${IMAGE}."
OS_INFO="auto"
LOGIN_USER="<use the default account in your custom image>"
else
fetch_images
fi
# Verify the osinfo-db is up to date.
check_os_variant
# Check if domain already exists
domain_exists "${VMNAME}"
if [ "${DOMAIN_EXISTS}" -eq 1 ]; then
echo -n "[WARNING] ${VMNAME} already exists. Do you want to overwrite ${VMNAME} [y/N]? "
if [ "${ASSUME_YES}" -eq 1 ]; then
REPLY="y"
echo $REPLY
elif [ "${ASSUME_NO}" -eq 1 ]; then
REPLY="n"
echo $REPLY
else
read -r
fi
if [[ $REPLY =~ ^[Yy]$ ]]; then
delete_vm
else
echo -e "\nNot overwriting ${VMNAME}. Exiting..."
exit 1
fi
fi
# Set package manager
set_sudo_group
# Set flag for BIOS/UEFI/SecureBoot mode
set_boot_flag
# Finally, create requested VM
create_vm
}
function attach-disk ()
{
# Set default variables
FORMAT=qcow2
# Parse command line arguments
while getopts ":d:f:ps:t:h" opt
do
case "$opt" in
d ) DISKSIZE="${OPTARG}G" ;;
f ) FORMAT="${OPTARG}" ;;
p ) PERSISTENT="${OPTARG}" ;;
s ) SOURCE="${OPTARG}" ;;
t ) TARGET="${OPTARG}" ;;
h ) usage ;;
* ) die "Unsupported option. Run 'kvm-install-vm help attach-disk'." ;;
esac
done
shift $((OPTIND - 1))
[ ! -z ${TARGET} ] || die "You must specify a target device, for e.g. '-t vdb'"
[ ! -z ${DISKSIZE} ] || die "You must specify a size (in GB) for the new device, for e.g. '-d 5'"
if [ "$#" != 1 ]
then
printf "Please specify a single host to attach a disk to.\n"
printf "Run 'kvm-install-vm help attach-disk' for usage.\n"
exit 1
else
# Set variables
VMNAME=$1
# Directory to create attached disk (Checks both images an vms directories for backward compatibility!)
[[ -d ${VMDIR}/${VMNAME} ]] && DISKDIR=${VMDIR}/${VMNAME} || DISKDIR=${IMAGEDIR}/${VMNAME}
DISKNAME=${VMNAME}-${TARGET}-${DISKSIZE}.${FORMAT}
if [ ! -f "${DISKDIR}/${DISKNAME}" ]
then
outputn "Creating new '${TARGET}' disk image for domain ${VMNAME}"
(qemu-img create -f ${FORMAT} -o size=$DISKSIZE,preallocation=metadata \
${DISKDIR}/${DISKNAME} &>> ${DISKDIR}/${VMNAME}.log && ok ) && \
outputn "Attaching ${DISKNAME} to domain ${VMNAME}"
(virsh attach-disk ${VMNAME} \
--source $DISKDIR/${DISKNAME} \
--target ${TARGET} \
--subdriver ${FORMAT} \
--cache none \
--persistent &>> ${DISKDIR}/${VMNAME}.log && ok ) \
|| die "Could not attach disk."
else
die "Target ${TARGET} is already created or in use."
fi
fi
}
#--------------------------------------------------
# Main
#--------------------------------------------------
subcommand="${1:-none}"
[[ "${subcommand}" != "none" ]] && shift
case "${subcommand}" in
none)
usage
;;
help)
if [[ "${1:-none}" == "none" ]]; then
usage
elif [[ "$1" =~ ^create$|^remove$|^list$|^attach-disk$ ]]; then
usage_subcommand "$1"
else
printf "'$1' is not a valid subcommand.\n\n"
usage
fi
;;
list)
virsh list --all
exit 0
;;
create|remove|attach-disk|remove-disk)
set_defaults
set_custom_defaults
if [[ "${1:-none}" == "none" ]]; then
usage_subcommand "${subcommand}"
elif [[ "$1" =~ ^help$ ]]; then
usage_subcommand "${subcommand}"
else
"${subcommand}" "$@"
exit $?
fi
;;
*)
die "'${subcommand}' is not a valid subcommand. See 'kvm-install-vm help' for a list of subcommands."
;;
esac