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.

1028 lines
32 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: centos7)\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 " 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 " 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 " ubuntu1604 Ubuntu 16.04 LTS (Xenial Xerus) ubuntu\n"
printf " ubuntu1804 Ubuntu 18.04 LTS (Bionic Beaver) ubuntu\n"
printf "\n"
printf "EXAMPLES\n"
printf " $prog create foo\n"
printf " Create VM with the default parameters: CentOS 7, 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 ${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
;;
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
;;
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
;;
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
;;
*)
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 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??|*-atomic|amazon? )
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"
;;
centos7|amazon?|fedora??|ubuntu*|debian? )
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
cp $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
if [[ "$DISTRO" = "ubuntu1804" ]] || [[ "$DISTRO" = "amazon2" ]]
then
qemu-img resize $DISK $DISK_SIZE &>> ${VMNAME}.log \
&& ok \
|| die "Could not resize disk."
else
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."
fi
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} \
$(param device ${CI_ISO_DEVICE}) \
${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 change-media ${VMNAME} --path ${VMDIR}/${VMNAME}/${CI_ISO} --eject --config &>> ${VMNAME}.log
# Remove the unnecessary cloud init files
outputn "Cleaning up cloud-init files"
rm -f $USER_DATA $META_DATA $CI_ISO && ok
if [ -f "/var/lib/libvirt/dnsmasq/${BRIDGE}.status" ]
then
outputn "Waiting for domain to get an IP address"
MAC=$(virsh dumpxml ${VMNAME} | awk -F\' '/mac address/ {print $2}')
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=centos7 # 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_DEVICE=cdrom
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
# 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