aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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});
+ }
+}