summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorHelmut Grohne <helmutg@debian.org>2023-01-15 08:54:06 +0000
committerHelmut Grohne <helmutg@debian.org>2023-01-15 08:54:06 +0000
commitb6aee3905cc52fa206cfbef3fe01d2c6388435a1 (patch)
tree86ab25a9c7434d62019359da0f996d2097aa8247 /bin
parent59c77e0c3422287651c6e270917bcfa04a6c17a6 (diff)
parentca2cbf6d84dcc96845fcc6521aebe3b5076cdb32 (diff)
downloaddebvm-b6aee3905cc52fa206cfbef3fe01d2c6388435a1.tar.gz
Merge branch 'helmut' into 'main'
reduce usage of DEBVER See merge request helmutg/debvm!22
Diffstat (limited to 'bin')
-rwxr-xr-xbin/debvm-create358
-rwxr-xr-xbin/debvm-run292
-rwxr-xr-xbin/debvm-waitssh178
3 files changed, 828 insertions, 0 deletions
diff --git a/bin/debvm-create b/bin/debvm-create
new file mode 100755
index 0000000..b34399b
--- /dev/null
+++ b/bin/debvm-create
@@ -0,0 +1,358 @@
+#!/bin/sh
+# Copyright 2022 Helmut Grohne <helmut@subdivi.de>
+# SPDX-License-Identifier: MIT
+
+# shellcheck disable=SC2016 # Intentional quoting technique
+
+: <<'POD2MAN'
+=head1 NAME
+
+debvm-create - Create a VM image for various Debian releases and architectures
+
+=head1 SYNOPSIS
+
+B<debvm-create> [B<-a> I<architecture>] [B<-h> I<hostname>] [B<-k> F<sshkey>] [B<-m> I<mirror>] [B<-o> F<output>] [B<-r> I<release>] [B<-s> <task>] [B<-z> I<size_in_GB>] [B<--> I<mmdebstrap options>]
+
+=head1 DESCRIPTION
+
+B<debvm-create> is essentially a thin wrapper around B<mmdebstrap> for creating a raw ext4 filesystem image for booting with B<debvm-run>.
+The purpose of these images primarily is testing the different releases and architectures without access to a physical machine of that architecture.
+Beyond essential packages, the image will contain B<apt>, an init system and a suitable kernel package.
+Notably absent is a bootloader and a partition table.
+In order to boot such an image, one is supposed to extract the kernel and initrd from the image and pass it to a suitable bootloader.
+No user account is created and root can login without specifying a password.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<-a> I<architecture>, B<--architecture>=I<architecture>
+
+Specify a Debian architecture name.
+By default, the native architecture is being used.
+A suitable kernel image is automatically selected and installed into the image.
+
+=item B<-h> I<hostname>, B<--hostname>=I<hostname>
+
+Set the hostname of the virtual machine.
+By default, the hostname is B<testvm>.
+
+=item B<-k> F<sshkey>, B<--sshkey>=F<sshkey>
+
+Install the given ssh public key file into the virtual machine image for the root user.
+This option also causes the ssh server to be installed.
+By default, no key or server is installed.
+To connect to the vm, pass a port number to B<debvm-run> with the B<-s> option.
+
+=item B<-m> I<mirror>, B<--mirror>=I<mirror>
+
+Specify the Debian mirror to be used for downloading packages and to be configured inside the virtual machine image.
+By default, L<http://deb.debian.org/debian> is being used.
+
+=item B<-o> F<output>, B<--output>=F<output>
+
+Specify the file name of the resulting virtual machine image.
+By default, it is written to F<rootfs.ext4>.
+
+=item B<-r> I<release>, B<--release>=I<release>
+
+Use the given Debian release.
+By default, B<unstable> is being used.
+
+=item B<-s> I<task>, B<--skip>=I<task>
+
+Skip a particular task or feature.
+The option may be specified multiple times or list multiple tasks to be skipped by separating them with a comma.
+By default, no tasks are skipped.
+The following tasks may be skipped.
+
+=over 4
+
+=item B<kernel>
+
+skips installing a linux kernel image.
+This can be useful to install a custom kernel or to install a different kernel variant than is selected by default.
+
+=item B<packagelists>
+
+reduces the package lists inside the image.
+The B<available> database for B<dpkg> is not created.
+The package lists used by B<apt> are deleted.
+This generally produces a smaller image, but you need to run B<apt update> before installing packages and B<dpkg --set-selections> does not work.
+
+=item B<systemdnetwork>
+
+skips installing B<libnss-resolve> as well as automatic network configuration via B<systemd-networkd>.
+
+=item B<usrmerge>
+
+By default B<debvm> adds a hook to enable merged-/usr without the B<usrmerge> package given a sufficiently recent Debian release.
+Without the hook, dependencies will pull the B<usrmerge> package as needed, which may result in a larger installation.
+
+=back
+
+=item B<-z> I<size_in_GB>, B<--size>=I<size_in_GB>
+
+Specify the minimum image size in giga bytes.
+The resulting image will be grown as a sparse file to this size if necessary.
+The default is 1 GB.
+
+=item B<--> I<mmdebstrap options>
+
+All options beyond a double dash are passed to B<mmdebstrap> before the suite, target and mirror specification.
+This can be used to provide additional hooks for image customization.
+You can also request additional packages to be installed into the image using B<mmdebstrap>'s B<--include> option.
+
+=back
+
+=head1 SEE ALSO
+
+ debvm-run(1) mmdebstrap(1)
+
+=cut
+POD2MAN
+
+set -u
+
+ARCHITECTURE=$(dpkg --print-architecture)
+IMAGE=rootfs.ext4
+INCLUDE_PACKAGES=init
+MIRROR="http://deb.debian.org/debian"
+SIZE=$((1024*1024*1024))
+SKIP=,
+SSHKEY=
+SUITE=unstable
+VMNAME=testvm
+
+SHARE_DIR="${0%/*}/../share"
+
+nth_arg() {
+ shift "$1"
+ printf "%s" "$1"
+}
+
+die() {
+ echo "$*" 1>&2
+ exit 1
+}
+usage() {
+ die "usage: $0 [-a architecture] [-h hostname] [-k sshkey] [-m mirror] [-o output] [-r release] [-s task] [-z size_in_GB] [-- mmdebstrap options]"
+}
+usage_error() {
+ echo "error: $*" 1>&2
+ usage
+}
+
+opt_architecture() {
+ ARCHITECTURE=$1
+}
+opt_hostname() {
+ VMNAME=$1
+}
+opt_mirror() {
+ MIRROR=$1
+}
+opt_skip() {
+ SKIP="$SKIP$1,"
+}
+opt_sshkey() {
+ SSHKEY=$1
+}
+opt_output() {
+ IMAGE=$1
+}
+opt_release() {
+ SUITE=$1
+}
+opt_size() {
+ SIZE=$(($1*1024*1024*1024))
+}
+
+while getopts :a:h:k:m:o:r:s:z:-: OPTCHAR; do
+ case "$OPTCHAR" in
+ a) opt_architecture "$OPTARG" ;;
+ h) opt_hostname "$OPTARG" ;;
+ k) opt_sshkey "$OPTARG" ;;
+ m) opt_mirror "$OPTARG" ;;
+ o) opt_output "$OPTARG" ;;
+ r) opt_release "$OPTARG" ;;
+ s) opt_skip "$OPTARG" ;;
+ z) opt_size "$OPTARG" ;;
+ -)
+ case "$OPTARG" in
+ help)
+ usage
+ ;;
+ architecture|hostname|mirror|output|release|size|skip|sshkey)
+ test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG"
+ "opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")"
+ OPTIND=$((OPTIND+1))
+ ;;
+ architecture=*|hostname=*|mirror=*|output=*|release=*|size=*|skip=*|sshkey=*)
+ "opt_${OPTARG%%=*}" "${OPTARG#*=}"
+ ;;
+ *)
+ usage_error "unrecognized option --$OPTARG"
+ ;;
+ esac
+ ;;
+ :)
+ usage_error "missing argument for -$OPTARG"
+ ;;
+ '?')
+ usage_error "unrecognized option -$OPTARG"
+ ;;
+ *)
+ die "internal error while parsing command options, please report a bug"
+ ;;
+ esac
+done
+shift "$((OPTIND - 1))"
+
+if test -n "$SSHKEY" && ! test -f "$SSHKEY"; then
+ die "error: ssh keyfile '$SSHKEY' not found"
+fi
+
+check_skip() {
+ case "$SKIP" in
+ *",$1,"*) return 0 ;;
+ *) return 1 ;;
+ esac
+}
+
+case "$SUITE" in
+ jessie)
+ DEBVER=8
+ ;;
+ stretch)
+ DEBVER=9
+ ;;
+ buster)
+ DEBVER=10
+ ;;
+ bullseye|stable)
+ DEBVER=11
+ ;;
+ bookworm|testing)
+ DEBVER=12
+ ;;
+ trixie)
+ DEBVER=13
+ ;;
+ forky)
+ DEBVER=14
+ ;;
+ sid|unstable)
+ DEBVER=999
+ ;;
+ *)
+ die "unrecognized Debian release: $SUITE"
+ ;;
+esac
+
+KERNEL_SUFFIX=-$ARCHITECTURE
+case "$ARCHITECTURE" in
+ amd64|arm64)
+ KERNEL_SUFFIX="-cloud-$ARCHITECTURE"
+ if test "$DEBVER" -le 9; then
+ KERNEL_SUFFIX="-$ARCHITECTURE"
+ fi
+ ;;
+ armhf)
+ KERNEL_SUFFIX=-armmp
+ ;;
+ i386)
+ KERNEL_SUFFIX=-686-pae
+ ;;
+ mips64el)
+ KERNEL_SUFFIX=-5kc-malta
+ ;;
+ mipsel)
+ KERNEL_SUFFIX=-4kc-malta
+ ;;
+ ppc64el)
+ KERNEL_SUFFIX=-powerpc64le
+ ;;
+esac
+
+if ! check_skip kernel; then
+ INCLUDE_PACKAGES="$INCLUDE_PACKAGES,linux-image$KERNEL_SUFFIX"
+fi
+
+if test -n "$SSHKEY"; then
+ INCLUDE_PACKAGES="$INCLUDE_PACKAGES,openssh-server"
+fi
+
+if ! check_skip systemdnetwork; then
+ # add a DNS resolver
+ INCLUDE_PACKAGES="$INCLUDE_PACKAGES,?exact-name(libnss-resolve)"
+ set -- "--customize-hook=$SHARE_DIR/customize-resolved.sh" "$@"
+fi
+
+# construct mmdebstrap options as $@:
+set -- \
+ --verbose \
+ --variant=apt \
+ --format=ext2 \
+ "--architecture=$ARCHITECTURE" \
+ "--include=$INCLUDE_PACKAGES" \
+ '--customize-hook=echo "LABEL=debvm / ext4 defaults 0 1" >"$1/etc/fstab"' \
+ "$@"
+
+
+# set up a hostname
+set -- \
+ "--customize-hook=echo $VMNAME >"'"$1/etc/hostname"' \
+ "--customize-hook=echo 127.0.0.1 localhost $VMNAME >"'"$1/etc/hosts"' \
+ "$@"
+
+# allow password-less root login
+set -- '--customize-hook=chroot "$1" passwd --delete root' "$@"
+
+if ! check_skip systemdnetwork; then
+ # dhcp on all network interfaces
+ set -- "--customize-hook=$SHARE_DIR/customize-networkd.sh" "$@"
+fi
+
+# add ssh key for root
+if test -n "$SSHKEY"; then
+ set -- \
+ '--customize-hook=mkdir -m700 -p "$1/root/.ssh"' \
+ "--customize-hook=upload $SSHKEY /root/.ssh/authorized_keys" \
+ "$@"
+fi
+
+if ! check_skip packagelists; then
+ set -- --skip=cleanup/apt/lists "$@"
+ set -- "--customize-hook=$SHARE_DIR/customize-dpkgavailable.sh" "$@"
+fi
+
+if test "$DEBVER" -le 8; then
+ # Use obsolete and expired keys.
+ set -- '--keyring=/usr/share/keyrings/debian-archive-removed-keys.gpg' "$@"
+ set -- --aptopt='Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"' "$@"
+ set -- --hook-dir=/usr/share/mmdebstrap/hooks/jessie-or-older "$@"
+fi
+
+if test "$DEBVER" -ge 12 && ! check_skip usrmerge; then
+ # Avoid the usrmerge package
+ set -- --hook-dir=/usr/share/mmdebstrap/hooks/merged-usr "$@"
+fi
+
+set -- "--customize-hook=$SHARE_DIR/customize-autologin.sh" "$@"
+
+# suite target mirror
+set -- "$@" "$SUITE" "$IMAGE" "deb $MIRROR $SUITE main"
+
+set -ex
+
+mmdebstrap "$@"
+
+IMAGESIZE=$(stat -c %s "$IMAGE")
+if test "$IMAGESIZE" -lt "$SIZE"; then
+ truncate -s "$SIZE" "$IMAGE"
+ /sbin/resize2fs "$IMAGE"
+fi
+/sbin/tune2fs -L debvm -i 0 -O extents,uninit_bg,dir_index,has_journal "$IMAGE"
+# Must fsck after tune2fs: https://ext4.wiki.kernel.org/index.php/UpgradeToExt4
+/sbin/fsck.ext4 -fDp "$IMAGE"
diff --git a/bin/debvm-run b/bin/debvm-run
new file mode 100755
index 0000000..6ed47c4
--- /dev/null
+++ b/bin/debvm-run
@@ -0,0 +1,292 @@
+#!/bin/sh
+# Copyright 2022 Helmut Grohne <helmut@subdivi.de>
+# SPDX-License-Identifier: MIT
+
+: <<'POD2MAN'
+=head1 NAME
+
+debvm-run - Run a VM image created by debvm-create
+
+=head1 SYNOPSIS
+
+B<debvm-run> [B<-g>] [B<-i> F<image>] [B<-s> I<sshport>] [B<--> I<qemu options>]
+
+=head1 DESCRIPTION
+
+B<debvm-run> is essentially a thin wrapper around B<qemu> for running a virtual machine image created by B<debvm-create> or something compatible.
+The virtual machine image is expected to be a raw ext4 image with file system label B<debvm>.
+The architecture of the machine is detected from the contained F</bin/true>.
+It must contain a symbolic link pointing to a kernel image at F</vmlinuz> or F</vmlinux> depending on the architecture and a symbolic link pointing to an initrd image at F</initrd.img>.
+Both are extracted and passed to B<qemu>.
+A net interface configured for user mode is added automatically.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<-g>, B<--graphical>
+
+By default, the option B<-nographic> is passed to B<qemu> and one interacts with the serial console of the machine.
+This configuration is skipped in the presence of this option.
+
+=item B<-i> F<image>, B<--image>=F<image>
+
+This option specifies the location of the virtual machine image file.
+By default F<rootfs.ext4> in the working directory is used.
+
+=item B<-s> I<sshport>, B<--sshport>=I<sshport>
+
+If given, B<qemu> is configured to pass connections to I<127.0.0.1:sshport> to port 22 of the virtual machine.
+You can connect to your virtual machine without updating your known hosts like this:
+
+ ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $sshport root@127.0.0.1
+
+=item B<--> I<qemu options>
+
+All options beyond a double dash are passed to B<qemu>.
+This can be used to configure additional hardware components.
+One possible use of this method is passing B<-snapshot> to avoid modifying the virtual machine image.
+
+=back
+
+=head1 EXAMPLES
+
+Run a virtual machine stored in the image F<rootfs.ext4> (the default) with
+local port 8022 routed to port 22 of the virtual machine. The B<-snapshot>
+argument is passed to QEMU and prevents any permanent changes to
+F<rootfs.ext4>, resulting in an ephemeral run.
+
+ debvm-run -s 8022 -i rootfs.ext4 -- -snapshot
+
+=head1 FAQ
+
+=over 8
+
+=item The debvm-run console renders wrong.
+
+Get C<echo $LINES $COLUMNS> from an other terminal and run C<stty rows $LINES cols $COLUMNS> in the console or use ssh.
+Another option is to run C<eval $(resize)>, which is available from the B<xterm> package.
+Also set C<$TERM> to the outside value.
+
+=item How can I kill debvm-run?
+
+The wrapped B<qemu> can be terminated by pressing Ctrl-a x.
+Refer to the B<qemu> manual page for more escape sequences.
+
+=back
+
+=head1 LIMITATIONS
+
+Due to the way kernel and bootloader are being extracted before running B<qemu>, one cannot upgrade a kernel and then just reboot.
+Attempting to do so, will still use the old kernel.
+Instead, B<qemu> must be terminated and B<debvm-run> should be launched again to pick up the new kernel.
+In order to avoid accidental reboots, one may pass B<-no-reboot> to B<qemu>.
+
+=head1 SEE ALSO
+
+ debvm-create(1) qemu(1)
+
+=cut
+POD2MAN
+
+set -u
+
+IMAGE=rootfs.ext4
+SSHPORT=
+GRAPHICAL=
+
+nth_arg() {
+ shift "$1"
+ printf "%s" "$1"
+}
+
+die() {
+ echo "$*" 1>&2
+ exit 1
+}
+usage() {
+ die "usage: $0 [-g] [-i image] [-s sshport] [-- qemu options]"
+}
+usage_error() {
+ echo "error: $*" 1>&2
+ usage
+}
+
+opt_graphical() {
+ GRAPHICAL=1
+}
+opt_image() {
+ IMAGE=$1
+}
+opt_sshport() {
+ SSHPORT=$1
+}
+
+while getopts :gi:s:-: OPTCHAR; do
+ case "$OPTCHAR" in
+ g) opt_graphical ;;
+ i) opt_image "$OPTARG" ;;
+ s) opt_sshport "$OPTARG" ;;
+ -)
+ case "$OPTARG" in
+ help)
+ usage
+ ;;
+ graphical|image|sshport)
+ test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG"
+ "opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")"
+ OPTIND=$((OPTIND+1))
+ ;;
+ image=*|sshport=*)
+ "opt_${OPTARG%%=*}" "${OPTARG#*=}"
+ ;;
+ *)
+ usage_error "unrecognized option --$OPTARG"
+ ;;
+ esac
+ ;;
+ :)
+ usage_error "missing argument for -$OPTARG"
+ ;;
+ '?')
+ usage_error "unrecognized option -$OPTARG"
+ ;;
+ *)
+ die "internal error while parsing command options, please report a bug"
+ ;;
+ esac
+done
+shift "$((OPTIND - 1))"
+
+test -f "$IMAGE" || die "image '$IMAGE' not found"
+test -s "$IMAGE" || die "image '$IMAGE' is empty"
+
+if ! printf '\123\357' | cmp --bytes=2 "$IMAGE" - 1080; then
+ die "image '$IMAGE' is not in ext4 format"
+fi
+
+if ! printf 'debvm\000' | cmp --bytes=6 "$IMAGE" - 1144; then
+ die "image '$IMAGE' was not created by debvm-create (wrong disk label)"
+fi
+
+cleanup() {
+ set +x
+ test -n "$KERNELTMP" && rm -f "$KERNELTMP"
+ test -n "$INITRDTMP" && rm -f "$INITRDTMP"
+}
+
+trap cleanup EXIT INT TERM QUIT
+
+KERNELTMP=$(mktemp)
+INITRDTMP=$(mktemp)
+
+ARCHITECTURE=$(dpkg --print-architecture)
+VMARCH=$ARCHITECTURE
+if command -v elf-arch >/dev/null 2>&1; then
+ /sbin/debugfs "$IMAGE" -R "cat /bin/true" > "$KERNELTMP"
+ VMARCH=$(elf-arch "$KERNELTMP")
+fi
+case "$VMARCH" in
+ mips*|ppc64el)
+ KERNELLINK=vmlinux
+ ;;
+ *)
+ KERNELLINK=vmlinuz
+ ;;
+esac
+
+KERNELNAME=$(/sbin/debugfs "$IMAGE" -R "stat $KERNELLINK" | sed 's/Fast link dest: "\(.*\)"/\1/;t;d')
+INITRDNAME=$(/sbin/debugfs "$IMAGE" -R "stat initrd.img" | sed 's/Fast link dest: "\(.*\)"/\1/;t;d')
+
+test -n "$KERNELNAME" || die "failed to discover kernel image"
+test -n "$INITRDNAME" || die "failed to discover initrd image"
+
+KERNEL_CMDLINE="root=LABEL=debvm rw"
+NETDEV="user,id=net0"
+
+set -- \
+ -no-user-config \
+ -name "debvm-run $IMAGE" \
+ -m 1G \
+ -kernel "$KERNELTMP" \
+ -initrd "$INITRDTMP" \
+ -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0 \
+ -drive "media=disk,format=raw,discard=unmap,file=$IMAGE,if=virtio,cache=unsafe" \
+ -device "virtio-net-pci,netdev=net0" \
+ "$@"
+
+MAX_SMP=
+if test "$ARCHITECTURE" = "$VMARCH"; then
+ QEMU=kvm
+ # While kvm will fall back gracefully, the following options can only
+ # be passed when kvm really is available.
+ if test -w /dev/kvm; then
+ set -- -enable-kvm -cpu host "$@"
+ fi
+ case "$VMARCH" in
+ arm64)
+ set -- -machine type=virt,gic-version=host "$@"
+ ;;
+ esac
+else
+ QEMU="qemu-system-$VMARCH"
+ case "$VMARCH" in
+ arm64)
+ QEMU=qemu-system-aarch64
+ set -- -machine virt -cpu max "$@"
+ ;;
+ arm|armel|armhf)
+ QEMU=qemu-system-arm
+ set -- -machine virt -cpu max "$@"
+ ;;
+ ppc64el)
+ QEMU=qemu-system-ppc64
+ ;;
+ mips64el)
+ MAX_SMP=1
+ set -- -cpu 5KEc "$@"
+ ;;
+ mipsel)
+ MAX_SMP=1
+ ;;
+ riscv64)
+ set -- -machine virt "$@"
+ ;;
+ esac
+fi
+if test -z "$MAX_SMP" || test "$MAX_SMP" -gt 1; then
+ NPROC=$(nproc)
+ test -n "$MAX_SMP" && test "$NPROC" -gt "$MAX_SMP" && NPROC=$MAX_SMP
+ set -- -smp "$NPROC" "$@"
+fi
+
+if test -z "$GRAPHICAL"; then
+ set -- -nographic "$@"
+ case "$VMARCH" in
+ amd64|i386)
+ KERNEL_CMDLINE="$KERNEL_CMDLINE console=ttyS0"
+ ;;
+ esac
+ if test -t 0 && test -t 1 && test -n "$TERM"; then
+ KERNEL_CMDLINE="$KERNEL_CMDLINE TERM=$TERM"
+ fi
+fi
+
+if test -n "$SSHPORT"; then
+ NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:$SSHPORT-:22"
+fi
+DNSSEARCH=$(dnsdomainname)
+if test -n "$DNSSEARCH"; then
+ NETDEV="$NETDEV,domainname=$DNSSEARCH"
+fi
+set -- \
+ -append "$KERNEL_CMDLINE" \
+ -netdev "$NETDEV" \
+ "$@"
+
+set -ex
+
+/sbin/debugfs "$IMAGE" -R "cat $KERNELNAME" > "$KERNELTMP"
+/sbin/debugfs "$IMAGE" -R "cat $INITRDNAME" > "$INITRDTMP"
+
+"$QEMU" "$@"
diff --git a/bin/debvm-waitssh b/bin/debvm-waitssh
new file mode 100755
index 0000000..82eb14d
--- /dev/null
+++ b/bin/debvm-waitssh
@@ -0,0 +1,178 @@
+#!/bin/sh
+# Copyright 2023 Helmut Grohne <helmut@subdivi.de>
+# SPDX-License-Identifier: MIT
+
+: <<'POD2MAN'
+=head1 NAME
+
+debvm-waitssh - Wait for a ssh server to be reachable
+
+=head1 SYNOPSIS
+
+B<debvm-waitssh> [B<-q>] [B<-t> I<timeout>] [I<hostname>:]I<port>
+
+=head1 DESCRIPTION
+
+B<debvm-waitssh> can be used to wait for a virtual machine with exposed ssh port to be reachable on that port.
+If no hostname is given, B<127.0.0.1> is assumed. No authentication is attempted by B<debvm-waitssh>, so neither
+a username nor a key have to be supplied.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<-t> I<timeout>, B<--timeout>=I<timeout>
+
+Set the maximum duration for waiting in seconds.
+Defaults to one minute.
+
+=item B<-q>, B<--quiet>
+
+Be quiet.
+Do not output a message when the timeout has been reached without success.
+
+=back
+
+=head1 EXIT VALUES
+
+=over 8
+
+=item B<0>
+
+The server is reachable.
+
+=item B<1>
+
+A timeout was reached before the server answered.
+
+=item B<2>
+
+Usage error.
+
+=back
+
+=head1 SEE ALSO
+
+ debvm-run(1)
+
+=cut
+POD2MAN
+
+set -u
+
+TOTALTIMEOUT=60
+SCANTIMEOUT=10
+SCANDELAY=1
+VERBOSITY=1
+
+nth_arg() {
+ shift "$1"
+ printf "%s" "$1"
+}
+
+die() {
+ echo "$*" >&2
+ exit 2
+}
+usage() {
+ die "usage: $0 [-q] [-t <timeout>] [<host>:]<port>"
+}
+usage_error() {
+ echo "error: $*" >&2
+ usage
+}
+
+opt_help() {
+ # shellcheck disable=SC2317 # not dead, called as "opt_$OPTARG"
+ usage
+}
+opt_quiet() {
+ VERBOSITY=0
+}
+opt_timeout() {
+ TOTALTIMEOUT=$1
+}
+
+while getopts :qt:-: OPTCHAR; do
+ case "$OPTCHAR" in
+ q) opt_quiet ;;
+ t) opt_timeout "$OPTARG" ;;
+ -)
+ case "$OPTARG" in
+ help|quiet)
+ "opt_$OPTARG"
+ ;;
+ timeout)
+ test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG"
+ "opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")"
+ OPTIND=$((OPTIND+1))
+ ;;
+ timeout=)
+ "opt_${OPTARG%%=*}" "${OPTARG#*=}"
+ ;;
+ *)
+ usage_error "unrecognized option --$OPTARG"
+ ;;
+ esac
+ ;;
+ :)
+ usage_error "missing argument for -$OPTARG"
+ ;;
+ '?')
+ usage_error "unrecognized option -$OPTARG"
+ ;;
+ *)
+ die "internal error while parsing command options, please report a bug"
+ ;;
+ esac
+done
+shift "$((OPTIND - 1))"
+
+test "$#" = 1 || usage
+
+case "$1" in
+ "")
+ usage
+ ;;
+ *:*)
+ HOST=${1%:*}
+ PORT=${1##*:}
+ ;;
+ *)
+ HOST=127.0.0.1
+ PORT=$1
+ ;;
+esac
+
+case "$HOST" in *@*)
+ die "$0: hostname '$HOST' must not contain the '@' character. No username is required."
+;; esac
+
+# Guard against strings containing anything but digits, strings starting with
+# zero and empty strings as the port number.
+#
+# We cannot use [!0-9] because that matches on any character (or possibly
+# multi-character collation element) that sorts in between 0 and 9.
+case "$PORT" in *[!0123456789]*|0?*|""|??????*)
+ die "$0: port '$PORT' is not an integer between 1 and 65535"
+;; esac
+if test "$PORT" -lt 1 -o "$PORT" -gt 65535; then
+ die "$0: port '$PORT' is not an integer between 1 and 65535"
+fi
+
+now=$(date +%s)
+deadline=$((now + TOTALTIMEOUT))
+while test "$now" -lt "$deadline"; do
+ start=$now
+ ssh-keyscan -t rsa -T "$SCANTIMEOUT" -p "$PORT" "$HOST" >/dev/null 2>&1 && exit 0
+ now=$(date +%s)
+ if test "$((now - start))" -lt "$SCANTIMEOUT"; then
+ sleep "$SCANDELAY"
+ now=$(date +%s)
+ fi
+done
+if [ "$VERBOSITY" -ge 1 ]; then
+ echo "$0: timeout reached trying to contact $HOST:$PORT after waiting $TOTALTIMEOUT seconds." >&2
+fi
+exit 1
+