summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorMarc Vertes <mvertes@free.fr>2025-01-21 15:02:16 +0100
committerMarc Vertes <mvertes@free.fr>2025-01-21 15:02:16 +0100
commitcaec9ce908ae967238bcba5df96a73287b512ddd (patch)
tree31c42aadc659e82e4e182b05228eccbb355460b6 /bin
parent981054fa3d6b446b5697ad1e95fd9d89a17b7297 (diff)
fixup
Diffstat (limited to 'bin')
-rwxr-xr-xbin/vm414
1 files changed, 184 insertions, 230 deletions
diff --git a/bin/vm b/bin/vm
index 35a107e..779672e 100755
--- a/bin/vm
+++ b/bin/vm
@@ -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 "$@"