Move all of NixOS to nixos/ in preparation of the repository merge
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
{ system, minimal ? false }:
|
||||
|
||||
let pkgs = import <nixpkgs> { 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_;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{ system ? builtins.currentSystem }:
|
||||
|
||||
{ pkgs =
|
||||
(import nixpkgs/default.nix { inherit system; })
|
||||
// { recurseForDerivations = true; };
|
||||
}
|
||||
@@ -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 ? <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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
'';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
];
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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, "</dev/null" or die;
|
||||
dup2(fileno(NUL), fileno(STDIN));
|
||||
dup2(fileno($serialC), fileno(STDOUT));
|
||||
dup2(fileno($serialC), fileno(STDERR));
|
||||
}
|
||||
$ENV{TMPDIR} = $self->{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;
|
||||
@@ -0,0 +1,135 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
|
||||
<xsl:output method='html' encoding="UTF-8"
|
||||
doctype-public="-//W3C//DTD HTML 4.01//EN"
|
||||
doctype-system="http://www.w3.org/TR/html4/strict.dtd" />
|
||||
|
||||
<xsl:template match="logfile">
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
|
||||
<script type="text/javascript" src="treebits.js" />
|
||||
<link rel="stylesheet" href="logfile.css" type="text/css" />
|
||||
<title>Log File</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>VM build log</h1>
|
||||
<p>
|
||||
<a href="javascript:" class="logTreeExpandAll">Expand all</a> |
|
||||
<a href="javascript:" class="logTreeCollapseAll">Collapse all</a>
|
||||
</p>
|
||||
<ul class='toplevel'>
|
||||
<xsl:for-each select='line|nest'>
|
||||
<li>
|
||||
<xsl:apply-templates select='.'/>
|
||||
</li>
|
||||
</xsl:for-each>
|
||||
</ul>
|
||||
|
||||
<xsl:if test=".//*[@image]">
|
||||
<h1>Screenshots</h1>
|
||||
<ul class="vmScreenshots">
|
||||
<xsl:for-each select='.//*[@image]'>
|
||||
<li><a href="{@image}"><xsl:value-of select="@image" /></a></li>
|
||||
</xsl:for-each>
|
||||
</ul>
|
||||
</xsl:if>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
|
||||
|
||||
<xsl:template match="nest">
|
||||
|
||||
<!-- The tree should be collapsed by default if all children are
|
||||
unimportant or if the header is unimportant. -->
|
||||
<xsl:variable name="collapsed" select="not(./head[@expanded]) and count(.//*[@error]) = 0"/>
|
||||
|
||||
<xsl:variable name="style"><xsl:if test="$collapsed">display: none;</xsl:if></xsl:variable>
|
||||
|
||||
<xsl:if test="line|nest">
|
||||
<a href="javascript:" class="logTreeToggle">
|
||||
<xsl:choose>
|
||||
<xsl:when test="$collapsed"><xsl:text>+</xsl:text></xsl:when>
|
||||
<xsl:otherwise><xsl:text>-</xsl:text></xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</a>
|
||||
<xsl:text> </xsl:text>
|
||||
</xsl:if>
|
||||
|
||||
<xsl:apply-templates select='head'/>
|
||||
|
||||
<!-- Be careful to only generate <ul>s if there are <li>s, otherwise it’s malformed. -->
|
||||
<xsl:if test="line|nest">
|
||||
|
||||
<ul class='nesting' style="{$style}">
|
||||
<xsl:for-each select='line|nest'>
|
||||
|
||||
<!-- Is this the last line? If so, mark it as such so that it
|
||||
can be rendered differently. -->
|
||||
<xsl:variable name="class"><xsl:choose><xsl:when test="position() != last()">line</xsl:when><xsl:otherwise>lastline</xsl:otherwise></xsl:choose></xsl:variable>
|
||||
|
||||
<li class='{$class}'>
|
||||
<span class='lineconn' />
|
||||
<span class='linebody'>
|
||||
<xsl:apply-templates select='.'/>
|
||||
</span>
|
||||
</li>
|
||||
</xsl:for-each>
|
||||
</ul>
|
||||
</xsl:if>
|
||||
|
||||
</xsl:template>
|
||||
|
||||
|
||||
<xsl:template match="head|line">
|
||||
<code>
|
||||
<xsl:if test="@error">
|
||||
<xsl:attribute name="class">errorLine</xsl:attribute>
|
||||
</xsl:if>
|
||||
<xsl:if test="@warning">
|
||||
<xsl:attribute name="class">warningLine</xsl:attribute>
|
||||
</xsl:if>
|
||||
<xsl:if test="@priority = 3">
|
||||
<xsl:attribute name="class">prio3</xsl:attribute>
|
||||
</xsl:if>
|
||||
|
||||
<xsl:if test="@type = 'serial'">
|
||||
<xsl:attribute name="class">serial</xsl:attribute>
|
||||
</xsl:if>
|
||||
|
||||
<xsl:if test="@machine">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@type = 'serial'">
|
||||
<span class="machine"><xsl:value-of select="@machine"/># </span>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<span class="machine"><xsl:value-of select="@machine"/>: </span>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:if>
|
||||
|
||||
<xsl:choose>
|
||||
<xsl:when test="@image">
|
||||
<a href="{@image}"><xsl:apply-templates/></a>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<xsl:apply-templates/>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</code>
|
||||
</xsl:template>
|
||||
|
||||
|
||||
<xsl:template match="storeref">
|
||||
<em class='storeref'>
|
||||
<span class='popup'><xsl:apply-templates/></span>
|
||||
<span class='elided'>/...</span><xsl:apply-templates select='name'/><xsl:apply-templates select='path'/>
|
||||
</em>
|
||||
</xsl:template>
|
||||
|
||||
</xsl:stylesheet>
|
||||
@@ -0,0 +1,129 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: white;
|
||||
}
|
||||
|
||||
h1
|
||||
{
|
||||
color: #005aa0;
|
||||
font-size: 180%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
ul.nesting, ul.toplevel {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul.toplevel {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.line, .head {
|
||||
padding-top: 0em;
|
||||
}
|
||||
|
||||
ul.nesting li.line, ul.nesting li.lastline {
|
||||
position: relative;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
ul.nesting li.line {
|
||||
padding-left: 2.0em;
|
||||
}
|
||||
|
||||
ul.nesting li.lastline {
|
||||
padding-left: 2.1em; /* for the 0.1em border-left in .lastline > .lineconn */
|
||||
}
|
||||
|
||||
li.line {
|
||||
border-left: 0.1em solid #6185a0;
|
||||
}
|
||||
|
||||
li.line > span.lineconn, li.lastline > span.lineconn {
|
||||
position: absolute;
|
||||
height: 0.65em;
|
||||
left: 0em;
|
||||
width: 1.5em;
|
||||
border-bottom: 0.1em solid #6185a0;
|
||||
}
|
||||
|
||||
li.lastline > span.lineconn {
|
||||
border-left: 0.1em solid #6185a0;
|
||||
}
|
||||
|
||||
|
||||
em.storeref {
|
||||
color: #500000;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
em.storeref:hover {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
*.popup {
|
||||
display: none;
|
||||
/* background: url('http://losser.st-lab.cs.uu.nl/~mbravenb/menuback.png') repeat; */
|
||||
background: #ffffcd;
|
||||
border: solid #555555 1px;
|
||||
position: absolute;
|
||||
top: 0em;
|
||||
left: 0em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
em.storeref:hover span.popup {
|
||||
display: inline;
|
||||
width: 40em;
|
||||
}
|
||||
|
||||
|
||||
.logTreeToggle {
|
||||
text-decoration: none;
|
||||
font-family: monospace;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.errorLine {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.warningLine {
|
||||
color: darkorange;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.prio3 {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.serial {
|
||||
color: #56115c;
|
||||
}
|
||||
|
||||
.machine {
|
||||
color: #002399;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
ul.vmScreenshots {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
ul.vmScreenshots li {
|
||||
font-family: monospace;
|
||||
list-style: square;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
#! /somewhere/perl -w
|
||||
|
||||
use strict;
|
||||
use Machine;
|
||||
use Term::ReadLine;
|
||||
use IO::File;
|
||||
use IO::Pty;
|
||||
use Logger;
|
||||
use Cwd;
|
||||
use POSIX qw(_exit dup2);
|
||||
|
||||
$SIG{PIPE} = 'IGNORE'; # because Unix domain sockets may die unexpectedly
|
||||
|
||||
STDERR->autoflush(1);
|
||||
|
||||
my $log = new Logger;
|
||||
|
||||
|
||||
# Start vde_switch for each network required by the test.
|
||||
my %vlans;
|
||||
foreach my $vlan (split / /, $ENV{VLANS} || "") {
|
||||
next if defined $vlans{$vlan};
|
||||
# Start vde_switch as a child process. We don't run it in daemon
|
||||
# mode because we want the child process to be cleaned up when we
|
||||
# die. Since we have to make sure that the control socket is
|
||||
# ready, we send a dummy command to vde_switch (via stdin) and
|
||||
# wait for a reply. Note that vde_switch requires stdin to be a
|
||||
# TTY, so we create one.
|
||||
$log->log("starting VDE switch for network $vlan");
|
||||
my $socket = Cwd::abs_path "./vde$vlan.ctl";
|
||||
my $pty = new IO::Pty;
|
||||
my ($stdoutR, $stdoutW); pipe $stdoutR, $stdoutW;
|
||||
my $pid = fork(); die "cannot fork" unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
dup2(fileno($pty->slave), 0);
|
||||
dup2(fileno($stdoutW), 1);
|
||||
exec "vde_switch -s $socket" or _exit(1);
|
||||
}
|
||||
close $stdoutW;
|
||||
print $pty "version\n";
|
||||
readline $stdoutR or die "cannot start vde_switch";
|
||||
$ENV{"QEMU_VDE_SOCKET_$vlan"} = $socket;
|
||||
$vlans{$vlan} = $pty;
|
||||
die unless -e "$socket/ctl";
|
||||
}
|
||||
|
||||
|
||||
my %vms;
|
||||
my $context = "";
|
||||
|
||||
sub createMachine {
|
||||
my ($args) = @_;
|
||||
my $vm = Machine->new({%{$args}, log => $log, redirectSerial => ($ENV{USE_SERIAL} // "0") ne "1"});
|
||||
$vms{$vm->name} = $vm;
|
||||
return $vm;
|
||||
}
|
||||
|
||||
foreach my $vmScript (@ARGV) {
|
||||
my $vm = createMachine({startCommand => $vmScript});
|
||||
$context .= "my \$" . $vm->name . " = \$vms{'" . $vm->name . "'}; ";
|
||||
}
|
||||
|
||||
|
||||
sub startAll {
|
||||
$log->nest("starting all VMs", sub {
|
||||
$_->start foreach values %vms;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
# Wait until all VMs have terminated.
|
||||
sub joinAll {
|
||||
$log->nest("waiting for all VMs to finish", sub {
|
||||
$_->waitForShutdown foreach values %vms;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
# In interactive tests, this allows the non-interactive test script to
|
||||
# be executed conveniently.
|
||||
sub testScript {
|
||||
eval "$context $ENV{testScript};\n";
|
||||
warn $@ if $@;
|
||||
}
|
||||
|
||||
|
||||
my $nrTests = 0;
|
||||
my $nrSucceeded = 0;
|
||||
|
||||
|
||||
sub subtest {
|
||||
my ($name, $coderef) = @_;
|
||||
$log->nest("subtest: $name", sub {
|
||||
$nrTests++;
|
||||
eval { &$coderef };
|
||||
if ($@) {
|
||||
$log->log("error: $@", { error => 1 });
|
||||
} else {
|
||||
$nrSucceeded++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
sub runTests {
|
||||
if (defined $ENV{tests}) {
|
||||
$log->nest("running the VM test script", sub {
|
||||
eval "$context $ENV{tests}";
|
||||
if ($@) {
|
||||
$log->log("error: $@", { error => 1 });
|
||||
die $@;
|
||||
}
|
||||
}, { expanded => 1 });
|
||||
} else {
|
||||
my $term = Term::ReadLine->new('nixos-vm-test');
|
||||
$term->ReadHistory;
|
||||
while (defined ($_ = $term->readline("> "))) {
|
||||
eval "$context $_\n";
|
||||
warn $@ if $@;
|
||||
}
|
||||
$term->WriteHistory;
|
||||
}
|
||||
|
||||
# Copy the kernel coverage data for each machine, if the kernel
|
||||
# has been compiled with coverage instrumentation.
|
||||
$log->nest("collecting coverage data", sub {
|
||||
foreach my $vm (values %vms) {
|
||||
my $gcovDir = "/sys/kernel/debug/gcov";
|
||||
|
||||
next unless $vm->isUp();
|
||||
|
||||
my ($status, $out) = $vm->execute("test -e $gcovDir");
|
||||
next if $status != 0;
|
||||
|
||||
# Figure out where to put the *.gcda files so that the
|
||||
# report generator can find the corresponding kernel
|
||||
# sources.
|
||||
my $kernelDir = $vm->mustSucceed("echo \$(dirname \$(readlink -f /run/current-system/kernel))/.build/linux-*");
|
||||
chomp $kernelDir;
|
||||
my $coverageDir = "/tmp/xchg/coverage-data/$kernelDir";
|
||||
|
||||
# Copy all the *.gcda files.
|
||||
$vm->execute("for d in $gcovDir/nix/store/*/.build/linux-*; do for i in \$(cd \$d && find -name '*.gcda'); do echo \$i; mkdir -p $coverageDir/\$(dirname \$i); cp -v \$d/\$i $coverageDir/\$i; done; done");
|
||||
}
|
||||
});
|
||||
|
||||
if ($nrTests != 0) {
|
||||
$log->log("$nrSucceeded out of $nrTests tests succeeded",
|
||||
($nrSucceeded < $nrTests ? { error => 1 } : { }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Create an empty raw virtual disk with the given name and size (in
|
||||
# MiB).
|
||||
sub createDisk {
|
||||
my ($name, $size) = @_;
|
||||
system("qemu-img create -f raw $name ${size}M") == 0
|
||||
or die "cannot create image of size $size";
|
||||
}
|
||||
|
||||
|
||||
END {
|
||||
$log->nest("cleaning up", sub {
|
||||
foreach my $vm (values %vms) {
|
||||
if ($vm->{pid}) {
|
||||
$log->log("killing " . $vm->{name} . " (pid " . $vm->{pid} . ")");
|
||||
kill 9, $vm->{pid};
|
||||
}
|
||||
}
|
||||
});
|
||||
$log->close();
|
||||
}
|
||||
|
||||
|
||||
runTests;
|
||||
|
||||
exit ($nrSucceeded < $nrTests ? 1 : 0);
|
||||
@@ -0,0 +1,30 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
/* When a toggle is clicked, show or hide the subtree. */
|
||||
$(".logTreeToggle").click(function() {
|
||||
if ($(this).siblings("ul:hidden").length != 0) {
|
||||
$(this).siblings("ul").show();
|
||||
$(this).text("-");
|
||||
} else {
|
||||
$(this).siblings("ul").hide();
|
||||
$(this).text("+");
|
||||
}
|
||||
});
|
||||
|
||||
/* Implementation of the expand all link. */
|
||||
$(".logTreeExpandAll").click(function() {
|
||||
$(".logTreeToggle", $(this).parent().siblings(".toplevel")).map(function() {
|
||||
$(this).siblings("ul").show();
|
||||
$(this).text("-");
|
||||
});
|
||||
});
|
||||
|
||||
/* Implementation of the collapse all link. */
|
||||
$(".logTreeCollapseAll").click(function() {
|
||||
$(".logTreeToggle", $(this).parent().siblings(".toplevel")).map(function() {
|
||||
$(this).siblings("ul").hide();
|
||||
$(this).text("+");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
{ system, minimal ? false }:
|
||||
|
||||
with import ./build-vms.nix { inherit system minimal; };
|
||||
with pkgs;
|
||||
|
||||
rec {
|
||||
|
||||
inherit pkgs;
|
||||
|
||||
|
||||
testDriver = stdenv.mkDerivation {
|
||||
name = "nixos-test-driver";
|
||||
|
||||
buildInputs = [ makeWrapper perl ];
|
||||
|
||||
unpackPhase = "true";
|
||||
|
||||
installPhase =
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
cp ${./test-driver/test-driver.pl} $out/bin/nixos-test-driver
|
||||
chmod u+x $out/bin/nixos-test-driver
|
||||
|
||||
libDir=$out/lib/perl5/site_perl
|
||||
mkdir -p $libDir
|
||||
cp ${./test-driver/Machine.pm} $libDir/Machine.pm
|
||||
cp ${./test-driver/Logger.pm} $libDir/Logger.pm
|
||||
|
||||
wrapProgram $out/bin/nixos-test-driver \
|
||||
--prefix PATH : "${pkgs.qemu_kvm}/bin:${pkgs.vde2}/bin:${imagemagick}/bin:${coreutils}/bin" \
|
||||
--prefix PERL5LIB : "${lib.makePerlPath [ perlPackages.TermReadLineGnu perlPackages.XMLWriter perlPackages.IOTty ]}:$out/lib/perl5/site_perl"
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
# Run an automated test suite in the given virtual network.
|
||||
# `driver' is the script that runs the network.
|
||||
runTests = driver:
|
||||
stdenv.mkDerivation {
|
||||
name = "vm-test-run";
|
||||
|
||||
requiredSystemFeatures = [ "kvm" "nixos-test" ];
|
||||
|
||||
buildInputs = [ pkgs.libxslt ];
|
||||
|
||||
buildCommand =
|
||||
''
|
||||
mkdir -p $out/nix-support
|
||||
|
||||
LOGFILE=$out/log.xml tests='eval $ENV{testScript}; die $@ if $@;' ${driver}/bin/nixos-test-driver || failed=1
|
||||
|
||||
# Generate a pretty-printed log.
|
||||
xsltproc --output $out/log.html ${./test-driver/log2html.xsl} $out/log.xml
|
||||
ln -s ${./test-driver/logfile.css} $out/logfile.css
|
||||
ln -s ${./test-driver/treebits.js} $out/treebits.js
|
||||
|
||||
touch $out/nix-support/hydra-build-products
|
||||
echo "report testlog $out log.html" >> $out/nix-support/hydra-build-products
|
||||
|
||||
for i in */xchg/coverage-data; do
|
||||
mkdir -p $out/coverage-data
|
||||
mv $i $out/coverage-data/$(dirname $(dirname $i))
|
||||
done
|
||||
|
||||
[ -z "$failed" ] || touch $out/nix-support/failed
|
||||
''; # */
|
||||
};
|
||||
|
||||
|
||||
# Generate a coverage report from the coverage data produced by
|
||||
# runTests.
|
||||
makeReport = x: runCommand "report" { buildInputs = [rsync]; }
|
||||
''
|
||||
mkdir -p $TMPDIR/gcov/
|
||||
|
||||
for d in ${x}/coverage-data/*; do
|
||||
echo "doing $d"
|
||||
[ -n "$(ls -A "$d")" ] || continue
|
||||
|
||||
for i in $(cd $d/nix/store && ls); do
|
||||
if ! test -e $TMPDIR/gcov/nix/store/$i; then
|
||||
echo "copying $i"
|
||||
mkdir -p $TMPDIR/gcov/$(echo $i | cut -c34-)
|
||||
rsync -rv /nix/store/$i/.build/* $TMPDIR/gcov/
|
||||
fi
|
||||
done
|
||||
|
||||
chmod -R u+w $TMPDIR/gcov
|
||||
|
||||
find $TMPDIR/gcov -name "*.gcda" -exec rm {} \;
|
||||
|
||||
for i in $(cd $d/nix/store && ls); do
|
||||
rsync -rv $d/nix/store/$i/.build/* $TMPDIR/gcov/
|
||||
done
|
||||
|
||||
find $TMPDIR/gcov -name "*.gcda" -exec chmod 644 {} \;
|
||||
|
||||
echo "producing info..."
|
||||
${pkgs.lcov}/bin/geninfo --ignore-errors source,gcov $TMPDIR/gcov --output-file $TMPDIR/app.info
|
||||
cat $TMPDIR/app.info >> $TMPDIR/full.info
|
||||
done
|
||||
|
||||
echo "making report..."
|
||||
mkdir -p $out/coverage
|
||||
${pkgs.lcov}/bin/genhtml --show-details $TMPDIR/full.info -o $out/coverage
|
||||
cp $TMPDIR/full.info $out/coverage/
|
||||
|
||||
mkdir -p $out/nix-support
|
||||
cat ${x}/nix-support/hydra-build-products >> $out/nix-support/hydra-build-products
|
||||
echo "report coverage $out/coverage" >> $out/nix-support/hydra-build-products
|
||||
[ ! -e ${x}/nix-support/failed ] || touch $out/nix-support/failed
|
||||
''; # */
|
||||
|
||||
|
||||
makeTest = testFun: complete (call testFun);
|
||||
makeTests = testsFun: lib.mapAttrs (name: complete) (call testsFun);
|
||||
|
||||
apply = makeTest; # compatibility
|
||||
call = f: f { inherit pkgs system; };
|
||||
|
||||
complete = t: t // rec {
|
||||
nodes = buildVirtualNetwork (
|
||||
if t ? nodes then t.nodes else
|
||||
if t ? machine then { machine = t.machine; }
|
||||
else { } );
|
||||
|
||||
testScript =
|
||||
# Call the test script with the computed nodes.
|
||||
if builtins.isFunction t.testScript
|
||||
then t.testScript { inherit nodes; }
|
||||
else t.testScript;
|
||||
|
||||
vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes);
|
||||
|
||||
vms = map (m: m.config.system.build.vm) (lib.attrValues nodes);
|
||||
|
||||
# Generate onvenience wrappers for running the test driver
|
||||
# interactively with the specified network, and for starting the
|
||||
# VMs from the command line.
|
||||
driver = runCommand "nixos-test-driver"
|
||||
{ buildInputs = [ makeWrapper];
|
||||
inherit testScript;
|
||||
preferLocalBuild = true;
|
||||
}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
echo "$testScript" > $out/test-script
|
||||
ln -s ${testDriver}/bin/nixos-test-driver $out/bin/
|
||||
vms="$(for i in ${toString vms}; do echo $i/bin/run-*-vm; done)"
|
||||
wrapProgram $out/bin/nixos-test-driver \
|
||||
--add-flags "$vms" \
|
||||
--run "testScript=\"\$(cat $out/test-script)\"" \
|
||||
--set testScript '"$testScript"' \
|
||||
--set VLANS '"${toString vlans}"'
|
||||
ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms
|
||||
wrapProgram $out/bin/nixos-run-vms \
|
||||
--add-flags "$vms" \
|
||||
--set tests '"startAll; joinAll;"' \
|
||||
--set VLANS '"${toString vlans}"' \
|
||||
${lib.optionalString (builtins.length vms == 1) "--set USE_SERIAL 1"}
|
||||
''; # "
|
||||
|
||||
test = runTests driver;
|
||||
|
||||
report = makeReport test;
|
||||
};
|
||||
|
||||
|
||||
runInMachine =
|
||||
{ drv
|
||||
, machine
|
||||
, preBuild ? ""
|
||||
, postBuild ? ""
|
||||
, ... # ???
|
||||
}:
|
||||
let
|
||||
vm = buildVM { }
|
||||
[ machine
|
||||
{ key = "hostname"; networking.hostName = "client"; }
|
||||
];
|
||||
|
||||
buildrunner = writeText "vm-build" ''
|
||||
source $1
|
||||
|
||||
${coreutils}/bin/mkdir -p $TMPDIR
|
||||
cd $TMPDIR
|
||||
|
||||
$origBuilder $origArgs
|
||||
|
||||
exit $?
|
||||
'';
|
||||
|
||||
testscript = ''
|
||||
startAll;
|
||||
${preBuild}
|
||||
$client->succeed("env -i ${pkgs.bash}/bin/bash ${buildrunner} /tmp/xchg/saved-env >&2");
|
||||
${postBuild}
|
||||
'';
|
||||
|
||||
vmRunCommand = writeText "vm-run" ''
|
||||
${coreutils}/bin/mkdir $out
|
||||
${coreutils}/bin/mkdir -p vm-state-client/xchg
|
||||
export > vm-state-client/xchg/saved-env
|
||||
export tests='${testscript}'
|
||||
${testDriver}/bin/nixos-test-driver ${vm.config.system.build.vm}/bin/run-*-vm
|
||||
''; # */
|
||||
|
||||
in
|
||||
lib.overrideDerivation drv (attrs: {
|
||||
requiredSystemFeatures = [ "kvm" ];
|
||||
builder = "${bash}/bin/sh";
|
||||
args = ["-e" vmRunCommand];
|
||||
origArgs = attrs.args;
|
||||
origBuilder = attrs.builder;
|
||||
});
|
||||
|
||||
|
||||
runInMachineWithX = { require ? [], ... } @ args:
|
||||
let
|
||||
client =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
inherit require;
|
||||
virtualisation.memorySize = 1024;
|
||||
services.xserver.enable = true;
|
||||
services.xserver.displayManager.slim.enable = false;
|
||||
services.xserver.displayManager.auto.enable = true;
|
||||
services.xserver.windowManager.default = "icewm";
|
||||
services.xserver.windowManager.icewm.enable = true;
|
||||
services.xserver.desktopManager.default = "none";
|
||||
};
|
||||
in
|
||||
runInMachine ({
|
||||
machine = client;
|
||||
preBuild =
|
||||
''
|
||||
$client->waitForX;
|
||||
'';
|
||||
} // args);
|
||||
|
||||
|
||||
simpleTest = as: (makeTest ({ ... }: as)).test;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pkgs: with pkgs.lib;
|
||||
|
||||
rec {
|
||||
|
||||
# Escape a path according to the systemd rules, e.g. /dev/xyzzy
|
||||
# becomes dev-xyzzy. FIXME: slow.
|
||||
escapeSystemdPath = s:
|
||||
replaceChars ["/" "-" " "] ["-" "\\x2d" "\\x20"] (substring 1 (stringLength s) s);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user