diff options
| author | Marc Vertes <mvertes@free.fr> | 2025-01-21 15:02:16 +0100 |
|---|---|---|
| committer | Marc Vertes <mvertes@free.fr> | 2025-01-21 15:02:16 +0100 |
| commit | caec9ce908ae967238bcba5df96a73287b512ddd (patch) | |
| tree | 31c42aadc659e82e4e182b05228eccbb355460b6 /bin | |
| parent | 981054fa3d6b446b5697ad1e95fd9d89a17b7297 (diff) | |
fixup
Diffstat (limited to 'bin')
| -rwxr-xr-x | bin/vm | 414 |
1 files changed, 184 insertions, 230 deletions
@@ -1,286 +1,240 @@ #!/bin/sh -# Manage virtual machines -# -# Prereq: -# - curl -# - cdrtools (isoinfo) -# - expect -# - qemu -# - screen -# +# vm is a command line tool to manage and operate virtual machines. + +vm_version='vm-0.1' + # 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 +# - 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=' ' -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_arch() { - usage 'create_arch [-s size] name' 'Create an archlinux disk image' && return - size=8g - while getopts :s: opt; do +add() { + usage 'add [Options] name' 'Add a new VM' && return + while getopts :c:i:k:p:s: opt; do case $opt in - s) size=$OPTARG ;; - *) Opth=2 create_arch; return ;; + [cikps]) eval "$opt=$OPTARG";; + *) Opth=1 add "$1"; exit ;; 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 - - mkfs.ext4 "$1.raw" - mkdir -p mnt - sudo mount "$1.raw" mnt - sudo pacstrap mnt base base-devel - sudo umount mnt + + [ -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 + } } -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_arch; 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" +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 - printf "$version\n Manage virtual machines\n\nUsage: vm command [options] [args]\n" + echo "$vm_version - Manage virtual machines" + echo "Usage: vm command [options] [args]" + echo "Commands:" Opth=1; for c in $Cmdlist; do $c; done } -init() { - mkdir -p "$dir" +info() { + usage 'info name' 'Print informations on a VM' && return + echo 'not implemented yet' } -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 -} +# CAUTION: be careful to preserve tabs in the following Makefile template string. -is_running() { screen -ls "vm!$1!" >/dev/null 2>&1; } +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 -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 -} +all: initrd vmlinux config -new_ip() { - read index < $dir/index - index=$((index + 1)) - echo "$index" > $dir/index - echo "10.0.2.$index/24" -} +initrd: $(iso) $(isoinfo) + isoinfo -i $(iso) -J -x /boot/initramfs-virt > $@ -new_macaddr() { printf 'de:ad:be:ef:%02x:%02x\n' $((RANDOM % 256)) $((RANDOM % 256)); } +vmlinux: $(iso) $(isoinfo) + isoinfo -i $(iso) -J -x /boot/vmlinuz-virt | gunzip > $@ -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 -} +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 +' -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" - ' +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 } -# 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 - ' +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' } -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 +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 } -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 +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 [-acd] name' 'start a virtual machine' && return - while getopts :acd opt; do + usage 'start [-afs] name' 'Start a VM' && return + while getopts :afs opt; do case $opt in - a) opta=1 ;; - c) boot=c ;; - d) boot=d ;; + (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' - cd "$dir/$1" || die "start failed: invalid directory $dir/$1" - is_running "$1" || start_qemu "$1" - [ "$opta" ] && console "$1" -} + shift $((OPTIND - 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} + [ "$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 virtual machine' && return - is_running "$1" && kill "$(pidof "$1")" + 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() { - case $Opth in - 1) printf " %-34s %s\n" "$1" "$2" ;; - 2) printf "$0 $1\n\t$2\n" ;; - *) return 1 ;; - esac -} +usage() { [ "$Opth" ] && printf " %-34s %s\n" "$1" "$2"; } version() { usage 'version' 'Print version' && return - echo "$version" + echo "$vm_version" } -Cmdlist='console create help init_alpine_iso ls pidof setup_alpine start start_vde stop version' +vftool_tty() { grep -om 1 '\/dev\/tty.*' 'vftool.log'; } + +# Main starts here. +dir="$HOME/.vm" +Cmdlist='add console del edit 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=$C +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 "$@" |
