From 1cddbe0234efe158b6c05f21d742eb9489887cc4 Mon Sep 17 00:00:00 2001 From: Silvio Rhatto Date: Thu, 9 Mar 2017 19:04:24 -0300 Subject: Project revamp: full workflow --- README.md | 33 +++++- kvmx | 358 +++++++++++++++++++++++++++++++++++++++++++++++------------ kvmx-create | 246 ++++++++++++++++++++++++++++++++++++++++ kvmx-vdagent | 17 +++ kvmxfile | 50 ++++++++- 5 files changed, 625 insertions(+), 79 deletions(-) create mode 100755 kvmx-create create mode 100755 kvmx-vdagent diff --git a/README.md b/README.md index 0156622..a64544d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,32 @@ -KVMX: QEMU KVM Wrapper -====================== +KVMX: vagrant-like QEMU KVM Wrapper +=================================== + +KVMX is a lightweight implementation of a virtual machine manager +inspired by vagrant. + +It may be used for development or as a wrapper for GUI isolation. + +This is simple stuff. Don't use it if you need any complex behavior +or integration. In the other hand, if you're looking for a small +application that doesn't depend on software installed from unstrusted +sources, you'll feel welcome here :) + +## Instalation + +Simply clone it and add to your `$PATH`: + + git clone https://git.fluxo.info/kvmx + cd kvmx && git verify-commit HEAD + +## Basic usage + + kvmx init [project-name] [project-folder] # initialize + kvmx edit [project-name] # customize + kvmx up [project-name] # bring it up! + +If no project name is specified, the current folder name is assumed as the project name. +If no folder is specified, the current folder is assumed as the project home. + +## Further reading -Wrapper to provide easy to use GUI isolation. See https://blog.fluxo.info/suckless/virtual for details. diff --git a/kvmx b/kvmx index 00a41be..75c0c3a 100755 --- a/kvmx +++ b/kvmx @@ -1,66 +1,56 @@ #!/bin/bash # -# KVM and SPICE client wrapper +# KVMX Manager. # -# Parameters +# Basic parameters BASENAME="`basename $0`" DIRNAME="`dirname $0`" -STORAGE="/var/cache/qemu" -SHARED="/var/data/load" -PORT="$(($RANDOM + 1024))" -SSH="$(($PORT + 22))" ACTION="$1" VM="$2" -BOX="$STORAGE/$VM/box.img" -PIDFILE="$STORAGE/$VM/pid" -PORTFILE="$STORAGE/$VM/port" -SSHFILE="$STORAGE/$VM/ssh" -SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=FATAL -i $DIRNAME/ssh/insecure_private_key" -LOGIN="user" +GLOBAL_USER_CONFIG_FOLDER="$HOME/.config/kvmx" # Run spice client -function kvmx_spice_client { - # https://lists.freedesktop.org/archives/spice-devel/2013-September/014643.html - SPICE_NOGRAB=1 spicec --host localhost --port $PORT & - #spicy -h localhost -p $PORT - #remote-viewer spice://localhost:$PORT +function kvmx_spice { + if [ "$run_spice_client" == "1" ]; then + # https://lists.freedesktop.org/archives/spice-devel/2013-September/014643.html + SPICE_NOGRAB=1 spicec --host localhost --port $PORT & + #spicy -h localhost -p $PORT + #remote-viewer spice://localhost:$PORT - # Give time to boot - sleep 5 + # Give time to boot + sleep 5 - # Fix window titles - xdotool search --name "SPICEc:0" set_window --name $VM -} - -# Restart vdagent inside the guest -function kvmx_clip { - instances="`ps -o pid,command -e | grep "spice-vdagent$" | cut -d ' ' -f 2 | xargs`" - - # Kill old instances - for pid in $instances; do - kill -9 $pid &> /dev/null - done - - # Just to make sure we're inside a virtual machine - if which spice-vdagent &> /dev/null ; then - spice-vdagent + # Fix window titles + if which /usr/bin/xdotool &> /dev/null; then + xdotool search --name "SPICEc:0" set_window --name $VM + fi fi } # Bring virtual machine up function kvmx_up { - # FIXME - # Check if machine is up + if kvmx_running; then + echo "$BASENAME: guest $VM is already running" + exit 1 + fi + + if [ "$shared_folder" ]; then + local shared="-fsdev local,id=shared,path=$shared_folder,security_model=none -device virtio-9p-pci,fsdev=shared,mount_tag=shared" + fi + + # Check if image exists, create otherwise + if [ ! -e "$image" ]; then + kvmx-create $GLOBAL_USER_CONFIG_FOLDER/$VM + fi # Run virtual machine - kvm -m 2048 -name $VM -drive file=$BOX,if=virtio -vga qxl \ + kvm -m 2048 -name $VM -drive file=$image,if=virtio -vga qxl $shared \ -spice port=$PORT,addr=127.0.0.1,disable-ticketing,streaming-video=off,jpeg-wan-compression=never,playback-compression=off,zlib-glz-wan-compression=never,image-compression=off \ -device virtio-serial-pci \ -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0 \ -chardev spicevmc,id=spicechannel0,name=vdagent \ - -smp 2 -soundhw ac97 -cpu host -balloon virtio \ - -fsdev local,id=shared,path=$SHARED,security_model=none -device virtio-9p-pci,fsdev=shared,mount_tag=shared \ + -smp 2 -soundhw ac97 -cpu host -balloon virtio \ -net nic,model=virtio \ -net user,hostfwd=tcp:127.0.0.1:$SSH-:22 & @@ -71,50 +61,270 @@ function kvmx_up { echo $PORT > $PORTFILE echo $SSH > $SSHFILE - kvmx_spice_client + kvmx_spice } -# Check -if [ -z "$VM" ] && [ "$ACTION" != "clip" ]; then - echo "usage: $BASENAME " - exit 1 -elif [ ! -e "$BOX" ] && [ "$ACTION" != "clip" ]; then - echo "file not found: $BOX" +# Display usage +function kvmx_usage { + echo "usage: $BASENAME [options]" + echo "examples:" + echo "" + echo "$BASENAME list" + echo "$BASENAME init [folder]" + echo "$BASENAME clone " + exit 1 -fi +} -# TODO: check for a ~/.kvmx config -# TODO: check for a kvmxfile +# Log into the guest using SSH +function kvmx_ssh { + if ! kvmx_running; then + echo "$BASENAME: guest $VM is not running" + exit 1 + fi + + shift 2 + SSH="`cat $SSHFILE`" + $SSH_COMMAND -p $SSH $SSH_LOGIN@127.0.0.1 $* +} + +# Suspend the virtual machine +function kvmx_suspend { + if ! kvmx_running; then + echo "$BASENAME: guest $VM is not running" + exit 1 + fi -# Dispatch -if [ "$ACTION" == "up" ]; then - kvmx_up -elif [ "$ACTION" == "clip" ]; then - kvmx_clip -elif [ "$ACTION" == "suspend" ]; then PID="`cat $PIDFILE`" kill -STOP $PID -elif [ "$ACTION" == "resume" ]; then +} + +# Check if a guest is running +function kvmx_running { + if [ ! -e "$PIDFILE" ]; then + return 1 + fi + + PID="`cat $PIDFILE`" + ps $PID &> /dev/null + + return $? +} + +# Resume the guest +function kvmx_resume { + if ! kvmx_running; then + echo "$BASENAME: guest $VM is not running" + exit 1 + fi + PID="`cat $PIDFILE`" kill -CONT $PID -elif [ "$ACTION" == "poweroff" ]; then - echo TODO -elif [ "$ACTION" == "ssh" ]; then - shift 2 - SSH="`cat $SSHFILE`" - $SSH_COMMAND -p $SSH $LOGIN@127.0.0.1 $* -elif [ "$ACTION" == "rsync" ]; then +} + +# Poweroff the guest +function kvmx_poweroff { + kvmx_ssh /usr/bin/sudo poweroff +} + +# Rsync files to the guest +function kvmx_rsync { + if ! kvmx_running; then + echo "$BASENAME: guest $VM is not running" + exit 1 + fi + ORIG="$3" DEST="$4" SSH="`cat $SSHFILE`" - rsync -av "$SSH_COMMAND -p $SSH" $ORIG/ $LOGIN@127.0.0.1:$DEST/ -elif [ "$ACTION" == "provision" ]; then - echo TODO -elif [ "$ACTION" == "create" ]; then - echo TODO -elif [ "$ACTION" == "init" ]; then - # TODO: copy from template - touch .kvmxfile -elif [ "$ACTION" == "upgrade" ]; then - echo TODO + rsync -av "$SSH_COMMAND -p $SSH" $ORIG/ $SSH_LOGIN@127.0.0.1:$DEST/ +} + +# List guests +function kvmx_list { + ls $GLOBAL_USER_CONFIG_FOLDER +} + +# Upgrade guest +function kvmx_upgrade { + echo "sudo apt-get update && sudo apt-get dist-upgrade -y && sudo apt-get autoremove -y" | kvmx_ssh +} + +# Initialize +function kvmx_initialize { + if [ "$ACTION" == "init" ] || [ "$ACTION" == "list" ]; then + return + fi + + if [ -z "$VM" ]; then + VM="$(basename `pwd`)" + fi + + # Default parameters + PORT="$(($RANDOM + 1024))" + SSH="$(($PORT + 22))" + SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=FATAL -i $DIRNAME/ssh/insecure_private_key" + SSH_LOGIN="user" + + # Initalize + mkdir -p $GLOBAL_USER_CONFIG_FOLDER + + # Load and check guest config + if [ "$ACTION" != "init" ] && [ "$ACTION" != "list" ] && [ "$ACTION" != "edit" ]; then + if [ ! -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then + echo "$BASENAME: config not found: $GLOBAL_USER_CONFIG_FOLDER/$VM" + exit 1 + else + source $GLOBAL_USER_CONFIG_FOLDER/$VM + fi + + if [ -z "$image" ]; then + image="/var/cache/qemu/$VM/box.img" + fi + + if [ -z "$KVMXFILE" ]; then + KVMXFILE="/var/cache/qemu/$VM/kvmxfile" + fi + + # Box and folder config + STORAGE="`dirname $image`" + STATE_DIR="$STORAGE/state" + PIDFILE="$STATE_DIR/pid" + PORTFILE="$STATE_DIR/port" + SSHFILE="$STATE_DIR/ssh" + mkdir -p $STATE_DIR + + if [ ! -e "$image" ] && [ "$ACTION" != "up" ]; then + echo "$BASENAME: file not found: $image" + exit 1 + fi + fi +} + +# Initializes a new guest +function kvmx_init { + FOLDER="$3" + + if [ -z "$VM" ]; then + VM="$(basename `pwd`)" + fi + + if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then + echo "$BASENAME: guest $VM already exists" + exit 1 + fi + + if [ -z "$FOLDER" ]; then + FOLDER="." + fi + + if [ ! -d "$FOLDER" ]; then + mkdir -p $FOLDER + fi + + # Ensure we have an absolute folder name + FOLDER="`cd $FOLDER &> /dev/null && pwd`" + + # Copy config from template + if [ ! -e "$FOLDER/kvmxfile" ]; then + cp $DIRNAME/kvmxfile $FOLDER/ + fi + + # Create config entry + ( cd $GLOBAL_USER_CONFIG_FOLDER && ln -s $FOLDER/kvmxfile $VM ) +} + +# Clone a guest +function kvmx_clone { + if kvmx_running; then + echo "$BASENAME: orig $VM is running, cannot clone." + exit 1 + fi + + FOLDER="$3" + DEST="`basename $FOLDER`" + + if [ -z "$FOLDER" ]; then + kvmx_usage + fi + + # Check if dest machine exists + if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$DEST" ]; then + echo "$BASENAME: destination guest $DEST already exists." + exit 1 + fi + + if [ -d "$FOLDER" ]; then + echo "$BASENAME: destination $FOLDER already exists." + exit 1 + fi + + # Ensure we have an absolute folder name + mkdir -p $FOLDER + FOLDER="`cd $FOLDER &> /dev/null && pwd`" + rmdir $FOLDER + + # Copy image and configuration + cp -r `dirname $image` $FOLDER/ + cp $GLOBAL_USER_CONFIG_FOLDER/$VM $GLOBAL_USER_CONFIG_FOLDER/$DEST + + # Update config file + new_image="$FOLDER/`basename $image`" + sed -i -e "s|image=\"$image\"|image=\"$new_image\"|g" $GLOBAL_USER_CONFIG_FOLDER/$DEST +} + +# Edit guest config +function kvmx_edit { + if [ -z "$EDITOR" ]; then + EDITOR="vi" + fi + + if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then + $EDITOR $GLOBAL_USER_CONFIG_FOLDER/$VM + else + echo "$BASENAME: $GLOBAL_USER_CONFIG_FOLDER/$VM: file not found." + fi +} + +# Stop a guest +function kvmx_stop { + if kvmx_running; then + PID="`cat $PIDFILE`" + kill $PID + fi +} + +# Destroy a guest +function kvmx_destroy { + kvmx_stop + + #rm -f $image + rm -f $PIDFILE + rm -f $SSHFILE + rm -f $PORTFILE + + echo "$BASENAME: please inspect and remove `dirname $image` manually." +} + +# Purge a guest and all its configuration +function kvmx_purge { + kvmx_destroy + rm -f $GLOBAL_USER_CONFIG_FOLDER/$VM +} + +# Print guest status +function kvmx_status { + if kvmx_running; then + echo "$BASENAME: $VM guest is running" + PID="`cat $PIDFILE`" + ps $PID + else + echo "$BASENAME: $VM guest is stopped" + fi +} + +# Dispatch +if type kvmx_$ACTION 2> /dev/null | grep -q 'function'; then + kvmx_initialize + kvmx_$ACTION $* fi diff --git a/kvmx-create b/kvmx-create new file mode 100755 index 0000000..2eb97d5 --- /dev/null +++ b/kvmx-create @@ -0,0 +1,246 @@ +#!/bin/bash +# +# System installer, vmdebootstrap version. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . + + +# Parameters +BASENAME="`basename $0`" + +# Load configuration +function kvmx_config_load { + if [ ! -z "$1" ] && [ -e "$1" ]; then + source $1 + fi +} + +# Read a parameter from user +function kvmx_user_input { + local input + local param="$1" + local default="$2" + shift 2 + + if echo $param | grep -q 'passwd'; then + read -s -rep "$* (defaults to $default): " input + else + read -rep "$* (defaults to $default): " input + fi + + if [ -z "$input" ]; then + export $param="$default" + else + export $param="$input" + fi +} + +# Get a configuration parameter if not previously defined by a sourced file +function kvmx_user_config { + local param="$1" + local default="$2" + shift 2 + + if [ -z "`eval echo '$'$param`" ]; then + kvmx_user_input $param $default $* + fi +} + +# Install a package +function kvmx_install_package { + if [ -z "$1" ]; then + return + fi + + dpkg -s $1 &> /dev/null + + if [ "$?" == "1" ]; then + echo "Installing package $1..." + DEBIAN_FRONTEND=noninteractive $SUDO apt-get install $1 -y || exit 1 + fi +} + +# Abort on error +function kvmx_exit_on_error { + if [ "$?" != "0" ]; then + echo "Error: $*" + exit 1 + fi +} + +# Run a command using sudo and abort on error +function kvmx_sudo_run { + if [ "`whoami`" != 'root' ]; then + SUDO="sudo" + fi + + $SUDO $* + kvmx_exit_on_error $* +} + +# Make sure there is provision config. +function kvmx_config { + kvmx_user_config image /var/cache/qemu/debian/box.img "Destination image" + kvmx_user_config size 3G "Image size" + kvmx_user_config format qcow2 "Image format: raw or qcow2" + kvmx_user_config method custom "Bootstrap method: custom or vmdeboostrap" + kvmx_user_config hostname machine "Hostname" + kvmx_user_config domain example.org "Domain" + kvmx_user_config arch amd64 "System arch" + kvmx_user_config version stretch "Distro version" + kvmx_user_config mirror http://http.debian.net/debian/ "Debian mirror" +} + +# Load config file +kvmx_config_load $1 + +# Get config parameters +kvmx_config + +# Check +if [ -e "$image" ]; then + echo "error: $image already exists." + exit 1 +fi + +# Ensure base folder exists +kvmx_sudo_run mkdir -p `dirname $image` + +# +# vmdebootstrap version +# +function kvmx_create_vmdebootstrap { + # Check for requirements + for req in vmdebootstrap mbr; do + kvmx_install_package $req + done + + # Image format + if [ "$format" == "qcow2" ]; then + format="--convert-qcow2" + else + formt="" + fi + + # Run + kvmx_sudo_run vmdebootstrap --verbose --image=$image --size=$size --distribution=$version \ + --mirror=$mirror --arch=$arch --hostname=$hostname.$domain \ + --grub $format + + # Fix permissions + kvmx_sudo_run chown -R `whoami`. `dirname $image` + + # Cleanup + kvmx_sudo_run rm debootstrap.log + kvmx_sudo_run rm ${image}.raw +} + +# +# Custom version +# +function kvmx_create_custom { + WORK="`mktemp -d`" + + # Check for requirements. + for req in debootstrap grub-pc parted; do + kvmx_install_package $req + done + + echo "Creating image..." + #kvmx_sudo_run dd if=/dev/zero of=$image bs=$size count=1 + kvmx_sudo_run qemu-img create -f raw $image $size + device="`sudo losetup --find --show $image`" + + echo "Partitioning image at $device..." + kvmx_sudo_run parted -s -- $device mklabel gpt + kvmx_sudo_run parted -s -- $device unit MB mkpart non-fs 2 3 + kvmx_sudo_run parted -s -- $device set 1 bios_grub on + kvmx_sudo_run parted -s -- $device unit MB mkpart ext2 3 -1 + kvmx_sudo_run parted -s -- $device set 2 boot on + kvmx_sudo_run mkfs.ext4 ${device}p2 + kvmx_sudo_run mount ${device}p2 $WORK/ + + # Non-interactive installation + APT_INSTALL="kvmx_sudo_run LC_ALL=C DEBIAN_FRONTEND=noninteractive chroot $WORK/ apt-get install -y" + + # Initial system install. + echo "Installing base system..." + kvmx_sudo_run LC_ALL=C DEBIAN_FRONTEND=noninteractive debootstrap --arch=$arch $version $WORK/ $mirror + + # Initial configuration. + echo "Applying initial configuration..." + kvmx_sudo_run mount none -t proc $WORK/proc + kvmx_sudo_run mount none -t sysfs $WORK/sys + kvmx_sudo_run mount -o bind /dev/ $WORK/dev + echo LANG=C | $SUDO tee $WORK/etc/default/locale > /dev/null + + # Hostname configuration. + echo $hostname.$domain | $SUDO tee $WORK/etc/hostname > /dev/null + echo "127.0.0.1 localhost" | $SUDO tee -a $WORK/etc/hosts > /dev/null + + # This ordering is important for facter correctly guess the domain name + echo "127.0.0.1 $hostname.$domain $hostname" | $SUDO tee -a $WORK/etc/hosts > /dev/null + + # Invert hostname contents to avoid http://projects.puppetlabs.com/issues/2533 + tac $WORK/etc/hosts | $SUDO tee $WORK/etc/hosts.new > /dev/null + kvmx_sudo_run mv $WORK/etc/hosts.new $WORK/etc/hosts + + # Initial upgrade + echo "Applying initial upgrades..." + kvmx_sudo_run chroot $WORK/ apt-get update + kvmx_sudo_run chroot $WORK/ apt-get upgrade -y + + if [ "$arch" == "i386" ]; then + kernel_arch="686" + else + kernel_arch="$arch" + fi + + $APT_INSTALL locales + $APT_INSTALL screen cron lsb-release openssl -y + $APT_INSTALL linux-image-$kernel_arch -y + $APT_INSTALL grub-pc -y + kvmx_sudo_run chroot $WORK/ update-grub + kvmx_sudo_run chroot $WORK/ grub-install $device + + # Teardown + kvmx_sudo_run umount $WORK/proc + kvmx_sudo_run umount $WORK/sys + kvmx_sudo_run umount $WORK/dev + kvmx_sudo_run umount $WORK + kvmx_sudo_run rmdir $WORK + kvmx_sudo_run losetup -d $device + + # Image conversion + if [ "$format" == "qcow2" ]; then + echo "Converting raw image to qcow2..." + kvmx_sudo_run mv $image $image.raw + kvmx_sudo_run qemu-img convert -O qcow2 ${image}.raw $image + kvmx_sudo_run rm ${image}.raw + fi + + # Fix permissions + kvmx_sudo_run chown -R `whoami`. `dirname $image` +} + +# Dispatch +if [ "$method" == "custom" ]; then + kvmx_create_custom +elif [ "$method" == "vmdebootstrap" ]; then + kvmx_create_vmdebootstrap +else + echo "$BASENAME: invalid method $method" + exit 1 +fi diff --git a/kvmx-vdagent b/kvmx-vdagent new file mode 100755 index 0000000..d29e2b3 --- /dev/null +++ b/kvmx-vdagent @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Restart spice-vdagent inside the guest +# + +# Get instances +instances="`ps -o pid,command -e | grep "spice-vdagent$" | cut -d ' ' -f 2 | xargs`" + +# Kill old instances +for pid in $instances; do + kill -9 $pid &> /dev/null +done + +# Just to make sure we're inside a virtual machine +if which spice-vdagent &> /dev/null ; then + spice-vdagent +fi diff --git a/kvmxfile b/kvmxfile index c35b081..7f65bc9 100644 --- a/kvmxfile +++ b/kvmxfile @@ -1,2 +1,48 @@ -HEADLESS="0" -BASEBOX="stretch" +# +# Sample kvmx file +# + +# Which base box you should use. +# If none is set, kvmx will bootstrap one for you. +#basebox="stretch" + +# Absolute or relative path for a provision script. +#provision_script="default" + +# Set this is you want to be able to share folders between host and guest. +#shared_folder="." +#shared_folder_mountpoint="/media/shared" + +# Set this if you want to automatically attach an spice client when the machine +# boots. +run_spice_client="1" + +# Set host_port-:guest_port mapping pairs. +#port_mapping="8080-:80,8443-:443" + +# Where the guest image is stored +image="$HOME/.local/share/kvmx/$VM/box.img" + +# Image size +size="10G" + +# Image format: raw or qcow2 +format="qcow2" + +# Bootstrap method: custom or vmdeboostrap +method="custom" + +# Hostname +hostname="machine" + +# Domain +domain="example.org" + +# System arch +arch="amd64" + +# Box distribution when bootstraping a new image +version="stretch" + +# Debian mirror +mirror="http://http.debian.net/debian/" -- cgit v1.2.3