source: fedkit/splitter.pl @ c9f5490

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since c9f5490 was 9c00d41, checked in by Ted Faber <faber@…>, 17 years ago

Mods to generate gateway parameter file and place them in the local project's
/proj hierarchy as well as shipping around the keys required for setup.

Moved to configuration file to avoid remembering all the command line opts.

Moved over to IO:: file handling to stop polluting the global filehandle space.

Updated the docs.

  • Property mode set to 100644
File size: 20.5 KB
Line 
1#!/usr/bin/perl
2
3use Getopt::Std;
4use IO::File;
5use IO::Dir;
6use IO::Pipe;
7use File::Copy;
8
9@scripts = ("federate.sh", "smbmount.pl");
10$local_script_dir = ".";
11
12# Parse the config file.  The format is a colon-separated parameter name
13# followed by the value of that parameter to the end of the line.  This parses
14# that format and puts the parameters into the referenced hash.  Parameter
15# names are mapped to lower case, parameter values are unchanged.  Returns 0 on
16# failure (e.g. file open) and 1 on success.
17sub parse_config {
18    my($file, $href) = @_;
19    my($fh) = new IO::File($file);
20       
21    unless ($fh) {
22        warn "Can't open $file: $!\n";
23        return 0;
24    }
25
26    while (<$fh>) {
27        next if /^\s*#/ || /^\s*$/;     # Skip comments & blanks
28        chomp;
29        /^([^:]+):\s*(.*)/ && do {
30            my($key) = $1; 
31
32            $key =~ tr/A-Z/a-z/;
33            $href->{$key} = $2;
34            next;
35        };
36        warn "Unparasble line in $file: $_\n";
37    }
38    $fh->close();   # It will close when it goes out of scope, but...
39    return 1;
40}
41
42
43# use scp to transfer a file, reporting true if successful and false otherwise.
44# Parameters are the local file name, the ssh host destination (either hostname
45# oe user@host), and an optional destination file name or directory.  If no
46# destination is given, the file is transferred to the given user's home
47# directory.  If only a machine is given in the ssh host destination, the
48# current user is used.
49sub scp_file {
50    my($file, $where, $dest) = @_;
51
52    # XXX system with a relative pathname is sort of gross
53    system("scp $file $where:$dest");
54    if ($?) {
55        warn "scp failed $?\n";
56        return 0;
57    }
58    else { return 1; }
59}
60
61# use ssh to execute the given command on the machine (and as the user) in
62# $where.  Parameters are the ssh destination directive ($where) and the
63# command to execute, and a prefix to be placed on a message generated if the
64# command fails.   On failure print a warning if a warning prefix was given and
65# return false.
66sub ssh_cmd {
67    my($user, $host, $cmd, $wname) = @_;
68
69    # XXX system with a relative pathname is sort of gross
70    system ("ssh $user\@$host $cmd");
71    if ($?) {
72        warn "$wname failed $?\n" if $wname;
73        return 0;
74    }
75    else { return 1; }
76}
77
78# Ship local copies of the federation scripts out to the given host.  If any of
79# the script transfers fails, return 0.  The scripts to transfer are from the
80# global @scripts and are found locally in $local_script_dir (another global).
81sub ship_scripts {
82    my($host, $user, $dest_dir) = @_;       # Where, who, where remotely
83    my($s);
84
85    for $s (@scripts) {
86        &scp_file("$local_script_dir/$s", "$user\@$host", $dest_dir) || 
87            return 0;
88    }
89    return 1;
90}
91
92# Ship per-testbed configuration generated by this script to the remote /proj
93# directories on the remote testbeds
94sub ship_configs {
95    my($host, $user, $src_dir, $dest_dir) = @_;     # Where, who, where remotely
96    my($d, $f);
97
98    $d = IO::Dir->new($src_dir) || return 0;
99
100    while ( $f = $d->read()) {
101        next if $f =~ /^\./;
102        &scp_file("$src_dir/$f", "$user\@$host", $dest_dir) || return 0;
103    }
104    return 1;
105}
106
107
108
109
110# Start a sub section of the experiment on a given testbed.  The testbed and
111# the user to start the experiment as are pulled from the global per-testbed
112# hash, as is the project name on the remote testbed.  Parameters are the
113# testbed and the experiment id.  Configuration files are scp-ed over to the
114# target testbed from the global $tmpdir/$tb directory.  Then the current state
115# of the experiment determined using expinfo.  From that state, the experiment
116# is either created, modified or spapped in.  If everything succeeds, true is
117# returned.
118sub start_segment {
119    my($tb, $eid) = @_;                     # testbed and experiment ID
120    my($host) = "$host{$tb}$domain{$tb}";   # Host name of remote ops (FQDN)
121    my($user) = $user{$tb};                 # user to pass to ssh
122    my($pid) = $project{$tb};               # remote project to start the
123                                            # experiment under
124    my($tclfile) = "./$eid.$tb.tcl";        # Local tcl file with the
125                                            # sub-experiment
126    my($proj_dir) = "/proj/$pid/exp/$eid/tmp";  # Where to stash federation stuff
127    my($to_hostname) = "$proj_dir/hosts";   # remote hostnames file
128    my($status) = new IO::Pipe;             # The pipe to get status
129
130    # Determine the status of the remote experiment
131    $status->reader("ssh $user\@$host /usr/testbed/bin/expinfo $pid $eid") || 
132        die "Can't ssh to $user\@$host:$!\n";
133
134    # XXX: this is simple now.  Parsing may become more complex
135    while (<$status>) {
136        /State: (\w+)/ && ($state = $1);
137        /No\s+such\s+experiment/ && ($state = "none");
138    }
139    $status->close();
140    print "$tb: $state\n";
141
142    # Copy the experiment definition data over (unless the host is local)
143    &scp_file("$tmpdir/$tb/$tclfile", "$user\@$host") || return 0;
144    # Remote experiment is active.  Modify it.
145    if ($state eq "active") {
146        # First copy new scripts and hostinfo into the remote /proj
147        &scp_file("$tmpdir/$tb/hostnames", "$user\@$host", $to_hostname) ||
148            return 0;
149        &ship_scripts($host, $user, $proj_dir) || return 0;
150        &ship_configs($host, $user, "$tmpdir/$tb", $proj_dir) || return 0;
151        &ssh_cmd($user, $host, "/usr/testbed/bin/modexp -r -s -w $pid " . 
152            "$eid $tclfile", "modexp") || return 0;
153        return 1;
154    }
155
156    # Remote experiment is swapped out, modify it and swap it in.
157    if ($state eq "swapped") {
158        &ssh_cmd($user, $host, "/usr/testbed/bin/modexp -w $pid $eid $tclfile", 
159            "modexp") || return 0;
160        # First copy new scripts and hostinfo into the remote /proj
161        &scp_file("./hostnames", "$user\@$host", $to_hostname) || return 0;
162        &ship_scripts($host, $user, $proj_dir) || return 0;
163        &ship_configs($host, $user, "$tmpdir/$tb", $proj_dir) || return 0;
164        # Now start up
165        &ssh_cmd($user, $host, "/usr/testbed/bin/swapexp -w $pid $eid in", 
166            "swapexp") || return 0;
167        return 1;
168    }
169
170    # No remote experiment.  Create one.  We do this in 2 steps so we can put
171    # the configuration files and scripts into the new experiment directories.
172    if ($state eq "none") {
173        &ssh_cmd($user, $host, "/usr/testbed/bin/startexp -f -w -p " . 
174            "$pid -e $eid $tclfile", "startexp") || return 0;
175        # First copy new scripts and hostinfo into the remote /proj
176        &scp_file("./hostnames", "$user\@$host", $to_hostname) || return 0;
177        &ship_scripts($host, $user, $proj_dir) || return 0;
178        &ship_configs($host, $user, "$tmpdir/$tb", $proj_dir) || return 0;
179        # Now start up
180        &ssh_cmd($user, $host, "/usr/testbed/bin/swapexp -w $pid $eid in", 
181            "swapexp") || return 0;
182        return 1;
183    }
184
185    # Every branch for a known state returns.  If execution gets here, the
186    # state is unknown.
187    warn "unknown state: $state\n";
188    return 0;
189}
190
191# Swap out a sub-experiment - probably because another has failed.  Arguments
192# are testbed and experiment.  Most of the control flow is similar to
193# start_segment, though much simpler.
194sub stop_segment {
195    my($tb, $eid) = @_;
196    my($user) = "$user{$tb}";
197    my($host) = "$host{$tb}$domain{$tb}";
198    my($pid) = $project{$tb};
199
200    &ssh_cmd($user, $host, "/usr/testbed/bin/swapexp -w $pid $eid out", 
201        "swapexp (out)") || return 0;
202    return 1;
203}
204
205
206$pid = $gid = "dummy";              # Default project and group to pass to
207                                    # $tcl_splitter above.  These are total
208                                    # dummy arguments;  the splitter doesn't
209                                    # use them at all, but we supply them to
210                                    # keep our changes to the parser minimal.
211# Argument processing.
212getopts('c:f:nd', \%opts);
213$splitter_config = $opts{'c'} || "./splitter.conf";
214$debug = $opts{'d'};
215&parse_config("$splitter_config", \%opts) || 
216    die "Cannot read config file $splitter_conf: $!\n";
217
218
219$startem = $opts{'n'} ? 0 : 1;          # If true, start the sub-experiments
220$eid = $opts{'experiment'};             # Experiment ID
221$tcl = $opts{'f'} || shift;             # The experiment description
222$master = $opts{'master'};              # Master testbed
223$tmpdir = $opts{'tmpdir'} || $opts{'tempdir'}|| "/tmp"; # tmp files
224$tb_config = $opts{'testbeds'} || "./testbeds"; # testbed configurations
225$local_script_dir = $opts{'scriptdir'}; # Local scripts
226# For now specify these.  We may want to generate them later.
227$gw_pubkey = $opts{'gatewaypubkey'};
228($gw_pubkey_base = $gw_pubkey) =~ s#.*/##;
229$gw_secretkey = $opts{'gatewaysecretkey'};
230($gw_secretkey_base = $gw_secretkey) =~ s#.*/##;
231
232# tcl program to split experiments (changed during devel)
233$tcl_splitter = $opts{'tclparse'} || "/usr/testbed/lib/ns2ir/parse.tcl";
234# tclsh to call directly (changed during devel)
235$tclsh = $opts{'tclsh'} || "/usr/local/bin/otclsh";
236
237# Prefix to avoid collisions
238$tmpdir .= "/split$$";
239
240# Create a workspace
241unless (-d "$tmpdir") {
242    mkdir("$tmpdir") || die "Can't create $tmpdir: $!";
243}
244
245# Validate scripts directory
246for $s (@scripts) {
247    die "$local_script_dir/$s not in local script directory. Try -d\n"
248        unless -r "$local_script_dir/$s";
249}
250
251die "Must supply file, master and experiment" unless $master && $tcl && $eid;
252
253# Read a hash of per-testbed parameters from the local configurations.
254$conf = new IO::File($tb_config) || 
255    die "can't read testbed configutions from $tb_config: $!\n";
256while (<$conf>) {
257    next if /^#/;
258    chomp;
259    ($tb, $h, $d, $u, $p, $es, $gs, $mes, $mgs, $t, $i) = split(":", $_);
260    $host{$tb} = $h;
261    $user{$tb} = $u;
262    $domain{$tb} = $d;
263    $project{$tb} = $p;
264    $gwtype{$tb} = $t;
265    $expstart{$tb} = $es;
266    $gwstart{$tb} = $gs;
267    $mexpstart{$tb} = $mes;
268    $mgwstart{$tb} = $mgs;
269    $gwimage{$tb} = $i;
270
271    # Make sure the domain starts with a period
272    $domain{$tb} = ".$domain{$tb}" unless $domain{$tb} =~ /^\./;
273}
274$conf->close();
275
276# Open a pipe to the splitter program and start it parsing the experiments
277$pipe = new IO::Pipe;
278$pipe->reader("$tclsh $tcl_splitter -s -m $master -p $pid $gid $eid $tcl") || 
279    die "Cannot execute $tclsh $tcl_splitter -s -p $pid $gid $eid $tcl:$!\n";
280
281# Parse the splitter output.  This loop creates the sub experiments, gateway
282# configurations and hostnames file
283while (<$pipe>) {
284    # Start of a sub-experiment
285    /^#\s+Begin\s+Testbed\s+\((\w+)\)/ && do {
286        $ctb = $1;
287
288        # If we know the testbed, start collecting its sub experiment tcl
289        # description.  If not, warn the caller and ignore the configuration of
290        # this testbed.
291        if ($host{$ctb}) {
292            $allocated{$ctb}++; # Keep track of the testbeds allocated
293
294            unless (-d "$tmpdir/$ctb") {
295                mkdir("$tmpdir/$ctb") || die "Can't create $tmpdir/$ctb: $!";
296            }
297            $destfile = "$tmpdir/$ctb/$eid.$ctb.tcl";
298
299            open(FILE, ">$destfile") || die "Cannot open $destfile:$!\n";
300        }
301        else { 
302            warn "No such testbed $ctb\n";
303            $destfile = "";
304        }
305        next;
306    };
307
308    # End of that experiment
309    /^#\s+End\s+Testbed\s+\((\w+)\)/ && do {
310        # Simple syntax check and close out this experiment's tcl description
311        die "Mismatched testbed markers ($1, $ctb)\n" unless ($1 eq $ctb);
312        close(FILE);
313        $destfile = $ctb = "";
314        next;
315    };
316
317    # Beginning of a gateway set
318    /^#\s+Begin\s+gateways\s+\((\w+)\)/ && do {
319        $gateways = $1;
320        # If we've heard of this tb, create the config lines for it one at a
321        # time.
322        if ($allocated{$gateways}) {
323            # Just in case.  This directory should already have been created
324            # above.
325            unless (-d "$tmpdir/$gateways") {
326                mkdir("$tmpdir/$gateways") || 
327                    die "Can't create $tmpdir/$gateways: $!";
328            }
329        }
330        else {
331            warn "Gateways given (and ignored) for testbed not in use: " .
332                "$gateways\n";
333            $gateways = 0;
334        }
335        next;
336    };
337    /^#\s+End\s+gateways\s+\((\w+)\)/ && do {
338        die "Mismatched gateway markers ($1, $gateways)\n" 
339            unless !$gateways || $gateways == $1;
340        $gateways = 0;
341        next;
342    };
343    # Beginning of the hostnames list.  Collection is always in the hostnames
344    # file.
345    /^#\s+Begin\s+hostnames/ && do {
346        $destfile = "$tmpdir/hostnames";
347        open(FILE, ">$destfile") || die "Can't open $destfile:$!\n";
348        next;
349    };
350    # end of the hostnames list.
351    /^#\s+End\s+hostnames/ && do {
352        close(FILE);
353        $destfile = "";
354        next;
355    };
356
357    # Generate gateway configuration info, one file per line
358    $gateways && do {
359        chomp;
360        my($dtb, $myname, $desthost, $type) = split(" ", $_);
361        my($sdomain) = $domain{$gateways};      # domain for the source
362        my($ddomain) = $domain{$dtb};           # domain for the destination
363        my($sproject) = $project{$gateways};    # Project of the destination
364
365        $sdomain = ".$eid.$project{$gateways}$sdomain";
366        $ddomain = ".$eid.$project{$dtb}$ddomain";
367
368        # If either end of this link is in the master side of the testbed, that
369        # side is the active end. Otherwise the first testbed encountered in
370        # the file will be the active end.  The $active_end variable keeps
371        # track of those decisions
372        if ( $dtb eq $master ) { $active = "false"; }
373        elsif ($gateways eq $master ) { $active = "true"; }
374        elsif ( $active_end{"$dtb-$gateways"} ) { $active="false"; }
375        else { $active_end{"$gateways-$dtb"}++; $active = "true"; }
376
377        # Write out the file
378        open(GWCONFIG, ">$tmpdir/$gateways/$myname$sdomain.gw.conf") || 
379            die "can't open $tmpdir/$gateways/$myname$sdomain.gw.conf: $!\n";
380        print GWCONFIG "Active: $active\n";
381        print GWCONFIG "Type: $type\n";
382        print GWCONFIG "Peer: $desthost$ddomain\n";
383        print GWCONFIG "Pubkeys: " . 
384            "/proj/$sproject/exp/$eid/tmp/$gw_pubkey_base\n";
385        print GWCONFIG "Privkeys: " .
386            "/proj/$sproject/exp/$eid/tmp/$gw_secretkey_base\n";
387        close(GWCONFIG);
388
389        # This testbed has a gateway (most will) so make a copy of the keys it
390        # needs in this testbed's subdirectory.  start_segment will transfer
391        # them.
392        unless (-r "$tmpdir/$gateways/$gw_pubkey_base" ) {
393            copy($gw_pubkey, "$tmpdir/$gateways/$gw_pubkey_base") ||
394                die "Can't copy pubkeys ($gw_pubkey to " . 
395                    "$tmpdir/$gateways/$gw_pubkey_base): $!\n";
396        }
397        if ($active eq "true" ) {
398            unless (-r "$tmpdir/$gateways/$gw_secretkey_base" ) {
399                copy($gw_secretkey, "$tmpdir/$gateways/$gw_secretkey_base") ||
400                    die "Can't copy secret keys ($gw_secretkey to " . 
401                        "$tmpdir/$gateways/$gw_secretkey_base): $!\n";
402            }
403        }
404
405        #done processing gateway entry, ready for next line
406        next; 
407    };
408
409
410    next unless $destfile;  # Unidentified testbed, ignore config
411
412    # Substitute variables
413    s/GWTYPE/$gwtype{$ctb}/g;
414    s/GWIMAGE/$gwimage{$ctb}/g;
415    if ($ctb eq $master ) {
416        s/GWSTART/$mgwstart{$ctb}/g;
417        s/EXPSTART/$mexpstart{$ctb}/g;
418    }
419    else {
420        s/GWSTART/$gwstart{$ctb}/g;
421        s/EXPSTART/$expstart{$ctb}/g;
422    }
423    # XXX: oh is this bad
424    s#GWCONF#FEDDIR\`hostname\`.gw.conf#g;
425    s#FEDDIR#/proj/$project{$ctb}/exp/$eid/tmp/#g;
426    print FILE;
427}
428$pipe->close();
429die "No nodes in master testbed ($master)\n" unless $allocated{$master};
430
431exit(0) unless $startem;
432
433# Start up the slave sub-experiments first
434TESTBED:
435for $tb (keys %allocated) {
436    if ($tb ne $master) {
437        if (&start_segment($tb, $eid)) { $started{$tb}++; }
438        else { last TESTBED; }
439    }
440}
441
442# Now the master
443if (&start_segment($master, $eid)) { 
444    $started{$master}++;
445}
446
447# If any testbed failed, swap the rest out.
448if ( scalar(keys %started) != scalar(keys %allocated)) {
449    for $tb (keys %started) { &stop_segment($tb, $eid); }
450    print "Error starting experiment\n";
451    exit(1);
452}
453print "Experiment started\n";
454system("rm -rf $tmpdir") unless $debug;
455exit(0);    # set the exit value
456
457=pod
458
459=head1 NAME
460
461B<splitter.pl>
462
463=head1 SYNOPSIS
464
465B<splitter.pl> [B<-nd>] [B<-c> F<config_file>] [B<-f> F<experiment_tcl>]
466    [F<experiment_tcl>]
467
468=head1 DESCRIPTION
469
470B<splitter.pl> invokes the DETER experiment parser to split an annotated
471experiment into multiple sub-experments and instantiates the sub-experiments on
472their intended testbeds.  Annotation is accomplished using the
473tb-set-node-testbed command, added to the parser.
474
475The testbed labels are meaningful based on their presence in the testbeds file.
476that file can be specified in the configuration file using the B<Testbeds>
477directive, and defaults to F<./testbeds>.  The syntax is described below.
478
479Most of the intermediate files are staged in a sub-directory of a temporary
480files directory and deleted at the end of the script.  Specifying the B<-d>
481flag on the command line avoids the deletion for debbugging.  By default the
482temporary files directory is directory is F</tmp> and can be reset in the
483configuration file using the B<Tmpdir> directive.  Intermediate files are
484stored under a subdirectory formed by adding the process ID of the splitter
485process.  For example, if the temporary files directory is F</tmp> and the
486B<splitter.pl> process ID is 2323, the temporary files will be stored in
487F</tmp/split2323/>.
488
489The expreriment is split out into one experiment description per testbed in the
490temporary directory named as F<experiment.testbed.tcl> where the experiment is
491the experiment ID given in the configuration file, and the testbed is the
492tb-set-node-testbed parameter for the nodes in the file.
493
494If the B<-n> option is absent the sub-experiments are then instantiated on
495their testbeds.  (Here B<-n> is analogous to its use in L<make(1)>).
496Per-testbed parameters are set in the testbeds file.  Sub-experiments on
497slave testbeds are instantiated in a random order, but the master testbed is
498currently instantiated last.
499
500Scripts to start federation (the federation kit) are copied into the local
501experiment's tmp file - e.g., F</proj/DETER/exp/simple-split/tmp>.  These are
502taken from the directory given by the B<ScriptDir> directive in the
503configuration file.
504
505If any sub-experiment fails to instantiate, the other sub-exeriments are
506swapped out.
507
508=head2 Configuration File
509
510The configuration file is a simple attribute-value pair set of colon-separated
511parameters and values.  A configuration file must be present, either specified
512in the B<-c> flag or the default F<./splitter.conf>.  All the parameter names
513are case insensitive, but should not include any whitespace.  Parameter values
514may include whitespace, but no newlines.
515
516Possible parameters are:
517
518=over 5
519
520=item Experiment
521
522The name of the experiment on the various testbeds
523
524=item Master
525
526The master testbed label from the testbeds file, described below.
527
528=item Testbeds
529
530The testbeds file described below, giving per-testbed parameters.  If this
531directive is absent the testbeds file defaults to F<./testbeds>
532
533=item ScriptDir
534
535Location of the default federation scripts, i.e. the federation kit.
536
537=item GatewayPubkey
538
539=item GatewaySecretKey
540
541The names of the files containing secret and public keys to use in setting up
542tunnels between testbeds.  These will eventually be automatically generated.
543
544=item TmpDir
545
546=item TempDir
547
548The directory where temporary files are created.  These are synonyms, but
549should both be specified, B<TmpDir> has priority.  If neither is specified,
550F</tmp> is used.
551
552=item Tclparse
553
554The pathname to the experiment parsing program.  Only developers should set
555this.
556
557=item Tclsh
558
559The pathname to the local oTcl shell.  Only developers should set
560this.
561
562=back
563
564=head2 Testbeds file
565
566The configuration file (F<./testbeds> unless overridden by B<-c>) is a
567colon-separated set of parameters keyed by testbed name.  The fields, in order,
568are:
569
570=over 5
571
572=item name
573
574The testbed to which this line of parameters applies.
575
576=item user
577
578The user under which to make requests to this testbed.  The user running
579B<splitter.pl> must be able to authenicate as this user under L<ssh(1)> to this
580testbed.
581
582=item host
583
584The host name of the testbed's ops node.  The user calling B<splitter.pl> must
585be able to execute commands on this host via L<ssh(1)>.
586
587=item domain
588
589The domain of nodes in this testbed (including the ops host).
590
591=item project
592
593The project under which to instantiate sub-experiments on this testbed.
594
595=item gateway type
596
597The node type for inter-testbed gateway nodes on this testbed.
598
599=item experiment start (slave)
600
601The start command to run on experimental nodes when this testbed is used as a
602slave.  In all the start commands the string FEDDIR will be replaced by the
603local experiment's federation scripts directory and the string GWCONF replaced
604by the gatway configuration file.
605
606=item gateway start (slave)
607
608The start command to run on gateway nodes when this testbed is used as a slave.
609The same string substitutions are made in this command as in experiment start.
610
611=item experiment start (master)
612
613The start command to run on experimental nodes when this testbed is used as a
614master.  The same string substitutions are made in this command as in
615experiment start.
616
617=item gateway start (master)
618
619The start command to run on gateway nodes when this testbed is used as a
620master.  The same string substitutions are made in this command as in
621experiment start.
622
623=item gateway image
624
625The disk image to be loaded on a gateway node on this testbed.
626
627=back
628
629The parsing of the testbeds is extremely simple.  Colons separate each
630field and there is n provision for escaping them at this time.
631
632=head1 ENVIRONMENT
633
634B<splitter.pl> does not directly make use of environment variables, but calls
635out to L<ssh(1)> and (indirectly) to L<sh(1)>, which may be influenced by the
636environment.
637
638=head1 SEE ALSO
639
640L<sh(1)>, L<ssh(1)>
641
642=cut
Note: See TracBrowser for help on using the repository browser.