diff options
-rw-r--r-- | README | 3 | ||||
-rwxr-xr-x | tdf-postinst-udeb/finish-install.d/07tdf-postinst | 24 | ||||
-rwxr-xr-x | tdfvm-install | 136 | ||||
-rwxr-xr-x | virsh-ga | 92 |
4 files changed, 198 insertions, 57 deletions
@@ -14,6 +14,7 @@ packages: virtinst xmlstarlet xorriso + libjson-perl Download Debian stable's netinst ISO image and verify its integrity: @@ -29,7 +30,7 @@ Download Debian stable's netinst ISO image and verify its integrity: Create a new transient virtual machine: - ~$ sudo TMPDIR=/run/shm ./tdfvm-install \ + ~$ ./tdfvm-install \ --iso=./dist/debian-8.6.0-amd64-netinst.iso \ -u$USER -p --authorized-keys=$HOME/.ssh/authorized_keys \ -o ./vm170 \ diff --git a/tdf-postinst-udeb/finish-install.d/07tdf-postinst b/tdf-postinst-udeb/finish-install.d/07tdf-postinst index 0ee458c..428b018 100755 --- a/tdf-postinst-udeb/finish-install.d/07tdf-postinst +++ b/tdf-postinst-udeb/finish-install.d/07tdf-postinst @@ -3,12 +3,7 @@ set -e . /usr/share/debconf/confmodule || true -modprobe -va -d/target virtio-rng -modprobe -va -d/target 9pnet_virtio 9p - -virtfs="$(mktemp -d)" -mount -t 9p -o trans=virtio,version=9p2000.L virtfs "$virtfs" || true -trap 'umount "$virtfs"; rmdir "$virtfs"' EXIT TERM INT +modprobe -v -d/target virtio-rng ####################################################################### @@ -20,9 +15,6 @@ if [ -d /target/etc/ssh ]; then -delete in-target ssh-keygen -b 4096 -t rsa -N '' -C /etc/ssh/ssh_host_rsa_key -f /etc/ssh/ssh_host_rsa_key in-target ssh-keygen -t ed25519 -N '' -C /etc/ssh/ssh_host_ed25519_key -f /etc/ssh/ssh_host_ed25519_key - for pk in $(find /target/etc/ssh -maxdepth 1 -type f -name "ssh_host_*_key.pub"); do - cp -f "$pk" "$virtfs" - done cat >/target/etc/ssh/sshd_config <<- EOF # What ports, IPs and protocols we listen for @@ -111,6 +103,16 @@ if [ -d /target/etc/salt ]; then echo "master_finger: '$RET'" >>/target/etc/salt/minion.d/9999user.conf fi echo "id: $(hostname).documentfoundation.org" >>/target/etc/salt/minion.d/9999user.conf - - cp /target/etc/salt/pki/minion/minion.pub "$virtfs" fi + + +####################################################################### +# Start the QEMU Guest Agent and wait until the host tells us to continue + +modprobe -v -d/target virtio-console +in-target qemu-ga --daemonize --pidfile=/var/run/qemu-ga.pid +while :; do + [ -f /target/etc/tdf-install-continue ] && break + sleep 1 +done +kill `cat /var/run/qemu-ga.pid` diff --git a/tdfvm-install b/tdfvm-install index 052e173..d0138c6 100755 --- a/tdfvm-install +++ b/tdfvm-install @@ -13,15 +13,60 @@ usage() { 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 -unset OUTPUT CPU="host" unset VCPUS @@ -34,13 +79,14 @@ TRANSIENT=n 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 + --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") @@ -69,6 +115,7 @@ while [ $# -gt 0 ]; do -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=}";; @@ -108,7 +155,6 @@ 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='*' @@ -116,7 +162,7 @@ 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 @@ -138,19 +184,14 @@ 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" - +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" @@ -211,7 +252,9 @@ install -o libvirt-qemu -m 0700 --directory "$VMTMPDIR/virtfs" -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" ./ + ./ \ + | if [ ${SSH_ARGS+x} ]; then run "cat >$CDROM"; else cat >"$CDROM"; fi + run chmod g+r "$CDROM" ) @@ -226,6 +269,7 @@ if [ "$FORCE" = y ]; then 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" \ @@ -233,60 +277,62 @@ virt-install -q --connect "$LIBVIRT_URI" \ --virt-type "kvm" \ --cpu "$CPU" ${VCPUS+--vcpus "$VCPUS"} \ --memory "$MEMORY" --memballoon "virtio" \ - --disk "path=$VMTMPDIR/install.iso,device=cdrom,bus=sata,readonly=on" \ + --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=no" \ - --filesystem "source=$VMTMPDIR/virtfs,target=virtfs" \ + --security "type=static,model=dac,label=libvirt-qemu:libvirt-qemu,relabel=yes" \ --network "$NETWORK" \ --graphics "$GRAPHICS" \ --controller "virtio-serial" \ --noautoconsole \ - --print-xml 1 >"$VMTMPDIR/domain.xml" + --print-xml 1 >"$VMDIR/$VM_NAME.xml" -virsh -c "$LIBVIRT_URI" create "$VMTMPDIR/domain.xml" +virsh -c "$LIBVIRT_URI" create "$VMDIR/$VM_NAME.xml" # never boot again on CDROM, detach unnecessary devices and remove # unnecessary controllers xmlstarlet edit --inplace \ --delete "/domain/os/boot[@dev='cdrom']" \ - --delete "/domain/devices/filesystem[source/@dir='$VMTMPDIR/virtfs'][target/@dir='virtfs']" \ --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']" \ - "$VMTMPDIR/domain.xml" + "$VMDIR/$VM_NAME.xml" + +[ "$TRANSIENT" = y ] || virsh -c "$LIBVIRT_URI" define "$VMDIR/$VM_NAME.xml" -[ "$TRANSIENT" = y ] || virsh -c "$LIBVIRT_URI" define "$VMTMPDIR/domain.xml" +# wait until the guest starts its QEMU Agent at the end of the installation +while :; do + ./virsh-ga -c "$LIBVIRT_URI" "$VM_NAME" ping && break || rv=$? + [ $rv -eq 128 ] || exit $rv + sleep 1 +done + +# 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 + ./virsh-ga -c "$LIBVIRT_URI" "$VM_NAME" cat "$path" >"$VMDIR/${path##*/}" +done -# 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 +./virsh-ga -c "$LIBVIRT_URI" "$VM_NAME" touch /etc/tdf-install-continue +virsh -c "$LIBVIRT_URI" console "$VM_NAME" --safe >/dev/null # wait until shutdown ( - 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 "Salt minion MD5 fingerprint:" + # salt uses a bizarre scheme... + grep -v '^-----.*-----$' "$VMDIR/minion.pub" \ + | openssl dgst -md5 -c | sed '/.*=\s*/ {s//\t/;q}' echo - sshkeys=$(find "$VMTMPDIR/virtfs" -maxdepth 1 -type f -name 'ssh_host_*_key.pub') - if [ "$sshkeys" ]; then - echo "SSH hostkey fingerprints:" - for pk in $sshkeys; do - ssh-keygen -lf "$pk" - done | sed 's/^/\t/' - fi + 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 - cp --no-preserve=mode "$VMTMPDIR/domain.xml" "$OUTPUT/$VM_NAME.xml" - find "$VMTMPDIR/virtfs" -name '*.pub' -print0 \ - | xargs -r0 cp --no-preserve=mode -t "$OUTPUT" printf "\nExported files:\n" >&2 find "$OUTPUT" -type f -printf '\t%p\n' >&2 fi diff --git a/virsh-ga b/virsh-ga new file mode 100755 index 0000000..2b26c82 --- /dev/null +++ b/virsh-ga @@ -0,0 +1,92 @@ +#!/usr/bin/perl + +# Send command to a guest's QEMU Agent to access its file. +# Usage: virsh-ga [--connect=URI] DOMAIN COMMAND [ARGUMENT..] +# +# Return value: +# - 0 on success, +# - 128 if the QEMU agent is not connected, and +# - 1 on error. + + +# Clean up PATH +$ENV{PATH} = join ':', qw{/usr/bin /bin}; +delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; + +use warnings; +use strict; + +use Symbol 'gensym'; +use IO::Select (); +use IPC::Open3 'open3'; +use JSON (); +use MIME::Base64 'decode_base64'; +use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version auto_help/; + +my %OPTIONS; +GetOptions(\%OPTIONS, qw/connect|c=s/) or die; +my $DOMAIN = shift // die; +my $COMMAND = shift // die; + +my @VIRSH = ('virsh'); +push @VIRSH, '--connect='.$OPTIONS{connect} if defined $OPTIONS{connect}; + +sub ga_send(@) { + my @command = (@VIRSH, 'qemu-agent-command', '--block', $DOMAIN, JSON->new->encode({ @_ })); + my $pid = open3(my $in, my $out, my $err = gensym(), @command); + my $sel = IO::Select::->new($out, $err); + my %str; + while (my @fhs = $sel->can_read) { + foreach my $fh (@fhs) { + my $x = $fh->getline; + if (defined $x) { + $str{$fh} .= $x; + } else { + $sel->remove($fh); + } + } + } + waitpid $pid, 0; + close $_ foreach ($in, $out, $err); + if ($? == 0) { + my $h = JSON->new->utf8->allow_nonref->decode($str{$out}); + return $h->{return}; + } + elsif ($str{$err} eq "error: Guest agent is not responding: QEMU guest agent is not connected\n") { + exit 128; + } else { + die $str{$err}; + } +} + +# the JSON domain definition can be found in QEMU's qga/qapi-schema.json +if ($COMMAND eq 'info') { + ga_send(execute => 'guest-info'); +} +elsif ($COMMAND eq 'ping') { + ga_send(execute => 'guest-ping'); +} +elsif ($COMMAND eq 'cat') { + die unless @ARGV; + foreach my $path (@ARGV) { + my $fh = ga_send(execute => 'guest-file-open', arguments => {path => $path, mode => 'r'}); + + my ($b64, $eof); + do { + # keep reading until we reach EOF + my $r = ga_send(execute => 'guest-file-read', arguments => {handle => $fh}); + $b64 .= $r->{'buf-b64'}; + $eof = $r->{eof}; + } until ($eof); + print decode_base64($b64); + + ga_send(execute => 'guest-file-close', arguments => {handle => $fh}); + } +} +elsif ($COMMAND eq 'touch') { + die unless @ARGV; + foreach my $path (@ARGV) { + my $fh = ga_send(execute => 'guest-file-open', arguments => {path => $path, mode => 'a'}); + ga_send(execute => 'guest-file-close', arguments => {handle => $fh}); + } +} |