#!/usr/bin/perl

use Getopt::Std;

@scripts = ("federate.sh", "smbmount.pl");
$local_script_dir = ".";

# use scp to transfer a file, reporting true if successful and false otherwise.
# Parameters are the local file name, the ssh host destination (either hostname
# oe user@host), and an optional destination file name or directory.  If no
# destination is given, the file is transferred to the given user's home
# directory.  If only a machine is given in the ssh host destination, the
# current user is used.
sub scp_file {
    my($file, $where, $dest) = @_;

    # XXX system with a relative pathname is sort of gross
    system("scp $file $where:$dest");
    if ($?) {
	warn "scp failed $?\n";
	return 0;
    }
    else { return 1; }
}

# use ssh to execute the given command on the machine (and as the user) in
# $where.  Parameters are the ssh destination directive ($where) and the
# command to execute, and a prefix to be placed on a message generated if the
# command fails.   On failure print a warning if a warning prefix was given and
# return false.
sub ssh_cmd {
    my($user, $host, $cmd, $wname) = @_;

    # XXX system with a relative pathname is sort of gross
    if ( $host ne "localhost") {
	system ("ssh $user\@$host $cmd");
    }
    else {
	system ("$cmd");
    }
    if ($?) {
	warn "$wname failed $?\n" if $wname;
	return 0;
    }
    else { return 1; }
}

# Ship local copies of the federation scripts out to the given host.  If any of
# the script transfers fails, return 0.  The scripts to transfer are from the
# global @scripts and are found locally in $local_script_dir (another global).
sub ship_scripts {
    my($host, $user, $dest_dir) = @_;	    # Where, who, where remotely
    my($s);

    for $s (@scripts) {
	&scp_file("$local_script_dir/$s", "$user\@$host", $dest_dir) || 
	    return 0;
    }
    return 1;
}



# Start a sub section of the experiment on a given testbed.  The testbed and
# the user to start the experiment as are pulled from the global per-testbed
# hash, as is the project name on the remote testbed.  Parameters are the
# testbed and the experiment id.  Configuration files are scp-ed over to the
# target testbed.  Then the current state of the experiment determined using
# expinfo.  From that state, the experiment is either created, modified or
# spapped in.  If everything succeeds, true is returned.
sub start_segment {
    my($tb, $eid) = @_;			    # testbed and experiment ID
    my($host) = $host{$tb};		    # Host name of remote users
    my($user) = $user{$tb};		    # user to pass to ssh
    my($pid) = $project{$tb};		    # remote project to start the
					    # experiment under
    my($tclfile) = "./$eid.$tb.tcl";	    # Local tcl file with the
					    # sub-experiment
    my($proj_dir) = "/proj/$pid/exp/$eid/tmp";  # Where to stash federation stuff
    my($to_hostname) = "$proj_dir/hosts";   # remote hostnames file

    # Determine the status of the remote experiment
    if ( $host ne "localhost") {
	open(STATUS, "ssh $user\@$host /usr/testbed/bin/expinfo $pid $eid|") || 
	    die "Can't ssh to $user\@$host:$!\n";
    }
    else {
	open(STATUS, "/usr/testbed/bin/expinfo $pid $eid|") || 
	    die "Can't call expinfo locally";
    }
    # XXX: this is simple now.  Parsing may become more complex
    while (<STATUS>) {
	/State: (\w+)/ && ($state = $1);
	/No\s+such\s+experiment/ && ($state = "none");
    }
    close(STATUS);
    print "$tb: $state\n";

    # Copy the experiment definition data over (unless the host is local)
    if ( $host ne "localhost") {
	&scp_file($tclfile, "$user\@$host") || return 0;
    }

    # Remote experiment is active.  Modify it.
    if ($state eq "active") {
	# First copy new scripts and hostinfo into the remote /proj
	&scp_file("./hostnames", "$user\@$host", $to_hostname) || return 0;
	&ship_scripts($host, $user, $proj_dir) || return 0;
	&ssh_cmd($user, $host, "/usr/testbed/bin/modexp -r -s -w $pid " . 
	    "$eid $tclfile", "modexp") || return 0;
	return 1;
    }

    # Remote experiment is swapped out, modify it and swap it in.
    if ($state eq "swapped") {
	&ssh_cmd($user, $host, "/usr/testbed/bin/modexp -w $pid $eid $tclfile", 
	    "modexp") || return 0;
	# First copy new scripts and hostinfo into the remote /proj
	&scp_file("./hostnames", "$user\@$host", $to_hostname) || return 0;
	&ship_scripts($host, $user, $proj_dir) || return 0;
	# Now start up
	&ssh_cmd($user, $host, "/usr/testbed/bin/swapexp -w $pid $eid in", 
	    "swapexp") || return 0;
	return 1;
    }

    # No remote experiment.  Create one.  We do this in 2 steps so we can put
    # the configuration files and scripts into the new experiment directories.
    if ($state eq "none") {
	&ssh_cmd($user, $host, "/usr/testbed/bin/startexp -f -w -p " . 
	    "$pid -e $eid $tclfile", "startexp") || return 0;
	# First copy new scripts and hostinfo into the remote /proj
	&scp_file("./hostnames", "$user\@$host", $to_hostname) || return 0;
	&ship_scripts($host, $user, $proj_dir) || return 0;
	# Now start up
	&ssh_cmd($user, $host, "/usr/testbed/bin/swapexp -w $pid $eid in", 
	    "swapexp") || return 0;
	return 1;
    }

    # Every branch for a known state returns.  If execution gets here, the
    # state is unknown.
    warn "unknown state: $state\n";
    return 0;
}

# Swap out a sub-experiment - probably because another has failed.  Arguments
# are testbed and experiment.  Most of the control flow is similar to
# start_segment, though much simpler.
sub stop_segment {
    my($tb, $eid) = @_;
    my($user) = "$user{$tb}";
    my($host) = "$host{$tb}";
    my($pid) = $project{$tb};

    &ssh_cmd($user, $host, "/usr/testbed/bin/swapexp -w $pid $eid out", 
	"swapexp (out)") || return 0;
    return 1;
}

$tcl_splitter = "/usr/testbed/lib/ns2ir/parse.tcl";	    # tcl program to split experiments
$tclsh = "/usr/local/bin/otclsh";   # tclsh to call directly

$pid = $gid = "dummy";		    # Default project and group to pass to
				    # $tcl_splitter above.  These are total
				    # dummy arguments;  the splitter doesn't
				    # use them at all, but we supply them to
				    # keep our changes to the parser minimal.

# Argument processing.
getopts('d:c:m:e:f:n', \%opts);

$eid = $opts{'e'};		    # Experiment ID
$tcl = $opts{'f'} || shift;	    # The experiment description
$master = $opts{'m'};		    # Master testbed
$startem = $opts{'n'} ? 0 : 1;	    # If true, start the sub-experiments
$config = $opts{'c'} || "./testbeds";
$local_script_dir = $opts{'d'};	    # Local scripts

for $s (@scripts) {
    die "$local_script_dir/$s not in local script directory. Try -d\n"
	unless -r "$local_script_dir/$s";
}

die "Must supply file, master and experiment" unless $master && $tcl && $eid;

# Read a hash of per-testbed parameters from the local configurations.
open(CONF, $config) || die "can't read testbed configutions from $config: $!\n";
while (<CONF>) {
    next if /^#/;
    chomp;
    ($tb, $h, $u, $p, $es, $gs, $mes, $mgs, $t, $i) = split(":", $_);
    $host{$tb} = $h;
    $user{$tb} = $u;
    $project{$tb} = $p;
    $gwtype{$tb} = $t;
    $expstart{$tb} = $es;
    $gwstart{$tb} = $gs;
    $mexpstart{$tb} = $mes;
    $mgwstart{$tb} = $mgs;
    $gwimage{$tb} = $i;
}
close(CONF);

# Open a pipe to the splitter program and start it parsing the experiments
open(PIPE, "$tclsh $tcl_splitter -s -p $pid $gid $eid $tcl|") || 
    die "Cannot execute $tclsh $tcl_splitter -s -p $pid $gid $eid $tcl:$!\n";

# Parse the splitter output.
while (<PIPE>) {
    # Start of a sub-experiment
    /^#\s+Begin\s+Testbed\s+\((\w+)\)/ && do {
	$ctb = $1;

	# If we know the testbed, start collecting its sub experiment tcl
	# description.  If not, warn the caller and ignore the configuration of
	# this testbed.
	if ($host{$ctb}) {
	    $allocated{$ctb}++;	# Keep track of the testbeds allocated
	    $destfile = "./$eid.$ctb.tcl";

	    open(FILE, ">$destfile") || die "Cannot open $destfile:$!\n";
	}
	else { 
	    warn "No such testbed $ctb\n";
	    $destfile = "";
	}
	next;
    };
    # End of that experiment
    /^#\s+End\s+Testbed\s+\((\w+)\)/ && do {
	# Simple syntax check and close out this experiment's tcl description
	die "Mismatched testbed markers ($1, $ctb)\n" unless ($1 eq $ctb);
	close(FILE);
	$destfile = $ctb = "";
	next;
    };
    # Beginning of the hostnames list.  Collection is always in the hostnames
    # file.
    /^#\s+Begin\s+hostnames/ && do {
	$destfile = "./hostnames";
	open(FILE, ">$destfile") || die "Can't open $destfile:$!\n";
	next;
    };
    # end of the hostnames list.
    /^#\s+End\s+hostnames/ && do {
	close(FILE);
	$destfile = "";
	next;
    };

    next unless $destfile;  # Unidentified testbed, ignore config

    # Substitute variables
    s/GWTYPE/$gwtype{$ctb}/g;
    s/GWIMAGE/$gwimage{$ctb}/g;
    if ($ctb eq $master ) {
	s/GWSTART/$mgwstart{$ctb}/g;
	s/EXPSTART/$mexpstart{$ctb}/g;
    }
    else {
	s/GWSTART/$gwstart{$ctb}/g;
	s/EXPSTART/$expstart{$ctb}/g;
    }
    print FILE;
}
close(PIPE);
die "No nodes in master testbed ($master)\n" unless $allocated{$master};

exit(0) unless $startem;

# Start up the slave sub-experiments first
TESTBED:
for $tb (keys %allocated) {
    if ($tb ne $master) {
	if (&start_segment($tb, $eid)) { $started{$tb}++; }
	else { last TESTBED; }
    }
}

# Now the master
if (&start_segment($master, $eid)) { 
    $started{$master}++;
}

# If any testbed failed, swap the rest out.
if ( scalar(keys %started) != scalar(keys %allocated)) {
    for $tb (keys %started) { &stop_segment($tb, $eid); }
    print "Error starting experiment\n";
    exit(1);
}
print "Experiment started\n";
exit(0);    # set the exit value

=pod

=head1 NAME

B<splitter.pl>

=head1 SYNOPSIS

B<splitter.pl> B<-e> I<experiment> B<-m> I<master_testbed> [B<-n>] 
    [B<-d> F<script_dir>] [B<-c> F<config_file>] [B<-f> F<experiment_tcl>]
    [F<experiment_tcl>]

=head1 DESCRIPTION

B<splitter.pl> invokes the DETER experiment parser to split an annotated
experiment into multiple sub-experments and instantiates the sub-experiments on
their intended testbeds.  Annotation is accomplished using the
tb-set-node-testbed command, added to the parser.

The testbed labels are meaningful based on their presence in the testbeds file.
that file can be specified with the B<-c> option, and defaults to
F<./testbeds>.  The syntax is described below.

The expreriment is split out into one experiment description per testbed in the
current directory named as F<experiment.testbed.tcl> where the experiment is
the argument to B<-e> and the testbed is the tb-set-node-testbed parameter for
the nodes in the file.

If the B<-n> option is absent the sub-experiments are then instantiated on
their testbeds.  (Here B<-n> is analogous to its use in L<make(1)>).
Per-testbed parameters are set in the configuration file.  Sub-experiments on
slave testbeds are instantiated in a random order, but the master testbed is
currently instantiated last.

Scripts to start federation are copied into the local experiment's tmp file -
e.g., F</proj/DETER/exp/simple-split/tmp>.  These are taken from the directory
given by the B<-d> option.

If any sub-experiment fails to instantiate, the other sub-exeriments are
swapped out.

=head2 Configuration file

The configuration file (F<./testbeds> unless overridden by B<-c>) is a
colon-separated set of parameters keyed by testbed name.  The fields, in order,
are:

=over 5

=item name

The testbed to which this line of parameters applies.

=item user

The user under which to make requests to this testbed.  The user running
B<splitter.pl> must be able to authenicate as this user under L<ssh(1)> to this
testbed.

=item project

The project under which to instantiate sub-experiments on this testbed.

=item gateway type

The node type for inter-testbed gateway nodes on this testbed.

=item experiment start (slave)

The start command to run on experimental nodes when this testbed is used as a
slave.

=item gateway start (slave)

The start command to run on gateway nodes when this testbed is used as a
slave.

=item experiment start (master)

The start command to run on experimental nodes when this testbed is used as a
master.

=item gateway start (master)

The start command to run on gateway nodes when this testbed is used as a
master.

=item gateway image

The disk image to be loaded on a gateway node on this testbed.

=back

The parsing of the configuration is extremely simple.  Colons separate each
field and there is n provision for escaping them at this time.

=head1 ENVIRONMENT

B<splitter.pl> does not directly make use of environment variables, but calls
out to L<ssh(1)> and (indirectly) to L<sh(1)>, which may be influenced by the
environment.

=head1 SEE ALSO

L<sh(1)>, L<ssh(1)> 

=cut
