aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@libreoffice.org>2016-10-23 00:34:05 +0200
committerGuilhem Moulin <guilhem@libreoffice.org>2016-10-23 00:38:45 +0200
commit83bf907908ac713d334bf3ed4424989c86be9294 (patch)
tree91580d47239b3597e621f4419faa743919ff771b
parentcbf0cecd44a6b422e208f3043f2ceaf7fd0a25a9 (diff)
Use the QEMU Guest Agent to retrive public key material.
Unlike filesystem passthrough (9p VirtFS), this allows us to create guests remotely without using sudo privileges. (We can't do this with VirtFS currently due to lack of relabelling, and the kernel won't let us `chgrp libvirt-qemu` without sudoing.)
-rw-r--r--README3
-rwxr-xr-xtdf-postinst-udeb/finish-install.d/07tdf-postinst24
-rwxr-xr-xtdfvm-install136
-rwxr-xr-xvirsh-ga92
4 files changed, 198 insertions, 57 deletions
diff --git a/README b/README
index 75c585f..e3f7ac5 100644
--- a/README
+++ b/README
@@ -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});
+ }
+}