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.
1132 lines
36 KiB
1132 lines
36 KiB
#!/bin/bash |
|
set -e |
|
|
|
# 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 " $prog create [COMMANDS] [OPTIONS] VMNAME\n" |
|
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: $HOME/.ssh/id_rsa.pub)\n" |
|
printf " -l Location of Images (default: $HOME/virt/images)\n" |
|
printf " -L Location of VMs (default: $HOME/virt/vms)\n" |
|
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 " -t Linux Distribution (default: centos8)\n" |
|
printf " -T Timezone (default: US/Eastern)\n" |
|
printf " -u Custom user (default: $USER)\n" |
|
printf " -y Assume yes to prompts (default: false)\n" |
|
printf " -n Assume no to prompts (default: false)\n" |
|
printf " -v Be verbose\n" |
|
printf "\n" |
|
printf "DISTRIBUTIONS\n" |
|
printf " NAME DESCRIPTION LOGIN\n" |
|
printf " amazon2 Amazon Linux 2 ec2-user\n" |
|
printf " centos8 CentOS 8 centos\n" |
|
printf " centos7 CentOS 7 centos\n" |
|
printf " centos7-atomic CentOS 7 Atomic Host centos\n" |
|
printf " centos6 CentOS 6 centos\n" |
|
printf " debian9 Debian 9 (Stretch) debian\n" |
|
printf " debian10 Debian 10 (Buster) debian\n" |
|
printf " fedora29 Fedora 29 fedora\n" |
|
printf " fedora29-atomic Fedora 29 Atomic Host fedora\n" |
|
printf " fedora30 Fedora 30 fedora\n" |
|
printf " fedora31 Fedora 31 fedora\n" |
|
printf " fedora32 Fedora 32 fedora\n" |
|
printf " fedora33 Fedora 33 fedora\n" |
|
printf " fedora34 Fedora 34 fedora\n" |
|
printf " opensuse15 OpenSUSE Leap 15.2 opensuse\n" |
|
printf " ubuntu1604 Ubuntu 16.04 LTS (Xenial Xerus) ubuntu\n" |
|
printf " ubuntu1804 Ubuntu 18.04 LTS (Bionic Beaver) ubuntu\n" |
|
printf " ubuntu2004 Ubuntu 20.04 LTS (Focal Fossa) ubuntu\n" |
|
printf " ubuntu2204 Ubuntu 22.04 LTS (Jammy Jellyfish) ubuntu\n" |
|
printf " ubuntu2404 Ubuntu 24.04 LTS (Noble Numbat) ubuntu\n" |
|
printf " rocky8.5 Rocky Linux rocky\n" |
|
printf "\n" |
|
printf "EXAMPLES\n" |
|
printf " $prog create foo\n" |
|
printf " Create VM with the default parameters: CentOS 8, 1 vCPU, 1GB RAM, 10GB\n" |
|
printf " disk capacity.\n" |
|
printf "\n" |
|
printf " $prog create -c 2 -m 2048 -d 20 foo\n" |
|
printf " Create VM with custom parameters: 2 vCPUs, 2GB RAM, and 20GB disk\n" |
|
printf " capacity.\n" |
|
printf "\n" |
|
printf " $prog create -t debian9 foo\n" |
|
printf " Create a Debian 9 VM with the default parameters.\n" |
|
printf "\n" |
|
printf " $prog create -T UTC foo\n" |
|
printf " Create a default VM with UTC timezone.\n" |
|
printf "\n" |
|
;; |
|
remove) |
|
printf "NAME\n" |
|
printf " $prog remove [COMMANDS] VMNAME\n" |
|
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: $HOME/virt/images)\n" |
|
printf " -L Location of VMs (default: $HOME/virt/vms)\n" |
|
printf " -v Be verbose\n" |
|
printf "\n" |
|
printf "EXAMPLE\n" |
|
printf " $prog remove foo\n" |
|
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 " $prog attach-disk [OPTIONS] [COMMANDS] VMNAME\n" |
|
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 " $prog attach-disk -d 10 -s example-5g.qcow2 -t vdb foo\n" |
|
printf " Attach a 10GB disk device named example-5g.qcow2 to the foo guest\n" |
|
printf " domain.\n" |
|
;; |
|
list) |
|
printf "NAME\n" |
|
printf " $prog list\n" |
|
printf "\n" |
|
printf "DESCRIPTION\n" |
|
printf " Lists all running and stopped guest domains.\n" |
|
;; |
|
*) |
|
printf "'$subcommand' is not a valid subcommand.\n" |
|
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 ${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} |
|
|
|
# Set variables based on $DISTRO |
|
# Use the command "osinfo-query os" to get the list of the accepted OS variants. |
|
case "$DISTRO" in |
|
amazon2) |
|
QCOW=amzn2-kvm-2.0.20190313-x86_64.xfs.gpt.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="auto" |
|
IMAGE_URL=https://cdn.amazonlinux.com/os-images/2.0.20190313/kvm |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=ec2-user |
|
;; |
|
centos8) |
|
QCOW=CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="centos8" |
|
IMAGE_URL=https://cloud.centos.org/centos/8/x86_64/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=centos |
|
;; |
|
centos7) |
|
QCOW=CentOS-7-x86_64-GenericCloud.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="centos7.0" |
|
IMAGE_URL=https://cloud.centos.org/centos/7/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=centos |
|
;; |
|
centos7-atomic) |
|
QCOW=CentOS-Atomic-Host-7-GenericCloud.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="centos7.0" |
|
IMAGE_URL=http://cloud.centos.org/centos/7/atomic/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=centos |
|
;; |
|
centos6) |
|
QCOW=CentOS-6-x86_64-GenericCloud.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="centos6.9" |
|
IMAGE_URL=https://cloud.centos.org/centos/6/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=centos |
|
;; |
|
debian8) |
|
# FIXME: Not yet working. |
|
QCOW=debian-8-openstack-amd64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="debian8" |
|
IMAGE_URL=https://cdimage.debian.org/cdimage/openstack/current-8 |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=debian |
|
;; |
|
debian9) |
|
QCOW=debian-9-openstack-amd64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="debian9" |
|
IMAGE_URL=https://cdimage.debian.org/cdimage/openstack/current-9 |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=debian |
|
;; |
|
debian10) |
|
QCOW=debian-10-openstack-amd64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="debian10" |
|
IMAGE_URL=https://cdimage.debian.org/cdimage/openstack/current-10 |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=debian |
|
;; |
|
fedora29) |
|
QCOW=Fedora-Cloud-Base-29-1.2.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="fedora29" |
|
IMAGE_URL=https://download.fedoraproject.org/pub/fedora/linux/releases/29/Cloud/x86_64/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=fedora |
|
;; |
|
fedora29-atomic) |
|
QCOW=Fedora-AtomicHost-29-20190611.0.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="fedora29" |
|
IMAGE_URL=https://download.fedoraproject.org/pub/alt/atomic/stable/Fedora-29-updates-20190611.0/AtomicHost/x86_64/images/ |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=fedora |
|
;; |
|
fedora30) |
|
QCOW=Fedora-Cloud-Base-30-1.2.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="fedora29" |
|
IMAGE_URL=https://download.fedoraproject.org/pub/fedora/linux/releases/30/Cloud/x86_64/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=fedora |
|
;; |
|
fedora31) |
|
QCOW=Fedora-Cloud-Base-31-1.9.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="fedora31" |
|
IMAGE_URL=https://download.fedoraproject.org/pub/fedora/linux/releases/31/Cloud/x86_64/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=fedora |
|
;; |
|
fedora32) |
|
QCOW=Fedora-Cloud-Base-32-1.6.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="fedora32" |
|
IMAGE_URL=https://download.fedoraproject.org/pub/fedora/linux/releases/32/Cloud/x86_64/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=fedora |
|
;; |
|
fedora33) |
|
QCOW=Fedora-Cloud-Base-33-1.2.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="fedora33" |
|
IMAGE_URL=https://download.fedoraproject.org/pub/fedora/linux/releases/33/Cloud/x86_64/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=fedora |
|
;; |
|
fedora34) |
|
QCOW=Fedora-Cloud-Base-34-1.2.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="fedora34" |
|
IMAGE_URL=https://download.fedoraproject.org/pub/fedora/linux/releases/34/Cloud/x86_64/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=fedora |
|
;; |
|
ubuntu1604) |
|
QCOW=ubuntu-16.04-server-cloudimg-amd64-disk1.img |
|
OS_TYPE="linux" |
|
OS_VARIANT="ubuntu16.04" |
|
IMAGE_URL=https://cloud-images.ubuntu.com/releases/16.04/release |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=ubuntu |
|
;; |
|
ubuntu1804) |
|
QCOW=ubuntu-18.04-server-cloudimg-amd64.img |
|
OS_TYPE="linux" |
|
OS_VARIANT="ubuntu18.04" |
|
IMAGE_URL=https://cloud-images.ubuntu.com/releases/18.04/release |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=ubuntu |
|
;; |
|
ubuntu2004) |
|
QCOW=ubuntu-20.04-server-cloudimg-amd64.img |
|
OS_TYPE="linux" |
|
OS_VARIANT="ubuntu20.04" |
|
IMAGE_URL=https://cloud-images.ubuntu.com/releases/20.04/release |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=ubuntu |
|
;; |
|
ubuntu2204) |
|
QCOW=ubuntu-22.04-server-cloudimg-amd64.img |
|
OS_TYPE="linux" |
|
OS_VARIANT="ubuntu22.04" |
|
IMAGE_URL=https://cloud-images.ubuntu.com/releases/22.04/release |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=ubuntu |
|
;; |
|
ubuntu2404) |
|
QCOW=ubuntu-24.04-server-cloudimg-amd64.img |
|
OS_TYPE="linux" |
|
OS_VARIANT="ubuntu24.04" |
|
IMAGE_URL=https://cloud-images.ubuntu.com/releases/24.04/release |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=ubuntu |
|
;; |
|
opensuse15) |
|
QCOW=openSUSE-Leap-15.2-OpenStack.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="opensuse15.0" |
|
IMAGE_URL=https://download.opensuse.org/repositories/Cloud:/Images:/Leap_15.2/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=opensuse |
|
;; |
|
rocky85) |
|
QCOW=Rocky-8-GenericCloud-8.5-20211114.2.x86_64.qcow2 |
|
OS_TYPE="linux" |
|
OS_VARIANT="rocky8.5" |
|
IMAGE_URL=https://dl.rockylinux.org/pub/rocky/8.5/images |
|
DISK_FORMAT=qcow2 |
|
LOGIN_USER=rocky |
|
;; |
|
*) |
|
die "${DISTRO} not a supported OS. Run 'kvm-install-vm create help'." |
|
;; |
|
esac |
|
|
|
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_VARIANT} != auto ]]; then |
|
osinfo-query os short-id=${OS_VARIANT} >/dev/null \ |
|
|| die "Unknown OS variant '${OS_VARIANT}'. 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 |
|
centos*|fedora*|rocky*|*-atomic|amazon*|opensuse* ) |
|
SUDOGROUP="wheel" |
|
;; |
|
ubuntu*|debian* ) |
|
SUDOGROUP="sudo" |
|
;; |
|
*) |
|
die "OS not supported." |
|
;; |
|
esac |
|
} |
|
|
|
function set_cloud_init_remove () |
|
{ |
|
case "${DISTRO}" in |
|
centos6 ) |
|
CLOUDINITDISABLE="chkconfig cloud-init off" |
|
;; |
|
centos8|centos7|amazon*|fedora*|rocky*|ubuntu*|debian*|opensuse* ) |
|
CLOUDINITDISABLE="systemctl disable cloud-init.service" |
|
;; |
|
*-atomic) |
|
CLOUDINITDISABLE="/usr/bin/true" |
|
;; |
|
esac |
|
} |
|
|
|
function set_network_restart_cmd () |
|
{ |
|
case "${DISTRO}" in |
|
centos6 ) NETRESTART="service network stop && service network start" ;; |
|
ubuntu*|debian*) NETRESTART="systemctl stop networking && systemctl start networking" ;; |
|
*) NETRESTART="systemctl stop network && systemctl start network" ;; |
|
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 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 where output will go |
|
output: |
|
all: ">> /var/log/cloud-init.log" |
|
|
|
# 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 (which is centos for CentOS cloud images) |
|
ssh_authorized_keys: |
|
- ${KEY} |
|
|
|
timezone: ${TIMEZONE} |
|
|
|
# Remove cloud-init when finished with it |
|
runcmd: |
|
- ${NETRESTART} |
|
- ${CLOUDINITDISABLE} |
|
_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 CD-ROM ISO with cloud-init config |
|
outputn "Generating ISO for cloud-init" |
|
if command -v genisoimage &>/dev/null |
|
then |
|
genisoimage -output $CI_ISO \ |
|
-volid cidata \ |
|
-joliet -r $USER_DATA $META_DATA &>> ${VMNAME}.log \ |
|
&& ok \ |
|
|| die "Could not generate ISO." |
|
else |
|
mkisofs -o $CI_ISO -V cidata -J -r $USER_DATA $META_DATA &>> ${VMNAME}.log \ |
|
&& ok \ |
|
|| die "Could not generate ISO." |
|
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})" |
|
|
|
# Assemble CI ISO disk parameters. |
|
CI_ISO_PARAMS="$(join ',' \ |
|
${CI_ISO} \ |
|
${CI_ISO_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})" |
|
CI_ISO_OPTION="$(param --disk ${CI_ISO_PARAMS})" |
|
GRAPHICS_OPTION="$(param --graphics ${GRAPHICS_PARAMS})" |
|
|
|
# Call virt-install to import the cloud image and create a new VM |
|
run "Installing the domain" \ |
|
virt-install --import \ |
|
--name=${VMNAME} \ |
|
--memory=${MEMORY} \ |
|
--vcpus=${CPUS} \ |
|
--cpu=${FEATURE} \ |
|
${DISK_OPTION} \ |
|
${CI_ISO_OPTION} \ |
|
${NETWORK_OPTION} \ |
|
--os-type=${OS_TYPE} \ |
|
--os-variant=${OS_VARIANT} \ |
|
--noautoconsole \ |
|
${GRAPHICS_OPTION} \ |
|
${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 |
|
|
|
# Eject cdrom |
|
virsh detach-disk --domain ${VMNAME} ${VMDIR}/${VMNAME}/${CI_ISO} --config &>> ${VMNAME}.log |
|
|
|
# Remove the unnecessary cloud init files |
|
outputn "Cleaning up cloud-init files" |
|
rm -f $USER_DATA $META_DATA $CI_ISO && ok |
|
|
|
MAC=$(virsh dumpxml ${VMNAME} | awk -F\' '/mac address/ {print $2}') |
|
output "MAC address: ${MAC}" |
|
|
|
if [ -f "/var/lib/libvirt/dnsmasq/${BRIDGE}.status" ] |
|
then |
|
outputn "Waiting for domain to get an IP address" |
|
while true |
|
do |
|
IP=$(grep -B1 $MAC /var/lib/libvirt/dnsmasq/$BRIDGE.status | head \ |
|
-n 1 | awk '{print $2}' | sed -e s/\"//g -e s/,//) |
|
if [ "$IP" = "" ] |
|
then |
|
sleep 1 |
|
else |
|
ok |
|
break |
|
fi |
|
done |
|
printf "\n" |
|
check_delete_known_host |
|
else |
|
outputn "Bridge looks like a layer 2 bridge, get the domain's IP address 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 |
|
CPUS=1 # Number of virtual CPUs |
|
FEATURE=host # Use host cpu features to the guest |
|
MEMORY=1024 # 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=centos8 # 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 |
|
|
|
# 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="" |
|
} |
|
|
|
function set_custom_defaults () |
|
{ |
|
# Source custom defaults, if set |
|
if [ -f ~/.kivrc ]; |
|
then |
|
source ${HOME}/.kivrc |
|
fi |
|
} |
|
|
|
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:ahynv" 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}" ;; |
|
y ) ASSUME_YES=1 ;; |
|
n ) ASSUME_NO=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 help create' 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 |
|
CI_ISO=${VMNAME}-cidata.iso |
|
|
|
# Check for ssh key |
|
check_ssh_key |
|
|
|
if [ ! -z "${IMAGE+x}" ] |
|
then |
|
output "Using custom QCOW2 image: ${IMAGE}." |
|
OS_VARIANT="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 network restart command |
|
set_network_restart_cmd |
|
|
|
# Set cloud init remove command |
|
set_cloud_init_remove |
|
|
|
# Set package manager |
|
set_sudo_group |
|
|
|
# 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) |
|
if [[ "${1:-none}" == "none" ]]; then |
|
usage_subcommand "${subcommand}" |
|
elif [[ "$1" =~ ^help$ ]]; then |
|
usage_subcommand "${subcommand}" |
|
else |
|
set_defaults |
|
set_custom_defaults |
|
"${subcommand}" "$@" |
|
exit $? |
|
fi |
|
;; |
|
*) |
|
die "'${subcommand}' is not a valid subcommand. See 'kvm-install-vm help' for a list of subcommands." |
|
;; |
|
esac
|
|
|