#!/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"
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 {
if [ ! -z "$1" ]; then
#local ssh_key_param="-i $1"
# See https://makandracards.com/makandra/512-how-to-fix-too-many-authentic-authentication-failures-with-ssh-and-or-capistrano
local ssh_key_param="-o IdentityFile=$1 -o IdentitiesOnly=yes"
fi
# 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 $ssh_key_param"
SSH_COMMAND="ssh $SSH_OPTS -o User=$SSH_LOGIN"
SCP_COMMAND="scp $SSH_OPTS -o User=$SSH_LOGIN"
}
# 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" ] || [ "$ACTION" == "version" ]; then
return
fi
# Load basic functions
export APP_BASE="`$DIRNAME/kvmx app_base`"
source $APP_BASE/lib/kvmx/functions || exit 1
# Check dependencies
__kvmx_check_dependencies
# 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
# Check if second argument is a VM name or option
if [ -z "$2" ]; then
VM="$(basename `pwd`)"
SHIFTARGS="1"
elif [ -e 'kvmxfile' ] && [ ! -e "$GLOBAL_USER_CONFIG_FOLDER/$2" ] && [ "$2" != "$(basename `pwd`)" ]; then
VM="$(basename `pwd`)"
SHIFTARGS="1"
else
VM="$2"
SHIFTARGS="2"
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
source $GLOBAL_USER_CONFIG_FOLDER/$VM
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 of 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"
SPICESOCKET="$STATE_DIR/spice.socket"
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 [ -z "$ssh_custom_pubkey" ]; then
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
fi
if [ ! -z "$user" ]; then
SSH_LOGIN="$user"
else
SSH_LOGIN="user"
fi
__kvmx_ssh_command $SSHKEY
mkdir -p $STATE_DIR $LOG_DIR
# Additional checks
if [ "$ACTION" != "up" ] && [ "$ACTION" != "provision" ] && [ "$ACTION" != "purge" ] && \
[ "$ACTION" != "destroy" ] && [ "$ACTION" != "install" ] && [ "$ACTION" != "config" ] && \
[ "$ACTION" != "config_unset" ] && [ "$ACTION" != "create" ] && [ "$ACTION" != "shell" ] && \
[ "$ACTION" != "boot" ]; then
if [ ! -e "$image" ]; then
echo "$BASENAME: file not found: $image"
exit 1
fi
# See http://www.linux-kvm.org/page/FAQ
if ! egrep -q '^flags.*(vmx|svm)' /proc/cpuinfo; then
echo "$BASENAME: WARNING: Intel VT or AMD-V not present at /proc/cpuinfo, expect slow performance"
fi
if ! lsmod | grep -q '^kvm '; then
echo "$BASENAME: WARNING: kvm kernel module not loaded, expect slow performance"
fi
if ! groups `whoami` | grep -q 'kvm'; then
echo "$BASENAME: WARNING: user `whoami` not in kvm group, expect slow performance"
fi
fi
fi
}
# Run spice client
function kvmx_spice {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if [ "$spice" == "0" ]; then
echo "$BASENAME: spice is disabled for guest $VM"
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 &
spicy --uri=spice+unix://$SPICESOCKET &
elif [ "$spice_client" == "virt-viewer" ] && which virt-viewer &> /dev/null; then
#remote-viewer spice://localhost:$PORT &
remote-viewer spice+unix://$SPICESOCKET &
#elif [ ! -z "$spice_client" ] && [ "$spice_client" != "spicec" ]; then
elif [ ! -z "$spice_client" ]; then
echo "$BASENAME: spice_client $spice_client not currently supported"
exit 1
else
if which spicy &> /dev/null; then
#spicy -h localhost -p $PORT &
spicy --uri=spice+unix://$SPICESOCKET &
fi
#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 2
# 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
xdotool search --name "spice display 0:0" windowmove $xclient_windowmove
fi
#xdotool search --name "SPICEc:0" set_window --name $VM
xdotool search --name "spice display 0:0" set_window --name $VM
fi
if [ "$ACTION" == "spice" ]; then
# Set screen resolution
if [ "$xrandr" == "1" ]; then
kvmx_xrandr
fi
# Restart vdagent
if [ "$ssh_support" == "y" ]; then
echo "which kvmx-vdagent > /dev/null && DISPLAY=:0 kvmx-vdagent" | kvmx_ssh
fi
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
mkdir -p $shared_folder
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
mkdir -p $shared_folder
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
unset id
done
IFS="$old_ifs"
unset shared_item
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
if [ ! -z "$backing_file" ] && [ "$backing_file" == "1" ]; then
echo "Creating image $image as an overlay of $baseimage..."
baseimage_format="`qemu-img info $baseimage | grep "^file format: " | cut -d : -f 2 | sed -e 's/ //g'` "
qemu-img create -o backing_file=$baseimage,backing_fmt=$baseimage_format -f $format $image
else
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
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
fi
if [ "$wait" == "y" ]; then
echo "Waiting before starting the new guest..."
sleep 10
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 "$cdrom" ]; then
cdrom_opts="-cdrom $cdrom"
fi
if [ ! -z "$boot" ]; then
boot_opts="-boot $boot"
fi
if [ ! -z "$usb" ] && [ "$usb" != "0" ]; then
# Basic USB support
if [ "$usb" == "1" ]; then
usb_opts="-usb"
fi
# USB support with 1.0 and 2.0 hub
if [ "$usb" == "2" ]; then
usb_opts="-usb -device usb-ehci,id=ehci -device usb-host,bus=ehci.0,vendorid=1452"
fi
# USB support with 1.0, 2.0 and 3.0 hubs
if [ "$usb" == "3" ]; then
usb_opts="-usb -device usb-ehci,id=ehci -device usb-host,bus=ehci.0,vendorid=1452 -device qemu-xhci,id=xhci"
fi
fi
# Check kvm version
if kvm --help | grep -q -- "^-balloon"; then
local new_qemu="0"
else
local new_qemu="1"
fi
if [ -z "$net" ] || [ "$net" == "user" ]; then
if [ "$new_qemu" == "0" ]; 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"
net_opts="user,hostfwd=tcp:127.0.0.1:$SSH-:22$hostfwd -net nic,model=$nic_model"
else
net_opts="user,id=net0,hostfwd=tcp:127.0.0.1:$SSH-:22$hostfwd -net nic,netdev=net0,model=$nic_model"
fi
elif [ "$net" == "tap" ]; then
# Thanks to 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"
if [ "$new_qemu" == "0" ]; then
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"
else
#net_opts="tap,ifname=$tap,script=no,downscript=no,id=n1 -device virtio-net-pci,netdev=n1,id=net0,mac=$mac_address,bus=pci.0"
net_opts="tap,ifname=$tap,script=no,downscript=no,id=net0 -device virtio-net-pci,netdev=net0,mac=$mac_address,bus=pci.0"
fi
fi
if [ ! -z "$net_dns" ] && [ "$net_dns" != "host" ]; then
net_opts="$net_opts,dns=$net_dns"
fi
# Check screen version
if screen --help | grep -q -- "-Logfile"; then
local screen_log="-L -Logfile"
else
local screen_log="-L"
fi
# Additional net and balloon options depending on qemu version
#if kvm --help | grep -q -- "^-balloon"; then
if [ "$new_qemu" == "0" ]; then
local balloon="-balloon virtio"
net_opts="-net $net_opts"
else
local balloon="-device virtio-balloon"
net_opts="-netdev $net_opts"
fi
# Always run spice using a socket to provide some GUI isolation between guest
# Otherwise any guest could open a spice connection to another guest using the host local IP (10.0.2.2) and the other guest spice port
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 unix,addr=$SPICESOCKET,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
if [ "$sound" != "0" ]; then
if [ -z "$sound" ]; then
sound="ac97"
fi
sound_opts="-soundhw $sound"
fi
if [ ! -z "$image_drive" ] && [ "$image_drive" == "cdrom" ]; then
image_opts="-cdrom $image"
else
image_opts="-drive file=$image,if=$drive_interface"
# TRIM support
if [ "$image_discards" != "n" ]; then
image_opts="$image_opts,discard=unmap"
fi
fi
if [ ! -z "$virtio_rng" ]; then
rng_opts="-device virtio-rng-pci,$virtio_rng"
else
rng_opts="-device virtio-rng-pci,max-bytes=128,period=1000"
fi
# USB redirect support
# See https://people.freedesktop.org/~teuf/spice-doc/html/ch02s06.html
#usb_opts="-device ich9-usb-ehci1,id=usb"
#usb_opts="$usb_opts -device ich9-usb-uhci1,masterbus=usb.0,firstport=0,multifunction=on"
#usb_opts="$usb_opts -device ich9-usb-uhci2,masterbus=usb.0,firstport=2"
#usb_opts="$usb_opts -device ich9-usb-uhci3,masterbus=usb.0,firstport=4"
#usb_opts="$usb_opts -chardev spicevmc,name=usbredir,id=usbredirchardev1"
#usb_opts="$usb_opts -device usb-redir,chardev=usbredirchardev1,id=usbredirdev1"
#usb_opts="$usb_opts -chardev spicevmc,name=usbredir,id=usbredirchardev2"
#usb_opts="$usb_opts -device usb-redir,chardev=usbredirchardev2,id=usbredirdev2"
#usb_opts="$usb_opts -chardev spicevmc,name=usbredir,id=usbredirchardev3"
#usb_opts="$usb_opts -device usb-redir,chardev=usbredirchardev3,id=usbredirdev3"
# Run virtual machine, nohup approach
# See https://en.wikipedia.org/wiki/Nohup#Overcoming_hanging
#nohup setsid kvm -m $memory -name $VM \
# -chardev "socket,id=monitor,path=$MONITORFILE,server,nowait" -mon chardev=monitor,mode=readline \
# -chardev "socket,id=serial0,path=$CONSOLEFILE,server,nowait" -device isa-serial,chardev=serial0 \
# -smp $smp -cpu host \
# $balloon \
# $graphics $shared \
# $image_opts \
# $spice_opts \
# $sound_opts \
# $cdrom_opts \
# $boot_opts \
# $net_opts \
# $rng_opts \
# $qemu_opts &> $LOGFILE < /dev/null &
# Run virtual machine, screen approach
# This is more immune to hangups
screen $screen_log $LOGFILE -S kvmx-qemu-$VM -d -m kvm -m $memory -name $VM \
-chardev "socket,id=monitor,path=$MONITORFILE,server,nowait" -mon chardev=monitor,mode=readline \
-chardev "socket,id=serial0,path=$CONSOLEFILE,server,nowait" -device isa-serial,chardev=serial0 \
-smp $smp -cpu host \
$balloon \
$graphics $shared \
$image_opts \
$spice_opts \
$sound_opts \
$cdrom_opts \
$boot_opts \
$net_opts \
$rng_opts \
$usb_opts \
-pidfile $PIDFILE \
-D $LOGFILE \
$qemu_opts
# Only if nohup approach is being used
#PID="$!"
# Save state
# Save PID here only if nohup approach is being used
#echo $PID > $PIDFILE
echo $PORT > $PORTFILE
echo $SSH > $SSHFILE
echo $GUEST_DISPLAY > $DISPLAYFILE
echo $XDMCP_PORT > $XDMCPPORTFILE
# Give time to qemu
sleep 1
# Thanks kvm-manager code for that portion
/usr/bin/screen -D -m $screen_log $LOG_DIR/servicelog \
-c $APP_BASE/share/screen/screenrc \
-S "kvmx-$VM" -t "kvmx-$VM" socat STDIO,raw,echo=0 "UNIX:${CONSOLEFILE},retry=30" &
if [ "$run_spice_client" == "1" ]; then
if [ "$spice" == "0" ]; then
echo "$BASENAME: spice is disabled for guest $VM"
else
kvmx_spice
fi
fi
# Check if on install mode
if [ "$install" == "1" ]; then
kvmx_status
return
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
kvmx_poweroff
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 using 9p..."
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=$shared_folders_cache -o sync -o dirsync" | kvmx_ssh
IFS=","
unset shared_folder
unset shared_folder_mountpoint
unset id
done
IFS="$old_ifs"
unset shared_item
fi
# Shall we add an umount hook when powering off the guest?
if [ ! -z "$shared_folders_sshfs" ]; then
local old_ifs="$IFS"
local shared_item
IFS=","
for shared_item in $shared_folders_sshfs; 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`"
# Temporaly reset IFS so kvmx_sshfs executes correctly
IFS="$old_ifs"
echo "Mounting $shared_folder on $shared_folder_mountpoint ($id) on host using SSHFS..."
kvmx_sshfs $shared_folder $shared_folder_mountpoint
IFS=","
unset shared_folder
unset shared_folder_mountpoint
unset id
done
IFS="$old_ifs"
unset shared_item
fi
if [ "$xrandr" == "1" ] && [ "$run_spice_client" == "1" ]; then
echo "Waiting for X11 to come up so we can set machine resolution..."
sleep 8
kvmx_xrandr
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
if [ ! -z "$startup_rsync_to_guest" ] && [ "$ssh_support" == "y" ]; then
local old_ifs="$IFS"
local item
IFS=","
for item in $startup_rsync_to_guest; do
if [ -z "$item" ]; then
continue
fi
local id="`echo $item | cut -d ':' -f 1`"
local startup_rsync_to_guest_orig="`echo $item | cut -d ':' -f 2`"
local startup_rsync_to_guest_dest="`echo $item | cut -d ':' -f 3`"
if [ -z "$id" ] || [ -z "$startup_rsync_to_guest_orig" ] || [ -z "$startup_rsync_to_guest_dest" ]; then
continue
fi
echo "Rsyncing to guest: $startup_rsync_to_guest ($id)..."
kvmx_rsync_to $startup_rsync_to_guest_orig $startup_rsync_to_guest_dest
unset startup_rsync_to_guest_orig
unset startup_rsync_to_guest_dest
unset id
done
IFS="$old_ifs"
unset item
fi
kvmx_status
}
# Set hostname
function kvmx_hostname {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if [ "$ssh_support" != "y" ]; then
echo "$BASENAME: ssh_support is disabled for guest $VM"
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 [vm] [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 [ "$ssh_support" != "y" ]; then
echo "$BASENAME: SSH support for $VM is disabled"
exit 1
fi
if ! kvmx_running || kvmx_suspended; then
echo "$BASENAME: $VM not running, trying to start it..."
kvmx up $VM || exit 1
#kvmx_up || exit 1
#echo "$BASENAME: guest $VM is not running"
#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 $*
}
# Get guest SSH key fingerprints
function kvmx_ssh_finger {
if ls $DATADIR/ssh/*.pub.* &> /dev/null; then
for finger in $DATADIR/ssh/*.pub.*; do
cat $finger
done
else
# Try to get list of keys in the server
keys="$(
echo | kvmx_ssh << EOF
for key in /etc/ssh/*pub; do
echo \$key
#ssh-keygen -l -f \$key
#ssh-keygen -l -E md5 -f \$key
done
EOF
)"
# Get fingerprint for each key
if [ ! -z "$keys" ]; then
for key in $keys; do
fingerprint="$(echo ssh-keygen -l -f $key | kvmx_ssh)"
echo $fingerprint | tee $DATADIR/ssh/`basename $key`.sha256
fingerprint="$(echo ssh-keygen -l -E md5 -f $key | kvmx_ssh)"
echo $fingerprint | tee $DATADIR/ssh/`basename $key`.md5
done
else
echo "$BASENAME: could not get SSH fingerprints for $VM"
exit 1
fi
fi
}
# Mount a guest folder into the host using sshfs
function kvmx_sshfs {
local folder="$1"
local mountpoint="$2"
if [ -z "$mountpoint" ]; then
kvmx_usage
fi
if ! which sshfs &> /dev/null; then
echo "$BASENAME: this action requires sshfs to be installed on your system."
exit 1
fi
SSH="`cat $SSHFILE`"
# See https://github.com/libfuse/sshfs/issues/82 about "-o writeback_cache=no"
# http://www.admin-magazine.com/HPC/Articles/Sharing-Data-with-SSHFS
sshfs $SSH_LOGIN@127.0.0.1:$folder $mountpoint $SSH_OPTS -o nonempty \
-o sshfs_sync \
-o sync_readdir \
-o cache=no \
-o transform_symlinks \
-o sync_read \
-o workaround=none \
-o noforget \
-o reconnect \
-o no_readahead \
-o compression=no -p $SSH
}
# Get guest PID
function kvmx_pid {
if [ -e "$PIDFILE" ]; then
# QEMU might put weird things into pidfile, so we need a simple filter
#cat $PIDFILE
cut -d ' ' -f 1 $PIDFILE | head -1
else
return 1
fi
}
# Suspend the virtual machine
function kvmx_suspend {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
PID="`kvmx_pid`"
if [ -z "$PID" ]; then
return 1
fi
kill -STOP $PID
# Alternative
#kvmx_monitor stop
if [ -e "$SPICEFILE" ]; then
SPICEPID="`cat $SPICEFILE`"
if [ -z "$SPICEPID" ]; then
return
fi
if ps $SPICEPID &> /dev/null; then
kill $SPICEPID
fi
fi
}
# Check if a guest is running
function kvmx_running {
if [ ! -e "$PIDFILE" ]; then
return 1
fi
PID="`kvmx_pid`"
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 -E '(^kvm)|(^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 'T'; 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="`kvmx_pid`"
if [ -z "$PID" ]; then
return 1
fi
kill -CONT $PID
# Alternative
#kvmx_monitor system_wakeup
}
# Poweroff the guest
function kvmx_poweroff {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if [ ! -z "$poweroff_pre_command" ] && [ "$ssh_support" == "y" ]; then
echo "Running $poweroff_pre_command..."
echo "nohup $poweroff_pre_command" | kvmx ssh $VM &> /dev/null &
fi
if [ ! -z "$poweroff_rsync_from_guest" ] && [ "$ssh_support" == "y" ]; then
local old_ifs="$IFS"
local item
IFS=","
for item in $poweroff_rsync_from_guest; do
if [ -z "$item" ]; then
continue
fi
local id="`echo $item | cut -d ':' -f 1`"
local poweroff_rsync_from_guest_orig="`echo $item | cut -d ':' -f 2`"
local poweroff_rsync_from_guest_dest="`echo $item | cut -d ':' -f 3`"
if [ -z "$id" ] || [ -z "$poweroff_rsync_from_guest_orig" ] || [ -z "$poweroff_rsync_from_guest_dest" ]; then
continue
fi
echo "Rsyncing from guest: $poweroff_rsync_from_guest ($id)..."
kvmx_rsync_from $poweroff_rsync_from_guest_orig $poweroff_rsync_from_guest_dest
unset poweroff_rsync_from_guest_orig
unset poweroff_rsync_from_guest_dest
unset id
done
IFS="$old_ifs"
unset item
fi
if [ "$run_xpra" == "1" ]; then
$DIRNAME/$BASENAME xpra $VM stop
fi
if [ "$ssh_support" == "y" ]; then
echo /usr/bin/sudo poweroff | kvmx_ssh &> /dev/null
else
kvmx_monitor system_powerdown
fi
kvmx_xephyr_stop
let poweroff_attempts="0"
echo -n "Waiting for machine to stop..."
while true; do
kvmx_running || break
echo -n "."
let poweroff_attempts++
if [ "$poweroff_attempts" == "20" ]; then
echo "$BASENAME: guest $VM is still running"
echo "$BASENAME: please consider to stop it using \"kvmx $VM stop\""
#kvmx_stop
exit 1
fi
sleep 3
done
echo " done."
#sleep 3
#echo ""
kvmx_status
}
# Alias for poweroff
function kvmx_down {
kvmx_poweroff
}
# Alias for poweroff
function kvmx_shutdown {
kvmx_poweroff
}
# Alias for stop
function kvmx_halt {
kvmx_stop
}
# Hibernate
function kvmx_hibernate {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if [ "$ssh_support" != "y" ]; then
echo "$BASENAME: ssh_support is disabled for guest $VM"
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
if [ "$ssh_support" != "y" ]; then
echo "$BASENAME: ssh_support is disabled for guest $VM"
exit 1
fi
ORIG="$1"
DEST="$2"
if [ -z "$ORIG" ]; then
echo "usage $BASENAME rsync_to $GUEST [dest]"
exit 1
fi
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 from the guest
function kvmx_rsync_from {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if [ "$ssh_support" != "y" ]; then
echo "$BASENAME: ssh_support is disabled for guest $VM"
exit 1
fi
ORIG="$1"
DEST="$2"
if [ -z "$ORIG" ]; then
echo "usage $BASENAME rsync_from $GUEST [dest]"
exit 1
fi
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
if [ "$ssh_support" != "y" ]; then
echo "$BASENAME: ssh_support is disabled for guest $VM"
exit 1
fi
ORIG="$1"
DEST="$2"
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
if [ "$ssh_support" != "y" ]; then
echo "$BASENAME: ssh_support is disabled for guest $VM"
exit 1
fi
ORIG="$1"
DEST="$2"
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="$1"
if [ -z "$FOLDER" ]; then
if [ -z "$VM" ]; then
VM="$(basename `pwd`)"
FOLDER="$(dirname `pwd`)/$VM"
else
FOLDER="$(pwd)/$VM"
fi
else
VM="$FOLDER"
if [ ! -z "$2" ]; then
FOLDER="$2"
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
sed -i -e "s|hostname=\"kvmx\"|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="$1"
DEST="`basename $FOLDER`"
OPT="$2"
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
if [ "$OPT" == "--skell" ]; then
local exclude="--exclude=box.img"
fi
rsync -ah --sparse --progress $exclude $STORAGE/ $FOLDER/
else
cp -r --sparse=always $STORAGE/ $FOLDER/
fi
# Remove old state folder
rm -rf $FOLDER/state/*
# Copy kvmxfile if not present on $FOLDER
if [ ! -e "$FOLDER/kvmxfile" ]; then
cat $GLOBAL_USER_CONFIG_FOLDER/$VM > $FOLDER/kvmxfile
fi
# 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
if [ "$DATADIR" != "$STORAGE" ]; then
echo "$BASENAME: please copy datadir $DATADIR manually"
fi
}
# Alias to clone
function kvmx_copy {
kvmx_clone $*
}
# Get, set or edit guest config
function kvmx_config {
if [ -z "$1" ]; then
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
else
#if [ -z "$2" ]; then
# echo "usage: $BASENAME $VM edit $1 "
# exit 1
#fi
param="$1"
shift
if [ -z "$1" ]; then
grep "^$param=" $KVMXFILE | \
sed -e 's/="/=/' -e 's/"$//' -e "s/='/=/" -e "s/'$//" -e "s/^$param=//"
elif ! grep -q "^$param=" $KVMXFILE; then
echo "$param=\"$*\"" >> $KVMXFILE
else
sed -i -e "s#^$param=.*#$param=\"$*\"#" $KVMXFILE
fi
fi
}
# Unset a guest config by commenting it
function kvmx_config_unset {
if [ ! -z "$1" ]; then
sed -i -e "s/^$1=/#$1=/" $KVMXFILE
else
echo $BASENAME: missing config parameter
exit 1
fi
}
# Alias to config
function kvmx_edit {
kvmx_config $*
}
# Stop a guest
function kvmx_stop {
if kvmx_running; then
PID="`kvmx_pid`"
if [ -z "$PID" ]; then
return 1
fi
kill $PID
kvmx_xephyr_stop
fi
}
# Kill a guest
function kvmx_kill {
if kvmx_running; then
PID="`kvmx_pid`"
if [ -z "$PID" ]; then
return 1
fi
kill -9 $PID
kvmx_xephyr_stop
fi
}
# Destroy a guest
function kvmx_destroy {
#kvmx_stop
if kvmx_running; then
echo "$BASENAME: $VM is running, cannot destroy."
exit 1
fi
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 kvmx_running; then
echo "$BASENAME: $VM is running, cannot shred."
exit 1
fi
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 kvmx_running; then
echo "$BASENAME: $VM is running, cannot wipe."
exit 1
fi
if which wipe &> /dev/null; then
wipe -f $image
rm -f $image
else
echo "$BASENAME: error wiping $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 [ "$ssh_support" != "y" ]; then
echo "$BASENAME: ssh_support is disabled for guest $VM"
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 folder name
function kvmx_list_folder {
echo $KVMX_PROJECT_FOLDER
}
# Print guest status
function kvmx_status {
if kvmx_suspended; then
echo "$BASENAME: $VM guest is suspended"
elif kvmx_running; then
echo "$BASENAME: $VM guest is running"
else
echo "$BASENAME: $VM guest is stopped"
return
fi
PID="`kvmx_pid`"
if [ -z "$PID" ]; then
return 1
fi
ps $PID
}
# 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
if [ -z "$logs" ]; then
echo "$BASENAME: $VM: all logs are empty"
exit
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
if ! kvmx_running || kvmx_suspended; then
echo "$BASENAME: $VM not running"
exit 1
fi
local action="$1"
shift
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
}
# X2Go integration
#function kvmx_x2go {
# if ! which x2goclient &> /dev/null; then
# echo "$BASENAME: please install x2goclient package"
# exit 1
# fi
#
# if ! kvmx_running || kvmx_suspended; then
# echo "$BASENAME: $VM not running"
# exit 1
# fi
#
# SSH="`cat $SSHFILE`"
#
# x2goclient --ssh-port=$SSH --ssh-key=$SSHKEY --session=$VM
#}
# Alias for up command
function kvmx_start {
kvmx_up $*
}
# Alias for up command
function kvmx_run {
kvmx_up $*
}
# Restart machine
function kvmx_restart {
if ! kvmx_running; then
echo "Guest $VM was not running, so starting it anyway..."
kvmx_start
else
#if [ "$ssh_support" != "y" ]; then
# echo sudo reboot | kvmx_ssh
# exit
#fi
echo "Powering off guest $VM..."
kvmx_poweroff
echo "Starting guest $VM again..."
kvmx_start
fi
}
# Alias to restart
function kvmx_reboot {
kvmx_restart $*
}
# 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="$1"
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="$1"
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
if [ "$DATADIR" != "$STORAGE" ]; then
echo "$BASENAME: please move datadir $DATADIR manually"
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
if [ -z "$1" ]; then
socat $MONITORFILE STDIO
else
socat STDIO $MONITORFILE <"
# Possible places to find existing ISOs
local candidates="/var/cache/media/distros /usr/local/share/isos"
for candidate in $candidates; do
if [ -d "$candidate" ]; then
results="`find $candidate -not -iwholename '*.git*' -name '*.iso' | sed -e 's/^/\t/'`"
if [ ! -z "$results" ]; then
echo "available images at $candidate:"
echo ""
echo -n "$results"
fi
fi
done
exit 1
elif [ ! -e "$media" ]; then
echo "$BASENAME: file not found: $media"
exit 1
fi
if [ -z "$memory" ]; then
memory="2048"
fi
if [ -z "$format" ]; then
format="qcow2"
fi
if [ ! -e "$image" ]; then
echo "Creating $image with size $size..."
qemu-img create -f $format $image $size
fi
# Basic install command
#kvm -m $memory -net nic,model=virtio -net user -drive file=$image -cdrom $media
# Install using kvmx_up
install=1
cdrom=$media
boot="once=dc"
kvmx_up
}
# Alias to install
function kvmx_boot {
kvmx_install $*
}
# Serial console
function kvmx_console {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
# Script hack, useful for servers when using su before attaching to a console
# See https://serverfault.com/questions/116775/sudo-as-different-user-and-running-screen
#script /dev/null && screen -x kvmx-$VM
#script -q -c "screen -x kvmx-$VM" /dev/null
screen -x kvmx-$VM
}
# Alias to console
function kvmx_serial {
kvmx_console $*
}
# Compress guest image
function kvmx_compress {
if kvmx_running; then
echo "$BASENAME: guest $VM is running"
exit 1
fi
if [ ! -e "$image" ]; then
echo "$BASENAME: image not found: $image"
exit 1
fi
# Avoid trying to convert guest using other images such as LVM volumes
if [ "$format" != "qcow2" ]; then
echo "$BASENAME: please convert guest $VM image to qcow2 and update your config"
exit 1
fi
if [ "$qcow2_compression" == "1" ]; then
compression="-c"
fi
# Size before compression
local size_before_bytes="`du $image | awk '{ print $1 }'`"
local size_before_human="`du -h $image | awk '{ print $1 }'`"
echo "$BASENAME: compressing $image..."
qemu-img convert -O qcow2 -p $compression $image $image.new && mv $image.new $image || exit 1
# Size after compression
local size_after_bytes="`du $image | awk '{ print $1 }'`"
local size_after_human="`du -h $image | awk '{ print $1 }'`"
# Ratio
#local ratio="$(($size_after_bytes / $size_before_bytes))"
local ratio="$(echo "scale=4 ; $size_after_bytes / $size_before_bytes" | bc -l)"
echo "size before: $size_before_human"
echo "size_after: $size_after_human"
echo "compression ratio: $ratio"
}
# Version
function kvmx_version {
echo $VERSION
}
# Shell
function kvmx_shell {
local last_exit_code="0"
local restricted="$1"
local restricted_actions=":status:start:stop:poweroff:suspend:resume:console:monitor"
restricted_actions="$restricted_actions:wipe:shred:app_base:version:list_image:kill:ssh_finger:"
# While a "quit" command isn't entered, read STDIN
while read -rep "$last_exit_code kvmx:/${USER}@${VM}> " STDIN; do
history -s "$STDIN"
if [ "$STDIN" == "quit" ] || [ "$STDIN" == "exit" ] || [ "$STDIN" == "bye" ]; then
break
elif [ "$STDIN" == "shell" ]; then
echo "Why you need nesting?"
elif [[ -n "$STDIN" && "$STDIN" != "#"* ]]; then
# If line is not empty or commented, process command
STDIN=($STDIN)
# But check first if we're in a restricted shell
if [ "$restricted" == "restricted" ]; then
if ! echo $restricted_actions | grep -q ":${STDIN[0]}:"; then
echo "Running in restricted shell mode."
echo "Allowed commands are only `echo $restricted_actions | tr ':' ' '`"
else
$APP_BASE/kvmx ${STDIN[0]} $VM ${STDIN[@]:1}
last_exit_code="$?"
fi
else
$APP_BASE/kvmx ${STDIN[0]} $VM ${STDIN[@]:1}
last_exit_code="$?"
fi
fi
done
}
# Xrandr integration
function kvmx_xrandr {
local size="$1"
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
if [ "$ssh_support" != "y" ]; then
echo "$BASENAME: xrandr needs ssh_support config"
exit 1
fi
if echo which xrandr | kvmx_ssh &> /dev/null; then
# Check for resolution configuration or explicit param
if [ ! -z "$size" ]; then
mode="`echo $size | tr 'x' ' '`"
elif [ ! -z "$resolution" ]; then
mode="`echo $resolution | tr 'x' ' '`"
else
# Set resolution according to the current active screen
#local mode="`xrandr | grep '*+' | awk '{ print $1 }' | tr 'x' ' '`"
# Set screen resolution depending on which screen the spice session is currently located
local id="`xdotool search --name $VM`"
local screen="`xdotool getwindowgeometry $id | grep screen: | cut -d '(' -f 2 | cut -d : -f 2 | cut -d ')' -f 1 | sed -e 's/ //'`"
local mode="`xrandr | grep "Screen ${screen}:" | cut -d , -f 2 | sed -e 's/ current //' -e 's/ //g' | tr 'x' ' '`"
if [ ! -z "$resolution_y_offset" ]; then
local x="`echo $mode | awk '{ print $1 }'`"
local y="`echo $mode | awk '{ print $2 }'`"
mode="$x $(($y $resolution_y_offset))"
fi
fi
if [ -z "$xrandr_device" ]; then
xrandr_device="Virtual-0"
fi
local line="`cvt $mode | tail -1 | sed -e 's/^Modeline//'`"
local name="`echo $line | awk '{ print $1 }'`"
echo "Setting Modeline $line..."
echo DISPLAY=:0 xrandr --newmode $line | kvmx_ssh
echo DISPLAY=:0 xrandr --addmode $xrandr_device $name | kvmx_ssh
echo DISPLAY=:0 xrandr --output $xrandr_device --mode $name | kvmx_ssh
fi
}
# Wrapper to kvmx-create
function kvmx_create {
if kvmx_running || kvmx_suspended; then
echo "$BASENAME: guest $VM is running or suspended, cannot (re-)create"
exit 1
fi
kvmx-create $KVMXFILE
if [ "$?" != "0" ]; then
exit $?
fi
}
# Disposable guest
function kvmx_disposable {
# Determine guest name by appending the current date
# This approache leads to the following UNIX socket error
# if the VM name is too long:
#
# UNIX socket path '/var/cache/qemu/.../monitor' is too long
# Path must be less than 108 bytes
#
#local date="`date +%Y%m%d%I%M%S`"
#local disposable="$VM-disposable-$date"
local disposable="$VM-disposable-"
local count=0
# Determine guest name using a sequential suffix
#
# This results in smaller guest names which are les prone to
# the UNIX socket path issue and also is easier to type
while true; do
disposable="${disposable}${count}"
if ! kvmx list_image ${disposable} &> /dev/null; then
break
fi
echo $count
let count++
done
# Clone and ensure we use a backing file
kvmx clone $VM $disposable --skell || exit 1
echo "basebox=$VM" >> $GLOBAL_USER_CONFIG_FOLDER/$disposable
echo 'backing_file="1"' >> $GLOBAL_USER_CONFIG_FOLDER/$disposable
kvmx up $disposable
echo "Waiting for the VM $disposable to stop before erasing it..."
local image="`kvmx list_image $disposable`"
local folder="`dirname $image`"
# Remove VM after it stopped
while true; do
if ! kvmx running $disposable; then
kvmx purge $disposable
rm -rf $folder
exit
fi
sleep 10
done
}
# Helper function to chose a device
function __kvmx_usb_devices_check {
local total="`lsusb | wc -l`"
# Check if USB is available in the system
if [ "$total" == "0" ]; then
echo "Sorry, but you don't have any available USB devices on your system."
exit 1
fi
# Check for usb config option
if [ -z "$usb" ] || [ "$usb" == "0" ]; then
echo "You need to enable USB support into the kvmxfile."
exit 1
fi
}
# Helper function to list available USB devices
function __kvmx_usb_devices_list {
# List available devices
echo "Available devices:"
echo ""
lsusb | cat -n
echo ""
}
# Helper function to choose an USB device
function __kvmx_usb_devices_choose {
local total="`lsusb | wc -l`"
# Read option
read -rep "Choose a device (1-$total): " choice
# Validate choice
if (($choice < 1)) || (($choice > $total)); then
echo "Invalid choice."
exit 1
fi
echo $choice
}
# USB attach
function kvmx_usb_attach {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
# Get a device
__kvmx_usb_devices_check
__kvmx_usb_devices_list
local choice="`__kvmx_usb_devices_choose`"
# Get bus, device, vendor and product IDs
local option="`lsusb | cat -n | grep \"^ $choice\"`"
local bus="`echo $option | awk '{ print $3 }'`"
local device="`echo $option | awk '{ print $5 }' | cut -d ':' -f 1`"
local vendor="`echo $option | awk '{ print $7 }' | cut -d ':' -f 1`"
local product="`echo $option | awk '{ print $7 }' | cut -d ':' -f 2`"
# Give permission to the device
echo "Changing ownership of /dev/bus/usb/${bus}/${device} to you..."
sudo chown `whoami` /dev/bus/usb/$bus/$device
# Attach into the guest
echo "Attaching device ${vendor}:${product} into the guest..."
kvmx_monitor device_add usb-host,vendorid=0x${vendor},productid=0x${product},id=usb-0x${vendor}-0x${product}
}
# USB detach
function kvmx_usb_detach {
if ! kvmx_running; then
echo "$BASENAME: guest $VM is not running"
exit 1
fi
# Get a device
__kvmx_usb_devices_check
__kvmx_usb_devices_list
local choice="`__kvmx_usb_devices_choose`"
# Get bus, device, vendor and product IDs
local option="`lsusb | cat -n | grep \"^ $choice\"`"
local bus="`echo $option | awk '{ print $3 }'`"
local device="`echo $option | awk '{ print $5 }' | cut -d ':' -f 1`"
local vendor="`echo $option | awk '{ print $7 }' | cut -d ':' -f 1`"
local product="`echo $option | awk '{ print $7 }' | cut -d ':' -f 2`"
# Detach into the guest
echo "Detaching device ${vendor}:${product} into the guest..."
kvmx_monitor device_del usb-0x${vendor}-0x${product}
# Restore permission to the device
echo "Restoring ownership of /dev/bus/usb/${bus}/${device}..."
sudo chown root /dev/bus/usb/$bus/$device
}
# Dispatch
if type kvmx_$ACTION 2> /dev/null | grep -q "kvmx_$ACTION ()"; then
__kvmx_initialize $*
if [ "$ACTION" != "app_base" ]; then
shift $SHIFTARGS
fi
kvmx_$ACTION $*
else
kvmx_usage
fi