#!/bin/bash # ===================================================================== # Import a prebuilt base image instead of building one. # # sudo ./vm/images/import.sh ubuntu # sudo ./vm/images/import.sh windows # sudo ./vm/images/import.sh both # # WHY THIS EXISTS # # Building Ubuntu takes about 25 unattended minutes. Windows has no # unattended path at all - it is a person clicking through setup in a VNC # console, loading a virtio driver by hand, and knowing when to stop. For # anyone who just wants the sub-agent working, downloading a finished # image is the difference between an afternoon and a coffee. # # WHY IT IS A SCRIPT AND NOT A LIST OF STEPS # # Three things have to be exactly right or the result fails later, in # ways that do not point at their cause: # # * the owner must be owuivmadm, or the maintenance tool cannot thaw it # * the mode must be 0444, because FROZEN IS THE OWNER WRITE BIT and # nothing else - a 0644 base means vmd refuses every session with # "freeze it and try again" # * the .sha256 must land beside the disk, or every saved fast-start # machine is treated as stale and sessions silently boot from cold # # A checklist gets those wrong. This does not. # ===================================================================== set -euo pipefail BASE=${OWUIVM_BASE_DIR:-/var/lib/owuivm/base} # Override to host your own. Any web server will do: the script only ever # does a ranged GET, and it verifies what it gets. SOURCE=${OWUIVM_IMAGE_SOURCE:-https://nextcloud.grantlan.com/s/OWUIToolsVMDisks} [ "$(id -u)" -eq 0 ] || { echo "import.sh must run as root (it writes $BASE)" >&2; exit 1; } command -v curl >/dev/null || { echo "FAILED: curl is not installed" >&2; exit 1; } getent passwd owuivmadm >/dev/null || { echo "FAILED: there is no owuivmadm user - run vm/host/install.sh first" >&2; exit 1; } # Roughly what each one needs, so a download does not die at 95% with the # filesystem full. qcow2 is sparse but these are not thin. declare -A NEED=( [ubuntu]=11 [windows]=44 ) fetch() { # fetch local name="$1" dest="$2" # Nextcloud's public-share download endpoint. `-C -` resumes, which # matters: windows.qcow2 is 40 GB and a dropped connection three # quarters of the way through should not mean starting again. curl -fSL --retry 3 --retry-delay 5 -C - \ -o "$dest" "$SOURCE/download?path=%2F&files=$name" } import_one() { local name="$1" local disk="$BASE/$name.qcow2" vars="$BASE/$name-OVMF_VARS.fd" sums="$BASE/$name.sha256" # A FINISHED IMAGE IS ONE THAT IS FROZEN. A part-downloaded one is not. # # The obvious guard - refuse if the file exists - contradicts the promise # that this is resumable: a download interrupted at 30 GB could never be # re-run. So the test is the same one everything else here uses, the # owner write bit. import.sh sets it 0444 only after the checksum has # passed, so a writable disk is by definition an unfinished one. if [ -e "$disk" ] && [ "$(( 8#$(stat -c %a "$disk") & 8#200 ))" -eq 0 ]; then echo "FAILED: $disk is already here and frozen." >&2 echo " Importing would replace a base image you may have changed. Delete it" >&2 echo " first if that is what you want - and note that any saved fast-start" >&2 echo " machine for it becomes stale." >&2 exit 1 fi if [ -e "$disk" ]; then echo "==> $name: an unfinished download is here; resuming it" fi local free_gb free_gb=$(df -BG --output=avail "$BASE" | tail -1 | tr -dc '0-9') if [ "$free_gb" -lt "${NEED[$name]}" ]; then echo "FAILED: $name needs about ${NEED[$name]} GB and $BASE has ${free_gb} GB free" >&2 exit 1 fi echo "==> $name: downloading (${NEED[$name]} GB or so; resumable, ^C and re-run is safe)" install -d -o owuivmadm -g owuivmadm -m 0775 "$BASE" fetch "$name.sha256" "$sums" fetch "$name-OVMF_VARS.fd" "$vars" fetch "$name.qcow2" "$disk" echo "==> $name: verifying" # BY BASENAME, NOT `sha256sum -c`. # # The manifest carries absolute paths - /var/lib/owuivm/base/... - because # that is where the freeze wrote it. `sha256sum -c` would therefore check # the files at THOSE paths, which are the ones already installed, not the # ones just downloaded. At the default location they are the same file and # it passes for the wrong reason; with OWUIVM_BASE_DIR set it verifies # somebody else's disk, or fails on a file that is not there. # # Measured: a test import into a scratch directory reported both files OK # while the scratch copy was a quarter downloaded. local bad=0 want got file while read -r want file; do [ -n "$want" ] || continue got=$(sha256sum "$BASE/$(basename "$file")" | awk '{print $1}') if [ "$want" = "$got" ]; then echo " $(basename "$file"): OK" else echo " $(basename "$file"): MISMATCH" >&2 bad=1 fi done < "$sums" if [ "$bad" -ne 0 ]; then echo "FAILED: the download is corrupt or incomplete." >&2 echo " Re-run to resume, or delete $BASE/$name.* and start again." >&2 exit 1 fi echo "==> $name: freezing" chown owuivmadm:owuivmadm "$disk" "$vars" "$sums" # 0444 IS the freeze. See the header. chmod 0444 "$disk" "$vars" chmod 0644 "$sums" echo " $name imported and frozen" } case "${1:-}" in ubuntu|windows) import_one "$1" ;; both) import_one ubuntu; import_one windows ;; *) echo "usage: import.sh {ubuntu|windows|both}" >&2; exit 64 ;; esac echo echo "Done. Open Tool Server VM Maintenance and the row should read 'frozen'." echo "Sessions boot these from cold; press 'Save fast start' to make them resume" echo "in seconds instead."