#!/bin/sh # vm is a command line tool to manage and operate virtual machines. vm_version='vm-0.1' # TODO: # - DONE: fetch, build and install vftool in .vm/vftool # - DONE: setup a config file per VM # - DONE: creation of hdd image # - DONE: delete a VM # - DONE: import iso, kernel, initrd from existing vm # - when no vm is specified, apply command to last one # - script install from CDROM iso: # - patch alpine setup-disk # - setup system, including static IP address # - setup user account from host # - setup ssh from host # - time synchronization from host # unset CDPATH export LC_ALL=C IFS=' ' add() { usage 'add [Options] name' 'Add a new VM' && return while getopts :c:i:k:p:s: opt; do case $opt in [cikps]) eval "$opt=$OPTARG";; *) Opth=1 add "$1"; exit ;; esac done shift $((OPTIND - 1)) [ -d "$dir/$1" ] && die "vm $1 already exists in $dir/$1" init_alpine || die "could not init alpine iso" [ "$1" = 'alpine-iso' ] && return mkdir -p "$dir/$1" && cd "$dir/$1" || die "add $1 failed" hdd=${p-$1.raw} [ -f "$hdd" ] || dd if=/dev/zero of="$hdd" bs=1 count=0 seek="${s-32g}" 2>/dev/null echo "hdd=\"$hdd\"" > config [ "$c" ] && { [ -d "../$c" ] || die "invalid directory: $dir/$c" iso=$(getconf "$c" iso) cp "../$c/vmlinux" "../$c/initrd" . [ "$iso" ] && echo "iso=\"../$c/$iso\"" >> config cat <<- EOT >> config kernel=${k-vmlinux} initrd=initrd cpu=1 ram=512 arg="console=hvc0" EOT } } console() { usage 'console name' 'Attach a console to a VM' && return [ "$1" ] || die 'console: name is missing' cd "$dir/$1" || die "console $1 failed" [ -f vftool.pid ] || die "vm $1 is not active" screen -r "$1" } del() { usage 'del name' 'Delete a VM' && return case $1 in ''|*/*|.*) die "invalid VM name: $1" ;; esac [ -d "$dir/$1" ] || die "$dir/$1 not found" [ -f "$dir/$1/vftool.pid" ] && die "vm $1 is still active, stop it first" rm -rf "${dir:?}/$1" } die() { [ "$1" ] && echo "$0: $*" >&2; exit 1; } edit() { usage 'edit name' 'Edit a VM configuration' && return [ -f "$dir/$1/config" ] || die "$dir/$1/config not found" ${EDITOR-vi} "$dir/$1/config" } finalize() { tty=$(vftool_tty) printf 'root\nuname -a\n' >> "$tty" } getconf() { awk -F '=' -v k="$2" '$1 == k {print $2}' "$dir/$1/config"; } help() { usage 'help' 'Print this help text' && return echo "$vm_version - Manage virtual machines" echo "Usage: vm command [options] [args]" echo "Commands:" Opth=1; for c in $Cmdlist; do $c; done } info() { usage 'info name' 'Print informations on a VM' && return echo 'not implemented yet' } # CAUTION: be careful to preserve tabs in the following Makefile template string. alpine_makefile='# Generated by "vm". DO NOT EDIT. # Check https://alpinelinux.org/downloads for possible upgrades iso_url = https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/aarch64/alpine-virt-3.21.3-aarch64.iso iso = $(notdir $(iso_url)) isoinfo ?= /opt/homebrew/bin/isoinfo all: initrd vmlinux config initrd: $(iso) $(isoinfo) isoinfo -i $(iso) -J -x /boot/initramfs-virt > $@ vmlinux: $(iso) $(isoinfo) isoinfo -i $(iso) -J -x /boot/vmlinuz-virt | gunzip > $@ config: $(iso) @echo "iso=$(iso)" >config @echo "initrd=initrd" >>config @echo "kernel=vmlinux" >>config @echo "cpu=1" >> config @echo "ram=512" >> config @echo "arg=\"console=hvc0\"" >> config $(iso): curl -LO $(iso_url) || { rm -f $@; false; } $(isoinfo): brew install cdrtools ' init_alpine() { mkdir -p "$dir/alpine-iso" && cd "$dir/alpine-iso" || die 'init alpine failed' [ -f 'Makefile' ] || printf '%s' "$alpine_makefile" > Makefile make -s all } init_vftool() { [ -x "$dir/vftool" ] && return cd '/tmp' && git clone --depth=1 'https://github.com/evansm7/vftool' && cd 'vftool' && make && cp 'build/vftool' "$dir/vftool" rm -rf '/tmp/vftool' } log() { usage 'log name' 'print logs of a VM' && return [ "$1" ] || die "log failed: name missing" cd "$dir/$1" || die "log $1 failed" cat vftool.log.old vftool.log 2>/dev/null } ls() { usage 'ls' 'list VMs' && return [ -d "$dir" ] && cd "$dir" || return for i in */; do i=${i%/} [ "$i" = '*' ] && continue [ -f "$i/vftool.pid" ] && state=active || state=stopped printf "%-20s %s\n" "$i" "$state" done } start() { usage 'start [-afs] name' 'Start a VM' && return while getopts :afs opt; do case $opt in (a) a=1 ;; (f) f=1 ;; (s) s=1 ;; (*) Opth=1 start "$1"; exit;; esac done shift $((OPTIND - 1)) [ "$1" ] || die "start failed: name missing" init_vftool cd "$dir/$1" || die "start $1 failed" [ -f vftool.pid ] && die "Error: process $(cat vftool.pid) is active or $PWD/vftool.pid should be removed" start_vm & sleep 2 ! [ "$f" ] || finalize ! [ "$a" ] || vm console "$1" ! [ "$s" ] || exec ssh "$1" } start_vm() ( [ -f vftool.log ] && cat vftool.log >> vftool.log.old exec 1>vftool.log 2>&1 . config || die "vm: could not source $PWD/config" trap 'rm -f vftool.pid' EXIT "$dir/vftool" \ ${kernel+-k "$kernel"} \ ${initrd+-i "$initrd"} \ ${hda+-d "$hda"} \ ${hdb+-d "$hdb"} \ ${hdc+-d "$hdc"} \ ${iso+-c "$iso"} \ ${cpu+-p "$cpu"} \ ${ram+-m "$ram"} \ -a "${arg-console=hvc0}" \ >>vftool.log 2>&1 & sleep 1 [ -f screenlog.0 ] && mv screenlog.0 screenlog.0.old echo "$!" >vftool.pid screen -L -S "${PWD##*/}" -d -m "$(vftool_tty)" wait ) stop() { usage 'stop name' 'Stop a VM' && return [ "$1" ] || die 'stop: name missing' cd "$dir/$1" || die "stop $1 failed" [ -f vftool.pid ] || die "stop: vm $1 is not active" kill "$(cat vftool.pid)" || rm -f vftool.pid } usage() { [ "$Opth" ] && printf " %-34s %s\n" "$1" "$2"; } version() { usage 'version' 'Print version' && return echo "$vm_version" } vftool_tty() { grep -om 1 '\/dev\/tty.*' 'vftool.log'; } # Main starts here. dir="$HOME/.vm" Cmdlist='add console del edit info init_alpine init_vftool help ls log start 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" ] || { help; exit 1; } && $cmd "$@"