]> xenbits.xensource.com Git - people/iwj/osstest.git/commitdiff
wip reorg move things into TestSupport
authorIan Jackson <iwj@woking.cam.xci-test.com>
Tue, 16 Oct 2012 14:06:42 +0000 (15:06 +0100)
committerIan Jackson <iwj@woking.cam.xci-test.com>
Tue, 16 Oct 2012 14:06:42 +0000 (15:06 +0100)
Osstest/Executive.pm
Osstest/TestSupport.pm

index d207a3ff1a44a27522f59614dfd04e2533890101..a8b9bbf75b4ad311fe5ad91265f12336f61f3d6c 100644 (file)
@@ -67,38 +67,13 @@ BEGIN {
     our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
     $VERSION     = 1.00;
     @ISA         = qw(Exporter);
-    @EXPORT      = qw(
-                      dbfl_check get_harness_rev grabrepolock_reexec
-                       findtask @all_lock_tables
+    @EXPORT      = qw(dbfl_check get_harness_rev grabrepolock_reexec
+                      findtask @all_lock_tables
                       tcpconnect_queuedaemon plan_search
                       alloc_resources alloc_resources_rollback_begin_work
                       resource_check_allocated resource_shared_mark_ready
-                      built_stash duration_estimator
-                      csreadconfig ts_get_host_guest
-                      opendb_state get_timeout
-                      host_involves_pcipassthrough host_get_pcipassthrough_devs
-                       link_file_contents create_webfile
-                      setup_pxeboot setup_pxeboot_local
-                      await_webspace_fetch_byleaf await_tcp
-                      remote_perl_script_open remote_perl_script_done sshopts
-                       host_pxedir 
-                      target_choose_vg target_umount_lv target_await_down
-                      target_ping_check_down target_ping_check_up
-                      target_kernkind_check target_kernkind_console_inittab
-                      target_var target_var_prefix
-                      selectguest prepareguest more_prepareguest_hvm
-                      guest_var guest_var_commalist
-                      prepareguest_part_lvmdisk prepareguest_part_xencfg
-                      guest_umount_lv guest_await guest_await_dhcp_tcp
-                      guest_checkrunning guest_check_ip guest_find_ether
-                      guest_find_domid guest_check_up guest_check_up_quick
-                      guest_get_state guest_await_reboot guest_destroy
-                      guest_vncsnapshot_begin guest_vncsnapshot_stash
-                     guest_check_remus_ok guest_editconfig
-                      dir_identify_vcs build_clone
-                      hg_dir_revision git_dir_revision vcs_dir_revision
-                      store_revision store_vcs_revision
-                      toolstack authorized_keys
+                      duration_estimator
+                      opendb_state
                       );
     %EXPORT_TAGS = ( );
 
@@ -116,21 +91,12 @@ augmentconfigdefaults(
     QueuePlanUpdateInterval => 300, # seconds
 );
 
-our %timeout= qw(RebootDown   100
-                 RebootUp     400
-                 HardRebootUp 600);
-
 #---------- configuration reader etc. ----------
 
 sub opendb_tests () {
     $dbh_tests ||= $mjobdb->open();
 }
 
-sub csreadconfig () {
-    readconfigonly();
-    opendb_tests();
-}
-
 sub grabrepolock_reexec {
     my (@org_argv) = @_;
     my $repos_lock= "$c{Repos}/lock";
@@ -158,18 +124,6 @@ sub get_harness_rev () {
     return $rev;
 }
 
-#---------- test script startup ----------
-
-sub ts_get_host_guest { # pass this @ARGV
-    my ($gn,$whhost) = reverse @_;
-    $whhost ||= 'host';
-    $gn ||= 'guest';
-
-    my $ho= selecthost($whhost);
-    my $gho= selectguest($gn,$ho);
-    return ($ho,$gho);
-}
-
 #---------- database access ----------#
 
 sub opendb_state () {
@@ -214,115 +168,6 @@ sub opendb ($) {
     return $dbh;
 }
 
-#---------- building, vcs's, etc. ----------
-
-sub build_clone ($$$$) {
-    my ($ho, $which, $builddir, $subdir) = @_;
-
-    need_runvars("tree_$which", "revision_$which");
-
-    my $tree= $r{"tree_$which"};
-    my $timeout= 4000;
-
-    my $vcs = $r{"treevcs_$which"};
-    if (defined $vcs) {
-    } elsif ($tree =~ m/\.hg$/) {
-        $vcs= 'hg';
-    } elsif ($tree =~ m/\.git$/) {
-        $vcs= 'git';
-    } else {
-        die "unknown vcs for $which $tree ";
-    }
-
-    if ($vcs eq 'hg') {
-        
-        target_cmd_build($ho, $timeout, $builddir, <<END.
-           hg clone '$tree' $subdir
-           cd $subdir
-END
-                         (length($r{"revision_$which"}) ? <<END : ''));
-           hg update '$r{"revision_$which"}'
-END
-    } elsif ($vcs eq 'git') {
-
-        target_cmd_build($ho, $timeout, $builddir, <<END.
-            git clone '$tree' $subdir
-            cd $subdir
-END
-                         (length($r{"revision_$which"}) ? <<END : ''));
-           git checkout '$r{"revision_$which"}'
-END
-    } else {
-        die "$vcs $which $tree ?";
-    }
-
-    my $rev= vcs_dir_revision($ho, "$builddir/$subdir", $vcs);
-    store_vcs_revision($which, $rev, $vcs);
-}
-
-sub dir_identify_vcs ($$) {
-    my ($ho,$dir) = @_;
-    return target_cmd_output($ho, <<END);
-        set -e
-        if ! test -e $dir; then echo none; exit 0; fi
-        cd $dir
-        (test -d .git && echo git) ||
-        (test -d .hg && echo hg) ||
-        (echo >&2 'unable to determine vcs'; fail)
-END
-}
-
-sub store_revision ($$$;$) {
-    my ($ho,$which,$dir,$optional) = @_;
-    my $vcs= dir_identify_vcs($ho,$dir);
-    return if $optional && $vcs eq 'none';
-    my $rev= vcs_dir_revision($ho,$dir,$vcs);
-    store_vcs_revision($which,$rev,$vcs);
-}
-
-sub store_vcs_revision ($$$) {
-    my ($which,$rev,$vcs) = @_;
-    store_runvar("built_vcs_$which", $vcs);
-    store_runvar("built_revision_$which", $rev);
-}
-
-sub built_stash ($$$$) {
-    my ($ho, $builddir, $distroot, $item) = @_;
-    target_cmd($ho, <<END, 300);
-       set -xe
-       cd $builddir
-        cd $distroot
-        tar zcf $builddir/$item.tar.gz *
-END
-    my $build= "build";
-    my $stashleaf= "$build/$item.tar.gz";
-    ensuredir("$stash/$build");
-    target_getfile($ho, 300,
-                   "$builddir/$item.tar.gz",
-                   "$stash/$stashleaf");
-    store_runvar("path_$item", $stashleaf);
-}
-
-sub vcs_dir_revision ($$$) {
-    my ($ho,$builddir,$vcs) = @_;
-    no strict qw(refs);
-    return &{"${vcs}_dir_revision"}($ho,$builddir);
-}
-
-sub hg_dir_revision ($$) {
-    my ($ho,$builddir) = @_;
-    my $rev= target_cmd_output($ho, "cd $builddir && hg identify -ni", 100);
-    $rev =~ m/^([0-9a-f]{10,}\+?) (\d+\+?)$/ or die "$builddir $rev ?";
-    return "$2:$1";
-}
-
-sub git_dir_revision ($$) {
-    my ($ho,$builddir) = @_;
-    my $rev= target_cmd_output($ho, "cd $builddir && git rev-parse HEAD^0");
-    $rev =~ m/^([0-9a-f]{10,})$/ or die "$builddir $rev ?";
-    return "$1";
-}
-
 #---------- host (and other resource) allocation ----------
 
 our $taskid;
@@ -870,822 +715,6 @@ END
     };
 }
 
-#---------- hosts and guests ----------
-
-sub get_hostflags ($) {
-    my ($ident) = @_;
-    # may be run outside transaction, or with flights locked
-    my $flags= get_runvar_default('all_hostflags',     $job, '').','.
-               get_runvar_default("${ident}_hostflags", $job, '');
-    return grep /./, split /\,/, $flags;
-}
-
-sub host_involves_pcipassthrough ($) {
-    my ($ho) = @_;
-    return !!grep m/^pcipassthrough\-/, get_hostflags($ho->{Ident});
-}
-
-sub host_get_pcipassthrough_devs ($) {
-    my ($ho) = @_;
-    my @devs;
-    foreach my $prop (values %{ $ho->{Properties} }) {
-        next unless $prop->{name} =~ m/^pcipassthrough (\w+)$/;
-        my $devtype= $1;
-        next unless grep { m/^pcipassthrough-$devtype$/ } get_hostflags($ho);
-        $prop->{val} =~ m,^([0-9a-f]+\:[0-9a-f]+\.\d+)/, or
-            die "$ho->{Ident} $prop->{val} ?";
-        push @devs, {
-            DevType => $devtype,
-            Bdf => $1,
-            Info => $' #'
-            };
-    }
-    return @devs;
-}
-
-sub get_timeout ($$$) {
-    my ($ho,$which,$default) = @_;
-    return $default + get_host_property($ho, "$which-time-extra", 0);
-}
-
-sub guest_find_tcpcheckport ($) {
-    my ($gho) = @_;
-    $gho->{TcpCheckPort}= $r{"$gho->{Guest}_tcpcheckport"};
-    $gho->{PingBroken}= $r{"$gho->{Guest}_pingbroken"};
-}
-
-sub selectguest ($$) {
-    my ($gn,$ho) = @_;
-    my $gho= {
-        Guest => $gn,
-        Name => $r{"${gn}_hostname"},
-        CfgPath => $r{"${gn}_cfgpath"},
-       Host => $ho,
-    };
-    foreach my $opt (guest_var_commalist($gho,'options')) {
-        $gho->{Options}{$opt}++;
-    }
-    guest_find_lv($gho);
-    guest_find_ether($gho);
-    guest_find_tcpcheckport($gho);
-    return $gho;
-}
-
-sub guest_find_lv ($) {
-    my ($gho) = @_;
-    my $gn= $gho->{Guest};
-    $gho->{Vg}= $r{"${gn}_vg"};
-    $gho->{Lv}= $r{"${gn}_disk_lv"};
-    $gho->{Lvdev}= (defined $gho->{Vg} && defined $gho->{Lv})
-        ? '/dev/'.$gho->{Vg}.'/'.$gho->{Lv} : undef;
-}
-
-sub guest_find_ether ($) {
-    my ($gho) = @_;
-    $gho->{Ether}= $r{"$gho->{Guest}_ether"};
-}
-
-sub report_once ($$$) {
-    my ($ho, $what, $msg) = @_;
-    my $k= "Lastmsg_$what";
-    return if defined($ho->{$k}) and $ho->{$k} eq $msg;
-    logm($msg);
-    $ho->{$k}= $msg;
-}
-
-sub guest_check_ip ($) {
-    my ($gho) = @_;
-
-    guest_find_ether($gho);
-
-    my $leases;
-    my $leasesfn = $gho->{DhcpLeases} || $gho->{Host}{DhcpLeases};
-
-    if ($leasesfn =~ m,/,) {
-       $leases= new IO::File $leasesfn, 'r';
-       if (!defined $leases) { return "open $leasesfn: $!"; }
-    } else {
-       $leases= new IO::Socket::INET(PeerAddr => $leasesfn);
-    }
-
-    my $lstash= "dhcpleases-$gho->{Guest}";
-    my $inlease;
-    my $props;
-    my $best;
-    my @warns;
-
-    my $copy= new IO::File "$stash/$lstash.new", 'w';
-    $copy or die "$lstash.new $!";
-
-    my $saveas= sub {
-        my ($fn,$keep) = @_;
-
-        while (<$leases>) { print $copy $_ or die $!; }
-        die $! unless $leases->eof;
-
-        my $rename= sub {
-            my ($src,$dst) = @_;
-            rename "$stash/$src", "$stash/$dst"
-                or $!==&ENOENT
-                or die "rename $fn.$keep $!";
-        };
-        while (--$keep>0) {
-            $rename->("$fn.$keep", "$fn.".($keep+1));
-        }
-        if ($keep>=0) {
-            die if $keep;
-            $rename->("$fn", "$fn.$keep");
-        }
-        $copy->close();
-        rename "$stash/$lstash.new", "$stash/$fn" or die "$lstash.new $fn $!";
-        logm("warning: $_") foreach grep { defined } @warns[0..5];
-        logm("$fn: rotated and stashed current leases");
-    };
-
-    my $badleases= sub {
-        my ($m) = @_;
-        $m= "$leasesfn:$.: unknown syntax";
-        $saveas->("$lstash.bad", 7);
-        return $m;
-    };
-
-    while (<$leases>) {
-        print $copy $_ or die $!;
-
-        chomp; s/^\s+//; s/\s+$//;
-        next if m/^\#/;  next unless m/\S/;
-        if (m/^lease\s+([0-9.]+)\s+\{$/) {
-            return $badleases->("lease inside lease") if defined $inlease;
-            $inlease= $1;
-            $props= { };
-            next;
-        }
-        if (!m/^\}$/) {
-            s/^( hardware \s+ ethernet |
-                 binding \s+ state
-               ) \s+//x
-               or
-            s/^( [-a-z0-9]+
-               ) \s+//x
-               or
-              return $badleases->("unknown syntax");
-            my $prop= $1;
-            s/\s*\;$// or return $badleases->("missing semicolon");
-            $props->{$prop}= $_;
-            next;
-        }
-        return $badleases->("end lease not inside lease")
-            unless defined $inlease;
-
-        $props->{' addr'}= $inlease;
-        undef $inlease;
-
-        # got a lease in $props
-
-        # ignore old leases
-        next if exists $props->{'binding state'} &&
-            lc $props->{'binding state'} ne 'active';
-
-        # ignore leases we don't understand
-        my @missing= grep { !defined $props->{$_} }
-            ('binding state', 'hardware ethernet', 'ends');
-        if (@missing) {
-            push @warns, "$leasesfn:$.: lease without \`$_'"
-                foreach @missing;
-            next;
-        }
-
-        # ignore leases for other hosts
-        next unless lc $props->{'hardware ethernet'} eq lc $gho->{Ether};
-
-        $props->{' ends'}= $props->{'ends'};
-        $props->{' ends'} =~
-            s/^[0-6]\s+(\S+)\s+(\d+)\:(\d+\:\d+)$/
-                sprintf "%s %02d:%s", $1,$2,$3 /e
-                or return $badleases->("unexpected syntax for ends");
-
-        next if $best &&
-            $best->{' ends'} gt $props->{' ends'};
-        $best= $props;
-    }
-
-    if (!$best) {
-        $saveas->("$lstash.nolease", 3);
-        return "no active lease";
-    }
-    $gho->{Ip}= $best->{' addr'};
-
-    report_once($gho, 'guest_check_ip', 
-               "guest $gho->{Name}: $gho->{Ether} $gho->{Ip}");
-    return undef;
-}
-
-sub guest_editconfig ($$$) {
-    my ($ho, $gho, $code) = @_;
-    target_editfile_root($ho, "$gho->{CfgPath}", sub {
-        while (<::EI>) {
-            $code->();
-            print ::EO or die $!;
-        }
-        die $! if ::EI->error;
-    });
-}
-
-sub guest_await_reboot ($$$) {
-    my ($ho,$gho, $timeout) = @_;
-    poll_loop($timeout, 30, "await reboot request from $gho->{Guest}", sub {
-        my $st= guest_get_state($ho,$gho);
-        return undef if $st eq 'sr';
-        fail("guest unexpectedly shutdown; state is '$st'")
-            if $st =~ m/^s/ || $st eq '';
-        return "guest state is $st";
-    });
-}
-
-sub guest_destroy ($$) {
-    my ($ho,$gho) = @_;
-    target_cmd_root($ho, toolstack()->{Command}." destroy $gho->{Name}", 40);
-}    
-
-sub target_choose_vg ($$) {
-    my ($ho, $mbneeded) = @_;
-    my $vgs= target_cmd_output_root($ho, 'vgdisplay --colon');
-    my $bestkb= 1.0e90;
-    my $bestvg;
-    foreach my $l (split /\n/, $vgs) {
-        $l =~ s/^\s+//; $l =~ s/\s+$//;
-        my @l= split /\:/, $l;
-        my $tvg= $l[0];
-        my $pesize= $l[12];
-        my $freepekb= $l[15];
-        my $tkb= $l[12] * 1.0 * $l[15];
-        if ($tkb < $mbneeded*1024.0) {
-            logm("vg $tvg ${tkb}kb free - too small");
-            next;
-        }
-        if ($tkb < $bestkb) {
-            $bestvg= $tvg;
-            $bestkb= $tkb;
-        }
-    }
-    die "no vg of sufficient size"
-        unless defined $bestvg;
-    logm("vg $bestvg ${bestkb}kb free - will use");
-    return $bestvg;
-}
-
-sub select_ether ($) {
-    my ($vn) = @_;
-    # must be run outside transaction
-    my $ether= $r{$vn};
-    return $ether if defined $ether;
-    my $prefix= sprintf "%s:%02x", $c{GenEtherPrefix}, $flight & 0xff;
-
-    db_retry($flight,'running', $dbh_tests,[qw(flights)], sub {
-        my $previous= $dbh_tests->selectrow_array(<<END, {}, $flight);
-            SELECT max(val) FROM runvars WHERE flight=?
-                AND name LIKE E'%\\_ether'
-                AND val LIKE '$prefix:%'
-END
-        if (defined $previous) {
-            $previous =~ m/^\w+:\w+:\w+:\w+:([0-9a-f]+):([0-9a-f]+)$/i
-                or die "$previous ?";
-            my $val= (hex($1)<<8) | hex($2);
-            $val++;  $val &= 0xffff;
-            $ether= sprintf "%s:%02x:%02x", $prefix, $val >> 8, $val & 0xff;
-            logm("select_ether $prefix:... $ether (previous $previous)");
-        } else {
-            $ether= "$prefix:00:01";
-            logm("select_ether $prefix:... $ether (first in flight)");
-        }
-        $dbh_tests->do(<<END, {}, $flight,$job,$vn,$ether);
-            INSERT INTO runvars VALUES (?,?,?,?,'t')
-END
-        my $chkrow= $dbh_tests->selectrow_hashref(<<END,{}, $flight);
-           SELECT val, count(*) FROM runvars WHERE flight=?
-                AND name LIKE E'%\\_ether'
-                AND val LIKE '$prefix:%'
-               GROUP BY val
-               HAVING count(*) <> 1
-               LIMIT 1
-END
-       die "$chkrow->{val} $chkrow->{count}" if $chkrow;
-    });
-    $r{$vn}= $ether;
-    return $ether;
-}
-
-sub guest_var ($$$) {
-    my ($gho, $runvartail, $default) = @_;
-    my $val= $r{ $gho->{Guest}."_".$runvartail };  return $val if defined $val;
-    $val= $r{ "guests_$runvartail" };              return $val if defined $val;
-    return $default;
-}
-
-sub guest_var_commalist ($$) {
-    my ($gho,$runvartail) = @_;
-    return split /\,/, guest_var($gho,$runvartail,'');
-}
-
-sub prepareguest ($$$$$$) {
-    my ($ho, $gn, $hostname, $tcpcheckport, $mb,
-        $boot_timeout) = @_;
-    # must be run outside transaction
-
-    # If we are passing through a nic, use its mac address not a generated one
-    my $ptnichostident= $r{"${gn}_pcipassthrough_nic"};
-    if (!$ptnichostident) {
-        select_ether("${gn}_ether");
-    } else {
-        my $ptnicho= selecthost($ptnichostident);
-        my $ptnicinfo= get_host_property($ptnicho,'pcipassthrough nic');
-        $ptnicinfo =~ m,/, or die "$ptnichostident $ptnicinfo ?";
-        my $ptether= $'; #'
-        $r{"${gn}_ether"}= $ptether;
-        logm("passthrough nic from $ptnichostident ether $ptether");
-    }
-
-    store_runvar("${gn}_hostname", $hostname);
-    store_runvar("${gn}_disk_lv", $r{"${gn}_hostname"}.'-disk');
-    store_runvar("${gn}_tcpcheckport", $tcpcheckport);
-    store_runvar("${gn}_boot_timeout", $boot_timeout);
-
-    my $gho= selectguest($gn, $ho);
-    store_runvar("${gn}_domname", $gho->{Name});
-
-    store_runvar("${gn}_vg", '');
-    if (!length $r{"${gn}_vg"}) {
-        store_runvar("${gn}_vg", target_choose_vg($ho, $mb));
-    }
-
-    guest_find_lv($gho);
-    guest_find_ether($gho);
-    guest_find_tcpcheckport($gho);
-    return $gho;
-}
-
-sub prepareguest_part_lvmdisk ($$$) {
-    my ($ho, $gho, $disk_mb) = @_;
-    target_cmd_root($ho, "lvremove -f $gho->{Lvdev} ||:");
-    target_cmd_root($ho, "lvcreate -L ${disk_mb}M -n $gho->{Lv} $gho->{Vg}");
-    target_cmd_root($ho, "dd if=/dev/zero of=$gho->{Lvdev} count=10");
-}    
-
-sub prepareguest_part_xencfg ($$$$$) {
-    my ($ho, $gho, $ram_mb, $xopts, $cfgrest) = @_;
-    my $onreboot= $xopts->{OnReboot} || 'restart';
-    my $vcpus= guest_var($gho, 'vcpus', $xopts->{DefVcpus} || 2);
-    my $xoptcfg= $xopts->{ExtraConfig};
-    $xoptcfg='' unless defined $xoptcfg;
-    my $xencfg= <<END;
-name        = '$gho->{Name}'
-memory = ${ram_mb}
-vif         = [ 'type=ioemu,mac=$gho->{Ether}' ]
-#
-on_poweroff = 'destroy'
-on_reboot   = '$onreboot'
-on_crash    = 'preserve'
-#
-vcpus = $vcpus
-#
-$cfgrest
-#
-$xoptcfg
-END
-
-    my $cfgpath= "/etc/xen/$gho->{Name}.cfg";
-    store_runvar("$gho->{Guest}_cfgpath", "$cfgpath");
-    $gho->{CfgPath}= $cfgpath;
-
-    target_putfilecontents_root_stash($ho,30,$xencfg, $cfgpath);
-
-    return $cfgpath;
-}
-
-sub more_prepareguest_hvm ($$$$;@) {
-    my ($ho, $gho, $ram_mb, $disk_mb, %xopts) = @_;
-    
-    my $passwd= 'xenvnc';
-
-    prepareguest_part_lvmdisk($ho, $gho, $disk_mb);
-    
-    my $specimage= $r{"$gho->{Guest}_image"};
-    die "$gho->{Guest} ?" unless $specimage;
-    my $limage= $specimage =~ m,^/, ? $specimage : "$c{Images}/$specimage";
-    $gho->{Rimage}= "/root/$flight.$job.".basename($specimage);
-    target_putfile_root($ho, 1000, $limage,$gho->{Rimage}, '-p');
-
-    my $postimage_hook= $xopts{PostImageHook};
-    $postimage_hook->() if $postimage_hook;
-
-    my $cfg = <<END;
-kernel      = 'hvmloader'
-builder     = 'hvm'
-#
-disk        = [
-            'phy:$gho->{Lvdev},hda,w',
-            'file:$gho->{Rimage},hdc:cdrom,r'
-            ]
-#
-usb=1
-usbdevice='tablet'
-#
-#stdvga=1
-keymap='en-gb';
-#
-sdl=0
-opengl=0
-vnc=1
-vncunused=1
-vncdisplay=0
-vnclisten='$ho->{Ip}'
-vncpasswd='$passwd'
-
-serial='file:/dev/stderr'
-#
-boot = 'dc'
-END
-
-    my $devmodel = $r{'device_model_version'};
-    if (defined $devmodel) {
-        $cfg .= "device_model_version='$devmodel'\n";
-    }
-
-    my $cfgpath= prepareguest_part_xencfg($ho, $gho, $ram_mb, \%xopts, $cfg);
-    target_cmd_root($ho, <<END);
-        (echo $passwd; echo $passwd) | vncpasswd $gho->{Guest}.vncpw
-END
-
-    return $cfgpath;
-}
-
-sub guest_check_via_ssh ($) {
-    my ($gho) = @_;
-    return $r{"$gho->{Guest}_tcpcheckport"} == 22;
-}
-
-sub guest_check_up_quick ($) {
-    my ($gho) = @_;
-    if (guest_check_via_ssh($gho)) {
-       target_cmd_root($gho, "date");
-    } else {
-       target_ping_check_up($gho);
-    }
-}
-
-sub guest_check_up ($) {
-    my ($gho) = @_;
-    guest_await_dhcp_tcp($gho,20);
-    target_ping_check_up($gho);
-    target_cmd_root($gho, "echo guest $gho->{Name}: ok")
-        if guest_check_via_ssh($gho);
-}
-
-sub guest_get_state ($$) {
-    my ($ho,$gho) = @_;
-    my $domains= target_cmd_output_root($ho, toolstack()->{Command}." list");
-    $domains =~ s/^Name.*\n//;
-    foreach my $l (split /\n/, $domains) {
-        $l =~ m/^(\S+) (?: \s+ \d+ ){3} \s+ ([-a-z]+) \s/x or die "$l ?";
-        next unless $1 eq $gho->{Name};
-        my $st= $2;
-        $st =~ s/\-//g;
-        $st='-' if !length $st;
-        logm("guest $gho->{Name} state is $st");
-        return $st;
-    }
-    logm("guest $gho->{Name} not present on this host");
-    return '';
-}
-
-our $guest_state_running_re= '[-rb]+';
-
-sub guest_checkrunning ($$) {
-    my ($ho,$gho) = @_;
-    my $s= guest_get_state($ho,$gho);
-    return $s =~ m/^$guest_state_running_re$/o;
-}
-
-sub guest_await_dhcp_tcp ($$) {
-    my ($gho,$timeout) = @_;
-    guest_find_tcpcheckport($gho);
-    poll_loop($timeout,1,
-              "guest $gho->{Name} $gho->{Ether} $gho->{TcpCheckPort}".
-              " link/ip/tcp",
-              sub {
-        my $err= guest_check_ip($gho);
-        return $err if defined $err;
-
-        return
-            ($gho->{PingBroken} ? undef : target_ping_check_up($gho))
-            ||
-            target_tcp_check($gho,5)
-            ||
-            undef;
-    });
-}
-
-sub guest_check_remus_ok {
-    my ($gho, @hos) = @_;
-    my @sts;
-    logm("remus check $gho->{Name}...");
-    foreach my $ho (@hos) {
-       my $st;
-       if (!eval {
-           $st= guest_get_state($ho, $gho)
-        }) {
-           $st= '_';
-           logm("could not get guest $gho->{Name} state on $ho->{Name}: $@");
-       }
-       push @sts, [ $ho, $st ];
-    }
-    my @ststrings= map { $_->[1] } @sts;
-    my $compound= join ',', @ststrings;
-    my $msg= "remus check $gho->{Name}: result \"$compound\":";
-    $msg .= " $_->[0]{Name}=$_->[1]" foreach @sts;
-    logm($msg);
-    my $runnings= scalar grep { m/$guest_state_running_re/o } @ststrings;
-    die "running on multiple hosts $compound" if $runnings > 1;
-    die "not running anywhere $compound" unless $runnings;
-    die "crashed somewhere $compound" if grep { m/c/ } @ststrings;
-}
-
-sub file_link_contents ($$) {
-    my ($fn, $contents) = @_;
-    # $contents as for file_write_contents
-    my ($dir, $base, $ext) =
-        $fn =~ m,^( (?: .*/ )? )( [^/]+? )( (?: \.[^./]+ )? )$,x
-        or die "$fn ?";
-    my $real= "$dir$base--osstest$ext";
-    my $linktarg= "$base--osstest$ext";
-
-    file_simple_write_contents($real, $contents);
-
-    my $newlink= "$dir$base--newlink$ext";
-
-    if (!lstat "$fn") {
-        $!==&ENOENT or die "$fn $!";
-    } elsif (!-l _) {
-        die "$fn not a symlink";
-        unlink $fn or die "$fn $!";
-    }
-    symlink $linktarg, $newlink or die "$newlink $!";
-    rename $newlink, $fn or die "$newlink $fn $!";
-    logm("wrote $fn");
-}
-
-sub host_pxedir ($) {
-    my ($ho) = @_;
-    my $dir= $ho->{Ether};
-    $dir =~ y/A-Z/a-z/;
-    $dir =~ y/0-9a-f//cd;
-    length($dir)==12 or die "$dir";
-    $dir =~ s/../$&-/g;
-    $dir =~ s/\-$//;
-    return $dir;
-}
-
-sub setup_pxeboot ($$) {
-    my ($ho, $bootfile) = @_;
-    my $dir= host_pxedir($ho);
-    file_link_contents($c{Tftp}."/$dir/pxelinux.cfg", $bootfile);
-}
-
-sub setup_pxeboot_local ($) {
-    my ($ho) = @_;
-    setup_pxeboot($ho, <<END);
-serial 0 $c{Baud}
-timeout 5
-label local
-       LOCALBOOT 0
-default local
-END
-}
-
-sub target_umount_lv ($$$) {
-    my ($ho,$vg,$lv) = @_;
-    my $dev= "/dev/$vg/$lv";
-    for (;;) {
-       my $link= target_cmd_output_root($ho, "readlink $dev");
-       return if $link =~ m,^/dev/nbd,; # can't tell if it's open
-        $lv= target_cmd_output_root($ho, "lvdisplay --colon $dev");
-        $lv =~ s/^\s+//;  $lv =~ s/\s+$//;
-        my @lv = split /:/, $lv;
-        die "@lv ?" unless $lv[0] eq $dev;
-        return unless $lv[5]; # "open"
-        logm("lvdisplay output says device is still open: $lv");
-        target_cmd_root($ho, "umount $dev");
-    }
-}
-
-sub guest_umount_lv ($$) {
-    my ($ho,$gho) = @_;
-    target_umount_lv($ho, $gho->{Vg}, $gho->{Lv});
-}
-
-sub await_webspace_fetch_byleaf ($$$$$) {
-    my ($maxwait,$interval,$logtailer, $ho, $url) = @_;
-    my $leaf= $url;
-    $leaf =~ s,.*/,,;
-    poll_loop($maxwait,$interval, "fetch $leaf", sub {
-        my ($line, $last);
-        $last= '(none)';
-        while (defined($line= $logtailer->getline())) {
-            my ($ip, $got) = $line =~
-                m,^([0-9.]+) \S+ \S+ \[[^][]+\] \"GET \S*/(\S+) ,
-                or next;
-            next unless $ip eq $ho->{Ip};
-            $last= $got;
-            next unless $got eq $leaf;
-            return undef;
-        }
-        return $last;
-    });
-}
-
-sub target_tcp_check ($$) {
-    my ($ho,$interval) = @_;
-    my $ncout= `nc -n -v -z -w $interval $ho->{Ip} $ho->{TcpCheckPort} 2>&1`;
-    return undef if !$?;
-    $ncout =~ s/\n/ | /g;
-    return "nc: $? $ncout";
-}
-
-sub await_tcp ($$$) {
-    my ($maxwait,$interval,$ho) = @_;
-    poll_loop($maxwait,$interval,
-              "await tcp $ho->{Name} $ho->{TcpCheckPort}",
-              sub {
-        return target_tcp_check($ho,$interval);
-    });
-}
-
-sub guest_await ($$) {
-    my ($gho,$dhcpwait) = @_;
-    guest_await_dhcp_tcp($gho,$dhcpwait);
-    target_cmd_root($gho, "echo guest $gho->{Name}: ok")
-        if guest_check_via_ssh($gho);
-    return $gho;
-}
-
-sub create_webfile ($$$) {
-    my ($ho, $tail, $contents) = @_; # $contents as for file_link_contents
-    my $wf_common= $c{WebspaceCommon}.$ho->{Name}."_".$tail;
-    my $wf_url= $c{WebspaceUrl}.$wf_common;
-    my $wf_file= $c{WebspaceFile}.$wf_common;
-    file_link_contents($wf_file, $contents);
-    return $wf_url;
-}
-
-sub target_var_prefix ($) {
-    my ($ho) = @_;
-    if (exists $ho->{Guest}) { return $ho->{Guest}.'_'; }
-    return '';
-}
-
-sub target_var ($$) {
-    my ($ho,$vn) = @_;
-    return $r{ target_var_prefix($ho). $vn };
-}
-
-sub target_kernkind_check ($) {
-    my ($gho) = @_;
-    my $pfx= target_var_prefix($gho);
-    my $kernkind= $r{$pfx."kernkind"};
-    my $isguest= exists $gho->{Guest};
-    if ($kernkind eq 'pvops') {
-        store_runvar($pfx."rootdev", 'xvda') if $isguest;
-        store_runvar($pfx."console", 'hvc0');
-    } elsif ($kernkind !~ m/2618/) {
-        store_runvar($pfx."console", 'xvc0') if $isguest;
-    }
-}
-
-sub target_kernkind_console_inittab ($$$) {
-    my ($ho, $gho, $root) = @_;
-
-    my $inittabpath= "$root/etc/inittab";
-    my $console= target_var($gho,'console');
-
-    if (defined $console && length $console) {
-        target_cmd_root($ho, <<END);
-            set -ex
-            perl -i~ -ne "
-                next if m/^xc:/;
-                print \\\$_ or die \\\$!;
-                next unless s/^1:/xc:/;
-                s/tty1/$console/;
-                print \\\$_ or die \\\$!;
-            " $inittabpath
-END
-    }
-    return $console;
-}
-
-sub target_extract_jobdistpath ($$$$$) {
-    my ($ho, $part, $path, $job, $distpath) = @_;
-    $distpath->{$part}= get_stashed($path, $job);
-    my $local= $path;  $local =~ s/path_//;
-    my $distcopy= "/root/extract_$local.tar.gz";
-    target_putfile_root($ho, 300, $distpath->{$part}, $distcopy);
-    target_cmd_root($ho, "cd / && tar zxf $distcopy", 300);
-}
-
-sub guest_find_domid ($$) {
-    my ($ho,$gho) = @_;
-    return if defined $gho->{Domid};
-    my $list= target_cmd_output_root($ho,
-                toolstack()->{Command}." list $gho->{Name}");
-    $list =~ m/^(?!Name\s)(\S+)\s+(\d+)\s+(\d+)+(\d+)\s.*$/m
-        or die "domain list: $list";
-    $1 eq $gho->{Name} or die "domain list name $1 expected $gho->{Name}";
-    $gho->{MemUsed}= $3;
-    $gho->{Vcpus}= $4;
-    return $gho->{Domid}= $2;
-}
-
-sub guest_vncsnapshot_begin ($$) {
-    my ($ho,$gho) = @_;
-    my $domid= $gho->{Domid};
-
-    my $backend= target_cmd_output_root($ho,
-        "xenstore-read /local/domain/$domid/device/vfb/0/backend ||:");
-
-    if ($backend eq '') {
-        my $port= target_cmd_output_root($ho,
-            "xenstore-read /local/domain/$domid/console/vnc-port");
-        $port =~ m/^\d+/ && $port >= 5900 or die "$port ?";
-        return {
-            vnclisten => $ho->{Ip},
-            vncdisplay => $port-5900,
-        };
-    }
-
-    $backend =~ m,^/local/domain/\d+/backend/vfb/\d+/\d+$,
-        or die "$backend ?";
-
-    my $v = {};
-    foreach my $k (qw(vnclisten vncdisplay)) {
-        $v->{$k}= target_cmd_output_root($ho,
-                "xenstore-read $backend/$k");
-    }
-    return $v;
-}
-sub guest_vncsnapshot_stash ($$$$) {
-    my ($ho,$gho,$v,$leaf) = @_;
-    my $rfile= "/root/$leaf";
-    target_cmd_root($ho,
-        "vncsnapshot -passwd $gho->{Guest}.vncpw".
-                   " -nojpeg -allowblank".
-                   " $v->{vnclisten}:$v->{vncdisplay}".
-                   " $rfile", 100);
-    target_getfile_root($ho,100, "$rfile", "$stash/$leaf");
-}
-
-our %toolstacks=
-    ('xend' => {
-        NewDaemons => [qw(xend)],
-        OldDaemonInitd => 'xend',
-        Command => 'xm',
-        CfgPathVar => 'cfgpath',
-        Dom0MemFixed => 1,
-        },
-     'xl' => {
-        NewDaemons => [],
-        Dom0MemFixed => 1,
-        Command => 'xl',
-        CfgPathVar => 'cfgpath',
-       RestoreNeedsConfig => 1,
-        }
-     );
-
-sub toolstack () {
-    my $tsname= $r{toolstack};
-    $tsname= 'xend' if !defined $tsname;
-    my $ts= $toolstacks{$tsname};
-    die "$tsname ?" unless defined $ts;
-    if (!exists $ts->{Name}) {
-        logm("toolstack $tsname");
-        $ts->{Name}= $tsname;
-    }
-    return $ts;
-}
-
-sub authorized_keys () {
-    my $authkeys= '';
-    my @akf= map {
-        "$ENV{'HOME'}/.ssh/$_"
-        } qw(authorized_keys id_dsa.pub id_rsa.pub);
-    push @akf, split ':', $c{AuthorizedKeysFiles};
-    push @akf, $c{TestHostKeypairPath}.'.pub';
-    foreach my $akf (@akf) {
-        next unless $akf =~ m/\S/;
-        $authkeys .= get_filecontents($akf, "# $akf ENOENT\n"). "\n";
-    }
-    $authkeys .= $c{AuthorizedKeysAppend};
-    return $authkeys;
-}
-
 #---------- logtailer ----------
 
 package Osstest::Logtailer;
index b3bb0569decddc943d1b58ac04ffab93eceff0ff..4587e6baad010a4f50d6281dafe324138c15d2a4 100644 (file)
@@ -18,14 +18,12 @@ BEGIN {
     @ISA         = qw(Exporter);
     @EXPORT      = qw(
                       tsreadconfig %r $flight $job $stash
+                      ts_get_host_guest
 
-fail logm
-                      broken                      $logm_handle
-
-
-store_runvar get_runvar get_runvar_maybe get_runvar_default need_runvars
- flight_otherjob
+                      fail broken logm $logm_handle
 
+                      store_runvar get_runvar get_runvar_maybe
+                      get_runvar_default need_runvars flight_otherjob
                       unique_incrementing_runvar 
 
                       target_cmd_root target_cmd target_cmd_build
@@ -37,19 +35,41 @@ store_runvar get_runvar get_runvar_maybe get_runvar_default need_runvars
                       target_editfile_root target_file_exists
                       target_install_packages target_install_packages_norec
                       target_extract_jobdistpath
-poll_loop
 
-selecthost get_hostflags
-                      get_host_property 
+                      poll_loop tcpconnect await_tcp
+                      contents_make_cpio file_simple_write_contents
 
-power_state power_cycle power_cycle_time
+                      selecthost get_hostflags get_host_property
+                      power_state power_cycle power_cycle_time
          
                       get_stashed open_unique_stashfile
-
-host_reboot target_reboot target_reboot_hard            
-tcpconnect
-                      contents_make_cpio file_simple_write_contents
+                      dir_identify_vcs build_clone built_stash 
+                      hg_dir_revision git_dir_revision vcs_dir_revision
+                      store_revision store_vcs_revision
+
+                      sshopts authorized_keys
+                      remote_perl_script_open remote_perl_script_done
+                      host_reboot target_reboot target_reboot_hard            
+                      target_choose_vg target_umount_lv target_await_down
+
+                      target_ping_check_down target_ping_check_up
+                      target_kernkind_check target_kernkind_console_inittab
+                      target_var target_var_prefix
+                      selectguest prepareguest more_prepareguest_hvm
+                      guest_var guest_var_commalist
+                      prepareguest_part_lvmdisk prepareguest_part_xencfg
+                      guest_umount_lv guest_await guest_await_dhcp_tcp
+                      guest_checkrunning guest_check_ip guest_find_ether
+                      guest_find_domid guest_check_up guest_check_up_quick
+                      guest_get_state guest_await_reboot guest_destroy
+                      guest_vncsnapshot_begin guest_vncsnapshot_stash
+                     guest_check_remus_ok guest_editconfig
+                      host_involves_pcipassthrough host_get_pcipassthrough_devs
+                      toolstack
+
+                      await_webspace_fetch_byleaf create_webfile
+                      file_link_contents get_timeout
+                      setup_pxeboot setup_pxeboot_local host_pxedir 
                       );
     %EXPORT_TAGS = ( );
 
@@ -58,6 +78,10 @@ tcpconnect
 
 our (%r,$flight,$job,$stash);
 
+our %timeout= qw(RebootDown   100
+                 RebootUp     400
+                 HardRebootUp 600);
+
 our $logm_handle= new IO::File ">& STDERR" or die $!;
 
 #---------- test script startup ----------
@@ -103,6 +127,18 @@ END
     } or warn $@;
 }
 
+#---------- test script startup ----------
+
+sub ts_get_host_guest { # pass this @ARGV
+    my ($gn,$whhost) = reverse @_;
+    $whhost ||= 'host';
+    $gn ||= 'guest';
+
+    my $ho= selecthost($whhost);
+    my $gho= selectguest($gn,$ho);
+    return ($ho,$gho);
+}
+
 #---------- general ----------
 
 sub logm ($) {
@@ -784,4 +820,933 @@ sub file_simple_write_contents ($$) {
     close $flc or die "$real $!";
 }
 
+#---------- building, vcs's, etc. ----------
+
+sub build_clone ($$$$) {
+    my ($ho, $which, $builddir, $subdir) = @_;
+
+    need_runvars("tree_$which", "revision_$which");
+
+    my $tree= $r{"tree_$which"};
+    my $timeout= 4000;
+
+    my $vcs = $r{"treevcs_$which"};
+    if (defined $vcs) {
+    } elsif ($tree =~ m/\.hg$/) {
+        $vcs= 'hg';
+    } elsif ($tree =~ m/\.git$/) {
+        $vcs= 'git';
+    } else {
+        die "unknown vcs for $which $tree ";
+    }
+
+    if ($vcs eq 'hg') {
+        
+        target_cmd_build($ho, $timeout, $builddir, <<END.
+           hg clone '$tree' $subdir
+           cd $subdir
+END
+                         (length($r{"revision_$which"}) ? <<END : ''));
+           hg update '$r{"revision_$which"}'
+END
+    } elsif ($vcs eq 'git') {
+
+        target_cmd_build($ho, $timeout, $builddir, <<END.
+            git clone '$tree' $subdir
+            cd $subdir
+END
+                         (length($r{"revision_$which"}) ? <<END : ''));
+           git checkout '$r{"revision_$which"}'
+END
+    } else {
+        die "$vcs $which $tree ?";
+    }
+
+    my $rev= vcs_dir_revision($ho, "$builddir/$subdir", $vcs);
+    store_vcs_revision($which, $rev, $vcs);
+}
+
+sub dir_identify_vcs ($$) {
+    my ($ho,$dir) = @_;
+    return target_cmd_output($ho, <<END);
+        set -e
+        if ! test -e $dir; then echo none; exit 0; fi
+        cd $dir
+        (test -d .git && echo git) ||
+        (test -d .hg && echo hg) ||
+        (echo >&2 'unable to determine vcs'; fail)
+END
+}
+
+sub store_revision ($$$;$) {
+    my ($ho,$which,$dir,$optional) = @_;
+    my $vcs= dir_identify_vcs($ho,$dir);
+    return if $optional && $vcs eq 'none';
+    my $rev= vcs_dir_revision($ho,$dir,$vcs);
+    store_vcs_revision($which,$rev,$vcs);
+}
+
+sub store_vcs_revision ($$$) {
+    my ($which,$rev,$vcs) = @_;
+    store_runvar("built_vcs_$which", $vcs);
+    store_runvar("built_revision_$which", $rev);
+}
+
+sub built_stash ($$$$) {
+    my ($ho, $builddir, $distroot, $item) = @_;
+    target_cmd($ho, <<END, 300);
+       set -xe
+       cd $builddir
+        cd $distroot
+        tar zcf $builddir/$item.tar.gz *
+END
+    my $build= "build";
+    my $stashleaf= "$build/$item.tar.gz";
+    ensuredir("$stash/$build");
+    target_getfile($ho, 300,
+                   "$builddir/$item.tar.gz",
+                   "$stash/$stashleaf");
+    store_runvar("path_$item", $stashleaf);
+}
+
+sub vcs_dir_revision ($$$) {
+    my ($ho,$builddir,$vcs) = @_;
+    no strict qw(refs);
+    return &{"${vcs}_dir_revision"}($ho,$builddir);
+}
+
+sub hg_dir_revision ($$) {
+    my ($ho,$builddir) = @_;
+    my $rev= target_cmd_output($ho, "cd $builddir && hg identify -ni", 100);
+    $rev =~ m/^([0-9a-f]{10,}\+?) (\d+\+?)$/ or die "$builddir $rev ?";
+    return "$2:$1";
+}
+
+sub git_dir_revision ($$) {
+    my ($ho,$builddir) = @_;
+    my $rev= target_cmd_output($ho, "cd $builddir && git rev-parse HEAD^0");
+    $rev =~ m/^([0-9a-f]{10,})$/ or die "$builddir $rev ?";
+    return "$1";
+}
+
+#---------- hosts and guests ----------
+
+sub get_hostflags ($) {
+    my ($ident) = @_;
+    # may be run outside transaction, or with flights locked
+    my $flags= get_runvar_default('all_hostflags',     $job, '').','.
+               get_runvar_default("${ident}_hostflags", $job, '');
+    return grep /./, split /\,/, $flags;
+}
+
+sub host_involves_pcipassthrough ($) {
+    my ($ho) = @_;
+    return !!grep m/^pcipassthrough\-/, get_hostflags($ho->{Ident});
+}
+
+sub host_get_pcipassthrough_devs ($) {
+    my ($ho) = @_;
+    my @devs;
+    foreach my $prop (values %{ $ho->{Properties} }) {
+        next unless $prop->{name} =~ m/^pcipassthrough (\w+)$/;
+        my $devtype= $1;
+        next unless grep { m/^pcipassthrough-$devtype$/ } get_hostflags($ho);
+        $prop->{val} =~ m,^([0-9a-f]+\:[0-9a-f]+\.\d+)/, or
+            die "$ho->{Ident} $prop->{val} ?";
+        push @devs, {
+            DevType => $devtype,
+            Bdf => $1,
+            Info => $' #'
+            };
+    }
+    return @devs;
+}
+
+sub get_timeout ($$$) {
+    my ($ho,$which,$default) = @_;
+    return $default + get_host_property($ho, "$which-time-extra", 0);
+}
+
+sub guest_find_tcpcheckport ($) {
+    my ($gho) = @_;
+    $gho->{TcpCheckPort}= $r{"$gho->{Guest}_tcpcheckport"};
+    $gho->{PingBroken}= $r{"$gho->{Guest}_pingbroken"};
+}
+
+sub selectguest ($$) {
+    my ($gn,$ho) = @_;
+    my $gho= {
+        Guest => $gn,
+        Name => $r{"${gn}_hostname"},
+        CfgPath => $r{"${gn}_cfgpath"},
+       Host => $ho,
+    };
+    foreach my $opt (guest_var_commalist($gho,'options')) {
+        $gho->{Options}{$opt}++;
+    }
+    guest_find_lv($gho);
+    guest_find_ether($gho);
+    guest_find_tcpcheckport($gho);
+    return $gho;
+}
+
+sub guest_find_lv ($) {
+    my ($gho) = @_;
+    my $gn= $gho->{Guest};
+    $gho->{Vg}= $r{"${gn}_vg"};
+    $gho->{Lv}= $r{"${gn}_disk_lv"};
+    $gho->{Lvdev}= (defined $gho->{Vg} && defined $gho->{Lv})
+        ? '/dev/'.$gho->{Vg}.'/'.$gho->{Lv} : undef;
+}
+
+sub guest_find_ether ($) {
+    my ($gho) = @_;
+    $gho->{Ether}= $r{"$gho->{Guest}_ether"};
+}
+
+sub report_once ($$$) {
+    my ($ho, $what, $msg) = @_;
+    my $k= "Lastmsg_$what";
+    return if defined($ho->{$k}) and $ho->{$k} eq $msg;
+    logm($msg);
+    $ho->{$k}= $msg;
+}
+
+sub guest_check_ip ($) {
+    my ($gho) = @_;
+
+    guest_find_ether($gho);
+
+    my $leases;
+    my $leasesfn = $gho->{DhcpLeases} || $gho->{Host}{DhcpLeases};
+
+    if ($leasesfn =~ m,/,) {
+       $leases= new IO::File $leasesfn, 'r';
+       if (!defined $leases) { return "open $leasesfn: $!"; }
+    } else {
+       $leases= new IO::Socket::INET(PeerAddr => $leasesfn);
+    }
+
+    my $lstash= "dhcpleases-$gho->{Guest}";
+    my $inlease;
+    my $props;
+    my $best;
+    my @warns;
+
+    my $copy= new IO::File "$stash/$lstash.new", 'w';
+    $copy or die "$lstash.new $!";
+
+    my $saveas= sub {
+        my ($fn,$keep) = @_;
+
+        while (<$leases>) { print $copy $_ or die $!; }
+        die $! unless $leases->eof;
+
+        my $rename= sub {
+            my ($src,$dst) = @_;
+            rename "$stash/$src", "$stash/$dst"
+                or $!==&ENOENT
+                or die "rename $fn.$keep $!";
+        };
+        while (--$keep>0) {
+            $rename->("$fn.$keep", "$fn.".($keep+1));
+        }
+        if ($keep>=0) {
+            die if $keep;
+            $rename->("$fn", "$fn.$keep");
+        }
+        $copy->close();
+        rename "$stash/$lstash.new", "$stash/$fn" or die "$lstash.new $fn $!";
+        logm("warning: $_") foreach grep { defined } @warns[0..5];
+        logm("$fn: rotated and stashed current leases");
+    };
+
+    my $badleases= sub {
+        my ($m) = @_;
+        $m= "$leasesfn:$.: unknown syntax";
+        $saveas->("$lstash.bad", 7);
+        return $m;
+    };
+
+    while (<$leases>) {
+        print $copy $_ or die $!;
+
+        chomp; s/^\s+//; s/\s+$//;
+        next if m/^\#/;  next unless m/\S/;
+        if (m/^lease\s+([0-9.]+)\s+\{$/) {
+            return $badleases->("lease inside lease") if defined $inlease;
+            $inlease= $1;
+            $props= { };
+            next;
+        }
+        if (!m/^\}$/) {
+            s/^( hardware \s+ ethernet |
+                 binding \s+ state
+               ) \s+//x
+               or
+            s/^( [-a-z0-9]+
+               ) \s+//x
+               or
+              return $badleases->("unknown syntax");
+            my $prop= $1;
+            s/\s*\;$// or return $badleases->("missing semicolon");
+            $props->{$prop}= $_;
+            next;
+        }
+        return $badleases->("end lease not inside lease")
+            unless defined $inlease;
+
+        $props->{' addr'}= $inlease;
+        undef $inlease;
+
+        # got a lease in $props
+
+        # ignore old leases
+        next if exists $props->{'binding state'} &&
+            lc $props->{'binding state'} ne 'active';
+
+        # ignore leases we don't understand
+        my @missing= grep { !defined $props->{$_} }
+            ('binding state', 'hardware ethernet', 'ends');
+        if (@missing) {
+            push @warns, "$leasesfn:$.: lease without \`$_'"
+                foreach @missing;
+            next;
+        }
+
+        # ignore leases for other hosts
+        next unless lc $props->{'hardware ethernet'} eq lc $gho->{Ether};
+
+        $props->{' ends'}= $props->{'ends'};
+        $props->{' ends'} =~
+            s/^[0-6]\s+(\S+)\s+(\d+)\:(\d+\:\d+)$/
+                sprintf "%s %02d:%s", $1,$2,$3 /e
+                or return $badleases->("unexpected syntax for ends");
+
+        next if $best &&
+            $best->{' ends'} gt $props->{' ends'};
+        $best= $props;
+    }
+
+    if (!$best) {
+        $saveas->("$lstash.nolease", 3);
+        return "no active lease";
+    }
+    $gho->{Ip}= $best->{' addr'};
+
+    report_once($gho, 'guest_check_ip', 
+               "guest $gho->{Name}: $gho->{Ether} $gho->{Ip}");
+    return undef;
+}
+
+sub guest_editconfig ($$$) {
+    my ($ho, $gho, $code) = @_;
+    target_editfile_root($ho, "$gho->{CfgPath}", sub {
+        while (<::EI>) {
+            $code->();
+            print ::EO or die $!;
+        }
+        die $! if ::EI->error;
+    });
+}
+
+sub guest_await_reboot ($$$) {
+    my ($ho,$gho, $timeout) = @_;
+    poll_loop($timeout, 30, "await reboot request from $gho->{Guest}", sub {
+        my $st= guest_get_state($ho,$gho);
+        return undef if $st eq 'sr';
+        fail("guest unexpectedly shutdown; state is '$st'")
+            if $st =~ m/^s/ || $st eq '';
+        return "guest state is $st";
+    });
+}
+
+sub guest_destroy ($$) {
+    my ($ho,$gho) = @_;
+    target_cmd_root($ho, toolstack()->{Command}." destroy $gho->{Name}", 40);
+}    
+
+sub target_choose_vg ($$) {
+    my ($ho, $mbneeded) = @_;
+    my $vgs= target_cmd_output_root($ho, 'vgdisplay --colon');
+    my $bestkb= 1.0e90;
+    my $bestvg;
+    foreach my $l (split /\n/, $vgs) {
+        $l =~ s/^\s+//; $l =~ s/\s+$//;
+        my @l= split /\:/, $l;
+        my $tvg= $l[0];
+        my $pesize= $l[12];
+        my $freepekb= $l[15];
+        my $tkb= $l[12] * 1.0 * $l[15];
+        if ($tkb < $mbneeded*1024.0) {
+            logm("vg $tvg ${tkb}kb free - too small");
+            next;
+        }
+        if ($tkb < $bestkb) {
+            $bestvg= $tvg;
+            $bestkb= $tkb;
+        }
+    }
+    die "no vg of sufficient size"
+        unless defined $bestvg;
+    logm("vg $bestvg ${bestkb}kb free - will use");
+    return $bestvg;
+}
+
+sub select_ether ($) {
+    my ($vn) = @_;
+    # must be run outside transaction
+    my $ether= $r{$vn};
+    return $ether if defined $ether;
+    my $prefix= sprintf "%s:%02x", $c{GenEtherPrefix}, $flight & 0xff;
+
+    db_retry($flight,'running', $dbh_tests,[qw(flights)], sub {
+        my $previous= $dbh_tests->selectrow_array(<<END, {}, $flight);
+            SELECT max(val) FROM runvars WHERE flight=?
+                AND name LIKE E'%\\_ether'
+                AND val LIKE '$prefix:%'
+END
+        if (defined $previous) {
+            $previous =~ m/^\w+:\w+:\w+:\w+:([0-9a-f]+):([0-9a-f]+)$/i
+                or die "$previous ?";
+            my $val= (hex($1)<<8) | hex($2);
+            $val++;  $val &= 0xffff;
+            $ether= sprintf "%s:%02x:%02x", $prefix, $val >> 8, $val & 0xff;
+            logm("select_ether $prefix:... $ether (previous $previous)");
+        } else {
+            $ether= "$prefix:00:01";
+            logm("select_ether $prefix:... $ether (first in flight)");
+        }
+        $dbh_tests->do(<<END, {}, $flight,$job,$vn,$ether);
+            INSERT INTO runvars VALUES (?,?,?,?,'t')
+END
+        my $chkrow= $dbh_tests->selectrow_hashref(<<END,{}, $flight);
+           SELECT val, count(*) FROM runvars WHERE flight=?
+                AND name LIKE E'%\\_ether'
+                AND val LIKE '$prefix:%'
+               GROUP BY val
+               HAVING count(*) <> 1
+               LIMIT 1
+END
+       die "$chkrow->{val} $chkrow->{count}" if $chkrow;
+    });
+    $r{$vn}= $ether;
+    return $ether;
+}
+
+sub guest_var ($$$) {
+    my ($gho, $runvartail, $default) = @_;
+    my $val= $r{ $gho->{Guest}."_".$runvartail };  return $val if defined $val;
+    $val= $r{ "guests_$runvartail" };              return $val if defined $val;
+    return $default;
+}
+
+sub guest_var_commalist ($$) {
+    my ($gho,$runvartail) = @_;
+    return split /\,/, guest_var($gho,$runvartail,'');
+}
+
+sub prepareguest ($$$$$$) {
+    my ($ho, $gn, $hostname, $tcpcheckport, $mb,
+        $boot_timeout) = @_;
+    # must be run outside transaction
+
+    # If we are passing through a nic, use its mac address not a generated one
+    my $ptnichostident= $r{"${gn}_pcipassthrough_nic"};
+    if (!$ptnichostident) {
+        select_ether("${gn}_ether");
+    } else {
+        my $ptnicho= selecthost($ptnichostident);
+        my $ptnicinfo= get_host_property($ptnicho,'pcipassthrough nic');
+        $ptnicinfo =~ m,/, or die "$ptnichostident $ptnicinfo ?";
+        my $ptether= $'; #'
+        $r{"${gn}_ether"}= $ptether;
+        logm("passthrough nic from $ptnichostident ether $ptether");
+    }
+
+    store_runvar("${gn}_hostname", $hostname);
+    store_runvar("${gn}_disk_lv", $r{"${gn}_hostname"}.'-disk');
+    store_runvar("${gn}_tcpcheckport", $tcpcheckport);
+    store_runvar("${gn}_boot_timeout", $boot_timeout);
+
+    my $gho= selectguest($gn, $ho);
+    store_runvar("${gn}_domname", $gho->{Name});
+
+    store_runvar("${gn}_vg", '');
+    if (!length $r{"${gn}_vg"}) {
+        store_runvar("${gn}_vg", target_choose_vg($ho, $mb));
+    }
+
+    guest_find_lv($gho);
+    guest_find_ether($gho);
+    guest_find_tcpcheckport($gho);
+    return $gho;
+}
+
+sub prepareguest_part_lvmdisk ($$$) {
+    my ($ho, $gho, $disk_mb) = @_;
+    target_cmd_root($ho, "lvremove -f $gho->{Lvdev} ||:");
+    target_cmd_root($ho, "lvcreate -L ${disk_mb}M -n $gho->{Lv} $gho->{Vg}");
+    target_cmd_root($ho, "dd if=/dev/zero of=$gho->{Lvdev} count=10");
+}    
+
+sub prepareguest_part_xencfg ($$$$$) {
+    my ($ho, $gho, $ram_mb, $xopts, $cfgrest) = @_;
+    my $onreboot= $xopts->{OnReboot} || 'restart';
+    my $vcpus= guest_var($gho, 'vcpus', $xopts->{DefVcpus} || 2);
+    my $xoptcfg= $xopts->{ExtraConfig};
+    $xoptcfg='' unless defined $xoptcfg;
+    my $xencfg= <<END;
+name        = '$gho->{Name}'
+memory = ${ram_mb}
+vif         = [ 'type=ioemu,mac=$gho->{Ether}' ]
+#
+on_poweroff = 'destroy'
+on_reboot   = '$onreboot'
+on_crash    = 'preserve'
+#
+vcpus = $vcpus
+#
+$cfgrest
+#
+$xoptcfg
+END
+
+    my $cfgpath= "/etc/xen/$gho->{Name}.cfg";
+    store_runvar("$gho->{Guest}_cfgpath", "$cfgpath");
+    $gho->{CfgPath}= $cfgpath;
+
+    target_putfilecontents_root_stash($ho,30,$xencfg, $cfgpath);
+
+    return $cfgpath;
+}
+
+sub more_prepareguest_hvm ($$$$;@) {
+    my ($ho, $gho, $ram_mb, $disk_mb, %xopts) = @_;
+    
+    my $passwd= 'xenvnc';
+
+    prepareguest_part_lvmdisk($ho, $gho, $disk_mb);
+    
+    my $specimage= $r{"$gho->{Guest}_image"};
+    die "$gho->{Guest} ?" unless $specimage;
+    my $limage= $specimage =~ m,^/, ? $specimage : "$c{Images}/$specimage";
+    $gho->{Rimage}= "/root/$flight.$job.".basename($specimage);
+    target_putfile_root($ho, 1000, $limage,$gho->{Rimage}, '-p');
+
+    my $postimage_hook= $xopts{PostImageHook};
+    $postimage_hook->() if $postimage_hook;
+
+    my $cfg = <<END;
+kernel      = 'hvmloader'
+builder     = 'hvm'
+#
+disk        = [
+            'phy:$gho->{Lvdev},hda,w',
+            'file:$gho->{Rimage},hdc:cdrom,r'
+            ]
+#
+usb=1
+usbdevice='tablet'
+#
+#stdvga=1
+keymap='en-gb';
+#
+sdl=0
+opengl=0
+vnc=1
+vncunused=1
+vncdisplay=0
+vnclisten='$ho->{Ip}'
+vncpasswd='$passwd'
+
+serial='file:/dev/stderr'
+#
+boot = 'dc'
+END
+
+    my $devmodel = $r{'device_model_version'};
+    if (defined $devmodel) {
+        $cfg .= "device_model_version='$devmodel'\n";
+    }
+
+    my $cfgpath= prepareguest_part_xencfg($ho, $gho, $ram_mb, \%xopts, $cfg);
+    target_cmd_root($ho, <<END);
+        (echo $passwd; echo $passwd) | vncpasswd $gho->{Guest}.vncpw
+END
+
+    return $cfgpath;
+}
+
+sub guest_check_via_ssh ($) {
+    my ($gho) = @_;
+    return $r{"$gho->{Guest}_tcpcheckport"} == 22;
+}
+
+sub guest_check_up_quick ($) {
+    my ($gho) = @_;
+    if (guest_check_via_ssh($gho)) {
+       target_cmd_root($gho, "date");
+    } else {
+       target_ping_check_up($gho);
+    }
+}
+
+sub guest_check_up ($) {
+    my ($gho) = @_;
+    guest_await_dhcp_tcp($gho,20);
+    target_ping_check_up($gho);
+    target_cmd_root($gho, "echo guest $gho->{Name}: ok")
+        if guest_check_via_ssh($gho);
+}
+
+sub guest_get_state ($$) {
+    my ($ho,$gho) = @_;
+    my $domains= target_cmd_output_root($ho, toolstack()->{Command}." list");
+    $domains =~ s/^Name.*\n//;
+    foreach my $l (split /\n/, $domains) {
+        $l =~ m/^(\S+) (?: \s+ \d+ ){3} \s+ ([-a-z]+) \s/x or die "$l ?";
+        next unless $1 eq $gho->{Name};
+        my $st= $2;
+        $st =~ s/\-//g;
+        $st='-' if !length $st;
+        logm("guest $gho->{Name} state is $st");
+        return $st;
+    }
+    logm("guest $gho->{Name} not present on this host");
+    return '';
+}
+
+our $guest_state_running_re= '[-rb]+';
+
+sub guest_checkrunning ($$) {
+    my ($ho,$gho) = @_;
+    my $s= guest_get_state($ho,$gho);
+    return $s =~ m/^$guest_state_running_re$/o;
+}
+
+sub guest_await_dhcp_tcp ($$) {
+    my ($gho,$timeout) = @_;
+    guest_find_tcpcheckport($gho);
+    poll_loop($timeout,1,
+              "guest $gho->{Name} $gho->{Ether} $gho->{TcpCheckPort}".
+              " link/ip/tcp",
+              sub {
+        my $err= guest_check_ip($gho);
+        return $err if defined $err;
+
+        return
+            ($gho->{PingBroken} ? undef : target_ping_check_up($gho))
+            ||
+            target_tcp_check($gho,5)
+            ||
+            undef;
+    });
+}
+
+sub guest_check_remus_ok {
+    my ($gho, @hos) = @_;
+    my @sts;
+    logm("remus check $gho->{Name}...");
+    foreach my $ho (@hos) {
+       my $st;
+       if (!eval {
+           $st= guest_get_state($ho, $gho)
+        }) {
+           $st= '_';
+           logm("could not get guest $gho->{Name} state on $ho->{Name}: $@");
+       }
+       push @sts, [ $ho, $st ];
+    }
+    my @ststrings= map { $_->[1] } @sts;
+    my $compound= join ',', @ststrings;
+    my $msg= "remus check $gho->{Name}: result \"$compound\":";
+    $msg .= " $_->[0]{Name}=$_->[1]" foreach @sts;
+    logm($msg);
+    my $runnings= scalar grep { m/$guest_state_running_re/o } @ststrings;
+    die "running on multiple hosts $compound" if $runnings > 1;
+    die "not running anywhere $compound" unless $runnings;
+    die "crashed somewhere $compound" if grep { m/c/ } @ststrings;
+}
+
+sub target_umount_lv ($$$) {
+    my ($ho,$vg,$lv) = @_;
+    my $dev= "/dev/$vg/$lv";
+    for (;;) {
+       my $link= target_cmd_output_root($ho, "readlink $dev");
+       return if $link =~ m,^/dev/nbd,; # can't tell if it's open
+        $lv= target_cmd_output_root($ho, "lvdisplay --colon $dev");
+        $lv =~ s/^\s+//;  $lv =~ s/\s+$//;
+        my @lv = split /:/, $lv;
+        die "@lv ?" unless $lv[0] eq $dev;
+        return unless $lv[5]; # "open"
+        logm("lvdisplay output says device is still open: $lv");
+        target_cmd_root($ho, "umount $dev");
+    }
+}
+
+sub guest_umount_lv ($$) {
+    my ($ho,$gho) = @_;
+    target_umount_lv($ho, $gho->{Vg}, $gho->{Lv});
+}
+
+sub target_tcp_check ($$) {
+    my ($ho,$interval) = @_;
+    my $ncout= `nc -n -v -z -w $interval $ho->{Ip} $ho->{TcpCheckPort} 2>&1`;
+    return undef if !$?;
+    $ncout =~ s/\n/ | /g;
+    return "nc: $? $ncout";
+}
+
+sub await_tcp ($$$) {
+    my ($maxwait,$interval,$ho) = @_;
+    poll_loop($maxwait,$interval,
+              "await tcp $ho->{Name} $ho->{TcpCheckPort}",
+              sub {
+        return target_tcp_check($ho,$interval);
+    });
+}
+
+sub guest_await ($$) {
+    my ($gho,$dhcpwait) = @_;
+    guest_await_dhcp_tcp($gho,$dhcpwait);
+    target_cmd_root($gho, "echo guest $gho->{Name}: ok")
+        if guest_check_via_ssh($gho);
+    return $gho;
+}
+
+sub target_var_prefix ($) {
+    my ($ho) = @_;
+    if (exists $ho->{Guest}) { return $ho->{Guest}.'_'; }
+    return '';
+}
+
+sub target_var ($$) {
+    my ($ho,$vn) = @_;
+    return $r{ target_var_prefix($ho). $vn };
+}
+
+sub target_kernkind_check ($) {
+    my ($gho) = @_;
+    my $pfx= target_var_prefix($gho);
+    my $kernkind= $r{$pfx."kernkind"};
+    my $isguest= exists $gho->{Guest};
+    if ($kernkind eq 'pvops') {
+        store_runvar($pfx."rootdev", 'xvda') if $isguest;
+        store_runvar($pfx."console", 'hvc0');
+    } elsif ($kernkind !~ m/2618/) {
+        store_runvar($pfx."console", 'xvc0') if $isguest;
+    }
+}
+
+sub target_kernkind_console_inittab ($$$) {
+    my ($ho, $gho, $root) = @_;
+
+    my $inittabpath= "$root/etc/inittab";
+    my $console= target_var($gho,'console');
+
+    if (defined $console && length $console) {
+        target_cmd_root($ho, <<END);
+            set -ex
+            perl -i~ -ne "
+                next if m/^xc:/;
+                print \\\$_ or die \\\$!;
+                next unless s/^1:/xc:/;
+                s/tty1/$console/;
+                print \\\$_ or die \\\$!;
+            " $inittabpath
+END
+    }
+    return $console;
+}
+
+sub target_extract_jobdistpath ($$$$$) {
+    my ($ho, $part, $path, $job, $distpath) = @_;
+    $distpath->{$part}= get_stashed($path, $job);
+    my $local= $path;  $local =~ s/path_//;
+    my $distcopy= "/root/extract_$local.tar.gz";
+    target_putfile_root($ho, 300, $distpath->{$part}, $distcopy);
+    target_cmd_root($ho, "cd / && tar zxf $distcopy", 300);
+}
+
+sub guest_find_domid ($$) {
+    my ($ho,$gho) = @_;
+    return if defined $gho->{Domid};
+    my $list= target_cmd_output_root($ho,
+                toolstack()->{Command}." list $gho->{Name}");
+    $list =~ m/^(?!Name\s)(\S+)\s+(\d+)\s+(\d+)+(\d+)\s.*$/m
+        or die "domain list: $list";
+    $1 eq $gho->{Name} or die "domain list name $1 expected $gho->{Name}";
+    $gho->{MemUsed}= $3;
+    $gho->{Vcpus}= $4;
+    return $gho->{Domid}= $2;
+}
+
+sub guest_vncsnapshot_begin ($$) {
+    my ($ho,$gho) = @_;
+    my $domid= $gho->{Domid};
+
+    my $backend= target_cmd_output_root($ho,
+        "xenstore-read /local/domain/$domid/device/vfb/0/backend ||:");
+
+    if ($backend eq '') {
+        my $port= target_cmd_output_root($ho,
+            "xenstore-read /local/domain/$domid/console/vnc-port");
+        $port =~ m/^\d+/ && $port >= 5900 or die "$port ?";
+        return {
+            vnclisten => $ho->{Ip},
+            vncdisplay => $port-5900,
+        };
+    }
+
+    $backend =~ m,^/local/domain/\d+/backend/vfb/\d+/\d+$,
+        or die "$backend ?";
+
+    my $v = {};
+    foreach my $k (qw(vnclisten vncdisplay)) {
+        $v->{$k}= target_cmd_output_root($ho,
+                "xenstore-read $backend/$k");
+    }
+    return $v;
+}
+sub guest_vncsnapshot_stash ($$$$) {
+    my ($ho,$gho,$v,$leaf) = @_;
+    my $rfile= "/root/$leaf";
+    target_cmd_root($ho,
+        "vncsnapshot -passwd $gho->{Guest}.vncpw".
+                   " -nojpeg -allowblank".
+                   " $v->{vnclisten}:$v->{vncdisplay}".
+                   " $rfile", 100);
+    target_getfile_root($ho,100, "$rfile", "$stash/$leaf");
+}
+
+our %toolstacks=
+    ('xend' => {
+        NewDaemons => [qw(xend)],
+        OldDaemonInitd => 'xend',
+        Command => 'xm',
+        CfgPathVar => 'cfgpath',
+        Dom0MemFixed => 1,
+        },
+     'xl' => {
+        NewDaemons => [],
+        Dom0MemFixed => 1,
+        Command => 'xl',
+        CfgPathVar => 'cfgpath',
+       RestoreNeedsConfig => 1,
+        }
+     );
+
+sub toolstack () {
+    my $tsname= $r{toolstack};
+    $tsname= 'xend' if !defined $tsname;
+    my $ts= $toolstacks{$tsname};
+    die "$tsname ?" unless defined $ts;
+    if (!exists $ts->{Name}) {
+        logm("toolstack $tsname");
+        $ts->{Name}= $tsname;
+    }
+    return $ts;
+}
+
+sub authorized_keys () {
+    my $authkeys= '';
+    my @akf= map {
+        "$ENV{'HOME'}/.ssh/$_"
+        } qw(authorized_keys id_dsa.pub id_rsa.pub);
+    push @akf, split ':', $c{AuthorizedKeysFiles};
+    push @akf, $c{TestHostKeypairPath}.'.pub';
+    foreach my $akf (@akf) {
+        next unless $akf =~ m/\S/;
+        $authkeys .= get_filecontents($akf, "# $akf ENOENT\n"). "\n";
+    }
+    $authkeys .= $c{AuthorizedKeysAppend};
+    return $authkeys;
+}
+
+#---------- webspace for installer ----------
+
+sub await_webspace_fetch_byleaf ($$$$$) {
+    my ($maxwait,$interval,$logtailer, $ho, $url) = @_;
+    my $leaf= $url;
+    $leaf =~ s,.*/,,;
+    poll_loop($maxwait,$interval, "fetch $leaf", sub {
+        my ($line, $last);
+        $last= '(none)';
+        while (defined($line= $logtailer->getline())) {
+            my ($ip, $got) = $line =~
+                m,^([0-9.]+) \S+ \S+ \[[^][]+\] \"GET \S*/(\S+) ,
+                or next;
+            next unless $ip eq $ho->{Ip};
+            $last= $got;
+            next unless $got eq $leaf;
+            return undef;
+        }
+        return $last;
+    });
+}
+
+sub create_webfile ($$$) {
+    my ($ho, $tail, $contents) = @_; # $contents as for file_link_contents
+    my $wf_common= $c{WebspaceCommon}.$ho->{Name}."_".$tail;
+    my $wf_url= $c{WebspaceUrl}.$wf_common;
+    my $wf_file= $c{WebspaceFile}.$wf_common;
+    file_link_contents($wf_file, $contents);
+    return $wf_url;
+}
+
+#---------- pxe handling ----------
+
+sub file_link_contents ($$) {
+    my ($fn, $contents) = @_;
+    # $contents as for file_write_contents
+    my ($dir, $base, $ext) =
+        $fn =~ m,^( (?: .*/ )? )( [^/]+? )( (?: \.[^./]+ )? )$,x
+        or die "$fn ?";
+    my $real= "$dir$base--osstest$ext";
+    my $linktarg= "$base--osstest$ext";
+
+    file_simple_write_contents($real, $contents);
+
+    my $newlink= "$dir$base--newlink$ext";
+
+    if (!lstat "$fn") {
+        $!==&ENOENT or die "$fn $!";
+    } elsif (!-l _) {
+        die "$fn not a symlink";
+        unlink $fn or die "$fn $!";
+    }
+    symlink $linktarg, $newlink or die "$newlink $!";
+    rename $newlink, $fn or die "$newlink $fn $!";
+    logm("wrote $fn");
+}
+
+sub host_pxedir ($) {
+    my ($ho) = @_;
+    my $dir= $ho->{Ether};
+    $dir =~ y/A-Z/a-z/;
+    $dir =~ y/0-9a-f//cd;
+    length($dir)==12 or die "$dir";
+    $dir =~ s/../$&-/g;
+    $dir =~ s/\-$//;
+    return $dir;
+}
+
+sub setup_pxeboot ($$) {
+    my ($ho, $bootfile) = @_;
+    my $dir= host_pxedir($ho);
+    file_link_contents($c{Tftp}."/$dir/pxelinux.cfg", $bootfile);
+}
+
+sub setup_pxeboot_local ($) {
+    my ($ho) = @_;
+    setup_pxeboot($ho, <<END);
+serial 0 $c{Baud}
+timeout 5
+label local
+       LOCALBOOT 0
+default local
+END
+}
+
 1;