Darryl L. Pierce
2009-Mar-31 16:35 UTC
[Ovirt-devel] [PATCH] Introduces automated testing.
This patch creates a virtual bridge, then runs an instance of dnsmasq on it. A virtual machine is then launched and PXE boots the node. Two tests are run in this mannner: one which performs a stateless boot of the node via PXE. The second performs a stateful install of the node via PXE. Signed-off-by: Darryl L. Pierce <dpierce at redhat.com> --- autobuild.sh | 16 ++ autotest.sh | 567 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 583 insertions(+), 0 deletions(-) create mode 100755 autotest.sh diff --git a/autobuild.sh b/autobuild.sh index e10ec6a..7ca8ca0 100755 --- a/autobuild.sh +++ b/autobuild.sh @@ -18,6 +18,14 @@ # MA 02110-1301, USA. A copy of the GNU General Public License is # also available at http://www.gnu.org/copyleft/gpl.html. +ME=$(basename "$0") +warn() { printf '%s: %s\n' "$ME" "$*" >&2; } +die() { warn "$*"; exit 1; } + +# trap '__st=$?; stop_log; exit $__st' 0 +trap '__st=$?; exit $__st' 0 +trap 'exit $?' 1 2 13 15 + echo "Running oVirt node image Autobuild" set -e @@ -45,3 +53,11 @@ if [ -e /usr/bin/rpmbuild ]; then --define "ovirt_local_repo file://$AUTOBUILD_PACKAGE_ROOT/rpm/RPMS" \ -ta --clean *.tar.gz fi + +#if [ -x ./autotest.sh ]; then +# echo "Testing the build." +# ./autotest.sh $1 +#else +# echo "NO AUTOTEST FOUND!" +# exit 1 +#fi diff --git a/autotest.sh b/autotest.sh new file mode 100755 index 0000000..0dbe934 --- /dev/null +++ b/autotest.sh @@ -0,0 +1,567 @@ +#!/bin/sh +# +# oVirt node image autotest script +# +# Copyright (C) 2009 Red Hat, Inc. +# Written by Darryl L. Pierce <dpierce at redhat.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. A copy of the GNU General Public License is +# also available at http://www.gnu.org/copyleft/gpl.html. + +# To include autotesting on the build system, you need to insert the +# following snippet *BEFORE* the text that reads "Output Stages": +# ---8<[begin]--- +# # Integration test +# { +# name = integration +# label = Test group +# module = Test::AutoBuild::Stage::Test +# # Don't abort entire cycle if the module test fails +# critical = 0 +# } +# ---8<[end]--- +# +# This will, for each module whose autobuild.sh is run, to have a matching +# autotest.sh to run as well. +# +# To run the test locally, all that's needed is for an ISO file named ovirt-node-image.iso +# be present in the local directory. This will then be put through its paces with test +# results being send to stdout. + +ME=$(basename "$0") +warn() { printf '%s: %s\n' "$ME" "$*" >&2; } +die() { warn "$*"; exit 1; } + +# trap '__st=$?; stop_log; exit $__st' 0 +trap '__st=$?; cleanup; exit $__st' 0 +trap 'cleanup; exit $?' 1 2 13 15 + +test -n "$1" && RESULTS=$1 || RESULTS=autotest.log + +echo "Running oVirt node image Autotest" + +set -e +# set -v + +OVIRT_NODE_IMAGE_ISO=$PWD/ovirt-node-image.iso + +if [ ! -f $OVIRT_NODE_IMAGE_ISO ]; then + die "Missing ovirt-node-image.iso file!" +fi + +log () { + local text="`date` $*" + printf "${text}\n" + # sudo bash -c "printf \"[$$] ${text}\n\" >> ${RESULTS}" +} + +cleanup () { + destroy_node + stop_dnsmasq + destroy_test_iface +} + +# Creates a HD disk file. +# $1 - filename for disk file +# $2 - size +create_hard_disk () { + local filename=$1 + local size=$2 + + sudo qemu-img create -f raw $filename $size + sudo chcon -t virt_image_t $filename +} + +# Creates the XML for a virtual machine. +# $1 - the file to write the xml +# $2 - the node name +# $3 - memory size (in bytes) +# $4 - the local hard disk (if blank then no disk is used) +# $5 - the cdrom disk (if blank then no cdrom is used) +# $6 - the network bridge (if blank then 'default' is used) +# $7 - optional arguments +define_node () { + local filename=$1 + local nodename=$2 + local memory=$3 + local harddrive=$4 + local cddrive=$5 + local bridge=$6 + local options=$7 + local result="" + + # flexible options + # define defaults, then allow the caller to override them as needed + local arch=$(uname -i) + local emulator=$(which qemu-kvm) + local serial="true" + local vncport="-1" + local bootdev='hd' + + # if a cdrom was defined, then assume it's the boot device + if [ -n "$cddrive" ]; then bootdev='cdrom'; fi + + if [ -n "$options" ]; then eval "$options"; fi + + result="<domain type='kvm'>\n<name>${nodename}</name>\n<memory>${memory}</memory>\n <vcpu>1</vcpu>" + + # begin the os section + # inject the boot device + result="${result}\n<os>\n<type arch='${arch}' machine='pc'>hvm</type>" + result="${result}\n<boot dev='${bootdev}' />" + result="${result}\n</os>" + + # virtual machine features + result="${result}\n<features>" + result="${result}\n<acpi />" + if [ -z "${noapic}" ]; then result="${result}\n<apic />"; fi + result="${result}\n<pae /></features>" + result="${result}\n<clock offset='utc' />" + result="${result}\n<on_poweroff>destroy</on_poweroff>" + result="${result}\n<on_reboot>restart</on_reboot>" + result="${result}\n<on_crash>restart</on_crash>" + + # add devices + result="${result}\n<devices>" + result="${result}\n<emulator>${emulator}</emulator>" + # inject the hard disk if defined + if [ -n "$harddrive" ]; then + result="${result}\n<disk type='file' device='disk'>" + result="${result}\n<source file='$harddrive' />" + result="${result}\n<target dev='vda' bus='virtio' />" + result="${result}\n</disk>" + fi + # inject the cdrom drive if defined + if [ -n "$cddrive" ]; then + result="${result}\n<disk type='file' device='cdrom'>" + result="${result}\n<source file='${cddrive}' />" + result="${result}\n<target dev='hdc' bus='ide' />" + result="${result}\n</disk>" + fi + # inject the bridge network + result="${result}\n<interface type='network'>" + result="${result}\n<source network='${bridge}' />" + result="${result}\n</interface>" + # inject the serial port + if [ -n "$serial" ]; then + result="${result}\n<serial type='pty' />" + fi + # inject the vnc port + if [ -n "$vncport" ]; then + result="${result}\n<console type='pty' />" + result="${result}\n<graphics type='vnc' port='${vncport}' autoport='yes' keyman='en-us' />" + fi + # finish the device section + result="${result}\n</devices>" + + result="${result}\n</domain>" + + log "Saving node definition to file: ${filename}" + sudo printf "$result" > $filename + + # now define the vm + sudo virsh define $filename + NODENAME=$nodename + log "Defined VM: name=${NODENAME}" + + if [ $? != 0 ]; then die "Unable to define virtual machine: $nodename"; fi +} + +# Returns the mac address for the given node. +# $1 - the node name +# $2 - the variable name to set +get_mac_address () { + local nodename=$1 + local varname=$2 + + if [ -z "$nodename" ]; then die "Cannot get mac address for node with a name"; fi + + address=$(sudo virsh dumpxml $nodename|awk '/<mac address/ { + match($0,"mac address='"'"'(.*)'"'"'",data); print data[1]}') + + if [ -z "$varname" ]; then die "Cannot set unnamed varilable"; fi + eval $varname="$address" +} + +# Starts the named node. +# $1 - the node name +start_node () { + local nodename=$1 + + if [ -z "$nodename" ]; then die "Cannot start node without a name"; fi + + sudo virsh start $nodename +} + +# Destroys any existing instance of the given node. +# $1 - the node name +destroy_node () { + local nodename=$1 + + if [ -z "${nodename}" ]; then + nodename=$NODENAME + fi + + if [ -n "${nodename}" ]; then + log "Destroying VM: ${nodename}" + check=$(sudo virsh list --all) + if [[ "${check}" =~ "${nodename}" ]]; then + if [[ "${check}" =~ running ]]; then + sudo virsh destroy $nodename + fi + sudo virsh undefine $nodename + fi + fi +} + +# PXE boots a node. +# $1 - the ISO file +# $2 - the working directory +# $3 - kernel arguments; if present then they replace all default flags +setup_pxeboot () { + local isofile=$1 + local workdir=$2 + local kernelargs=$3 + local pxedefault=$workdir/tftpboot/pxelinux.cfg/default + + (cd $workdir && sudo livecd-iso-to-pxeboot $isofile) + sudo chmod -R 777 $workdir + + # set default kernel arguments if none were provided + # the defaults boot in standalone mode + if [ -z "$kernelargs" ]; then + kernelargs="ovirt_standalone" + fi + + local definition="DEFAULT pxeboot\nTIMEOUT 20\nPROMPT 0\nLABEL pxeboot\n KERNEL vmlinuz0\n IPAPPEND 2\n APPEND rootflags=loop BOOTIF=link|eth*|<MAC> initrd=initrd0.img root=/ovirt-node-image.iso rootfstype=auto console=ttyS0,115200n8 $kernelargs\n" + + sudo bash -c "printf \"${definition}\" > $pxedefault" +} + +# Launches the node as a virtual machine. +# $1 - the node name +# $2 - the ISO filename +# $3 - the hard disk file +# $4 - the memory size (in MB) +# $5 - the network bridge to use +# $6 - kernel arguments +# $7 - verification method +pxeboot_node_vm () { + local nodename=$1 + local isofile=$2 + local diskfile=$3 + local memsize=$4 + local bridge=$5 + local kernel_args=$6 + local verify_method=$7 + local xmlfile=$(mktemp) + local tftproot=$(mktemp -d) + local node_mac_address="" + local return_code=0 + + destroy_node $nodename + + log "Beginning pxeboot for $nodename" + # setup the dnsmasq instance with the iso setup + setup_pxeboot "$isofile" "$tftproot" "$kernel_args" + create_test_iface $bridge + define_node $xmlfile $nodename $memsize "$diskfile" "" $bridge "local bootdev='network'; local noapic='yes'" + get_mac_address $nodename "node_mac_address" + start_dnsmasq $bridge $tftproot $node_mac_address + start_node $nodename + if [ -n "$verify_method" ]; then + eval $verify_method + return_code=$? + fi + destroy_node $nodename + stop_dnsmasq + destroy_test_iface $bridge + log "Finished pxeboot for $nodename (RC=${return_code})" + + if [ $return_code != 0 ]; then + log "Test ended in failure" + fi + + test $return_code == 0 && return 0 || exit 1 +} + +# Launches the node as a virtual machine with a CDROM. +# $1 - the node name +# $2 - the ISO filename +# $3 - the disk file to use +# $4 - the memory size (in MB) +# $5 - the network bridge +cdrom_boot_node_vm () { + local nodename=$1 + local isofile=$2 + local diskfile=$3 + local memsize=$4 + local bridge=$5 + local xmlfile=$(mktemp) + + destroy_node $nodename + + log "Beginning cdrom boot for $nodename" + create_test_iface $bridge "yes" + define_node $xmlfile $nodename $memsize "$diskfile" "$isofile" $bridge + start_node $nodename + # TODO make sure the node's booted + sleep 300 + # TODO verify the node's running + destroy_node $nodename + destroy_test_iface $bridge + log "Finished cdrom booting for $nodename" +} + +# Creates a virt network. +# $1 - the network interface name +# $2 - use DHCP (any value) +create_test_iface () { + local name=$1 + local dhcp=$2 + local definition=$(mktemp) + local network=$NETWORK + local definition="" + local xmlfile=$(mktemp) + + destroy_test_iface $name + NETWORK_NAME=$name + + log "Creating network definition file: $definition" + definition="<network>\n<name>${name}</name>\n<forward mode='nat' />\n<bridge name='${name}' stp='on' forwardDelay='0' />" + definition="${definition}\n<ip address='${network}.1' netmask='255.255.255.0'>" + if [ -n "$dhcp" ]; then + definition="${definition}\n<dhcp>\n<range start='${network}.100' end='${network}.199' />\n</dhcp>" + fi + definition="${definition}\n</ip>\n</network>" + + printf "Saving network definition file to: ${xmlfile}\n" + sudo printf "${definition}" > $xmlfile + sudo virsh net-define $xmlfile + log "Starting network" + sudo virsh net-start $name +} + +# Destroys the test network interface +# $1 - the network name +destroy_test_iface () { + local networkname=$1 + + # if no network was supplied, then check for the global network + if [ -z "$networkname" ]; then + networkname=$NETWORK_NAME + fi + + if [ -n "${networkname}" ]; then + log "Destroying network interface: ${networkname}" + check=$(sudo virsh net-list --all) + if [[ "${check}" =~ "${networkname}" ]]; then + log "- found existing instance" + if [[ "{$check}" =~ active ]]; then + log "- shutting down current instance" + sudo virsh net-destroy $networkname + fi + log "- undefining previous instance" + sudo virsh net-undefine $networkname + fi + + # ensure the bridge interface was destroyed + check=$(sudo /sbin/ifconfig) + if [[ "${check}" =~ "${networkname}" ]]; then + sudo /sbin/ifconfig $networkname down + fi + fi +} + +# Starts a simple instance of dnsmasq. +# $1 - the iface on which dnsmasq works +# $2 - the root for tftp files +# $3 - the mac address for the node (ignored if blank) +start_dnsmasq () { + local iface=$1 + local tftproot=$2 + local macaddress=$3 + local pidfile=$2/dnsmasq.pid + + stop_dnsmasq + log "Starting dnsmasq" + dns_startup="sudo /usr/sbin/dnsmasq --read-ethers + --dhcp-range=${NETWORK}.100,${NETWORK}.254,255.255.255.0,24h + --interface=${iface} + --bind-interfaces + --except-interface=lo + --dhcp-boot=tftpboot/pxelinux.0 + --enable-tftp + --tftp-root=${tftproot} + --log-queries + --log-dhcp + --pid-file=${pidfile}" + if [ -n "$macaddress" ]; then + dns_startup="${dns_startup} --dhcp-host=${macaddress},${NODE_ADDRESS}" + fi + # start dnsmasq + eval $dns_startup + DNSMASQ_PID=$(sudo cat $pidfile) +} + +# Kills the running instance of dnsmasq. +stop_dnsmasq () { + if [ -n "$DNSMASQ_PID" -a "$DNSMASQ_PID" != "0" ]; then + local check=$(ps -ef | awk "/${DNSMASQ_PID}/"' { if ($2 ~ '"${DNSMASQ_PID}"') print $2 }') + + if [[ "${check}" == "${DNSMASQ_PID}" ]]; then + log "Killing dnsmasq" + sudo kill -9 $DNSMASQ_PID + return + fi + fi + log "No running instance of dnsmasq found." + DNSMASQ_PID="0" +} + +# Boots a node via CDROM. +# $1 - the node name +# $2 - the network to use +# $3 - the working directory +# $4 - the ISO file to use as CDROM +cdrom_boot () { + local nodename=$1 + local network=$2 + local workdir=$3 + local isofile=$4 + local diskfile=$workdir/ovirt-harddisk.img + + create_hard_disk $diskfile "10G" + cdrom_boot_node_vm $nodename $isofile $diskfile "512" $network +} + +# verify that a node has booted properly +# $1 - the node's name +# $2 - the logfile to use +verify_pxeboot_stateless_standalone () { + local nodename=$1 + local port=$(sudo virsh ttyconsole $nodename) + local logfile=$2 + + log "Verifying the node is booted correctly" + local script=' +log_file -noappend '"${logfile}"' +set timeout 60 +expect { + "Linux version" {send_log "\n\n***\nGot first boot marker\n\n"} + timeout {send_log "\n\n***\nDid not receive in time\n\n" + exit 1} +} +expect { + -re "Kernel command line.*ovirt_standalone" {send_log "\n\n***\nGot kernel arguments marker\n\n"} + timeout { + send_log "\n\n***\nDid not receive in time\n\n" + exit 2 + } +} +expect { + "Starting ovirt-early:" {send_log "\n\n***\nGot ovirt-early marker\n\n"} + timeout {send_log "\n\n***\nDid not receive in time\n\n" + exit 3} +} +expect { + "Starting ovirt:" {send_log "\n\n***\nGot ovirt marker\n\n"} + timeout {send_log "\n\n***\nDid not receive in time\n\n" + exit 4} +} +expect { + "Starting ovirt-post:" {send_log "\n\n***\nGot ovirt-post marker\n\n"} + timeout {send_log "\n\n***\nDid not receive in time\n\n" + exit 5} +} +expect { + "Starting ovirt-firstboot:" {send_log "\n\n***\nGot ovirt-firstboot marker\n\n"} + timeout {send_log "\n\n***\nDid not receive in time\n\n" + exit 6} +}' + + sudo bash -c "/usr/bin/expect -c '${script}' < ${port}" + result=$? + printf "result=${result}\n" +} + +# Verify that a stateful node has booted properly. +# $1 - the node's name +# $2 - the logfile for recording the transcript +verify_pxeboot_stateful_standalone () { + local nodename=$1 + local port=$(sudo virsh ttyconsole $nodename) + local logfile=$2 + + # leverage the existing stateless test + verify_pxeboot_stateless_standalone $nodename + log "Verifying the node is booted correctly" + local script=' +log_file -noappend '"${logfile}"' +set timeout 180 +expect { + -re "login:$" {send_log "\n\n***\nGot login prompt!\n\n"} + timeout {send_log "\n\n***\nDid not receive in time\n\n" + exit 7} +}' + sudo bash -c "/usr/bin/expect -c '${script}' < ${port}" +} + +# TEST: Performs a PXE boot of the node as a standalone, stateless instance. +test_pxeboot_stateless_standalone () { + local nodename="pxe_stateless_standalone-$$" + local hdfile=$(mktemp) + + log "TEST: Booting a stateless standalone node via PXE." + create_hard_disk $hdfile "10G" + pxeboot_node_vm $nodename $OVIRT_NODE_IMAGE_ISO "${hdfile}" "524288" \ + $IFACE_NAME "ovirt_standalone OVIRT_FIRSTBOOT=no" \ + "verify_pxeboot_stateless_standalone $nodename 'pxeboot_stateless_standalone.log'" +} + +# TEST: Performs a PXE boot of the node as a standalone instance. The node then performs a full install +test_pxeboot_stateful_standalone () { + local nodename="pxe_stateful_standalone-$$" + local hdfile=$(mktemp) + + log "TEST: Installing a stateful standalone node via PXE." + create_hard_disk $hdfile "10G" + pxeboot_node_vm $nodename $OVIRT_NODE_IMAGE_ISO "${hdfile}" "524288" \ + $IFACE_NAME "ovirt_standalone OVIRT_FIRSTBOOT=no ovirt_init=/dev/vda" \ + "verify_pxeboot_stateful_standalone $nodename 'pxeboot_stateful_standalone.log'" +} + +# TEST: Performs a CDROM boot of the node as a standalone, stateless instance +test_cdrom_stateless_standalone () { + local nodename="stateless_cdrom_standalone-$$" + + log "TEST: Booting a stateless node from CDROM." + cdrom_boot $nodename "$IFACE_NAME" "$TFTP_ROOT_DIR" "$OVIRT_NODE_IMAGE_ISO" +} + +# automated testing entry points +{ + IFACE_NAME=testbr$$ + NODENAME+ NETWORK=192.168.$(echo "scale=0; print $$ % 255" | bc -l) + NODE_ADDRESS=$NETWORK.100 + DNSMASQ_PID=0 + + log "Starting tests" + log "Using network: ${NETWORK}.0" + + test_pxeboot_stateless_standalone + test_pxeboot_stateful_standalone +} | sudo tee --append $RESULTS -- 1.5.5.6