#!/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 <http://www.gnu.org/licenses/>. # # Basic parameters VERSION="0.2.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 the default config, providing defaults source $APP_BASE/kvmxfile || exit 1 # Set hostname if [ "$hostname" == "kvmx" ] && [ "$VM" != "kvmx" ]; then hostname="$VM" fi # Set domain (this is already done with the default config) #domain="${domain:-example.org}" # 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" == "remote-viewer" ] && which remote-viewer &> /dev/null; then #remote-viewer spice://localhost:$PORT & remote-viewer spice+unix://$SPICESOCKET & # This is untested due to libvirt requirements elif [ "$spice_client" == "virt-viewer" ] && which virt-viewer &> /dev/null; then #virt-viewer spice://localhost:$PORT & virt-viewer spice+unix://$SPICESOCKET & # Unsupported as spicec was deprecated #elif [ "$spice_client" != "spicec" ] && 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 & 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 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 # Ensure vdagent is running if [ "$ssh_support" == "y" ]; then if [ "$kvmx_vdagent" != "0" ]; then echo "which kvmx-vdagent > /dev/null && DISPLAY=:0 kvmx-vdagent" | kvmx_ssh fi 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" else echo "$BASENAME: basebox $basebox not available." exit 1 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="33554432" 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=on,addr=$SPICESOCKET,disable-ticketing=on,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="-device $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=on,wait=off" -mon chardev=monitor,mode=readline \ # -chardev "socket,id=serial0,path=$CONSOLEFILE,server=on,wait=off" -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=on,wait=off" -mon chardev=monitor,mode=readline \ -chardev "socket,id=serial0,path=$CONSOLEFILE,server=on,wait=off" -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,msize=$shared_folders_msize $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 <<EOF ##### BEGIN REMOTE SCRIPT ##### OLD_HOST="\$(hostname)" # Set hosts entry if ! grep -q "^127.0.0.1 $hostname.$domain $hostname$" /etc/hosts; then echo "127.0.0.1 $hostname.$domain $hostname" | sudo tee -a /etc/hosts &> /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 <action> [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 <machine> [folder]" echo -e "\t$BASENAME clone <orig-guest> <dest-guest-or-folder>" echo -e "\t$BASENAME ssh <machine> -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 $* } # Enhanced SSH login into the guest function kvmx_login { # This allows the usage of a custom login command # # It's not implemented directly in kvmx_ssh because it conflicts with use # cases where SSH commands are read from stdin and not from positional # arguments, like `echo something | kvmx_ssh`. if [ -z "$1" ] && [ ! -z "$ssh_login_command" ]; then # This needs the -t option to request a pseudo-terminal kvmx_ssh -t $ssh_login_command else kvmx_ssh $* fi } # 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 <orig> [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 <orig> [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 $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 <value>" # 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 <<EOF $* EOF fi } # Install system function kvmx_install { if kvmx_running; then echo "$BASENAME: guest $VM is running" exit 1 fi local media="$1" if [ -z "$media" ]; then echo "usage: $BASENAME install $VM <installation-media>" # 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`" # XrandR approach, matches the full screen size, not ideal when using # multiple monitors/outputs or if the window size is smaller than the # current screen size #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' ' '`" # Pure xdotool approach, matches the current window size # Better support for multiple monitors/outputs and for windows of arbitrary sizes local mode="`xdotool getwindowgeometry $id | grep -i geometry: | cut -d : -f 2 | 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 } # Grow a partition # This will restart the guest a few times # Inspired by https://ahelpme.com/linux/online-resize-of-a-root-ext4-file-system-increase-the-space/ function kvmx_growpart { local device="$1" local partition="$2" local size="$3" # Syntax check if [ -z "$size" ]; then echo "usage: $BASENAME growpart $VM <device> <partition> <additional_size>" echo "example: $BASENAME growpart test /dev/vda 2 5G" exit 1 fi # Ensure the guest is running if ! kvmx_running; then echo "Powering up guest..." kvmx_up fi # Check for $partition echo "Checking for partition ${device}${partition}..." echo /bin/test -e ${device}${partition} | kvmx_ssh if [ "$?" != "0" ]; then echo "Could not determine ${device}${partition} existence, aborting." exit 1 fi # Ensure cloud-guest-utils is installed echo "Checking for cloud-guest-utils availability..." echo which growpart | kvmx_ssh &> /dev/null if [ "$?" != "0" ]; then echo which apt-get | kvmx_ssh &> /dev/null if [ "$?" == "0" ]; then kvmx_ssh sudo apt-get install -y cloud-guest-utils else echo "Please install cloud-guest-utils in the guest first" exit 1 fi fi # Now make sure the guest is off echo "Powering off guest" kvmx_poweroff # Resize the image echo "Resizing the image to an additional ${size}" qemu-img resize $image +${size} # Power up echo "Powering up the guest again" kvmx_up # Resize virtual machine root partition - while the filesystem is mounted! # this parted command currently need to be done manually # # Check https://unix.stackexchange.com/questions/373063/auto-expand-last-partition-to-use-all-unallocated-space-using-parted-in-batch-m # https://unix.stackexchange.com/questions/190317/gnu-parted-resizepart-in-script#202872 # https://bugs.launchpad.net/ubuntu/+source/parted/+bug/1270203 # https://techtitbits.com/2018/12/using-parteds-resizepart-non-interactively-on-a-busy-partition/ # https://serverfault.com/questions/870594/resize-partition-to-maximum-using-parted-in-non-interactive-mode # #echo resizepart 2 -1 | kvmx ssh $guest sudo parted /dev/vda #kvmx_ssh sudo parted /dev/vda resizepart 2 -1 Yes echo "Growing ${device}${partition}..." kvmx_ssh sudo growpart ${device} ${partition} # Resize the file system and schedule a fsck for the next reboot echo "Resizing the ${device}${partition} filesystem..." kvmx_ssh sudo resize2fs ${device}${partition} kvmx_ssh sudo touch /forcefsck # Restart echo "Restarting the guest..." kvmx_restart } # Inotify dispatcher function kvmx_inotify { local watched="$1" shift local command="$*" # Syntax check if [ -z "$command" ]; then echo "usage $BASENAME inotify $GUEST <watched> <command>" echo "example: kvmx inotify $guest hostfolder make -C guestfolder compile" exit 1 fi # Check if watched exists if [ ! -e "$watched" ]; then echo "Not found: $watched" exit 1 fi # Dispatch while inotifywait -r $watched; do echo "$command" | kvmx_ssh done } # 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