#!/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; 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) [ "$iso" ] && echo "iso=\"../$c/$iso\"" >> config cp "../$c/vmlinux" "../$c/initrd" . echo "kernel=${k-vmlinux}" >> config echo "initrd=initrd" >> config } } 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" } exp() { usage 'exp name' 'experiment on a VM' && return cd "$dir/$1" || die "invalid VM: $1" [ -f vftool.pid ] || die "vm $1 is not active" sleep 1 && screen -X stuff 'root ' sleep 1 && screen -X stuff 'sed -i.bak "s/die..Bootloader/;; # die \"Bootloader/" /sbin/setup-disk ' sleep 1 && screen -X stuff 'setup-alpine -e ' sleep 1 && screen -X stuff 'none ' sleep 1 && screen -X stuff "$1 " sleep 1 && screen -X stuff 'eth0 ' sleep 1 && screen -X stuff '192.168.64.2 ' sleep 1 && screen -X stuff '255.255.255.0 ' sleep 1 && screen -X stuff '192.168.64.1 ' sleep 1 && screen -X stuff 'n ' sleep 3 && screen -X stuff ' ' sleep 1 && screen -X stuff '192.168.64.1 ' sleep 1 && screen -X stuff 'Europe/Paris ' sleep 5 && screen -X stuff 'none ' sleep 1 && screen -X stuff 'chrony ' sleep 5 && screen -X stuff '1 ' sleep 1 && screen -X stuff 'openssh ' sleep 5 && screen -X stuff 'vda ' sleep 3 && screen -X stuff 'sys ' sleep 1 && screen -X stuff 'y ' sleep 20 && screen -X stuff 'cat /etc/fstab ' } finalize() { tty=$(vftool_tty) echo "root\nuname -a" >> "$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\nManage virtual machines\nUsage: vm command [options] [args]" 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/v3.13/releases/aarch64/alpine-virt-3.13.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 [-af][-c vm] name' 'Start a VM' && return while getopts :ac:f opt; do case $opt in (a) a=1 ;; (c) from=$OPTARG ;; (f) f=1 ;; (*) Opth=1 start; 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 } 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"} \ ${hdd+-d "$hdd"} \ ${iso+-c "$iso"} \ ${cpu+-p "$cpu"} \ ${ram+-m "$ram"} \ -a "${arg-console=hvc0}" \ >>vftool.log 2>&1 & sleep 1 echo "$!" >vftool.pid screen -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) } 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 exp info 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 "$@"