summaryrefslogtreecommitdiff
path: root/bin/vm
diff options
context:
space:
mode:
authorMarc Vertes <mvertes@free.fr>2022-01-07 15:24:15 +0100
committerMarc Vertes <mvertes@free.fr>2022-01-07 15:24:15 +0100
commit053556962bb2d29ac76bcca51cabd1df238f80e3 (patch)
treec2c1c89e08f66e29ce73f77d925ee4f955857dec /bin/vm
parent6258df971a00f175cd434e7874c6ba5616b50e60 (diff)
update
Diffstat (limited to 'bin/vm')
-rwxr-xr-xbin/vm258
1 files changed, 258 insertions, 0 deletions
diff --git a/bin/vm b/bin/vm
new file mode 100755
index 0000000..8b4da42
--- /dev/null
+++ b/bin/vm
@@ -0,0 +1,258 @@
+#!/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 "$@"