#!/usr/bin/perl -w # Author: Mark Bergman # (c) 2008, University of Pennsylvania # # May be distributed under the terms of the GNU GENERAL PUBLIC LICENSE. use strict; use Getopt::Long qw(:config no_ignore_case bundling pass_through); use diagnostics; use integer; # speed up the script if we're not doing floating point math use Expect; ################################################################# my $receptLOW=1; # low-limit on receptacle number to accept my $receptHIGH=8; # high-limit on receptacle number to accept my $login; # user name, set with "-l login" my $operation; # operation, set with "-o operation" my $DEBUG=10; # debugging level, set with "-d #" my $address; # hostname/IP of PDU, set with "-a address" my $password; # password to login to PDU, set with "-p password" or "-S /program/to/retrieve/password" my $receptacle; # current receptacle (outlet) my $receptacleindex; # menu item # for the outlet my @receptacles; # array of receptacles, set with "-r #" my $childPID; # PID for fork() my $getpass; # command to execute to retreive password, set with "-S /program/to/retrieve/password" my $timeout=10; # seconds to wait for text during telnet connection to PDU my @children; # array of child PIDs my @realARGS; # array of arguments after "--" on command line or delivered via STDIN ################################################################### # This program uses POD (Perl's internal documentation). This makes it easy to # maintain the documentation within the executable itself, yet produce nicely # formatted output. This documentation can be printed with the "-D" option =head1 NAME fence_tripplite =head1 SYNOPSIS B [B<-d> #|B<-V>|B<-D>|B<-h>] B<-a> address B<-l> login [ B<-p> password | B<-S> path_to_password_producing_program] -r receptacle [-r receptacle|-r receptacle] -o operation -V|--Version report program version -D|--Documentation display the embedded POD documentation -d|--debug # debug level (value required) -h|--help display help -a|--address IP address (or hostname) of TrippLite PDU -l|--login login name (defaults to "admin") -p|--password password -S|--password_script program to run to produce password as stdout -r|--receptacle recptacle to operate upon, can be given multiple times -o|--operation op operation to perform: change (default), on, off, status =head1 DESCRIPTION This script provides a command-line method to control the power status of a TrippLite PDUMH15ATNET PDU. This script may work with other TrippLite products. The PDU login name for telnet seems to be fixed as "admin", which is the default. =head1 EXAMPLES ./fence_tripplite -r 8 -a managed-pdu1 -S /usr/local/sbin/getpw_pdu1 -o off Connect to PDU "managed-pdu1", run the program "/usr/local/sbin/getpw_pdu1" which will supply the login password on STDOUT, and set receptacle 8 to the "off" state. ./fence_tripplite -a managed-pdu1 -p foobar -r 1 -r 3 -r 5 -o status Connect to PDU "managed-pdu1" using the supplied password, and query the status of receptacles 1, 3, and 5. ./fence_tripplite -a managed-pdu1 -S /usr/local/sbin/getpw_pdu1 -r 1 -r 2 Connect to PDU "managed-pdu1", relying on the program "/usr/local/sbin/getpw_pdu1" to supply the logi npassword. Change the state of receptacles 1 and 2. Arguments to the script can be given on the command line or via standard input. If given via standard input, the arguments must be in the form "keyword=value", where the keywords are the long option names (without the leading hyphens). For example: echo "password=foobar operation=status address=pdu1 receptacle=8" | fence_tripplite would be equivalent to: fence_tripplite -p foobar -o status -a pdu1 -r 8 or: fence_tripplite --password foobar --operation status --address pdu1 --receptacle 8 In order to fence multiple receptacles "in parallel", the fence_triplite device in the /etc/cluster/cluster.conf file must be configured with a dummy name for each receptacle, followed by it's actual number. For example: There can be additional "receptacle" entries. =head1 REQUIREMENTS The script requires the Perl "Expect.pm" package. =head1 WARNING The TrippLite PDU is horribly unsuited for use as a fence device because it has a "random" delay when powering off an outlet. By observation, this delay seems to be between 17~35 seconds. This is probably based on the intentional delay when all outlets are powered on, to avoid an overload situation, but may be a serious problem when fencing a node. Because of this deficiency, the fence_tripplite script accepts multiple receptacles, and applies the same operation to each one as a child process. This allows multiple receptacles (typically powering multiple power supplies on a single server) to be addressed in parallel. In addition, the PDU does not support ssh access, and requires navigating horrible menus to change the power state, rather than being able to send discrete commands. =head1 DEBUGGING Setting the debugging level at or above 10 will also enable Expect.pm diagnostics, displaying all input and output from the PDU as well as the internal pattern matching tests within Expect.pm. To debug argument parsing for values delivered via STDIN (as when the script is run through RHCS), debugging must be enabled via a command-line option. This is because debugging that is enabled on STDIN won't take effect until after the arguments are parse. For example, this will turn on debugging of the fence_tripplite operation, but not of the command-parsing itself: echo "address=pdu1 debug=9 receptacle=8 operation=status password_script=/usr/sbin/getpw" |fence_tripplite To enable debugging of the command parsing, add the "-d" (or --debug) option to the fence_tripplite command line. This can have a different debug level than the option supplied via STDIN, as in: echo "address=pdu1 debug=12 receptacle=8 operation=status password_script=/usr/sbin/getpw" |fence_tripplite -d 3 =head1 AUTHOR Mark Bergman =head1 CONTACT For questions, contact =head1 RIGHTS (c) 2008, University of Pennsylvania May be distributed under the terms of the GNU GENERAL PUBLIC LICENSE. =cut # # # Functions: # version display the version of the script, from the RCS or SVN tag # # usage usage/help statement # # debug print debugging statements # # parse parse the command-line variables # ########## # Variables # ######## $|=1; # Flush output my $SVNversion='$Id: fence_tripplite 226 2009-03-06 16:15:35Z bergman $'; my $NAME; ($NAME=$0)=~s#^.*/##; #################### START OF STANDARD SUBROUTINES sub version { # Display the version number and date of modification extracted from # the SVN header. $SVNversion=~s/\S+\s\S+\s(\S+\s\S+).*/$1/; if ( $SVNversion !~ /^\s$/ ) { print $0 . ": " . $SVNversion . "\n"; } exit 0; } sub usage { print "\t$NAME [-V|-L|-C|-D] [-d#] -a address -l login [-p password | -S /path/to/password_script] -r receptacle [-r receptacle] -o operation\n"; print "\t$NAME -i\n"; print "\t\t-V atomic report program version\n"; print "\t\t-L atomic report program limits\n"; print "\t\t-D atomic view embedded documentation\n"; print "\t\t-d # set debugging level to #\n"; print "\n"; print "\t\t-a address address or hostname of PDU\n"; print "\t\t-l login login name (default: admin)\n"; print "\t\t-p password password\n"; print "\t\t-S script script that emits password on stdout\n"; print "\t\t-r receptacle outlet to power cycle\n"; print "\t\t-o operation operation to perform: \"change\" (default), \"on\", \"off\", \"status\"\n"; print "\t@_\n"; exit 1; } sub debug { # Handle debugging. The debug routine depends on the presence of # the variable "DEBUG", which should be set as follows: # =\d only print debug statements that exactly match # the specified level # # \d print debug statements at or less than the level my($level,$statement)=@_; my $debug=$DEBUG; if ( $DEBUG !~ /=\d+/ ) { if ( $debug >= $level ) { print STDERR $statement; } } else { $debug=~s/=//; if ( $debug == $level ) { print STDERR $statement; } } } sub parse { my $version; my $perldoc; my $help; my $debug; GetOptions("V|Version"=>\$version, "D|Documentation"=>\$perldoc, "d|debug=i"=>\$DEBUG, "h|help"=>\$help, "a|address=s"=>\$address, "p|password=s"=>\$password, "S|password_script=s"=>\$getpass, "o|operation=s"=>\$operation, "l|login=s"=>\$login, "r|receptacle=s"=>\@receptacles, ); if ( defined($version) ) { &version(); } if ( defined($perldoc) ) { exec("perldoc -t $0"); } if ( defined($help) ) { usage(""); } } #################### Parse and validate command-line options ##################### parse; if ( defined($ARGV[0])) { debug(2,"DEBUG\tadding additional arguments to \@realARGS=\"@ARGV\"\n"); @realARGS=@ARGV; } while (<>) { # read anything being sent to the program via STDIN my @args; my $arg; chomp; debug(2,"DEBUG\tRead \"$_\" from STDIN\n"); @args=split; foreach $arg (@args) { push(@realARGS,$arg); debug(2,"DEBUG\t\tpushed \"$arg\" into \@realARGS\n"); } } if (@realARGS) { # if there were additional arguments on the command line or via STDIN then process them my $arg; my $param; my $value; if ( $realARGS[0] =~ /^--$/ ) { # If the user gave -- as a separator between the end of the options # and other arguments. splice(@realARGS,0,1); } if ( $realARGS[0] =~ /^-/ ) { # Either the order of the options is wrong, or this is an unrecognized option usage("Unrecognized option: $realARGS[0]"); } debug(2,"DEBUG\tGot additional arguments: \"@realARGS\"\n"); # Now, parse @realARGS foreach $arg ( @realARGS ) { ($param,$value)=split(/=/,$arg); debug(2,"DEBUG\tfound \"$param\" was set to \"$value\"\n"); if ( $param =~ /^receptacle/ ) { # this allows multiple receptacle entries push(@receptacles,$value); next; } if ( $param eq "address" ) { if ( defined($address) ) { usage("Error: address already defined as \"$address\", cannot set argument \"$arg\""); } $address=$value; next; } if ( $param eq "login" ) { if ( defined($login) ) { usage("Error: login already defined as \"$login\", cannot set argument \"$arg\""); } $login=$value; next; } if ( $param eq "password" ) { if ( defined($password) ) { usage("Error: password already defined as \"$password\", cannot set argument \"$arg\""); } $password=$value; next; } if ( $param eq "password_script" ) { if ( defined($getpass) ) { usage("Error: password_script already defined as \"$getpass\", cannot set argument \"$arg\""); } $getpass=$value; next; } if ( $param eq "operation" ) { if ( defined($operation) ) { usage("Error: operation already defined as \"$operation\", cannot set argument \"$arg\""); } $operation=$value; next; } if ( $param eq "debug" ) { $DEBUG=$value; next; } # we should never get here...it means there was an argument that didn't match... usage("Unrecognized argument: \"$arg\""); } } ######################## # now the initial argument parsing is complete...make sure that the required values are there. if ( !(@receptacles)) { usage("No receptacle number given."); } if ( $operation ne "change" && $operation ne "status" && $operation ne "on" && $operation ne "off" ) { usage("Invalid operation \"$operation\". Choices are: \"change\" (default), \"status\", \"on\", \"off\""); } if ( ! defined($address)) { usage("No address given."); } if ( ! defined($password) && ! defined ($getpass)) { usage("No password or script to produce password was given."); } if ( defined($password) && defined ($getpass)) { usage("Conflict: supply either a password or a script to produce a password, not both."); } if (defined($getpass) ) { if ( ! -x $getpass ) { usage("Error: no such file: \"$getpass\""); } # run the password retrieval command, grab the results from stdout $password=`$getpass`; if ( ! defined($getpass) || $? != 0 ) { # there was an error getting the password usage("Error retrieving password via \"$getpass\" command"); } } ########### set defaults if ( !defined($operation)) { $operation="change"; } if ( !defined($login)) { $login="admin"; } ##################################################################################### debug(2,"DEBUG\tPDU=\"$address\"\tLogin=\"$login\"\tOperation=\"$operation\"\t\@receptacles=\"@receptacles\"\n"); # If we get here then the command line made sense # Now, fork() a new instance for each recptacle. This will allow operations to act in parallel. # Since the PDU imposes a [seemingly] random delay of 13~30 seconds for a power action (on/off), # doing actions serially could make a serious problem even worse. ############################################################################### # Note: Set the value of the $receptacleindex to correspond to the user-supplied # outlet number PLUS ONE! While receptacles are physically numbered # beginning with 1, in the telnet interface, each outlet is addressed by # a menu item number that's one greater than the receptacle number. The # danger is that menu item "1" controls power to the PDU itself, affecting # every device and eliminating network control. ############################################################################### foreach $receptacle ( @receptacles ) { debug(2,"DEBUG\tGot \$receptacle=\"$receptacle\" from \@receptacles\n"); if ( $receptacle < $receptLOW || $receptacle > $receptHIGH ) { usage("Invalid receptacle number $receptacle\n"); } $receptacleindex= $receptacle + 1; my $childPID; debug(2,"DEBUG\tIn parent($$), about to call fork()\n"); if (!defined($childPID = fork())) { # fork returned undef, so failed die "cannot fork: $!"; } elsif ($childPID == 0) { debug(2,"DEBUG\tfork() succeeded, child PID=\"$$\"\n"); # the fork was successful...set up the Expect params my $exp = new Expect; $exp->log_user(0); # supress normal STDOUT if ( $DEBUG >= 10 ) { # a debugging level greater or equal to 10 also turns # on internal Expect.pm diagnostics $exp->debug(1); #DEBUGGING $exp->exp_internal(1); #DEBUGGING $exp->log_user(1); #DEBUGGING } $exp->restart_timeout_upon_receive(1); $exp->raw_pty(1); my $patidx; $exp->spawn("telnet $address") or die "Cannot spawn \"telnet $address\": $!\n"; sleep 1; $patidx = $exp->expect($timeout, "login:"); if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot login prompt from PDU\n"); $exp->clear_accum(); $exp->send("$login\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent login name to PDU\n"); } else { print STDERR "failed to find login prompt\n"; $exp->do_soft_close(); next; } $patidx = $exp->expect($timeout,"Password:"); if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot password prompt from PDU\n"); $exp->clear_accum(); $exp->send("$password\r"); debug(5,"DEBUG\t\t=>\tsent password to PDU\n"); } else { print STDERR "failed to find password prompt\n"; $exp->do_soft_close(); next; } $patidx = $exp->expect($timeout,'-re',"-- Main Menu --*\r\n\r\n\t1- Devices\r\n\t2- Contacts\r\n\t3- Network\r\n\t4- System\r\n\tX- Logout\r\n\r\n\t- Refresh\r\n>"); if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot Main Menu from PDU\n"); $exp->clear_accum(); $exp->send("1\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent menu choice \"1\" to PDU\n"); } else { print STDERR "failed to find Main Menu\n"; $exp->do_soft_close(); next; } # Note: this menu includes the name of the PDU...which will not necessarily be $address, # so we use a regex here to match that part. The script doesn't know how to handle cascaded # PDUs, and will ALWAYS work with device #1 only. # $patidx = $exp->expect($timeout, '-re', "-- Device List --*\r\n\r\n\t1- .*TRIPP LITE PDUMH15AT.*\nX- Main Menu\r\n\r\n\t- Refresh\r\n> \r\n\r\n\r\n\r\n--* --*r\n\r\n\r\n\t1- Status\r\n\t2- Actions\r\n\t3- Settings\r\n\t4- Logs\r\n\tX- Main Menu\r\n\r\n\t- Refresh\r\n>"); $patidx = $exp->after(); if ( $patidx =~ /-- Device List --*\r\n\r\n\t1- .*TRIPP LITE PDUMH15AT.*\nX- Main Menu\r\n\r\n\t- Refresh\r\n> \r\n\r\n\r\n\r\n--* --*r\n\r\n\r\n\t1- Status\r\n\t2- Actions\r\n\t3- Settings\r\n\t4- Logs\r\n\tX- Main Menu\r\n\r\n\t- Refresh\r\n>/ ) { debug(5,"DEBUG\tDevice List menu found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, '-re', ".*TRIPP LITE PDUMH15AT.*"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot Device List menu from PDU\n"); $exp->clear_accum(); $exp->send("1\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent menu choice \"1\" to PDU\n"); } else { print STDERR "failed to find Device List menu\n"; $exp->do_soft_close(); next; } $patidx = $exp->after(); if ( $patidx =~ /1- Status\r\n\t2- Actions\r\n\t3- Settings\r\n\t4- Logs\r\n\tX- Main Menu\r\n\r\n\t- Refresh\r\n>/) { debug(5,"DEBUG\tAction Choice menu found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, '-re', ".*1- Status\r\n\t2- Actions\r\n\t3- Settings\r\n\t4- Logs\r\n\tX- Main Menu\r\n\r\n\t- Refresh\r\n>"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot Action Choice menu from PDU\n"); $exp->clear_accum(); $exp->send("2\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent menu choice \"2\" to PDU\n"); } else { print STDERR "failed to find Action Choice menu\n"; $exp->do_soft_close(); next; } $patidx = $exp->after(); if ( $patidx =~ /Actions\r\n\r\n\t1- Control\r\n\t2- Loads\r\n\tX- Main Menu\r\n\r\n\t- Refresh\r\n>/ ) { debug(5,"DEBUG\tControl menu found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, '-re', "Actions\r\n\r\n\t1- Control\r\n\t2- Loads\r\n\tX- Main Menu\r\n\r\n\t- Refresh\r\n>"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot Control menu from PDU\n"); $exp->clear_accum(); $exp->send("2\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent menu choice \"2\" to PDU\n"); } else { print STDERR "failed to find Control Menu\n"; $exp->do_soft_close(); next; } $patidx = $exp->after(); if ( $patidx =~ /.* Main Control.*9- Load 8 Control\r\n\tX- Actions Menu\r\n\r\n\t- Refresh\r\n>/m ) { debug(5,"DEBUG\tReceptacle Control menu found in \$exp->after()\n"); } else { # $patidx = $exp->expect($timeout,'-re',"Main Control.*9- Load 8 Control\r\n\tX- Actions Menu\r\n\r\n\t- Refresh\r\n>"); $patidx = $exp->expect($timeout,'-re',"LOAD.*STATE.*CONTROL.*DESCRIPTION"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot Receptacle Control menu from PDU\n"); $exp->clear_accum(); $exp->send("$receptacleindex\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent menu choice \"$receptacleindex\" (receptacle + 1) to PDU\n"); } else { print STDERR "failed to find Receptacle Control menu\n"; $exp->do_soft_close(); next; } # Now we're at the menu that allows us to change the # power status $patidx = $exp->after(); if ( $patidx =~ /Turn Load \d+ O/m) { debug(5,"DEBUG\tPower Change menu found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, '-re', "Turn Load [1-8] O.*"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot power change menu from PDU\n"); $exp->clear_accum(); # OK, we got something back from the last expect() call. Now choose the # action based on the the user-specified operation and the current status. # First, if the operation is "change" (the default), then change the # power state regardless of what it is currently. if ($operation eq "change" ) { $exp->send("2\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent option \"2\" (change current power status) to PDU\n"); $exp->send("\r"); $exp->clear_accum(); } else { debug(5,"DEBUG\t\t=>\trequested operation ($operation) != \"change\"\n"); # OK, the operation is one of "on", "off", "status"...those all require # parsing the current state my $state=$exp->match(); my $tempstate="Unknown"; chomp($state); $state=~s/^[\r\n]*//; $state=~s/[\r\n]*$//; debug(5,"DEBUG\tReceptacle $receptacle menu choice 2: \"$state\"\n"); $state=~s/.*Turn Load \d+ //; # $state=~s/[\r\n].*//; # Note: the "$state" needs to be reversed, since $state currently holds the # name of the possible action (ie., "Turn Load 8 On" implies that outlet 8 # is currently Off. debug(5,"DEBUG\t\$state=\"$state\"\n"); $tempstate="Off" if ( $state eq "On" ); $tempstate="On" if ( $state eq "Off" ); $state=$tempstate; if ( $operation eq "status" ) { print "PDU $address receptacle $receptacle is $state\n"; $exp->do_soft_close(); } else { debug(5,"DEBUG\t\t=>\trequested operation ($operation) != \"status\"\n"); ########## # if the state is "on" and the action is "off", then turn it off # OR # if the state is "off" and the action is "on", then turn it on ########## # ELSE ########## # do nothing ########## if (($state eq "On" && $operation eq "off") || ($state eq "Off" && $operation eq "on" )) { debug(5,"DEBUG\treceptacle state=\"$state\", operation=\"$operation\"...proceeding\n"); $exp->send("2\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent option \"2\" (change current power status) to PDU\n"); } } } } else { print STDERR "failed to find Power Change menu\n"; $exp->do_soft_close(); next; } ############################################################################## # Keep sending "X" to exit from menus $patidx = $exp->after(); if ( $patidx =~ /X- Load Menu/ ) { debug(5,"DEBUG\treceptacle control found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, "X- Load Menu"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot receptacle control menu from PDU\n"); $exp->send("x\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent command \"x\" to return to Load Menu\n"); } $patidx = $exp->after(); if ( $patidx =~ /X- Actions Menu/m) { debug(5,"DEBUG\tLoad Menu found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, "X- Actions Menu"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot Load Menu from PDU\n"); $exp->send("x\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent command \"x\" to return to Actions Menu\n"); } $patidx = $exp->after(); if ( $patidx =~ /X- Main Menu/m) { debug(5,"DEBUG\tMain menu(1) was found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, "X- Main Menu"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot Main menu(1) from PDU\n"); $exp->send("x\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent exit command \"x\" to PDU\n"); } $patidx = $exp->after(); if ( $patidx =~ /X- Main Menu/m) { debug(5,"DEBUG\tMain menu(2) was found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, "X- Main Menu"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot Main menu(2) from PDU\n"); $exp->send("x\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent exit command \"x\" to PDU\n"); } $patidx = $exp->after(); if ( $patidx =~ /X- Logout/m) { debug(5,"DEBUG\ttop-level menu found in \$exp->after()\n"); } else { $patidx = $exp->expect($timeout, "X- Logout"); } if ( defined($patidx)) { debug(5,"DEBUG\t\t<=\tgot top-level menu from PDU\n"); $exp->send("x\r"); $exp->clear_accum(); debug(5,"DEBUG\t\t=>\tsent logout command \"x\" to PDU\n"); } $exp->do_soft_close(); debug(5,"DEBUG\texited from menus, called do_soft_close()\n"); exit 0; # exit from the child } else { # fork returned neither 0 nor undef, # so this branch is the parent # save the list of child PIDs so that we know who to wait() for push(@children,$childPID); } debug(2,"DEBUG\tchild stuff for receptacle $receptacle done, calling next loop iteration\n"); } # all the child processes have been launched...wait for them to finish foreach $childPID (@children) { debug(1,"DEBUG\tIn parent($$), wait()ing for \$childPID=$childPID to exit\n"); waitpid($childPID,0); } debug(1,"DEBUG\tIn parent($$), about to exit()\n"); exit 0;