#!/bin/sh # Manage virtual machines # # Prereq: # - curl # - cdrtools (isoinfo) # - expect # - qemu # - screen # # TODO: # - extract bzImage and initrd.gz with isoinfo # - ssh scripts to finish install (once ssh is ready) # # DONE: # - setup vde and networking # - setup ssh keys so it is possible to ssh in vm from host # unset CDPATH export LC_ALL=C IFS=' ' version='vm-0.1' arch=$(uname -m) sys=$(uname -s) alpine_version='3.15.0' pubkey=$HOME/.ssh/id_rsa.pub dir="${VM_DIR:-$HOME/.vm}" console() { usage 'console name' 'Attach a console to a virtual machine' && return [ "$1" ] || die "missing argument" is_running "$1" && screen -r "vm!$1!" } create() { usage 'create [-s size] name' 'Create an alpinelinux disk image' && return size=8g while getopts :s: opt; do case $opt in s) size=$OPTARG ;; *) Opth=2 create_alpine_image; return ;; esac done shift $((OPTIND - 1)) [ -d "$dir/$1" ] && die "create failed: $dir/$1 already exists" mkdir "$dir/$1" cd "$dir/$1" || die "create failed: invalid directory $dir/$1" qemu-img create "$1.raw" "$size" || die "create failed" mac=$(new_macaddr) ip=$(new_ip) hdd="$1.raw" echo "hdd=$hdd mac=$mac ip=$ip" >> config # Before install do not use virtio, as devices may not recognized as bootable # TODO: alternate way: extract kernel and initrd files and pass them directly to qemu screen -S "vm!$1!" -d -m qemu-system-$arch -nographic \ -cdrom ../alpine-iso/alpine-virt-$alpine_version-$arch.iso \ -hdd "$hdd" -net nic,macaddr=$mac -net vde setup_alpine "$1" post_setup_alpine "$1" } die() { [ "$1" ] && echo "$0: $*" >&2; exit 1; } help() { usage 'help' 'Print this help text' && return printf "$version\n Manage virtual machines\n\nUsage: vm command [options] [args]\n" Opth=1; for c in $Cmdlist; do $c; done } init() { mkdir -p "$dir" } init_alpine_iso() { usage init_alpine_iso '' && return mkdir -p "$dir/alpine-iso" iso_url="https://dl-cdn.alpinelinux.org/alpine/v${alpine_version%.*}/releases/$arch" iso="alpine-virt-$alpine_version-$arch.iso" cd "$dir/alpine-iso" [ -f "$iso" ] || curl -LO "$iso_url/$iso" || rm -f "$iso" echo "iso=$iso " > config echo 10 > ../index } is_running() { screen -ls "vm!$1!" >/dev/null 2>&1; } ls() { usage 'ls' 'list virtual machines' && return init && cd "$dir" || die "could not change dir to $dir" for i in */; do i=${i%/} [ "$i" = '*' ] && continue is_running "$i" && state=active || state=stopped printf "%-20s %s\n" "$i" "$state" done } new_ip() { read index < $dir/index index=$((index + 1)) echo "$index" > $dir/index echo "10.0.2.$index/24" } new_macaddr() { printf 'de:ad:be:ef:%02x:%02x\n' $((RANDOM % 256)) $((RANDOM % 256)); } pidof() { usage 'pidof name' 'print the PID of a virtual machine' && return p=$(screen -ls "vm!$1!" | awk 'NR==2 {print substr($1, 1, index($1, ".")-1)}') [ "$p" ] && pgrep -P $p } post_setup_alpine() { start "$1" read proto key id < "$pubkey" # echo ' expect -c ' set timeout -1 spawn screen -x "vm!'$1'!" expect { " login: " { send "root\r"; exp_continue } "Password: " { send "root\r"; exp_continue } ":~# " } send "mkdir -pm 0700 .ssh\r" expect ":~# " send "echo '$proto' '$key' '$id' >.ssh/authorized_keys\r" expect ":~# " send "exit\r" ' } # setup_alpine automates alpine installation from iso to image. # When done, base image system is ready, with storage and network up. # No user nor ssh access configured yet. # TODO: custom network (in case of no dhcp). setup_alpine() { usage 'setup_alpine' && return expect -c ' set timeout -1 spawn screen -x "vm!'$1'!" expect { "localhost login: " { send "root\r"; exp_continue } "localhost:~# " { send "SWAP_SIZE=0 setup-alpine\r"; exp_continue } "Select keyboard layout: " { send "\r"; exp_continue } "Enter system hostname" { send "'$1'\r"; exp_continue } "Which one do you want to initialize?" { send "\r"; exp_continue } "Ip address for eth0?" { send "'$ip'\r"; exp_continue } "Gateway?" { send "10.0.2.2\r"; exp_continue } "manual network configuration?" { send "\r"; exp_continue } "DNS domain name?" { send "lan\r"; exp_continue } "DNS nameserver(s)?" { send "10.0.2.2 1.1.1.1\r"; exp_continue } "New password:" { send "root\r"; exp_continue } "Retype password:" { send "root\r"; exp_continue } "Which timezone are you in?" { send "\r"; exp_continue } "HTTP/FTP proxy URL?" { send "\r"; exp_continue } "Enter mirror number " { send "\r"; exp_continue } "Which SSH server?" { send "\r"; exp_continue } "Which disk(s) would you like to use?" { send "sda\r"; exp_continue } "How would you like to use it?" { send "sys\r"; exp_continue } "Erase the above disk(s) and continue?" { send "y\r"; exp_continue } "Installation is complete" { send "poweroff\r" } } interact ' } start_vde() { usage start_vde && return sudo sh <<- EOT vde_switch -tap tap0 -sock /tmp/vde.ctl -daemon -mod 666 sleep 1 ip address add 10.0.2.2/24 dev tap0 ip link set tap0 up echo 1 > /proc/sys/net/ipv4/ip_forward iptables -t nat -A POSTROUTING -s 10.0.2.0/24 -o eth0 -j MASQUERADE iptables -t nat -A POSTROUTING -s 10.0.2.0/24 -o wlan0 -j MASQUERADE EOT #slirpvde --dhcp --daemon } stop_vde() { # killall slirpvde sudo sh <<- EOT iptables -t nat -D POSTROUTING -s 10.0.2.0/24 -o eth0 -j MASQUERADE iptables -t nat -D POSTROUTING -s 10.0.2.0/24 -o wlan0 -j MASQUERADE ip link set tap0 down killall vde_switch EOT } start() { usage 'start [-acd] name' 'start a virtual machine' && return while getopts :acd opt; do case $opt in a) opta=1 ;; c) boot=c ;; d) boot=d ;; esac done shift $((OPTIND -1)) [ "$1" ] || die 'start failed: name missing' cd "$dir/$1" || die "start failed: invalid directory $dir/$1" is_running "$1" || start_qemu "$1" [ "$opta" ] && console "$1" } start_qemu() ( opt='-nographic -cpu max' [ "$sys" = Linux ] && opt="$opt -enable-kvm" . ./config || die "could not source $PWD/config" exec 1>>qemu.log 2>&1 date +%F_%T set -x screen -S "vm!$1!" -d -m qemu-system-$arch $opt \ ${smp+-smp $smp} \ ${ram+-m $ram} \ ${mac+-net nic,macaddr=$mac,model=virtio-net-pci} -net vde \ ${hdd+-drive file="$hdd",if=virtio,media=disk,format=raw} \ ${iso+-drive file="$iso",if=virtio,media=cdrom,format=raw} \ ${boot+-boot $boot} ) stop() { usage 'stop name' 'stop a virtual machine' && return is_running "$1" && kill "$(pidof "$1")" } usage() { case $Opth in 1) printf " %-34s %s\n" "$1" "$2" ;; 2) printf "$0 $1\n\t$2\n" ;; *) return 1 ;; esac } version() { usage 'version' 'Print version' && return echo "$version" } Cmdlist='console create help init_alpine_iso ls pidof setup_alpine start start_vde stop version' [ "$1" ] && C=$1 && shift 1 || { help; exit 1; } #for c in $Cmdlist; do # case $c in # ("$C") cmd=$c; break ;; # ("$C"*) [ "$cmd" ] && die "ambiguous command $C" || cmd=$c ;; # esac #done cmd=$C [ "$cmd" ] || { help; exit 1; } && $cmd "$@"