#!/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
VERSION="0.1.0"
BASENAME="`basename $0`"
DIRNAME="`dirname $0`"
ACTION="$1"
VM="$2"
GLOBAL_USER_CONFIG_FOLDER="$HOME/.config/kvmx"
GLOBAL_USER_CONFIG_FILE="$HOME/.config/kvmxconfig"
# Get the application base
function kvmx_app_base {
local dest
local base
# Determine if we are in a local or system-wide install.
if [ -h "$0" ]; then
dest="$(readlink $0)"
# Check again as the caller might be a symlink as well
if [ -h "$dest" ]; then
base="`dirname $dest`"
dest="$(dirname $(readlink $dest))"
else
base="`dirname $0`"
dest="`dirname $dest`"
fi
# Deal with relative or absolute links
if [ "`basename $dest`" == "$dest" ]; then
APP_BASE="$base"
else
APP_BASE="$dest"
fi
else
APP_BASE="`dirname $0`"
fi
echo $APP_BASE
}
# Build a SSH command
function __kvmx_ssh_command {
# See http://blog.djm.net.au/2013/11/chacha20-and-poly1305-in-openssh.html
SSH_OPTS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=FATAL -o ProxyCommand=none -o Ciphers=chacha20-poly1305@openssh.com -o User=$SSH_LOGIN -i $1"
SSH_COMMAND="ssh $SSH_OPTS"
SCP_COMMAND="scp $SSH_OPTS"
}
# Create a guest entry at the global user config folder
function __kvmx_create_config_entry {
if [ -z "$FOLDER" ]; then
return 1
fi
( cd $GLOBAL_USER_CONFIG_FOLDER && ln -sf $FOLDER/kvmxfile $VM )
}
# Initialize
function __kvmx_initialize {
if [ "$ACTION" == "app_base" ]; then
return
fi
# Load basic functions
export APP_BASE="`$DIRNAME/kvmx app_base`"
source $APP_BASE/lib/kvmx/functions || exit 1
# Alias to be used in config files
KVMX_BASE="$APP_BASE"
# Initialize
mkdir -p $GLOBAL_USER_CONFIG_FOLDER
# Stop processing here for some actions
if [ "$ACTION" == "init" ] || [ "$ACTION" == "list" ]; then
return
fi
if [ -z "$VM" ]; then
VM="$(basename `pwd`)"
fi
# Default parameters
PORT="$(($RANDOM + 1024))"
SSH="$(($PORT + 22))"
GUEST_DISPLAY="$(((RANDOM % 10) + 1))"
XDMCP_PORT="$(($RANDOM + 10000))"
# Load user config
if [ -e "$GLOBAL_USER_CONFIG_FILE" ]; then
source $GLOBAL_USER_CONFIG_FILE
fi
# Load and check guest config
if [ "$ACTION" != "ls" ] && [ "$ACTION" != "edit" ] && [ "$ACTION" != "usage" ]; then
if [ ! -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
if [ -e "kvmxfile" ]; then
# Existing kvmxfile but not registered at the global user config
FOLDER="$(pwd)"
__kvmx_create_config_entry
else
echo "$BASENAME: config not found: $GLOBAL_USER_CONFIG_FOLDER/$VM"
exit 1
fi
else
source $GLOBAL_USER_CONFIG_FOLDER/$VM
fi
if [ -z "$image" ]; then
if [ -z "$image_base" ]; then
image_base="$HOME/.local/share/kvmx"
fi
# There might be trouble managing the guest when project folder name is different from
# hostname and no image param is set (kvmx-create puts image in one
# place, kvmx expects in the other). So we try to guess first the image by hostname
# and then by guest name. Note that it might be possible to have conflicts if there
# are machines with hostnames set to the name os other machines. But here we hope that
# the user is not messing that much ;)
if [ ! -z "$hostname" ] && [ -e "$image_base/$hostname/box.img" ]; then
image="$image_base/$hostname/box.img"
else
image="$image_base/$VM/box.img"
fi
fi
if [ ! -h "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
echo "error: $GLOBAL_USER_CONFIG_FOLDER/$VM is not a symlink"
exit 1
fi
# Box and folder config
KVMXFILE="`readlink $GLOBAL_USER_CONFIG_FOLDER/$VM`"
KVMX_PROJECT_FOLDER="`dirname $KVMXFILE`"
STORAGE="`dirname $image`"
DATADIR="${datadir:-$STORAGE}"
STATE_DIR="$DATADIR/state/$VM"
LOG_DIR="$DATADIR/log"
PIDFILE="$STATE_DIR/pid"
PORTFILE="$STATE_DIR/port"
XDMCPPORTFILE="$STATE_DIR/xdmcp"
SSHFILE="$STATE_DIR/ssh"
DISPLAYFILE="$STATE_DIR/display"
SPICEFILE="$STATE_DIR/spice"
XEPHYRFILE="$STATE_DIR/xephyr"
LOGFILE="$LOG_DIR/qemu"
SPICELOG="$LOG_DIR/spice"
XPRALOG="$LOG_DIR/xpra"
XDMCPLOG="$LOG_DIR/xdmcp"
MONITORFILE="$STATE_DIR/monitor"
CONSOLEFILE="$STATE_DIR/console"
if [ -e "$DATADIR/ssh/$VM.key" ]; then
mkdir -p "$DATADIR/ssh"
SSHKEY="$DATADIR/ssh/$VM.key"
else
SSHKEY="$APP_BASE/share/ssh/insecure_private_key"
fi
if [ ! -z "$user" ]; then
SSH_LOGIN="$user"
else
SSH_LOGIN="user"
fi
__kvmx_ssh_command $SSHKEY
mkdir -p $STATE_DIR $LOG_DIR
if [ ! -e "$image" ] && [ "$ACTION" != "up" ] && [ "$ACTION" != "provision" ] && [ "$ACTION" != "purge" ] \
&& [ "$ACTION" != "destroy" ] && [ "$ACTION" != "install" ]; then
echo "$BASENAME: file not found: $image"
exit 1
fi
fi
}
# Run spice client
function kvmx_spice {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
# Ensure we have the right port configuration: we can also be
# running directly from command line.
PORT="`cat $PORTFILE`"
if [ -z "$PORT" ]; then
echo "$BASENAME: cannot get spice port for $VM."
exit 1
fi
if [ "$spice_client" == "spicy" ] && which spicy &> /dev/null; then
spicy -h localhost -p $PORT &
elif [ "$spice_client" == "virt-viewer" ] && which virt-viewer &> /dev/null; then
remote-viewer spice://localhost:$PORT &
else
if which spicec &> /dev/null; then
# https://lists.freedesktop.org/archives/spice-devel/2013-September/014643.html
SPICE_NOGRAB=1 spicec --host localhost --port $PORT &> $SPICELOG &
fi
fi
SPICEPID="$!"
echo "$SPICEPID" > $SPICEFILE
# Give time to connect
sleep 1
# Fix window title an position
if which /usr/bin/xdotool &> /dev/null; then
if [ ! -z "$xclient_windowmove" ]; then
xdotool search --name "SPICEc:0" windowmove $xclient_windowmove
fi
xdotool search --name "SPICEc:0" set_window --name $VM
fi
}
# Bring virtual machine up
function kvmx_up {
if kvmx_suspended; then
$DIRNAME/$BASENAME resume $VM
if [ "$run_spice_client" == "1" ]; then
$DIRNAME/$BASENAME spice $VM
fi
if [ "$run_xpra" == "1" ]; then
$DIRNAME/$BASENAME xpra $VM
fi
if [ "$run_xephyr" == "1" ]; then
$DIRNAME/$BASENAME xephyr $VM
fi
exit
elif kvmx_running; then
echo "$BASENAME: guest $VM is already running"
if [ "$run_spice_client" == "1" ]; then
kvmx spice $VM
exit
else
exit 1
fi
fi
if [ ! -z "$shared_folder" ]; then
# Get absolute path of shared folder relative to project path
shared_folder="`cd $KVMX_PROJECT_FOLDER && cd $shared_folder &> /dev/null && pwd`"
# Requires samba package installed in the host; see http://unix.stackexchange.com/a/183609
#local shared="-net user,smb=$shared_folder"
# See http://wiki.qemu-project.org/Documentation/9psetup
local shared="-fsdev local,id=shared,path=$shared_folder,security_model=none -device virtio-9p-pci,fsdev=shared,mount_tag=shared"
elif [ ! -z "$shared_folders" ]; then
local old_ifs="$IFS"
local shared_item
local shared
IFS=","
for shared_item in $shared_folders; do
local id="`echo $shared_item | cut -d ':' -f 1`"
local shared_folder="`echo $shared_item | cut -d ':' -f 2`"
local shared_folder_mountpoint="`echo $shared_item | cut -d ':' -f 3`"
# Get absolute path of shared folder relative to project path
shared_folder="`cd $KVMX_PROJECT_FOLDER && cd $shared_folder &> /dev/null && pwd`"
shared="$shared -fsdev local,id=$id,path=$shared_folder,security_model=none -device virtio-9p-pci,fsdev=$id,mount_tag=$id"
unset shared_folder
unset shared_folder_mountpoint
done
IFS="$old_ifs"
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`"
basekey="$(ls `dirname $baseimage`/ssh/$basebox.key)"
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..."
if which rsync &> /dev/null; then
rsync -ah --sparse --progress $baseimage $image
else
# See https://rwmj.wordpress.com/2010/10/19/tip-making-a-disk-image-sparse/
cp --sparse=always $baseimage $image
fi
if [ -e "$basekey" ]; then
vmname="$(basename $STORAGE)"
imagekey="$STORAGE/ssh/$vmname.key"
mkdir -p "$STORAGE/ssh"
cp $basekey $imagekey
cp $basekey.pub $imagekey.pub
# Re-evaluate this if there's a custom SSH key.
__kvmx_ssh_command $basekey
fi
local wait="y"
fi
else
local wait="y"
kvmx-create $GLOBAL_USER_CONFIG_FOLDER/$VM
fi
if [ "$wait" == "y" ]; then
echo "Waiting before starting the new guest..."
sleep 5
fi
fi
if [ -z "$graphics" ]; then
graphics="-vga qxl"
fi
if [ -z "$memory" ]; then
memory="2048"
fi
if [ -z "$smp" ]; then
smp="2"
fi
if [ -z "$drive_interface" ]; then
drive_interface="virtio"
fi
if [ -z "$nic_model" ]; then
nic_model="virtio"
fi
if [ -z "$shared_folder_msize" ]; then
shared_folders_msize="524288"
fi
if [ -z "$shared_folders_cache" ]; then
shared_folders_cache="none"
fi
if [ -z "$net" ] || [ "$net" == "user" ]; then
net_opts="user,hostfwd=tcp:127.0.0.1:$SSH-:22,hostfwd=udp:127.0.0.1:$XDMCP_PORT-:177$hostfwd -net nic,model=$nic_model"
elif [ "$net" == "tap" ]; then
# Thanks kvm-manager
tap="${VM}0"
# MAC address is derived from a hash of the host's name and the guest's name:
mac_address="$(printf "02:%s" "$(printf "%s\0%s" "$(hostname)" "${VM}" | sha256sum | sed 's/\(..\)/\1:/g' | cut -f1-5 -d:)")"
bridge="br0"
net_opts="tap,ifname=$tap,script=no,downscript=no,vlan=0,name=hostnet0 -device virtio-net-pci,vlan=0,id=net0,mac=$mac_address,bus=pci.0"
fi
if [ -z "$spice" ] || [ "$spice" == "1" ]; then
spice_opts="-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"
spice_opts="$spice_opts -device virtio-serial-pci"
spice_opts="$spice_opts -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0"
spice_opts="$spice_opts -chardev spicevmc,id=spicechannel0,name=vdagent"
fi
# Run virtual machine
# See https://en.wikipedia.org/wiki/Nohup#Overcoming_hanging
nohup kvm -m $memory -name $VM \
-drive file=$image,if=$drive_interface $graphics $shared \
$spice_opts \
-chardev "socket,id=monitor,path=$MONITORFILE,server,nowait" -mon chardev=monitor,mode=readline \
-chardev "socket,id=serial0,path=$CONSOLEFILE,server" -device isa-serial,chardev=serial0 \
-smp $smp -soundhw ac97 -cpu host -balloon virtio \
-net $net_opts \
$qemu_opts &> $LOGFILE < /dev/null &
PID="$!"
# Save state
echo $PID > $PIDFILE
echo $PORT > $PORTFILE
echo $SSH > $SSHFILE
echo $GUEST_DISPLAY > $DISPLAYFILE
echo $XDMCP_PORT > $XDMCPPORTFILE
# Thanks kvm-manager code for that portion
/usr/bin/screen -D -m -L $LOGDIR/servicelog \
-c $DIRNAME/share/screen/screenrc \
-S "kvmx-$VM" -t "kvmx-$VM" socat STDIO,raw,echo=0 "UNIX:${CONSOLEFILE},retry=30" &
if [ "$run_spice_client" == "1" ]; then
sleep 1
kvmx_spice
fi
if [ "$ssh_support" == "y" ]; then
let ssh_attempts="0"
echo -n "Waiting for machine to boot..."
while true; do
echo true | $SSH_COMMAND -o ConnectTimeout=2 -o NumberOfPasswordPrompts=0 -p $SSH 127.0.0.1 &> /dev/null && break
echo -n "."
let ssh_attempts++
if [ "$ssh_attempts" == "20" ]; then
echo "$BASENAME: timeout or access denied when trying to SSH into $VM."
echo "$BASENAME: please check if the image is in a good state and if it accepts passwordless ssh connections"
kvmx_stop
exit 1
fi
sleep 8
done
echo " done."
#sleep 5
#echo ""
kvmx_hostname
# Somehow it is starting before DBUS and then crashing, so we try to start again
echo "Ensure spice-vdagent is running..."
echo "sudo /usr/sbin/service spice-vdagent start" | kvmx_ssh
# See https://www.kernel.org/doc/Documentation/filesystems/9p.txt
if [ ! -z "$shared_folder" ] && [ ! -z "$shared_folder_mountpoint" ]; then
echo "Mounting $shared_folder on $shared_folder_mountpoint on guest..."
echo "sudo mkdir -p $shared_folder_mountpoint" | kvmx_ssh
echo "sudo mount -t 9p -o trans=virtio,msize=$shared_folders_msize shared $shared_folder_mountpoint -oversion=9p2000.L,posixacl,cache=$shared_folders_cache -o sync -o dirsync" | kvmx_ssh
#echo "sudo mount //10.0.2.4/qemu $shared_folder_mountpint" | kvmx_ssh
elif [ ! -z "$shared_folders" ]; then
local old_ifs="$IFS"
local shared_item
IFS=","
for shared_item in $shared_folders; do
local id="`echo $shared_item | cut -d ':' -f 1`"
local shared_folder="`echo $shared_item | cut -d ':' -f 2`"
local shared_folder_mountpoint="`echo $shared_item | cut -d ':' -f 3`"
# Get absolute path of shared folder relative to project path
shared_folder="`cd $KVMX_PROJECT_FOLDER && cd $shared_folder &> /dev/null && pwd`"
# Restore IFS for a while or kvmx_ssh won't work
IFS="$old_ifs"
echo "Mounting $shared_folder on $shared_folder_mountpoint $id on guest..."
echo "sudo mkdir -p $shared_folder_mountpoint" | kvmx_ssh
echo "sudo mount -t 9p -o trans=virtio $id $shared_folder_mountpoint -oversion=9p2000.L,posixacl,cache=none -o sync -o dirsync" | kvmx_ssh
IFS=","
done
IFS="$old_ifs"
fi
fi
if [ "$run_xpra" == "1" ]; then
$DIRNAME/$BASENAME xpra $VM
fi
if [ "$run_xephyr" == "1" ]; then
$DIRNAME/$BASENAME xephyr $VM
fi
if [ ! -z "$startup_command" ] && [ "$ssh_support" == "y" ]; then
echo "Running $startup_command..."
echo "nohup $startup_command" | kvmx ssh $VM &> /dev/null &
fi
kvmx_status
}
# Set hostname
function kvmx_hostname {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
echo "Setting hostname..."
$SSH_COMMAND -o ConnectTimeout=2 -p $SSH 127.0.0.1 < /dev/null
fi
echo "$hostname.$domain" | sudo tee /etc/hostname &> /dev/null
sudo hostname $hostname.$domain 2> /dev/null
# Remove old hostname from hosts file
if [ "\$OLD_HOST" != "$hostname.$domain" ]; then
if grep -q \$OLD_HOST /etc/hosts; then
sudo sed -i -e "/\$OLD_HOST/d" /etc/hosts 2> /dev/null
fi
fi
##### END REMOTE SCRIPT #######
EOF
}
# Display usage
function kvmx_usage {
echo "$BASENAME $VERSION - virtual machine manager"
echo ""
echo "usage: $BASENAME [options]"
echo ""
echo "available actions:"
echo ""
grep "^function kvmx_" $0 | cut -d ' ' -f 2 | sed -e 's/kvmx_//' | sort | xargs -L 6 | column -t -c 6 | sed -e 's/^/\t/'
echo ""
echo "examples:"
echo ""
echo -e "\t$BASENAME list"
echo -e "\t$BASENAME init [folder]"
echo -e "\t$BASENAME clone "
echo -e "\t$BASENAME ssh -X firefox"
echo ""
local list="`kvmx_list`"
if [ ! -z "$list" ]; then
echo "available virtual machines:"
echo ""
echo "$list" | sed -e 's/^/\t/'
echo ""
fi
exit 1
}
# Log into the guest using SSH
function kvmx_ssh {
if ! kvmx_running; then
kvmx up $VM || exit 1
#kvmx_up || exit 1
#echo "$BASENAME: guest $VM is not running"
#exit 1
fi
if [ "$ssh_support" != "y" ]; then
echo "$BASENAME: SSH support for $VM is disabled"
exit 1
fi
# Shift params according to how the program was called:
# either "kvmx ssh" or "kvmx ssh guest".
if [ "$ACTION" == "ssh" ]; then
if [ ! -z "$2" ]; then
shift 2
else
shift 1
fi
fi
SSH="`cat $SSHFILE`"
#TERM=xterm $SSH_COMMAND -p $SSH 127.0.0.1 $*
$ssh_env $SSH_COMMAND -p $SSH 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
SPICEPID="`cat $SPICEFILE`"
if ps $SPICEPID &> /dev/null; then
kill $SPICEPID
fi
}
# Check if a guest is running
function kvmx_running {
if [ ! -e "$PIDFILE" ]; then
return 1
fi
PID="`cat $PIDFILE`"
if [ -z "$PID" ]; then
return 1
fi
# Simpler check
#ps $PID &> /dev/null
# Better check were process should match a qemu binary
ps -o command $PID | grep -q '^qemu'
return $?
}
# Check if a guest is running
function kvmx_suspended {
if ! kvmx_running; then
return 1
else
if ps -p $PID -o stat --no-headers | grep -q 'Tl'; then
return 0
else
return 1
fi
fi
}
# 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
if [ "$run_xpra" == "1" ]; then
$DIRNAME/$BASENAME xpra $VM stop
fi
echo /usr/bin/sudo poweroff | kvmx_ssh &> /dev/null
kvmx_xephyr_stop
sleep 3
kvmx_status
}
# Alias for poweroff
function kvmx_down {
kvmx_poweroff
}
# Alias for poweroff
function kvmx_halt {
kvmx_poweroff
}
# Hibernate
function kvmx_hibernate {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if ! kvmx_ssh test -s /swapfile; then
echo "Seems like /swapfile is absent in the guest, aborting"
exit 1
fi
# Currently 9p driver won't survive a reboot
# Umount shared folders
#if [ ! -z "$shared_folder" ] && [ ! -z "$shared_folder_mountpoint" ]; then
# echo "Umounting $shared_folder_mountpoint on guest..."
# echo "sudo umount $shared_folder_mountpoint" | kvmx_ssh
# if [ "$?" != "1" ]; then
# echo "Problem umounting $shared_folder_mountpoint, you might have errors when restoring"
# fi
#elif [ ! -z "$shared_folders" ]; then
# local old_ifs="$IFS"
# local shared_item
# IFS=","
# for shared_item in $shared_folders; do
# local shared_folder_mountpoint="`echo $shared_item | cut -d ':' -f 3`"
# # Restore IFS for a while or kvmx_ssh won't work
# IFS="$old_ifs"
# echo "Umounting $shared_folder_mountpoint on guest..."
# echo "sudo umount $shared_folder_mountpoint" | kvmx_ssh
# if [ "$?" != "1" ]; then
# echo "Problem umounting $shared_folder_mountpoint, you might have errors when restoring"
# fi
# IFS=","
# done
# IFS="$old_ifs"
#fi
echo "which s2disk &> /dev/null && /usr/bin/sudo s2disk" | kvmx_ssh &> /dev/null
echo "Checking if hibernation was successful..."
sleep 3
if kvmx_running; then
echo "Unable to hibernate guest: please check guest configuration"
exit 1
else
kvmx_xephyr_stop
kvmx_status
fi
}
# 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_to {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
ORIG="$3"
DEST="$4"
if [ -z "$DEST" ]; then
# Error
#exit 1
# Assume same as origin
DEST="$ORIG"
fi
# Fix ~/ path
if echo $DEST | grep -q -e "^$HOME"; then
DEST="$(echo $DEST | sed -e "s|^$HOME|/home/$SSH_LOGIN|")"
fi
SSH="`cat $SSHFILE`"
rsync -av --delete -e "$SSH_COMMAND -o Port=$SSH" --rsync-path "sudo rsync" $ORIG/ 127.0.0.1:$DEST/
}
# Rsync files to the guest
function kvmx_rsync_from {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
ORIG="$3"
DEST="$4"
if [ -z "$DEST" ]; then
# Error
#exit 1
# Assume same as origin
DEST="$ORIG"
fi
# Fix ~/ path
if echo $ORIG | grep -q -e "^$HOME"; then
ORIG="$(echo $ORIG | sed -e "s|^$HOME|/home/$SSH_LOGIN|")"
fi
SSH="`cat $SSHFILE`"
rsync -av --delete -e "$SSH_COMMAND -o Port=$SSH" --rsync-path "sudo rsync" 127.0.0.1:$ORIG/ $DEST/
}
# Copy files from the guest
function kvmx_scp_from {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
ORIG="$3"
DEST="$4"
if [ -z "$DEST" ]; then
# Error
#exit 1
# Assume same as origin
DEST="$ORIG"
fi
# Fix ~/ path
if echo $ORIG | grep -q -e "^$HOME"; then
ORIG="$(echo $ORIG | sed -e "s|^$HOME|/home/$SSH_LOGIN|")"
fi
SSH="`cat $SSHFILE`"
$SCP_COMMAND -o Port=$SSH 127.0.0.1:$ORIG $DEST
}
# Copy files to the guest
function kvmx_scp_to {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
ORIG="$3"
DEST="$4"
if [ -z "$DEST" ]; then
# Error
#exit 1
# Assume same as origin
DEST="$ORIG"
fi
# Fix ~/ path
if echo $DEST | grep -q -e "^$HOME"; then
DEST="$(echo $DEST | sed -e "s|^$HOME|/home/$SSH_LOGIN|")"
fi
SSH="`cat $SSHFILE`"
$SCP_COMMAND -o Port=$SSH $ORIG 127.0.0.1:$DEST
}
# List guests
function kvmx_list {
if [ -e "$GLOBAL_USER_CONFIG_FOLDER" ]; then
ls -1 $GLOBAL_USER_CONFIG_FOLDER | xargs -L 6 | column -t -c 6
fi
}
# Alias to list command
function kvmx_ls {
kvmx_list
}
# Upgrade guest
function kvmx_upgrade {
echo "sudo apt-get update && sudo apt-get dist-upgrade -y && sudo apt-get autoremove -y" | kvmx_ssh
}
# 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 $APP_BASE/kvmxfile $FOLDER/
sed -i -e "s|hostname=\"machine\"|hostname=\"$VM\"|g" $FOLDER/kvmxfile
fi
# Create config entry
__kvmx_create_config_entry
}
# 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
# If dest is given without a full path, clone to the same basedir
# as the original guest.
if [ "$FOLDER" == "$DEST" ]; then
FOLDER="`dirname $STORAGE`/$DEST"
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
echo "Copying basebox..."
if which rsync &> /dev/null; then
rsync -ah --sparse --progress $STORAGE/ $FOLDER/
else
cp -r --sparse=always $STORAGE/ $FOLDER/
fi
# Remove old state folder
rm -rf $FOLDER/state/*
# Create config entry
( 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" $FOLDER/kvmxfile
sed -i -e "s|hostname=\"$VM\"|hostname=\"$DEST\"|g" $FOLDER/kvmxfile
# Rename keypair if exists
if [ -e "$FOLDER/ssh/$VM.key" ]; then
mv $FOLDER/ssh/$VM.key $FOLDER/ssh/$DEST.key
mv $FOLDER/ssh/$VM.key.pub $FOLDER/ssh/$DEST.key.pub
fi
}
# Alias to clone
function kvmx_copy {
kvmx_clone $*
}
# 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
kvmx_xephyr_stop
fi
}
# Kill a guest
function kvmx_kill {
if kvmx_running; then
PID="`cat $PIDFILE`"
kill -9 $PID
kvmx_xephyr_stop
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 $STORAGE folder."
}
# Shred a guest
function kvmx_shred {
kvmx_stop
if which shred &> /dev/null; then
shred $image
rm -f $image
else
echo "$BASENAME: error shreding $image: shred program not available."
exit 1
fi
}
# Wipe a guest
function kvmx_wipe {
kvmx_stop
if which wipe &> /dev/null; then
wipe -f $image
rm -f $image
else
echo "$BASENAME: error wipeing $image: wipe program not available."
exit 1
fi
}
# 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
kvmx up $VM || exit 1
#kvmx_up || exit 1
#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
echo "Syncing provision files into the guest..."
# Always sync default provisioners
SSH="`cat $SSHFILE`"
ORIG="$KVMX_BASE/share/provision/"
DEST="/usr/local/share/kvmx/provision/"
echo "sudo mkdir -p `dirname $DEST`" | kvmx_ssh
rsync -av --delete -e "$SSH_COMMAND -o Port=$SSH" $provision_rsync_opts --rsync-path "sudo rsync" $ORIG/ 127.0.0.1:$DEST/
if [ ! -z "$provision_rsync" ]; then
local old_ifs="$IFS"
IFS=","
for provision_item in $provision_rsync; do
IFS="$old_ifs"
ORIG="`echo $provision_item | cut -d ' ' -f 1`"
DEST="`echo $provision_item | cut -d ' ' -f 2`"
# Sync custom provisioners
if [ "$ORIG" != "$KVMX_BASE/share/provision/" ] && [ "$DEST" != "/usr/local/share/kvmx/provision/" ]; then
(
# Go inside the project folder so a relative $ORIG works
cd `dirname $KVMXFILE` &> /dev/null
echo "sudo mkdir -p `dirname $DEST`" | kvmx_ssh
rsync -av -e "$SSH_COMMAND -o Port=$SSH" $provision_rsync_opts --rsync-path "sudo rsync" $ORIG/ 127.0.0.1:$DEST/
)
fi
IFS=","
done
IFS="$old_ifs"
fi
echo "Running provision command inside the guest..."
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 {
local logs=""
if [ -s "$LOGFILE" ]; then
logs="$logs $LOGFILE"
fi
if [ -s "$SPICELOG" ]; then
logs="$logs $SPICELOG"
fi
if [ -s "$XPRALOG" ]; then
logs="$logs $XPRALOG"
fi
if [ -s "$XDMCPLOG" ]; then
logs="$logs $XDMCPLOG"
fi
tail -F $logs
}
# Rotate SSH keys
function kvmx_rotate_sshkeys {
# Generate new keypair
mkdir -p "$DATADIR/ssh"
SSHKEY="$DATADIR/ssh/$VM.key"
__kvmx_ssh_keygen $SSHKEY.new "$user@`basename $image .img`"
# Replace pubkey on server
echo "touch ~/.ssh/authorized_keys.new && chmod 600 ~/.ssh/authorized_keys.new" | kvmx_ssh
cat $SSHKEY.new.pub | kvmx_ssh "tee ~/.ssh/authorized_keys.new &> /dev/null"
echo "mv ~/.ssh/authorized_keys.new ~/.ssh/authorized_keys" | kvmx_ssh
# Replace keypair locally
mv $SSHKEY.new $SSHKEY
mv $SSHKEY.new.pub $SSHKEY.pub
}
# Xpra integration
function kvmx_xpra {
if ! which xpra &> /dev/null; then
echo "$BASENAME: please install xpra package"
exit 1
fi
local action="$3"
shift 3
SSH="`cat $SSHFILE`"
if [ -z "$action" ]; then
action="start"
fi
if [ "$action" == "start" ] || [ "$action" == "attach" ]; then
nohup xpra $action --ssh="$SSH_COMMAND -p $SSH" ssh:127.0.0.1 $* &> $XPRALOG < /dev/null &
else
xpra $action --ssh="$SSH_COMMAND -p $SSH" ssh:127.0.0.1 $*
fi
}
# Alias for up command
function kvmx_start {
kvmx_up $*
}
# Alias for up command
function kvmx_run {
kvmx_up $*
}
# Connect to the guest using VNC
function kvmx_vnc {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
GUEST_DISPLAY="`cat $DISPLAYFILE`"
if [ -z "$vnc_client" ]; then
vnc_client="virt-viewer"
fi
if which $vnc_client &> /dev/null; then
if [ "$vnclient_client" == "virt-viewer" ]; then
$vnc_client vnc://127.0.0.1:$GUEST_DISPLAY
else
GUEST_DISPLAY="`cat $DISPLAYFILE`"
$vnc_client :$GUEST_DISPLAY
fi
else
echo "$BASENAME: no vnc_client configured"
exit 1
fi
}
# Connect to the guest using XDMCP/Xephyr
# See http://jeffskinnerbox.me/posts/2014/Apr/29/howto-using-xephyr-to-create-a-new-display-in-a-window/
function kvmx_xephyr {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
# Clipboard sharing
# https://ubuntuforums.org/showthread.php?t=1430363
GUEST_DISPLAY="`cat $DISPLAYFILE`"
XDMCP_PORT="`cat $XDMCPPORTFILE`"
# Check for resolution configuration
if [ ! -z "$resolution" ]; then
resolution="-screen $resolution"
else
# Detect resolution dynamically
if which xwininfo &> /dev/null; then
resolution="`xwininfo -root | grep -- '-geometry' | cut -d '+' -f 1 | sed -e 's/-geometry//'`"
# Check for resolution_x_offset and resolution_y_offset configuration
if [ ! -z "$resolution_x_offset" ] || [ ! -z "$resolution_y_offset" ]; then
local width="`echo $resolution | cut -d 'x' -f 1`"
local height="`echo $resolution | cut -d 'x' -f 2`"
resolution="$(($width $resolution_x_offset))x$(($height $resolution_y_offset))"
fi
resolution="-screen $resolution"
fi
fi
if ! which Xephyr &> /dev/null; then
echo "$BASENAME: please install Xephyr"
exit 1
fi
Xephyr :$GUEST_DISPLAY -to 3 -ac -port $XDMCP_PORT -query 127.0.0.1 $resolution &> $XDMCPLOG < /dev/null &
XEPHYRPID="$!"
echo "$XEPHYRPID" > $XEPHYRFILE
# Give time to connect
sleep 1
# Set keyboard layout
# Thanks https://unix.stackexchange.com/questions/304391/xephyr-keyboard-mapping-not-working-properly
setxkbmap -display :0 -print | xkbcomp - :$GUEST_DISPLAY &> /dev/null
# Fix window titles
if which /usr/bin/xdotool &> /dev/null; then
if [ ! -z "$xclient_windowmove" ]; then
xdotool search --name "Xephyr on :" windowmove $xclient_windowmove
fi
xdotool search --name "Xephyr on :" set_window --name $VM
fi
}
# Close Xephyr client
function kvmx_xephyr_stop {
if [ ! -e "$XEPHYRFILE" ]; then
if [ "$ACTION" == "xephyr_stop" ]; then
echo "$BASENAME: Xephyr not running for guest $VM"
exit 1
else
return
fi
fi
XEPHYRPID="`cat $XEPHYRFILE`"
if [ ! -z "$XEPHYRPID" ]; then
kill $XEPHYRPID &> /dev/null
fi
}
# Open a file inside a guest
function kvmx_open {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
ORIG="$3"
if [ -z "$ORIG" ]; then
echo "$BASENAME: missing file argument."
exit 1
elif [ ! -e "$ORIG" ]; then
echo "$BASENAME: file not found: $ORIG"
exit 1
fi
DEST_FOLDER="`kvmx ssh $VM /bin/mktemp -d`"
DEST="$DEST_FOLDER/`basename $ORIG`"
# Copy and open
kvmx scp_to $VM $ORIG $DEST
kvmx ssh $VM DISPLAY=:0 /usr/bin/xdg-open $DEST
# Copy back
TMP_OPEN_FOLDER="`mktemp -d`"
TMP_OPEN="$TMP_OPEN_FOLDER/`basename $ORIG`"
kvmx scp_from $VM $DEST $TMP_OPEN
# Check for changes
if ! diff $TMP_OPEN $ORIG 2> /dev/null; then
mv $TMP_OPEN $ORIG
else
rm -rf $TMP_OPEN_FOLDER
fi
# Remove from guest
kvmx ssh $VM rm -rf $DEST_FOLDER
}
# Rename a guest
function kvmx_rename {
if kvmx_running; then
echo "$BASENAME: guest $VM is running"
exit 1
fi
FOLDER="$3"
DEST="`basename $FOLDER`"
if [ -z "$FOLDER" ]; then
kvmx_usage
fi
# If dest is given without a full path, rename to the same basedir
# as the original guest.
if [ "$FOLDER" == "$DEST" ]; then
FOLDER="`dirname $STORAGE`/$DEST"
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
echo "Renaming guest..."
mv "$STORAGE" "$FOLDER"
# Remove old state folder
rm -rf $FOLDER/state/*
# Create config entry
( cd $GLOBAL_USER_CONFIG_FOLDER && ln -s $FOLDER/kvmxfile $DEST )
# Remove old kvmxfile
rm $GLOBAL_USER_CONFIG_FOLDER/$VM
# Update config file
new_image="$FOLDER/`basename $image`"
sed -i -e "s|image=\"$image\"|image=\"$new_image\"|g" $FOLDER/kvmxfile
sed -i -e "s|hostname=\"$VM\"|hostname=\"$DEST\"|g" $FOLDER/kvmxfile
# Rename keypair if exists
if [ -e "$FOLDER/ssh/$VM.key" ]; then
mv $FOLDER/ssh/$VM.key $FOLDER/ssh/$DEST.key
mv $FOLDER/ssh/$VM.key.pub $FOLDER/ssh/$DEST.key.pub
fi
}
# Alias to rename
function kvmx_move {
kvmx_rename $*
}
# Alias to rename
function kvmx_mv {
kvmx_rename $*
}
# Interface to QEMU monitor (thanks kvm-manager)
function kvmx_monitor {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if ! which socat &> /dev/null; then
echo "$BASENAME: please install socat"
exit 1
fi
socat $MONITORFILE STDIO
}
# Install system
function kvmx_install {
if kvmx_running; then
echo "$BASENAME: guest $VM is running"
exit 1
fi
local media="$3"
if [ -z "$media" ]; then
echo "usage: $BASENAME install $VM "
exit 1
elif [ ! -e "$media" ]; then
echo "$BASENAME: file not found: $media"
exit 1
fi
if [ -z "$memory" ]; then
memory="2048"
fi
if [ ! -e "$image" ]; then
echo "Creating $image with size $size..."
qemu-img create -f qcow2 $image $size
fi
kvm -m $memory -net nic,model=virtio -net user -drive file=$image -cdrom $media
}
# Serial console
function kvmx_console {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
screen -x kvmx-$VM
}
# Alias to console
function kvmx_serial {
kvmx_serial $*
}
# Dispatch
if type kvmx_$ACTION 2> /dev/null | grep -q "kvmx_$ACTION ()"; then
__kvmx_initialize
kvmx_$ACTION $*
else
kvmx_usage
fi