#!/bin/bash set -ue error() { echo "Error:" "$@" >&2 exit 1 } usage() { [ ${1+x} ] && echo "Unknown option '$1'" >&2 echo "Usage: $0 [OPTIONS] NAME" >&2 echo " $0 --help" >&2 exit 1 } # Run a command locally or remotely unset SSH_ARGS run() { if [ ${SSH_ARGS+x} ]; then ssh $SSH_ARGS -- "$@"; else "$@"; fi } unset CDROM unset SSH_COMMAND unset OUTPUT unset VMDIR SEED=$(head -c24 /dev/urandom | base64 -w0 | tr '+/' '-_') SSH_CONTROLPATH="${XDG_RUNTIME_DIR:-$HOME/.ssh}/S.$SEED" # unique, unpredicable socket path cleanup() { set +e [ ! ${CDROM+x} ] || run rm -f "$CDROM" [ ! ${SSH_COMMAND+x} ] || rm -f "$SSH_COMMAND" [ "${OUTPUT+x}" -o ! "${VMDIR+x}" ] || rm -rf "$VMDIR" [ ! -S "$SSH_CONTROLPATH" ] || ssh -Oexit -S"$SSH_CONTROLPATH" x 2>/dev/null } trap cleanup EXIT TERM INT LIBVIRT_URI=qemu:///system VIRT_USER="$USER" prepare_remote_uri() { local dest="$1" user port [ ! ${SSH_COMMAND+x} ] || return # already defined if [ "${dest%:*}" != "$dest" ]; then port="${dest##*:}" dest="${dest%:*}" fi if [ "${dest%@*}" != "$dest" ]; then VIRT_USER="${dest%%@*}" dest="${dest#*@}" fi SSH_COMMAND="$(mktemp --tmpdir)" SSH_ARGS="-oControlMaster=auto -oControlPath=$SSH_CONTROLPATH ${VIRT_USER+-l $VIRT_USER} ${port+-p $port} $dest" cat >"$SSH_COMMAND" <<-EOF #!/bin/sh export SSH_AUTH_SOCK="${SSH_AUTH_SOCK-}" exec ssh -oControlPersist=yes $SSH_ARGS -- nc -q0 -U /run/libvirt/libvirt-sock EOF chmod u+x "$SSH_COMMAND" LIBVIRT_URI="qemu+ext:///system?command=${SSH_COMMAND//\//%2F}" } ARCH=$(dpkg-architecture -qDEB_TARGET_ARCH) unset ISO USER_NAME=root PROMPT_PASSWORD=n unset AUTHORIZED_KEYS FORCE=n CPU="host" unset VCPUS GRAPHICS="none" NETWORK="none" unset MEMORY unset DISK TRANSIENT=n HELP_MESSAGE="$(cat <<-EOF Install a new VM in an unattended fashion Usage $0 [OPTIONS] NAME --ssh=[USERNAME@]HOSTNAME[:PORT] connect over SSH to a remote libvirtd --arch=ARCH target architecture (default: "$ARCH") --iso=FILENAME path to the installation ISO image to preseed (required) -u,--username=USERNAME user account to create (default: "$USER_NAME") -p,--password prompt for USERNAME's password (password login are disabled by default) --authorized-keys=FILENAME path to USERNAME's authorized_keys(5) file -f,--force imediately the domain NAME if it exists, and remove existing configuration -o,--output=DIRNAME directory where to export the XML definition and key material from the guest virt-install(1) options --cpu=MODEL,... CPU model and CPU features exposed to the guest (default: "$CPU") --vcpus=STRING number of virtual cpus to configure for the guest --graphics=STRING graphical display configuration (default: "$GRAPHICS") --network=STRING network configuration (default: "$NETWORK") --memory=STRING memory to allocate for the guest, in MiB (required) --disk=STRING media to use as storage for the guest (required) --transient create a transient libvirt VM EOF )" while [ $# -gt 0 ]; do case "$1" in --arch) ARCH="$2"; shift;; --arch=*) ARCH="${1#--arch=}";; --iso) ISO="$2"; shift;; --iso=*) ISO="${1#--iso=}";; -u|--username) USER_NAME="$2"; shift;; -u*) USER_NAME="${1#-u}";; --username=*) USER_NAME="${1#--username=}";; -p|--password) PROMPT_PASSWORD=y;; --authorized-keys) AUTHORIZED_KEYS="$2"; shift;; --authorized-keys=*) AUTHORIZED_KEYS="${1#--authorized-keys=}";; -f|--force) FORCE=y;; -o|--output) OUTPUT="$2"; shift;; -o*) OUTPUT="${1#-o}";; --output=*) OUTPUT="${1#--output=}";; --ssh=*) prepare_remote_uri "${1#--ssh=}";; --vcpus) VCPUS="$2"; shift;; --vcpus=*) VCPUS="${1#--vcpus=}";; --graphics) GRAPHICS="$2"; shift;; --graphics=*) GRAPHICS="${1#--graphics=}";; --network) NETWORK="$2"; shift;; --network=*) NETWORK="${1#--network=}";; --memory) MEMORY="$2"; shift;; --memory=*) MEMORY="${1#--memory=}";; --disk) DISK="$2"; shift;; --disk=*) DISK="${1#--disk=}";; --transient) TRANSIENT=y;; --help|-\?) printf '%s\n' "$HELP_MESSAGE"; exit;; -*) usage "$1";; *) break;; esac shift done [ $# -eq 1 ] || usage VM_NAME="$1" for x in ISO MEMORY DISK; do if ! eval [ "\${$x+x}" ]; then echo "Error missing non-optional argument --$(echo "$x" | tr 'A-Z' 'a-z')" >&2 exit 1 fi done for prog in fuseiso fusermount rsync md5sum xorriso find xargs \ dpkg-architecture dpkg-buildpackage dpkg-scanpackages \ virsh virt-install xmlstarlet ssh-keygen openssl; do which "$prog" >/dev/null || error "Missing $prog" done ISOHDPFX=/usr/lib/ISOLINUX/isohdpfx.bin [ -f "$ISOHDPFX" ] || error "Missing $ISOHDPFX. Is the 'isolinux' package installed?" if [ "$PROMPT_PASSWORD" = n ]; then PASSWORD_CRYPTED='*' else PASSWORD_CRYPTED="$( [ "$USER_NAME" = root ] && prompt="Enter root password" || prompt="Enter password for $USER_NAME" read -rs -p "$prompt (leave blank to auto-generate): " pw if [ "$pw" ]; then printf '\n' >/dev/tty read -rs -p "Re-enter password to confirm: " pw2 printf '\n' >/dev/tty if [ "$pw" != "$pw2" ]; then echo "Password do not match, aborting" >&2 exit 1 fi else pw="$(pwgen -syn 32 1 | sed 's/\s$//')" printf '%s\n' "$pw" >/dev/tty fi printf '%s' "$pw" | mkpasswd --stdin --method=SHA-512 )" fi ####################################################################### # Presseed the ISO image # CDROM="$(run mktemp --tmpdir "$VM_NAME-XXXXXX.iso")" ( mountdir="$(mktemp --tmpdir --directory)" fuseiso "$ISO" "$mountdir" isoeditdir="$(mktemp --tmpdir --directory)" trap 'rm -rf "$isoeditdir"' EXIT TERM INT rsync -aH --exclude=TRANS.TBL --chmod=u+w "$mountdir/" "$isoeditdir/" fusermount -u "$mountdir" rmdir "$mountdir" . ./preseed-cfg >"$isoeditdir/preseed.cfg" [ ! "${AUTHORIZED_KEYS+x}" ] || cat "$AUTHORIZED_KEYS" >"$isoeditdir/authorized_keys" ( builddir="$(mktemp --tmpdir --directory)" trap 'rm -rf "$builddir"' EXIT TERM INT rsync -aH "./tdf-postinst-udeb" "$builddir" find "$builddir" -mindepth 2 -maxdepth 2 -name debian -type d \ -execdir dpkg-buildpackage -us -uc -b -a "$ARCH" \; mkdir "$isoeditdir/pool-extra" find "$builddir" -maxdepth 1 -type f -name '*.udeb' -print0 | \ xargs -r0 cp -vlt "$isoeditdir/pool-extra" cd "$isoeditdir" find ./dists -type f \ | grep -P "^\./dists/[^\/]+/main/debian-installer/binary-\Q$ARCH\E/Packages(\.gz)?$" \ | while read packages; do [ "${packages%.gz}" = "$packages" ] || gunzip -f "$packages" dpkg-scanpackages -tudeb -a"$ARCH" ./pool-extra >>"${packages%.gz}" [ "${packages%.gz}" = "$packages" ] || gzip -f "${packages%.gz}" done find ./pool-extra -maxdepth 1 -type f -name '*.udeb' -print0 | \ xargs -r0 md5sum >>./md5sum.txt ) cd "$isoeditdir" md5sums=$(mktemp --tmpdir="$isoeditdir" md5sum.txt-XXXXXX) while read sum file; do if [ "${file%/main/debian-installer/binary-$ARCH/Packages}" != "$file" ] || [ "${file%/main/debian-installer/binary-$ARCH/Packages.gz}" != "$file" ]; then md5sum "$file" else echo "$sum $file" fi done <./md5sum.txt >"$md5sums" mv -f "$md5sums" ./md5sum.txt md5sum ./preseed.cfg >>./md5sum.txt kernel="$(sed -rn '/^\s+kernel\s+/ {s///p; q}' ./isolinux/txt.cfg)" initrd="$(sed -rn '/^\s+append\s(.*\s)?initrd=(\S+)(\s.*)?$/ {s//\2/p;q}' ./isolinux/txt.cfg)" cat >./isolinux/isolinux.cfg <<-EOF default install label install kernel $kernel append initrd=$initrd preseed/file=/cdrom/preseed.cfg auto=true --- fb=false EOF xorriso -as mkisofs -r \ -checksum_algorithm_iso all \ -isohybrid-mbr "$ISOHDPFX" \ -b isolinux/isolinux.bin -c isolinux/boot.cat \ -partition_offset 16 \ -no-emul-boot -boot-load-size 4 -boot-info-table -eltorito-alt-boot \ --efi-boot boot/grub/efi.img -append_partition 2 0x01 ./boot/grub/efi.img \ ./ \ | if [ ${SSH_ARGS+x} ]; then run "cat >$CDROM"; else cat >"$CDROM"; fi run chmod g+r "$CDROM" ) ####################################################################### # grep -q '^kvm\s' /proc/modules || echo 'WARN: KVM not available!' >&2 [ "$NETWORK" = none ] || NETWORK="$NETWORK,model=virtio" [ ! ${OUTPUT+x} ] || mkdir -p "$OUTPUT" if [ "$FORCE" = y ]; then virsh -c "$LIBVIRT_URI" destroy "$VM_NAME" >/dev/null 2>&1 || true virsh -c "$LIBVIRT_URI" undefine "$VM_NAME" >/dev/null 2>&1 || true fi VMDIR="${OUTPUT-$(mktemp --tmpdir --directory)}" virt-install -q --connect "$LIBVIRT_URI" \ --name "$VM_NAME" \ --os-variant "debianwheezy" \ --arch "$(dpkg-architecture -A"$ARCH" -qDEB_TARGET_GNU_CPU)" \ --virt-type "kvm" \ --cpu "$CPU" ${VCPUS+--vcpus "$VCPUS"} \ --memory "$MEMORY" --memballoon "virtio" \ --disk "path=$CDROM,device=cdrom,bus=sata,readonly=on,seclabel.model=dac,seclabel.label=$VIRT_USER:libvirt-qemu" \ --disk "$DISK,bus=virtio" \ --channel "unix,target_type=virtio,name=org.qemu.guest_agent.0" \ --rng "random,device=/dev/random,model=virtio,rate_bytes=512" \ --security "type=static,model=dac,label=libvirt-qemu:libvirt-qemu,relabel=yes" \ --network "$NETWORK" \ --graphics "$GRAPHICS" \ --controller "virtio-serial" \ --noautoconsole \ --print-xml 1 >"$VMDIR/$VM_NAME.xml" virsh -c "$LIBVIRT_URI" create "$VMDIR/$VM_NAME.xml" # never boot again on CDROM, detach unnecessary devices and remove # unnecessary controllers xmlstarlet edit --omit-decl --inplace \ --delete "/domain/os/boot[@dev='cdrom']" \ --delete "/domain/devices/disk[@type='file'][@device='cdrom']" \ --delete "/domain/devices/sound" \ --delete "/domain/devices/controller[@type='usb']" \ --delete "/domain/devices/input[@type='tablet'][@bus='usb']" \ --delete "/domain/devices/redirdev[@type='spicevmc'][@bus='usb']" \ "$VMDIR/$VM_NAME.xml" [ "$TRANSIENT" = y ] || virsh -c "$LIBVIRT_URI" define "$VMDIR/$VM_NAME.xml" # wait until the guest starts its QEMU Agent at the end of the installation guest_agent() { local rv while :; do ./virsh-ga -c "$LIBVIRT_URI" "$VM_NAME" "$@" && break || rv=$? [ $rv -eq 128 ] || exit $rv sleep 1 done } guest_agent ping # then copy the public key material for path in /etc/ssh/ssh_host_rsa_key.pub \ /etc/ssh/ssh_host_ed25519_key.pub \ /etc/salt/pki/minion/minion.pub; do guest_agent cat "$path" >"$VMDIR/${path##*/}" done guest_agent touch /tmp/tdf-install-continue virsh -c "$LIBVIRT_URI" console "$VM_NAME" --safe >/dev/null # wait until shutdown ( echo "Salt minion MD5 fingerprint:" # salt uses a bizarre scheme... grep -v '^-----.*-----$' "$VMDIR/minion.pub" \ | openssl dgst -md5 -c | sed '/.*=\s*/ {s//\t/;q}' echo echo "SSH hostkey fingerprints:" find "$VMDIR" -maxdepth 1 -type f -name 'ssh_host_*_key.pub' \ -execdir ssh-keygen -lf {} \; | sed 's/^/\t/' ) >&2 if [ ${OUTPUT+x} ]; then printf "\nExported files:\n" >&2 find "$OUTPUT" -type f -printf '\t%p\n' >&2 fi