#!/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 { # See http://blog.djm.net.au/2013/11/chacha20-and-poly1305-in-openssh.html SSH_OPTS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=FATAL -o ProxyCommand=none -o Ciphers=chacha20-poly1305@openssh.com -o User=$SSH_LOGIN -i $1" SSH_COMMAND="ssh $SSH_OPTS" SCP_COMMAND="scp $SSH_OPTS" } # Create a guest entry at the global user config folder function __kvmx_create_config_entry { if [ -z "$FOLDER" ]; then return 1 fi ( cd $GLOBAL_USER_CONFIG_FOLDER && ln -sf $FOLDER/kvmxfile $VM ) } # Initialize function __kvmx_initialize { if [ "$ACTION" == "app_base" ] || [ "$ACTION" == "version" ]; then return fi # Load basic functions export APP_BASE="`$DIRNAME/kvmx app_base`" source $APP_BASE/lib/kvmx/functions || exit 1 # Alias to be used in config files KVMX_BASE="$APP_BASE" # Initialize mkdir -p $GLOBAL_USER_CONFIG_FOLDER # Stop processing here for some actions if [ "$ACTION" == "init" ] || [ "$ACTION" == "list" ]; then return fi # 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 else echo "$BASENAME: config not found: $GLOBAL_USER_CONFIG_FOLDER/$VM" exit 1 fi else source $GLOBAL_USER_CONFIG_FOLDER/$VM fi if [ -z "$image" ]; then if [ -z "$image_base" ]; then image_base="$HOME/.local/share/kvmx" fi # There might be trouble managing the guest when project folder name is different from # hostname and no image param is set (kvmx-create puts image in one # place, kvmx expects in the other). So we try to guess first the image by hostname # and then by guest name. Note that it might be possible to have conflicts if there # are machines with hostnames set to the name os other machines. But here we hope that # the user is not messing that much ;) if [ ! -z "$hostname" ] && [ -e "$image_base/$hostname/box.img" ]; then image="$image_base/$hostname/box.img" else image="$image_base/$VM/box.img" fi fi if [ ! -h "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then echo "error: $GLOBAL_USER_CONFIG_FOLDER/$VM is not a symlink" exit 1 fi # Box and folder config KVMXFILE="`readlink $GLOBAL_USER_CONFIG_FOLDER/$VM`" KVMX_PROJECT_FOLDER="`dirname $KVMXFILE`" STORAGE="`dirname $image`" DATADIR="${datadir:-$STORAGE}" STATE_DIR="$DATADIR/state/$VM" LOG_DIR="$DATADIR/log" PIDFILE="$STATE_DIR/pid" PORTFILE="$STATE_DIR/port" XDMCPPORTFILE="$STATE_DIR/xdmcp" SSHFILE="$STATE_DIR/ssh" DISPLAYFILE="$STATE_DIR/display" SPICEFILE="$STATE_DIR/spice" XEPHYRFILE="$STATE_DIR/xephyr" LOGFILE="$LOG_DIR/qemu" SPICELOG="$LOG_DIR/spice" XPRALOG="$LOG_DIR/xpra" XDMCPLOG="$LOG_DIR/xdmcp" MONITORFILE="$STATE_DIR/monitor" CONSOLEFILE="$STATE_DIR/console" if [ -e "$DATADIR/ssh/$VM.key" ]; then mkdir -p "$DATADIR/ssh" SSHKEY="$DATADIR/ssh/$VM.key" else SSHKEY="$APP_BASE/share/ssh/insecure_private_key" fi if [ ! -z "$user" ]; then SSH_LOGIN="$user" else SSH_LOGIN="user" fi __kvmx_ssh_command $SSHKEY mkdir -p $STATE_DIR $LOG_DIR if [ ! -e "$image" ] && [ "$ACTION" != "up" ] && [ "$ACTION" != "provision" ] && [ "$ACTION" != "purge" ] \ && [ "$ACTION" != "destroy" ] && [ "$ACTION" != "install" ]; then echo "$BASENAME: file not found: $image" exit 1 fi fi } # Run spice client function kvmx_spice { if ! kvmx_running; then echo "$BASENAME: guest $VM is not running" exit 1 fi 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 & elif [ "$spice_client" == "virt-viewer" ] && which virt-viewer &> /dev/null; then remote-viewer spice://localhost:$PORT & else if which spicec &> /dev/null; then # https://lists.freedesktop.org/archives/spice-devel/2013-September/014643.html SPICE_NOGRAB=1 spicec --host localhost --port $PORT &> $SPICELOG & fi fi SPICEPID="$!" echo "$SPICEPID" > $SPICEFILE # Give time to connect sleep 1 # Fix window title an position if which /usr/bin/xdotool &> /dev/null; then if [ ! -z "$xclient_windowmove" ]; then xdotool search --name "SPICEc:0" windowmove $xclient_windowmove fi xdotool search --name "SPICEc:0" set_window --name $VM fi } # Bring virtual machine up function kvmx_up { if kvmx_suspended; then $DIRNAME/$BASENAME resume $VM if [ "$run_spice_client" == "1" ]; then $DIRNAME/$BASENAME spice $VM fi if [ "$run_xpra" == "1" ]; then $DIRNAME/$BASENAME xpra $VM fi if [ "$run_xephyr" == "1" ]; then $DIRNAME/$BASENAME xephyr $VM fi exit elif kvmx_running; then echo "$BASENAME: guest $VM is already running" if [ "$run_spice_client" == "1" ]; then kvmx spice $VM exit else exit 1 fi fi if [ ! -z "$shared_folder" ]; then # Get absolute path of shared folder relative to project path shared_folder="`cd $KVMX_PROJECT_FOLDER && cd $shared_folder &> /dev/null && pwd`" # Requires samba package installed in the host; see http://unix.stackexchange.com/a/183609 #local shared="-net user,smb=$shared_folder" # See http://wiki.qemu-project.org/Documentation/9psetup local shared="-fsdev local,id=shared,path=$shared_folder,security_model=none -device virtio-9p-pci,fsdev=shared,mount_tag=shared" elif [ ! -z "$shared_folders" ]; then local old_ifs="$IFS" local shared_item local shared IFS="," for shared_item in $shared_folders; do local id="`echo $shared_item | cut -d ':' -f 1`" local shared_folder="`echo $shared_item | cut -d ':' -f 2`" local shared_folder_mountpoint="`echo $shared_item | cut -d ':' -f 3`" # Get absolute path of shared folder relative to project path shared_folder="`cd $KVMX_PROJECT_FOLDER && cd $shared_folder &> /dev/null && pwd`" shared="$shared -fsdev local,id=$id,path=$shared_folder,security_model=none -device virtio-9p-pci,fsdev=$id,mount_tag=$id" unset shared_folder unset shared_folder_mountpoint done IFS="$old_ifs" fi if [ ! -z "$port_mapping" ]; then local hostfwd=",$port_mapping" fi # Check if image exists, create otherwise if [ ! -e "$image" ]; then if [ ! -z "$basebox" ]; then if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$basebox" ]; then baseimage="`kvmx list_image $basebox`" basekey="$(ls `dirname $baseimage`/ssh/$basebox.key)" if [ ! -e "$baseimage" ]; then echo "$BASENAME: could not find basebox $baseimage. Please create it first." exit 1 fi echo "Copying base image $baseimage to $image..." if which rsync &> /dev/null; then rsync -ah --sparse --progress $baseimage $image else # See https://rwmj.wordpress.com/2010/10/19/tip-making-a-disk-image-sparse/ cp --sparse=always $baseimage $image fi if [ -e "$basekey" ]; then vmname="$(basename $STORAGE)" imagekey="$STORAGE/ssh/$vmname.key" mkdir -p "$STORAGE/ssh" cp $basekey $imagekey cp $basekey.pub $imagekey.pub # Re-evaluate this if there's a custom SSH key. __kvmx_ssh_command $basekey fi local wait="y" fi else local wait="y" kvmx-create $GLOBAL_USER_CONFIG_FOLDER/$VM fi if [ "$wait" == "y" ]; then echo "Waiting before starting the new guest..." sleep 5 fi fi if [ -z "$graphics" ]; then graphics="-vga qxl" fi if [ -z "$memory" ]; then memory="2048" fi if [ -z "$smp" ]; then smp="2" fi if [ -z "$drive_interface" ]; then drive_interface="virtio" fi if [ -z "$nic_model" ]; then nic_model="virtio" fi if [ -z "$shared_folder_msize" ]; then shared_folders_msize="524288" fi if [ -z "$shared_folders_cache" ]; then shared_folders_cache="none" fi if [ -z "$net" ] || [ "$net" == "user" ]; then net_opts="user,hostfwd=tcp:127.0.0.1:$SSH-:22,hostfwd=udp:127.0.0.1:$XDMCP_PORT-:177$hostfwd -net nic,model=$nic_model" elif [ "$net" == "tap" ]; then # Thanks kvm-manager tap="${VM}0" # MAC address is derived from a hash of the host's name and the guest's name: mac_address="$(printf "02:%s" "$(printf "%s\0%s" "$(hostname)" "${VM}" | sha256sum | sed 's/\(..\)/\1:/g' | cut -f1-5 -d:)")" bridge="br0" net_opts="tap,ifname=$tap,script=no,downscript=no,vlan=0,name=hostnet0 -device virtio-net-pci,vlan=0,id=net0,mac=$mac_address,bus=pci.0" fi if [ -z "$spice" ] || [ "$spice" == "1" ]; then spice_opts="-spice port=$PORT,addr=127.0.0.1,disable-ticketing,streaming-video=off,jpeg-wan-compression=never,playback-compression=off,zlib-glz-wan-compression=never,image-compression=off" spice_opts="$spice_opts -device virtio-serial-pci" spice_opts="$spice_opts -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0" spice_opts="$spice_opts -chardev spicevmc,id=spicechannel0,name=vdagent" fi if [ "$sound" != "0" ]; then if [ -z "$sound" ]; then sound="ac97" fi sound_opts="-soundhw $sound" fi # Run virtual machine # See https://en.wikipedia.org/wiki/Nohup#Overcoming_hanging nohup kvm -m $memory -name $VM \ -drive file=$image,if=$drive_interface $graphics $shared \ $spice_opts \ -chardev "socket,id=monitor,path=$MONITORFILE,server,nowait" -mon chardev=monitor,mode=readline \ -chardev "socket,id=serial0,path=$CONSOLEFILE,server,nowait" -device isa-serial,chardev=serial0 \ -smp $smp -cpu host -balloon virtio \ $sound_opts \ -net $net_opts \ $qemu_opts &> $LOGFILE < /dev/null & PID="$!" # Save state echo $PID > $PIDFILE echo $PORT > $PORTFILE echo $SSH > $SSHFILE echo $GUEST_DISPLAY > $DISPLAYFILE echo $XDMCP_PORT > $XDMCPPORTFILE # Thanks kvm-manager code for that portion /usr/bin/screen -D -m -L $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 sleep 1 kvmx_spice fi 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..." echo "sudo mkdir -p $shared_folder_mountpoint" | kvmx_ssh echo "sudo mount -t 9p -o trans=virtio $id $shared_folder_mountpoint -oversion=9p2000.L,posixacl,cache=none -o sync -o dirsync" | kvmx_ssh IFS="," done IFS="$old_ifs" fi fi if [ "$run_xpra" == "1" ]; then $DIRNAME/$BASENAME xpra $VM fi if [ "$run_xephyr" == "1" ]; then $DIRNAME/$BASENAME xephyr $VM fi if [ ! -z "$startup_command" ] && [ "$ssh_support" == "y" ]; then echo "Running $startup_command..." echo "nohup $startup_command" | kvmx ssh $VM &> /dev/null & fi kvmx_status } # Set hostname function kvmx_hostname { if ! kvmx_running; then echo "$BASENAME: guest $VM is not running" exit 1 fi 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 ! kvmx_running; then kvmx up $VM || exit 1 #kvmx_up || exit 1 #echo "$BASENAME: guest $VM is not running" #exit 1 fi if [ "$ssh_support" != "y" ]; then echo "$BASENAME: SSH support for $VM is disabled" exit 1 fi # Shift params according to how the program was called: # either "kvmx ssh" or "kvmx ssh guest". #if [ "$ACTION" == "ssh" ]; then # if [ ! -z "$2" ]; then # shift 2 # else # shift 1 # fi #fi SSH="`cat $SSHFILE`" #TERM=xterm $SSH_COMMAND -p $SSH 127.0.0.1 $* $ssh_env $SSH_COMMAND -p $SSH 127.0.0.1 $* } # Suspend the virtual machine function kvmx_suspend { if ! kvmx_running; then echo "$BASENAME: guest $VM is not running" exit 1 fi PID="`cat $PIDFILE`" kill -STOP $PID # 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="`cat $PIDFILE`" if [ -z "$PID" ]; then return 1 fi # Simpler check #ps $PID &> /dev/null # Better check were process should match a qemu binary ps -o command $PID | grep -q '^qemu' return $? } # Check if a guest is running function kvmx_suspended { if ! kvmx_running; then return 1 else if ps -p $PID -o stat --no-headers | grep -q 'Tl'; then return 0 else return 1 fi fi } # Resume the guest function kvmx_resume { if ! kvmx_running; then echo "$BASENAME: guest $VM is not running" exit 1 fi PID="`cat $PIDFILE`" kill -CONT $PID # 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 [ "$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_halt { kvmx_poweroff } # 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 "$DEST" ]; then # Error #exit 1 # Assume same as origin DEST="$ORIG" fi # Fix ~/ path if echo $DEST | grep -q -e "^$HOME"; then DEST="$(echo $DEST | sed -e "s|^$HOME|/home/$SSH_LOGIN|")" fi SSH="`cat $SSHFILE`" rsync -av --delete -e "$SSH_COMMAND -o Port=$SSH" --rsync-path "sudo rsync" $ORIG/ 127.0.0.1:$DEST/ } # Rsync files to the guest function kvmx_rsync_from { if ! kvmx_running; then echo "$BASENAME: guest $VM is not running" exit 1 fi 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`" 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 fi if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then echo "$BASENAME: guest $VM already exists" exit 1 fi if [ ! -d "$FOLDER" ]; then mkdir -p $FOLDER fi # Ensure we have an absolute folder name FOLDER="`cd $FOLDER &> /dev/null && pwd`" # Copy config from template if [ ! -e "$FOLDER/kvmxfile" ]; then cp $APP_BASE/kvmxfile $FOLDER/ sed -i -e "s|hostname=\"machine\"|hostname=\"$VM\"|g" $FOLDER/kvmxfile fi # Create config entry __kvmx_create_config_entry } # Clone a guest function kvmx_clone { if kvmx_running; then echo "$BASENAME: orig $VM is running, cannot clone." exit 1 fi FOLDER="$1" DEST="`basename $FOLDER`" if [ -z "$FOLDER" ]; then kvmx_usage fi # If dest is given without a full path, clone to the same basedir # as the original guest. if [ "$FOLDER" == "$DEST" ]; then FOLDER="`dirname $STORAGE`/$DEST" fi # Check if dest machine exists if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$DEST" ]; then echo "$BASENAME: destination guest $DEST already exists." exit 1 fi if [ -d "$FOLDER" ]; then echo "$BASENAME: destination $FOLDER already exists." exit 1 fi # Ensure we have an absolute folder name mkdir -p $FOLDER FOLDER="`cd $FOLDER &> /dev/null && pwd`" rmdir $FOLDER # Copy image and configuration echo "Copying basebox..." if which rsync &> /dev/null; then rsync -ah --sparse --progress $STORAGE/ $FOLDER/ else cp -r --sparse=always $STORAGE/ $FOLDER/ fi # Remove old state folder rm -rf $FOLDER/state/* # Create config entry ( cd $GLOBAL_USER_CONFIG_FOLDER && ln -s $FOLDER/kvmxfile $DEST ) # Update config file new_image="$FOLDER/`basename $image`" sed -i -e "s|image=\"$image\"|image=\"$new_image\"|g" $FOLDER/kvmxfile sed -i -e "s|hostname=\"$VM\"|hostname=\"$DEST\"|g" $FOLDER/kvmxfile # Rename keypair if exists if [ -e "$FOLDER/ssh/$VM.key" ]; then mv $FOLDER/ssh/$VM.key $FOLDER/ssh/$DEST.key mv $FOLDER/ssh/$VM.key.pub $FOLDER/ssh/$DEST.key.pub fi 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=" $GLOBAL_USER_CONFIG_FOLDER/$VM | \ sed -e 's/="/=/' -e 's/"$//' -e "s/='/=/" -e "s/'$//" -e 's/^.*=//' elif ! grep -q "^$param=" $GLOBAL_USER_CONFIG_FOLDER/$VM; then echo "$param=\"$*\"" >> $GLOBAL_USER_CONFIG_FOLDER/$VM else sed -i -e "s#^$param=.*#$param=\"$*\"#" $GLOBAL_USER_CONFIG_FOLDER/$VM fi fi } # Alias to config function kvmx_edit { kvmx_config $* } # Stop a guest function kvmx_stop { if kvmx_running; then PID="`cat $PIDFILE`" kill $PID kvmx_xephyr_stop fi } # Kill a guest function kvmx_kill { if kvmx_running; then PID="`cat $PIDFILE`" kill -9 $PID kvmx_xephyr_stop fi } # Destroy a guest function kvmx_destroy { kvmx_stop rm -f $image rm -rf $STATE_DIR echo "$BASENAME: removed image and state files, but not the whole $STORAGE folder." } # Shred a guest function kvmx_shred { kvmx_stop if which shred &> /dev/null; then shred $image rm -f $image else echo "$BASENAME: error shreding $image: shred program not available." exit 1 fi } # Wipe a guest function kvmx_wipe { kvmx_stop if which wipe &> /dev/null; then wipe -f $image rm -f $image else echo "$BASENAME: error wipeing $image: wipe program not available." exit 1 fi } # Purge a guest and all its configuration function kvmx_purge { kvmx_destroy rm -f $GLOBAL_USER_CONFIG_FOLDER/$VM echo "$BASENAME: removed $GLOBAL_USER_CONFIG_FOLDER/$VM config." } # Provision a machine function kvmx_provision { if ! kvmx_running; then kvmx up $VM || exit 1 #kvmx_up || exit 1 #echo "$BASENAME: guest $VM is not running" #exit 1 fi if [ "$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 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="`cat $PIDFILE`" 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 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 } # 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 } # 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 <" exit 1 elif [ ! -e "$media" ]; then echo "$BASENAME: file not found: $media" exit 1 fi if [ -z "$memory" ]; then memory="2048" fi if [ ! -e "$image" ]; then echo "Creating $image with size $size..." qemu-img create -f qcow2 $image $size fi kvm -m $memory -net nic,model=virtio -net user -drive file=$image -cdrom $media } # Serial console function kvmx_console { if ! kvmx_running; then echo "$BASENAME: guest $VM is not running" exit 1 fi screen -x kvmx-$VM } # Alias to console function kvmx_serial { kvmx_console $* } # Compress guest image function kvmx_compress { if kvmx_running; then echo "$BASENAME: guest $VM is running" 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 }'`" 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 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:" # While a "quit" command isn't entered, read STDIN while read -rep "kvmx:/${USER}@${VM}> " STDIN; do 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 # Process command $APP_BASE/kvmx ${STDIN[0]} $VM ${STDIN[@]:1} fi else # Process command $APP_BASE/kvmx ${STDIN[0]} $VM ${STDIN[@]:1} fi fi 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