diff options
| -rw-r--r-- | .bashrc | 31 | ||||
| -rw-r--r-- | .profile | 6 | ||||
| -rwxr-xr-x | bin/vm | 414 |
3 files changed, 208 insertions, 243 deletions
@@ -18,16 +18,30 @@ stty -ixon # disable Ctrl-S # Save and reload the history after each command finishes #export PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND" +case $(uname -o) in +(Darwin) + export GNUTERM='sixelgd enhanced truecolor size 1600,1200 font "arial,9"' + alias ls='ls -GF' + alias ll='ls -AlGFhv' + alias ldd='otool -L' + ;; +(GNU/Linux|Linux) + export GNUTERM='sixelgd enhanced truecolor font "arial,9"' + alias ls='ls --color=auto -v' + alias ll='ls -AlFhv' + ;; +esac + # Change font for urxvt. Examples: # cf 9x15 # cf xft:Monospace:size=12 # cf "xft:Bitstream Vera Sans Mono:size=8:antialias=true # cf() { printf '\e]710;%s\007' "${1:-fixed}"; } -p() { [ -f "$1" -a ! -x "$1" ] && less -XF $1 || "$@" 2>&1 | less -XF ; } +p() { [ -f "$1" -a ! -x "$1" ] && less "$1" || "$@" 2>&1 | less; } export -f p -fixab() { printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" ; tail -c +25 "$1"; } +fixab() { printf "\x1f\x8b\x08\x00\x00\x00\x00\x00"; tail -c +25 "$1"; } # Less: use colors in place of bold/underline # red: 1, green: 2, yellow: 3, blue: 4, magenta: 5, cyan: 6, grey: 7, black: 8 @@ -37,12 +51,9 @@ fixab() { printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" ; tail -c +25 "$1"; } #export LESS_TERMCAP_ue=$'\E[0m' # end underline export LESS=iXFRx4 -export BACKUP=bip:/home/backup/marc@$(hostname -s) +#export BACKUP=bip:/home/backup/marc@$(hostname -s) export LESS=XFRx4 -# gnuplot display in terminal -export GNUTERM='sixelgd enhanced truecolor font "arial,9"' - # Stopwatch alias timer='echo "Timer started. Stop with Ctrl-D." && date && time cat && date' @@ -50,8 +61,6 @@ alias timer='echo "Timer started. Stop with Ctrl-D." && date && time cat && date #alias cl='cf xft:Mono:size=12' #alias cp='cp --reflink' -alias ls='ls --color=auto -v' -alias ll='ls -AlFhv' alias more='less' alias vi='vim' alias view='vim -R' @@ -74,12 +83,12 @@ alias ww='vi ~/Wiki/home_page.md' alias s='vi ~/Wiki/scratch.md' alias dotfiles='git --git-dir=$HOME/dotfiles --work-tree=$HOME' -alias by='go build ./cmd/yaegi' -alias cy='cd ~/go/src/github.com/traefik/yaegi' +#alias by='go build ./cmd/yaegi' +#alias cy='cd ~/go/src/github.com/traefik/yaegi' #alias ty='go test -v -short ./interp' alias y='rlwrap -pblue yaegi' alias dy='YAEGI_AST_DOT=1 YAEGI_CFG_DOT=1 ./yaegi' -alias wai='~/go/src/github.co/traefik/whoami/whoami' +#alias wai='~/go/src/github.co/traefik/whoami/whoami' # yaegi debug export YAEGI_DOT_CMD='dotty -' @@ -1,6 +1,8 @@ # ~/.profile -case $(uname -s) in +case $(uname -o) in +(Android) + ;; (Darwin) PATH=~/bin:~/mu/bin:/opt/homebrew/bin:/opt/homebrew/opt/ruby/bin:/opt/homebrew/opt/tcl-tk/bin:$PATH:~/go/bin:~/.cargo/bin:~/.pyenv/versions/2.7.18/bin:~/.local/bin export REPLYTO='mvertes@free.fr' @@ -13,7 +15,7 @@ case $(uname -s) in tabs -4 export HOMEBREW_NO_ANALYTICS=1 ;; -(Linux) +(Linux|GNU/Linux) PATH=~/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:~/go/bin export XDG_RUNTIME_DIR=/run/user/$(id -u) ;; @@ -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 "$@" |
