From 5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 10 Oct 2013 13:28:20 +0200 Subject: Move all of NixOS to nixos/ in preparation of the repository merge --- nixos/lib/build-vms.nix | 87 ++++++ nixos/lib/channel-expr.nix | 6 + nixos/lib/eval-config.nix | 73 +++++ nixos/lib/from-env.nix | 4 + nixos/lib/make-iso9660-image.nix | 60 ++++ nixos/lib/make-iso9660-image.sh | 91 ++++++ nixos/lib/make-squashfs.nix | 30 ++ nixos/lib/make-system-tarball.nix | 38 +++ nixos/lib/make-system-tarball.sh | 58 ++++ nixos/lib/qemu-flags.nix | 10 + nixos/lib/test-driver/Logger.pm | 70 +++++ nixos/lib/test-driver/Machine.pm | 568 +++++++++++++++++++++++++++++++++++ nixos/lib/test-driver/log2html.xsl | 135 +++++++++ nixos/lib/test-driver/logfile.css | 129 ++++++++ nixos/lib/test-driver/test-driver.pl | 178 +++++++++++ nixos/lib/test-driver/treebits.js | 30 ++ nixos/lib/testing.nix | 244 +++++++++++++++ nixos/lib/utils.nix | 10 + 18 files changed, 1821 insertions(+) create mode 100644 nixos/lib/build-vms.nix create mode 100644 nixos/lib/channel-expr.nix create mode 100644 nixos/lib/eval-config.nix create mode 100644 nixos/lib/from-env.nix create mode 100644 nixos/lib/make-iso9660-image.nix create mode 100644 nixos/lib/make-iso9660-image.sh create mode 100644 nixos/lib/make-squashfs.nix create mode 100644 nixos/lib/make-system-tarball.nix create mode 100644 nixos/lib/make-system-tarball.sh create mode 100644 nixos/lib/qemu-flags.nix create mode 100644 nixos/lib/test-driver/Logger.pm create mode 100644 nixos/lib/test-driver/Machine.pm create mode 100644 nixos/lib/test-driver/log2html.xsl create mode 100644 nixos/lib/test-driver/logfile.css create mode 100644 nixos/lib/test-driver/test-driver.pl create mode 100644 nixos/lib/test-driver/treebits.js create mode 100644 nixos/lib/testing.nix create mode 100644 nixos/lib/utils.nix (limited to 'nixos/lib') diff --git a/nixos/lib/build-vms.nix b/nixos/lib/build-vms.nix new file mode 100644 index 000000000000..59f05bfd1043 --- /dev/null +++ b/nixos/lib/build-vms.nix @@ -0,0 +1,87 @@ +{ system, minimal ? false }: + +let pkgs = import { config = {}; inherit system; }; in + +with pkgs.lib; +with import ../lib/qemu-flags.nix; + +rec { + + inherit pkgs; + + + # Build a virtual network from an attribute set `{ machine1 = + # config1; ... machineN = configN; }', where `machineX' is the + # hostname and `configX' is a NixOS system configuration. Each + # machine is given an arbitrary IP address in the virtual network. + buildVirtualNetwork = + nodes: let nodesOut = mapAttrs (n: buildVM nodesOut) (assignIPAddresses nodes); in nodesOut; + + + buildVM = + nodes: configurations: + + import ./eval-config.nix { + inherit system; + modules = configurations ++ + [ ../modules/virtualisation/qemu-vm.nix + ../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs + { key = "no-manual"; services.nixosManual.enable = false; } + ] ++ optional minimal ../modules/testing/minimal-kernel.nix; + extraArgs = { inherit nodes; }; + }; + + + # Given an attribute set { machine1 = config1; ... machineN = + # configN; }, sequentially assign IP addresses in the 192.168.1.0/24 + # range to each machine, and set the hostname to the attribute name. + assignIPAddresses = nodes: + + let + + machines = attrNames nodes; + + machinesNumbered = zipTwoLists machines (range 1 254); + + nodes_ = flip map machinesNumbered (m: nameValuePair m.first + [ ( { config, pkgs, nodes, ... }: + let + interfacesNumbered = zipTwoLists config.virtualisation.vlans (range 1 255); + interfaces = flip map interfacesNumbered ({ first, second }: + nameValuePair "eth${toString second}" + { ipAddress = "192.168.${toString first}.${toString m.second}"; + subnetMask = "255.255.255.0"; + }); + in + { key = "ip-address"; + config = + { networking.hostName = m.first; + + networking.interfaces = listToAttrs interfaces; + + networking.primaryIPAddress = + optionalString (interfaces != []) (head interfaces).value.ipAddress; + + # Put the IP addresses of all VMs in this machine's + # /etc/hosts file. If a machine has multiple + # interfaces, use the IP address corresponding to + # the first interface (i.e. the first network in its + # virtualisation.vlans option). + networking.extraHosts = flip concatMapStrings machines + (m: let config = (getAttr m nodes).config; in + optionalString (config.networking.primaryIPAddress != "") + ("${config.networking.primaryIPAddress} " + + "${config.networking.hostName}\n")); + + virtualisation.qemu.options = + flip map interfacesNumbered + ({ first, second }: qemuNICFlags second first m.second); + }; + } + ) + (getAttr m.first nodes) + ] ); + + in listToAttrs nodes_; + +} diff --git a/nixos/lib/channel-expr.nix b/nixos/lib/channel-expr.nix new file mode 100644 index 000000000000..453bdd506b88 --- /dev/null +++ b/nixos/lib/channel-expr.nix @@ -0,0 +1,6 @@ +{ system ? builtins.currentSystem }: + +{ pkgs = + (import nixpkgs/default.nix { inherit system; }) + // { recurseForDerivations = true; }; +} diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix new file mode 100644 index 000000000000..47e7d1a0eafa --- /dev/null +++ b/nixos/lib/eval-config.nix @@ -0,0 +1,73 @@ +# From an end-user configuration file (`configuration'), build a NixOS +# configuration object (`config') from which we can retrieve option +# values. + +{ system ? builtins.currentSystem +, pkgs ? null +, baseModules ? import ../modules/module-list.nix +, extraArgs ? {} +, modules +, nixpkgs ? +}: + +let extraArgs_ = extraArgs; pkgs_ = pkgs; system_ = system; in + +rec { + + # These are the NixOS modules that constitute the system configuration. + configComponents = modules ++ baseModules; + + # Merge the option definitions in all modules, forming the full + # system configuration. It's not checked for undeclared options. + systemModule = + pkgs.lib.fixMergeModules configComponents extraArgs; + + optionDefinitions = systemModule.config; + optionDeclarations = systemModule.options; + inherit (systemModule) options; + + # These are the extra arguments passed to every module. In + # particular, Nixpkgs is passed through the "pkgs" argument. + extraArgs = extraArgs_ // { + inherit pkgs modules baseModules; + modulesPath = ../modules; + pkgs_i686 = import nixpkgs { system = "i686-linux"; }; + utils = import ./utils.nix pkgs; + }; + + # Import Nixpkgs, allowing the NixOS option nixpkgs.config to + # specify the Nixpkgs configuration (e.g., to set package options + # such as firefox.enableGeckoMediaPlayer, or to apply global + # overrides such as changing GCC throughout the system), and the + # option nixpkgs.system to override the platform type. This is + # tricky, because we have to prevent an infinite recursion: "pkgs" + # is passed as an argument to NixOS modules, but the value of "pkgs" + # depends on config.nixpkgs.config, which we get from the modules. + # So we call ourselves here with "pkgs" explicitly set to an + # instance that doesn't depend on nixpkgs.config. + pkgs = + if pkgs_ != null + then pkgs_ + else import nixpkgs ( + let + system = if nixpkgsOptions.system != "" then nixpkgsOptions.system else system_; + nixpkgsOptions = (import ./eval-config.nix { + inherit system extraArgs modules; + # For efficiency, leave out most NixOS modules; they don't + # define nixpkgs.config, so it's pointless to evaluate them. + baseModules = [ ../modules/misc/nixpkgs.nix ]; + pkgs = import nixpkgs { system = system_; config = {}; }; + }).optionDefinitions.nixpkgs; + in + { + inherit system; + inherit (nixpkgsOptions) config; + }); + + # Optionally check wether all config values have corresponding + # option declarations. + config = + let doCheck = optionDefinitions.environment.checkConfigurationOptions; in + assert doCheck -> pkgs.lib.checkModule "" systemModule; + systemModule.config; +} diff --git a/nixos/lib/from-env.nix b/nixos/lib/from-env.nix new file mode 100644 index 000000000000..6bd71e40e9a1 --- /dev/null +++ b/nixos/lib/from-env.nix @@ -0,0 +1,4 @@ +# TODO: remove this file. There is lib.maybeEnv now +name: default: +let value = builtins.getEnv name; in +if value == "" then default else value diff --git a/nixos/lib/make-iso9660-image.nix b/nixos/lib/make-iso9660-image.nix new file mode 100644 index 000000000000..5ad546e9534d --- /dev/null +++ b/nixos/lib/make-iso9660-image.nix @@ -0,0 +1,60 @@ +{ stdenv, perl, cdrkit, pathsFromGraph + +, # The file name of the resulting ISO image. + isoName ? "cd.iso" + +, # The files and directories to be placed in the ISO file system. + # This is a list of attribute sets {source, target} where `source' + # is the file system object (regular file or directory) to be + # grafted in the file system at path `target'. + contents + +, # In addition to `contents', the closure of the store paths listed + # in `packages' are also placed in the Nix store of the CD. This is + # a list of attribute sets {object, symlink} where `object' if a + # store path whose closure will be copied, and `symlink' is a + # symlink to `object' that will be added to the CD. + storeContents ? [] + +, # Whether this should be an El-Torito bootable CD. + bootable ? false + +, # Whether this should be an efi-bootable El-Torito CD. + efiBootable ? false + +, # The path (in the ISO file system) of the boot image. + bootImage ? "" + +, # The path (in the ISO file system) of the efi boot image. + efiBootImage ? "" + +, # Whether to compress the resulting ISO image with bzip2. + compressImage ? false + +, # The volume ID. + volumeID ? "" + +}: + +assert bootable -> bootImage != ""; +assert efiBootable -> efiBootImage != ""; + +stdenv.mkDerivation { + name = "iso9660-image"; + builder = ./make-iso9660-image.sh; + buildInputs = [perl cdrkit]; + + inherit isoName bootable bootImage compressImage volumeID pathsFromGraph efiBootImage efiBootable; + + # !!! should use XML. + sources = map (x: x.source) contents; + targets = map (x: x.target) contents; + + # !!! should use XML. + objects = map (x: x.object) storeContents; + symlinks = map (x: x.symlink) storeContents; + + # For obtaining the closure of `storeContents'. + exportReferencesGraph = + map (x: [("closure-" + baseNameOf x.object) x.object]) storeContents; +} diff --git a/nixos/lib/make-iso9660-image.sh b/nixos/lib/make-iso9660-image.sh new file mode 100644 index 000000000000..89b681ed2cd5 --- /dev/null +++ b/nixos/lib/make-iso9660-image.sh @@ -0,0 +1,91 @@ +source $stdenv/setup + +sources_=($sources) +targets_=($targets) + +objects=($objects) +symlinks=($symlinks) + + +# Remove the initial slash from a path, since genisofs likes it that way. +stripSlash() { + res="$1" + if test "${res:0:1}" = /; then res=${res:1}; fi +} + +stripSlash "$bootImage"; bootImage="$res" + + +if test -n "$bootable"; then + + # The -boot-info-table option modifies the $bootImage file, so + # find it in `contents' and make a copy of it (since the original + # is read-only in the Nix store...). + for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + if test "$res" = "$bootImage"; then + echo "copying the boot image ${sources_[$i]}" + cp "${sources_[$i]}" boot.img + chmod u+w boot.img + sources_[$i]=boot.img + fi + done + + bootFlags="-b $bootImage -c .boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table" +fi + +if test -n "$efiBootable"; then + bootFlags="$bootFlags -eltorito-alt-boot -e $efiBootImage -no-emul-boot" +fi + +touch pathlist + + +# Add the individual files. +for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + echo "$res=${sources_[$i]}" >> pathlist +done + + +# Add the closures of the top-level store objects. +storePaths=$(perl $pathsFromGraph closure-*) +for i in $storePaths; do + echo "${i:1}=$i" >> pathlist +done + + +# Also include a manifest of the closures in a format suitable for +# nix-store --load-db. +if [ -n "$object" ]; then + printRegistration=1 perl $pathsFromGraph closure-* > nix-path-registration + echo "nix-path-registration=nix-path-registration" >> pathlist +fi + + +# Add symlinks to the top-level store objects. +for ((n = 0; n < ${#objects[*]}; n++)); do + object=${objects[$n]} + symlink=${symlinks[$n]} + if test "$symlink" != "none"; then + mkdir -p $(dirname ./$symlink) + ln -s $object ./$symlink + echo "$symlink=./$symlink" >> pathlist + fi +done + +# !!! what does this do? +cat pathlist | sed -e 's/=\(.*\)=\(.*\)=/\\=\1=\2\\=/' | tee pathlist.safer + + +ensureDir $out/iso +genCommand="genisoimage -iso-level 4 -r -J $bootFlags -hide-rr-moved -graft-points -path-list pathlist.safer ${volumeID:+-V $volumeID}" +if test -z "$compressImage"; then + $genCommand -o $out/iso/$isoName +else + $genCommand | bzip2 > $out/iso/$isoName.bz2 +fi + + +ensureDir $out/nix-support +echo $system > $out/nix-support/system diff --git a/nixos/lib/make-squashfs.nix b/nixos/lib/make-squashfs.nix new file mode 100644 index 000000000000..3b640334e17a --- /dev/null +++ b/nixos/lib/make-squashfs.nix @@ -0,0 +1,30 @@ +{ stdenv, squashfsTools, perl, pathsFromGraph + +, # The root directory of the squashfs filesystem is filled with the + # closures of the Nix store paths listed here. + storeContents ? [] +}: + +stdenv.mkDerivation { + name = "squashfs.img"; + + buildInputs = [perl squashfsTools]; + + # For obtaining the closure of `storeContents'. + exportReferencesGraph = + map (x: [("closure-" + baseNameOf x) x]) storeContents; + + buildCommand = + '' + # Add the closures of the top-level store objects. + storePaths=$(perl ${pathsFromGraph} closure-*) + + # Also include a manifest of the closures in a format suitable + # for nix-store --load-db. + printRegistration=1 perl ${pathsFromGraph} closure-* > nix-path-registration + + # Generate the squashfs image. + mksquashfs nix-path-registration $storePaths $out \ + -keep-as-directory -all-root + ''; +} diff --git a/nixos/lib/make-system-tarball.nix b/nixos/lib/make-system-tarball.nix new file mode 100644 index 000000000000..8fed9a348827 --- /dev/null +++ b/nixos/lib/make-system-tarball.nix @@ -0,0 +1,38 @@ +{ stdenv, perl, xz, pathsFromGraph + +, # The file name of the resulting tarball + fileName ? "nixos-system-${stdenv.system}" + +, # The files and directories to be placed in the tarball. + # This is a list of attribute sets {source, target} where `source' + # is the file system object (regular file or directory) to be + # grafted in the file system at path `target'. + contents + +, # In addition to `contents', the closure of the store paths listed + # in `packages' are also placed in the Nix store of the tarball. This is + # a list of attribute sets {object, symlink} where `object' if a + # store path whose closure will be copied, and `symlink' is a + # symlink to `object' that will be added to the tarball. + storeContents ? [] +}: + +stdenv.mkDerivation { + name = "tarball"; + builder = ./make-system-tarball.sh; + buildInputs = [perl xz]; + + inherit fileName pathsFromGraph; + + # !!! should use XML. + sources = map (x: x.source) contents; + targets = map (x: x.target) contents; + + # !!! should use XML. + objects = map (x: x.object) storeContents; + symlinks = map (x: x.symlink) storeContents; + + # For obtaining the closure of `storeContents'. + exportReferencesGraph = + map (x: [("closure-" + baseNameOf x.object) x.object]) storeContents; +} diff --git a/nixos/lib/make-system-tarball.sh b/nixos/lib/make-system-tarball.sh new file mode 100644 index 000000000000..aadd0f6428c8 --- /dev/null +++ b/nixos/lib/make-system-tarball.sh @@ -0,0 +1,58 @@ +source $stdenv/setup +set -x + +sources_=($sources) +targets_=($targets) + +echo $objects +objects=($objects) +symlinks=($symlinks) + + +# Remove the initial slash from a path, since genisofs likes it that way. +stripSlash() { + res="$1" + if test "${res:0:1}" = /; then res=${res:1}; fi +} + +touch pathlist + +# Add the individual files. +for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + mkdir -p "$(dirname "$res")" + cp -a "${sources_[$i]}" "$res" +done + + +# Add the closures of the top-level store objects. +mkdir -p nix/store +storePaths=$(perl $pathsFromGraph closure-*) +for i in $storePaths; do + cp -a "$i" "${i:1}" +done + + +# TODO tar ruxo +# Also include a manifest of the closures in a format suitable for +# nix-store --load-db. +printRegistration=1 perl $pathsFromGraph closure-* > nix-path-registration + +# Add symlinks to the top-level store objects. +for ((n = 0; n < ${#objects[*]}; n++)); do + object=${objects[$n]} + symlink=${symlinks[$n]} + if test "$symlink" != "none"; then + mkdir -p $(dirname ./$symlink) + ln -s $object ./$symlink + fi +done + +ensureDir $out/tarball + +tar cvJf $out/tarball/$fileName.tar.xz * + +ensureDir $out/nix-support +echo $system > $out/nix-support/system +echo "file system-tarball $out/tarball/$fileName.tar.xz" > $out/nix-support/hydra-build-products + diff --git a/nixos/lib/qemu-flags.nix b/nixos/lib/qemu-flags.nix new file mode 100644 index 000000000000..de355b08918c --- /dev/null +++ b/nixos/lib/qemu-flags.nix @@ -0,0 +1,10 @@ +# QEMU flags shared between various Nix expressions. + +{ + + qemuNICFlags = nic: net: machine: + [ "-net nic,vlan=${toString nic},macaddr=52:54:00:12:${toString net}:${toString machine},model=virtio" + "-net vde,vlan=${toString nic},sock=$QEMU_VDE_SOCKET_${toString net}" + ]; + +} diff --git a/nixos/lib/test-driver/Logger.pm b/nixos/lib/test-driver/Logger.pm new file mode 100644 index 000000000000..6e62fdfd7708 --- /dev/null +++ b/nixos/lib/test-driver/Logger.pm @@ -0,0 +1,70 @@ +package Logger; + +use strict; +use Thread::Queue; +use XML::Writer; + +sub new { + my ($class) = @_; + + my $logFile = defined $ENV{LOGFILE} ? "$ENV{LOGFILE}" : "/dev/null"; + my $log = new XML::Writer(OUTPUT => new IO::File(">$logFile")); + + my $self = { + log => $log, + logQueue => Thread::Queue->new() + }; + + $self->{log}->startTag("logfile"); + + bless $self, $class; + return $self; +} + +sub close { + my ($self) = @_; + $self->{log}->endTag("logfile"); + $self->{log}->end; +} + +sub drainLogQueue { + my ($self) = @_; + while (defined (my $item = $self->{logQueue}->dequeue_nb())) { + $self->{log}->dataElement("line", sanitise($item->{msg}), 'machine' => $item->{machine}, 'type' => 'serial'); + } +} + +sub maybePrefix { + my ($msg, $attrs) = @_; + $msg = $attrs->{machine} . ": " . $msg if defined $attrs->{machine}; + return $msg; +} + +sub nest { + my ($self, $msg, $coderef, $attrs) = @_; + print STDERR maybePrefix("$msg\n", $attrs); + $self->{log}->startTag("nest"); + $self->{log}->dataElement("head", $msg, %{$attrs}); + $self->drainLogQueue(); + eval { &$coderef }; + my $res = $@; + $self->drainLogQueue(); + $self->{log}->endTag("nest"); + die $@ if $@; +} + +sub sanitise { + my ($s) = @_; + $s =~ s/[[:cntrl:]\xff]//g; + return $s; +} + +sub log { + my ($self, $msg, $attrs) = @_; + chomp $msg; + print STDERR maybePrefix("$msg\n", $attrs); + $self->drainLogQueue(); + $self->{log}->dataElement("line", $msg, %{$attrs}); +} + +1; diff --git a/nixos/lib/test-driver/Machine.pm b/nixos/lib/test-driver/Machine.pm new file mode 100644 index 000000000000..a28214ea934f --- /dev/null +++ b/nixos/lib/test-driver/Machine.pm @@ -0,0 +1,568 @@ +package Machine; + +use strict; +use threads; +use Socket; +use IO::Handle; +use POSIX qw(dup2); +use FileHandle; +use Cwd; +use File::Basename; +use File::Path qw(make_path); + + +my $showGraphics = defined $ENV{'DISPLAY'}; + +my $sharedDir; + + +sub new { + my ($class, $args) = @_; + + my $startCommand = $args->{startCommand}; + + my $name = $args->{name}; + if (!$name) { + $startCommand =~ /run-(.*)-vm$/ if defined $startCommand; + $name = $1 || "machine"; + } + + if (!$startCommand) { + # !!! merge with qemu-vm.nix. + $startCommand = + "qemu-kvm -m 384 " . + "-net nic,model=virtio \$QEMU_OPTS "; + my $iface = $args->{hdaInterface} || "virtio"; + $startCommand .= "-drive file=" . Cwd::abs_path($args->{hda}) . ",if=$iface,boot=on,werror=report " + if defined $args->{hda}; + $startCommand .= "-cdrom $args->{cdrom} " + if defined $args->{cdrom}; + $startCommand .= $args->{qemuFlags} || ""; + } else { + $startCommand = Cwd::abs_path $startCommand; + } + + my $tmpDir = $ENV{'TMPDIR'} || "/tmp"; + unless (defined $sharedDir) { + $sharedDir = $tmpDir . "/xchg-shared"; + make_path($sharedDir, { mode => 0700, owner => $< }); + } + + my $allowReboot = 0; + $allowReboot = $args->{allowReboot} if defined $args->{allowReboot}; + + my $self = { + startCommand => $startCommand, + name => $name, + allowReboot => $allowReboot, + booted => 0, + pid => 0, + connected => 0, + socket => undef, + stateDir => "$tmpDir/vm-state-$name", + monitor => undef, + log => $args->{log}, + redirectSerial => $args->{redirectSerial} // 1, + }; + + mkdir $self->{stateDir}, 0700; + + bless $self, $class; + return $self; +} + + +sub log { + my ($self, $msg) = @_; + $self->{log}->log($msg, { machine => $self->{name} }); +} + + +sub nest { + my ($self, $msg, $coderef, $attrs) = @_; + $self->{log}->nest($msg, $coderef, { %{$attrs || {}}, machine => $self->{name} }); +} + + +sub name { + my ($self) = @_; + return $self->{name}; +} + + +sub stateDir { + my ($self) = @_; + return $self->{stateDir}; +} + + +sub start { + my ($self) = @_; + return if $self->{booted}; + + $self->log("starting vm"); + + # Create a socket pair for the serial line input/output of the VM. + my ($serialP, $serialC); + socketpair($serialP, $serialC, PF_UNIX, SOCK_STREAM, 0) or die; + + # Create a Unix domain socket to which QEMU's monitor will connect. + my $monitorPath = $self->{stateDir} . "/monitor"; + unlink $monitorPath; + my $monitorS; + socket($monitorS, PF_UNIX, SOCK_STREAM, 0) or die; + bind($monitorS, sockaddr_un($monitorPath)) or die "cannot bind monitor socket: $!"; + listen($monitorS, 1) or die; + + # Create a Unix domain socket to which the root shell in the guest will connect. + my $shellPath = $self->{stateDir} . "/shell"; + unlink $shellPath; + my $shellS; + socket($shellS, PF_UNIX, SOCK_STREAM, 0) or die; + bind($shellS, sockaddr_un($shellPath)) or die "cannot bind shell socket: $!"; + listen($shellS, 1) or die; + + # Start the VM. + my $pid = fork(); + die if $pid == -1; + + if ($pid == 0) { + close $serialP; + close $monitorS; + close $shellS; + if ($self->{redirectSerial}) { + open NUL, "{stateDir}; + $ENV{SHARED_DIR} = $sharedDir; + $ENV{USE_TMPDIR} = 1; + $ENV{QEMU_OPTS} = + ($self->{allowReboot} ? "" : "-no-reboot ") . + "-monitor unix:./monitor -chardev socket,id=shell,path=./shell " . + "-device virtio-serial -device virtconsole,chardev=shell " . + ($showGraphics ? "-serial stdio" : "-nographic") . " " . ($ENV{QEMU_OPTS} || ""); + chdir $self->{stateDir} or die; + exec $self->{startCommand}; + die "running VM script: $!"; + } + + # Process serial line output. + close $serialC; + + threads->create(\&processSerialOutput, $self, $serialP)->detach; + + sub processSerialOutput { + my ($self, $serialP) = @_; + while (<$serialP>) { + chomp; + s/\r$//; + print STDERR $self->{name}, "# $_\n"; + $self->{log}->{logQueue}->enqueue({msg => $_, machine => $self->{name}}); # !!! + } + } + + eval { + local $SIG{CHLD} = sub { die "QEMU died prematurely\n"; }; + + # Wait until QEMU connects to the monitor. + accept($self->{monitor}, $monitorS) or die; + + # Wait until QEMU connects to the root shell socket. QEMU + # does so immediately; this doesn't mean that the root shell + # has connected yet inside the guest. + accept($self->{socket}, $shellS) or die; + $self->{socket}->autoflush(1); + }; + die "$@" if $@; + + $self->waitForMonitorPrompt; + + $self->log("QEMU running (pid $pid)"); + + $self->{pid} = $pid; + $self->{booted} = 1; +} + + +# Send a command to the monitor and wait for it to finish. TODO: QEMU +# also has a JSON-based monitor interface now, but it doesn't support +# all commands yet. We should use it once it does. +sub sendMonitorCommand { + my ($self, $command) = @_; + $self->log("sending monitor command: $command"); + syswrite $self->{monitor}, "$command\n"; + return $self->waitForMonitorPrompt; +} + + +# Wait until the monitor sends "(qemu) ". +sub waitForMonitorPrompt { + my ($self) = @_; + my $res = ""; + my $s; + while (sysread($self->{monitor}, $s, 1024)) { + $res .= $s; + last if $res =~ s/\(qemu\) $//; + } + return $res; +} + + +# Call the given code reference repeatedly, with 1 second intervals, +# until it returns 1 or a timeout is reached. +sub retry { + my ($coderef) = @_; + my $n; + for ($n = 0; $n < 900; $n++) { + return if &$coderef; + sleep 1; + } + die "action timed out after $n seconds"; +} + + +sub connect { + my ($self) = @_; + return if $self->{connected}; + + $self->nest("waiting for the VM to finish booting", sub { + + $self->start; + + local $SIG{ALRM} = sub { die "timed out waiting for the VM to connect\n"; }; + alarm 300; + readline $self->{socket} or die "the VM quit before connecting\n"; + alarm 0; + + $self->log("connected to guest root shell"); + $self->{connected} = 1; + + }); +} + + +sub waitForShutdown { + my ($self) = @_; + return unless $self->{booted}; + + $self->nest("waiting for the VM to power off", sub { + waitpid $self->{pid}, 0; + $self->{pid} = 0; + $self->{booted} = 0; + $self->{connected} = 0; + }); +} + + +sub isUp { + my ($self) = @_; + return $self->{booted} && $self->{connected}; +} + + +sub execute_ { + my ($self, $command) = @_; + + $self->connect; + + print { $self->{socket} } ("( $command ); echo '|!=EOF' \$?\n"); + + my $out = ""; + + while (1) { + my $line = readline($self->{socket}); + die "connection to VM lost unexpectedly" unless defined $line; + #$self->log("got line: $line"); + if ($line =~ /^(.*)\|\!\=EOF\s+(\d+)$/) { + $out .= $1; + $self->log("exit status $2"); + return ($2, $out); + } + $out .= $line; + } +} + + +sub execute { + my ($self, $command) = @_; + my @res; + $self->nest("running command: $command", sub { + @res = $self->execute_($command); + }); + return @res; +} + + +sub succeed { + my ($self, @commands) = @_; + + my $res; + foreach my $command (@commands) { + $self->nest("must succeed: $command", sub { + my ($status, $out) = $self->execute_($command); + if ($status != 0) { + $self->log("output: $out"); + die "command `$command' did not succeed (exit code $status)\n"; + } + $res .= $out; + }); + } + + return $res; +} + + +sub mustSucceed { + succeed @_; +} + + +sub waitUntilSucceeds { + my ($self, $command) = @_; + $self->nest("waiting for success: $command", sub { + retry sub { + my ($status, $out) = $self->execute($command); + return 1 if $status == 0; + }; + }); +} + + +sub waitUntilFails { + my ($self, $command) = @_; + $self->nest("waiting for failure: $command", sub { + retry sub { + my ($status, $out) = $self->execute($command); + return 1 if $status != 0; + }; + }); +} + + +sub fail { + my ($self, $command) = @_; + $self->nest("must fail: $command", sub { + my ($status, $out) = $self->execute_($command); + die "command `$command' unexpectedly succeeded" + if $status == 0; + }); +} + + +sub mustFail { + fail @_; +} + + +sub getUnitInfo { + my ($self, $unit) = @_; + my ($status, $lines) = $self->execute("systemctl --no-pager show '$unit'"); + return undef if $status != 0; + my $info = {}; + foreach my $line (split '\n', $lines) { + $line =~ /^([^=]+)=(.*)$/ or next; + $info->{$1} = $2; + } + return $info; +} + + +# Wait for a systemd unit to reach the "active" state. +sub waitForUnit { + my ($self, $unit) = @_; + $self->nest("waiting for unit ‘$unit’", sub { + retry sub { + my $info = $self->getUnitInfo($unit); + my $state = $info->{ActiveState}; + die "unit ‘$unit’ reached state ‘$state’\n" if $state eq "failed"; + return 1 if $state eq "active"; + }; + }); +} + + +sub waitForJob { + my ($self, $jobName) = @_; + return $self->waitForUnit($jobName); +} + + +# Wait until the specified file exists. +sub waitForFile { + my ($self, $fileName) = @_; + $self->nest("waiting for file ‘$fileName’", sub { + retry sub { + my ($status, $out) = $self->execute("test -e $fileName"); + return 1 if $status == 0; + } + }); +} + +sub startJob { + my ($self, $jobName) = @_; + $self->execute("systemctl start $jobName"); + # FIXME: check result +} + +sub stopJob { + my ($self, $jobName) = @_; + $self->execute("systemctl stop $jobName"); +} + + +# Wait until the machine is listening on the given TCP port. +sub waitForOpenPort { + my ($self, $port) = @_; + $self->nest("waiting for TCP port $port", sub { + retry sub { + my ($status, $out) = $self->execute("nc -z localhost $port"); + return 1 if $status == 0; + } + }); +} + + +# Wait until the machine is not listening on the given TCP port. +sub waitForClosedPort { + my ($self, $port) = @_; + retry sub { + my ($status, $out) = $self->execute("nc -z localhost $port"); + return 1 if $status != 0; + } +} + + +sub shutdown { + my ($self) = @_; + return unless $self->{booted}; + + print { $self->{socket} } ("poweroff\n"); + + $self->waitForShutdown; +} + + +sub crash { + my ($self) = @_; + return unless $self->{booted}; + + $self->log("forced crash"); + + $self->sendMonitorCommand("quit"); + + $self->waitForShutdown; +} + + +# Make the machine unreachable by shutting down eth1 (the multicast +# interface used to talk to the other VMs). We keep eth0 up so that +# the test driver can continue to talk to the machine. +sub block { + my ($self) = @_; + $self->sendMonitorCommand("set_link virtio-net-pci.1 off"); +} + + +# Make the machine reachable. +sub unblock { + my ($self) = @_; + $self->sendMonitorCommand("set_link virtio-net-pci.1 on"); +} + + +# Take a screenshot of the X server on :0.0. +sub screenshot { + my ($self, $filename) = @_; + my $dir = $ENV{'out'} || Cwd::abs_path("."); + $filename = "$dir/${filename}.png" if $filename =~ /^\w+$/; + my $tmp = "${filename}.ppm"; + my $name = basename($filename); + $self->nest("making screenshot ‘$name’", sub { + $self->sendMonitorCommand("screendump $tmp"); + system("convert $tmp ${filename}") == 0 + or die "cannot convert screenshot"; + unlink $tmp; + }, { image => $name } ); +} + + +# Wait until it is possible to connect to the X server. Note that +# testing the existence of /tmp/.X11-unix/X0 is insufficient. +sub waitForX { + my ($self, $regexp) = @_; + $self->nest("waiting for the X11 server", sub { + retry sub { + my ($status, $out) = $self->execute("xwininfo -root > /dev/null 2>&1"); + return 1 if $status == 0; + } + }); +} + + +sub getWindowNames { + my ($self) = @_; + my $res = $self->mustSucceed( + q{xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'}); + return split /\n/, $res; +} + + +sub waitForWindow { + my ($self, $regexp) = @_; + $self->nest("waiting for a window to appear", sub { + retry sub { + my @names = $self->getWindowNames; + foreach my $n (@names) { + return 1 if $n =~ /$regexp/; + } + } + }); +} + + +sub copyFileFromHost { + my ($self, $from, $to) = @_; + my $s = `cat $from` or die; + $self->mustSucceed("echo '$s' > $to"); # !!! escaping +} + + +sub sendKeys { + my ($self, @keys) = @_; + foreach my $key (@keys) { + $key = "spc" if $key eq " "; + $key = "ret" if $key eq "\n"; + $self->sendMonitorCommand("sendkey $key"); + } +} + + +sub sendChars { + my ($self, $chars) = @_; + $self->nest("sending keys ‘$chars’", sub { + $self->sendKeys(split //, $chars); + }); +} + + +# Sleep N seconds (in virtual guest time, not real time). +sub sleep { + my ($self, $time) = @_; + $self->succeed("sleep $time"); +} + + +# Forward a TCP port on the host to a TCP port on the guest. Useful +# during interactive testing. +sub forwardPort { + my ($self, $hostPort, $guestPort) = @_; + $hostPort = 8080 unless defined $hostPort; + $guestPort = 80 unless defined $guestPort; + $self->sendMonitorCommand("hostfwd_add tcp::$hostPort-:$guestPort"); +} + + +1; diff --git a/nixos/lib/test-driver/log2html.xsl b/nixos/lib/test-driver/log2html.xsl new file mode 100644 index 000000000000..8e907d85ffac --- /dev/null +++ b/nixos/lib/test-driver/log2html.xsl @@ -0,0 +1,135 @@ + + + + + + + + + + + +