#!/usr/bin/env bash
#
# kvmx virtual machine manager
#
# Copyright (C) 2017 Silvio Rhatto - rhatto at riseup.net
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# Basic parameters
BASENAME="`basename $0`"
DIRNAME="`dirname $0`"
ACTION="$1"
VM="$2"
GLOBAL_USER_CONFIG_FOLDER="$HOME/.config/kvmx"
# Alias to be used in config files
KVMX_BASE="$DIRNAME"
# Run spice client
function kvmx_spice {
# https://lists.freedesktop.org/archives/spice-devel/2013-September/014643.html
SPICE_NOGRAB=1 spicec --host localhost --port $PORT &
#spicy -h localhost -p $PORT
#remote-viewer spice://localhost:$PORT
# Give time to boot
sleep 5
# Fix window titles
if which /usr/bin/xdotool &> /dev/null; then
xdotool search --name "SPICEc:0" set_window --name $VM
fi
}
# Bring virtual machine up
function kvmx_up {
if kvmx_running; then
echo "$BASENAME: guest $VM is already running"
exit 1
fi
if [ ! -z "$shared_folder" ]; then
local shared="-fsdev local,id=shared,path=$shared_folder,security_model=none -device virtio-9p-pci,fsdev=shared,mount_tag=shared"
fi
if [ ! -z "$port_mapping" ]; then
local hostfwd=",$port_mapping"
fi
# Check if image exists, create otherwise
if [ ! -e "$image" ]; then
if [ ! -z "$basebox" ]; then
if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$basebox" ]; then
baseimage="`kvmx list_image $basebox`"
if [ ! -e "$baseimage" ]; then
echo "$BASENAME: could not find basebox $baseimage. Please create it first."
exit 1
fi
echo "Copying base image $baseimage to $image"
cp $baseimage $image
fi
else
kvmx-create $GLOBAL_USER_CONFIG_FOLDER/$VM
fi
fi
# Run virtual machine
kvm -m 2048 -name $VM -drive file=$image,if=virtio -vga qxl $shared \
-spice port=$PORT,addr=127.0.0.1,disable-ticketing,streaming-video=off,jpeg-wan-compression=never,playback-compression=off,zlib-glz-wan-compression=never,image-compression=off \
-device virtio-serial-pci \
-device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0 \
-chardev spicevmc,id=spicechannel0,name=vdagent \
-smp 2 -soundhw ac97 -cpu host -balloon virtio \
-net nic,model=virtio \
-net user,hostfwd=tcp:127.0.0.1:$SSH-:22$hostfwd &> $LOGFILE &
PID="$!"
# Save state
echo $PID > $PIDFILE
echo $PORT > $PORTFILE
echo $SSH > $SSHFILE
if [ "$run_spice_client" == "1" ]; then
kvmx_spice
fi
}
# Display usage
function kvmx_usage {
echo "usage: $BASENAME [options]"
echo "examples:"
echo ""
echo "$BASENAME list"
echo "$BASENAME init [folder]"
echo "$BASENAME clone "
exit 1
}
# Log into the guest using SSH
function kvmx_ssh {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
shift 2
SSH="`cat $SSHFILE`"
$SSH_COMMAND -p $SSH $SSH_LOGIN@127.0.0.1 $*
}
# Suspend the virtual machine
function kvmx_suspend {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
PID="`cat $PIDFILE`"
kill -STOP $PID
}
# Check if a guest is running
function kvmx_running {
if [ ! -e "$PIDFILE" ]; then
return 1
fi
PID="`cat $PIDFILE`"
ps $PID &> /dev/null
return $?
}
# Resume the guest
function kvmx_resume {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
PID="`cat $PIDFILE`"
kill -CONT $PID
}
# Poweroff the guest
function kvmx_poweroff {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
echo /usr/bin/sudo poweroff | kvmx_ssh &> /dev/null
sleep 3
kvmx_status
}
# Reboot the guest
function kvmx_reboot {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
echo /usr/bin/sudo reboot | kvmx_ssh &> /dev/null
sleep 3
kvmx_status
}
# Rsync files to the guest
function kvmx_rsync {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
ORIG="$3"
DEST="$4"
SSH="`cat $SSHFILE`"
rsync -av -e "$SSH_COMMAND -o Port=$SSH" --rsync-path "sudo rsync" $ORIG/ $SSH_LOGIN@127.0.0.1:$DEST/
}
# List guests
function kvmx_list {
ls $GLOBAL_USER_CONFIG_FOLDER
}
# Upgrade guest
function kvmx_upgrade {
echo "sudo apt-get update && sudo apt-get dist-upgrade -y && sudo apt-get autoremove -y" | kvmx_ssh
}
# Initialize
function kvmx_initialize {
if [ "$ACTION" == "init" ] || [ "$ACTION" == "list" ]; then
return
fi
if [ -z "$VM" ]; then
VM="$(basename `pwd`)"
fi
# Default parameters
PORT="$(($RANDOM + 1024))"
SSH="$(($PORT + 22))"
SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=FATAL -i $DIRNAME/ssh/insecure_private_key"
SSH_LOGIN="user"
# Initalize
mkdir -p $GLOBAL_USER_CONFIG_FOLDER
# Load and check guest config
if [ "$ACTION" != "init" ] && [ "$ACTION" != "list" ] && [ "$ACTION" != "edit" ]; then
if [ ! -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
echo "$BASENAME: config not found: $GLOBAL_USER_CONFIG_FOLDER/$VM"
exit 1
else
source $GLOBAL_USER_CONFIG_FOLDER/$VM
fi
if [ -z "$image" ]; then
image="/var/cache/qemu/$VM/box.img"
fi
if [ -z "$KVMXFILE" ]; then
KVMXFILE="/var/cache/qemu/$VM/kvmxfile"
fi
# Box and folder config
STORAGE="`dirname $image`"
STATE_DIR="$STORAGE/state/$VM"
PIDFILE="$STATE_DIR/pid"
PORTFILE="$STATE_DIR/port"
SSHFILE="$STATE_DIR/ssh"
LOGFILE="$STATE_DIR/log"
mkdir -p $STATE_DIR
if [ ! -e "$image" ] && [ "$ACTION" != "up" ] && [ "$ACTION" != "purge" ] && [ "$ACTION" != "destroy" ]; then
echo "$BASENAME: file not found: $image"
exit 1
fi
fi
}
# Initializes a new guest
function kvmx_init {
FOLDER="$3"
if [ -z "$FOLDER" ]; then
if [ -z "$VM" ]; then
VM="$(basename `pwd`)"
FOLDER="$(dirname `pwd`)/$VM"
else
FOLDER="$(pwd)/$VM"
fi
fi
if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
echo "$BASENAME: guest $VM already exists"
exit 1
fi
if [ ! -d "$FOLDER" ]; then
mkdir -p $FOLDER
fi
# Ensure we have an absolute folder name
FOLDER="`cd $FOLDER &> /dev/null && pwd`"
# Copy config from template
if [ ! -e "$FOLDER/kvmxfile" ]; then
cp $DIRNAME/kvmxfile $FOLDER/
sed -i -e "s|hostname=\"machine\"|hostname=\"$VM\"|g" $FOLDER/kvmxfile
fi
# Create config entry
( cd $GLOBAL_USER_CONFIG_FOLDER && ln -s $FOLDER/kvmxfile $VM )
}
# Clone a guest
function kvmx_clone {
if kvmx_running; then
echo "$BASENAME: orig $VM is running, cannot clone."
exit 1
fi
FOLDER="$3"
DEST="`basename $FOLDER`"
if [ -z "$FOLDER" ]; then
kvmx_usage
fi
# Check if dest machine exists
if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$DEST" ]; then
echo "$BASENAME: destination guest $DEST already exists."
exit 1
fi
if [ -d "$FOLDER" ]; then
echo "$BASENAME: destination $FOLDER already exists."
exit 1
fi
# Ensure we have an absolute folder name
mkdir -p $FOLDER
FOLDER="`cd $FOLDER &> /dev/null && pwd`"
rmdir $FOLDER
# Copy image and configuration
cp -r `dirname $image` $FOLDER/
( cd $GLOBAL_USER_CONFIG_FOLDER && ln -s $FOLDER/kvmxfile $DEST )
# Update config file
new_image="$FOLDER/`basename $image`"
sed -i -e "s|image=\"$image\"|image=\"$new_image\"|g" $GLOBAL_USER_CONFIG_FOLDER/$DEST
sed -i -e "s|hostname=\"$VM\"|hostname=\"$DEST\"|g" $GLOBAL_USER_CONFIG_FOLDER/$DEST
}
# Edit guest config
function kvmx_edit {
if [ -z "$EDITOR" ]; then
EDITOR="vi"
fi
if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
$EDITOR $GLOBAL_USER_CONFIG_FOLDER/$VM
else
echo "$BASENAME: $GLOBAL_USER_CONFIG_FOLDER/$VM: file not found."
fi
}
# Stop a guest
function kvmx_stop {
if kvmx_running; then
PID="`cat $PIDFILE`"
kill $PID
fi
}
# Destroy a guest
function kvmx_destroy {
kvmx_stop
rm -f $image
rm -rf $STATE_DIR
echo "$BASENAME: removed image and state files, but not the whole`dirname $image` folder."
}
# Purge a guest and all its configuration
function kvmx_purge {
kvmx_destroy
rm -f $GLOBAL_USER_CONFIG_FOLDER/$VM
echo "$BASENAME: removed $GLOBAL_USER_CONFIG_FOLDER/$VM config."
}
# Provision a machine
function kvmx_provision {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if [ -z "$provision_command" ]; then
echo "$BASENAME: error: parameter provision_command is not configured for $VM."
exit 1
fi
if [ ! -z "$provision_rsync" ]; then
SSH="`cat $SSHFILE`"
ORIG="`echo $provision_rsync | cut -d ' ' -f 1`"
DEST="`echo $provision_rsync | cut -d ' ' -f 2`"
echo "sudo mkdir -p `dirname $DEST`" | kvmx_ssh
rsync -av -e "$SSH_COMMAND -o Port=$SSH" --rsync-path "sudo rsync" $ORIG/ $SSH_LOGIN@127.0.0.1:$DEST/
fi
echo "$provision_command $hostname $domain $mirror" | kvmx_ssh
}
# Print guest image file name
function kvmx_list_image {
echo $image
}
# Print guest status
function kvmx_status {
if kvmx_running; then
echo "$BASENAME: $VM guest is running"
PID="`cat $PIDFILE`"
ps $PID
else
echo "$BASENAME: $VM guest is stopped"
fi
}
# Print guest log
function kvmx_log {
if [ -e "$LOGFILE" ]; then
cat $LOGFILE
fi
}
# Dispatch
if type kvmx_$ACTION 2> /dev/null | grep -q 'function'; then
kvmx_initialize
kvmx_$ACTION $*
else
kvmx_usage
fi