#!/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 } LIBVIRT_URI=qemu:///system ARCH=$(dpkg-architecture -qDEB_TARGET_ARCH) unset ISO USER_NAME=root PROMPT_PASSWORD=n unset AUTHORIZED_KEYS FORCE=n unset OUTPUT CPU="host" unset VCPUS GRAPHICS="none" NETWORK="none" unset MEMORY unset DISK TRANSIENT= HELP_MESSAGE="$(cat <<-EOF Install a new VM in an unattended fashion Usage $0 [OPTIONS] NAME --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 pass 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=}";; --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='--transient';; --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?" [ $(id -u) -eq 0 ] || error "This script needs to run as root" 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 # VMTMPDIR="$(mktemp --tmpdir=/var/lib/libvirt/images --directory "$VM_NAME.XXXXXX" )" trap 'rm -rf "$VMTMPDIR"' EXIT TERM INT chmod a+x "$VMTMPDIR" install -o libvirt-qemu -m 0400 /dev/null "$VMTMPDIR/install.iso" install -o libvirt-qemu -m 0700 --directory "$VMTMPDIR/virtfs" ( 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 \ -o "$VMTMPDIR/install.iso" ./ ) ####################################################################### # 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 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" \ --cdrom "$VMTMPDIR/install.iso" \ --disk "$DISK,bus=virtio" \ --channel "unix,target_type=virtio,name=org.qemu.guest_agent.0" \ --filesystem "source=$VMTMPDIR/virtfs,target=virtfs" \ --network "$NETWORK" \ --graphics "$GRAPHICS" \ --noautoconsole $TRANSIENT ( vmdef="$(mktemp --tmpdir)" trap 'rm -f "$vmdef"' EXIT TERM INT virsh -c "$LIBVIRT_URI" dumpxml "$VM_NAME" >"$vmdef" for xpath in \ "/domain/devices/filesystem[source/@dir=\"$VMTMPDIR/virtfs\"][target/@dir='virtfs']" \ "/domain/devices/disk[@type='file'][@device='cdrom']"; do if [ -z "$TRANSIENT" ]; then virsh -c "$LIBVIRT_URI" --quiet detach-device --config "$VM_NAME" \ <(xmlstarlet select --template --copy-of "$xpath" <"$vmdef") elif [ ${OUTPUT+x} ]; then xmlstarlet edit --inplace --delete "$xpath" "$vmdef" fi done [ ! ${OUTPUT+x} ] || cp --no-preserve=mode "$vmdef" "$OUTPUT/$VM_NAME.xml" ) # wait until the VM terminates (there is actually a race condition here, # but the XML massaging above should be faster than any install) virsh -c "$LIBVIRT_URI" console "$VM_NAME" --safe >/dev/null ( if [ -f "$VMTMPDIR/virtfs/minion.pub" ]; then echo "Salt minion MD5 fingerprint:" # salt uses a bizarre scheme... grep -v '^-----.*-----$' "$VMTMPDIR/virtfs/minion.pub" \ | openssl dgst -md5 -c | sed '/.*=\s*/ {s//\t/;q}' fi echo echo "SSH hostkey fingerprints:" find "$VMTMPDIR/virtfs" -maxdepth 1 -type f -name 'ssh_host_*_key.pub' \ -execdir ssh-keygen -lf {} \; | sed 's/^/\t/' ) >&2 if [ ${OUTPUT+x} ]; then find "$VMTMPDIR/virtfs" -name '*.pub' -print0 \ | xargs -r0 cp --no-preserve=mode -t "$OUTPUT" fi