--- loncom/Attic/lonManage 2003/11/03 10:24:49 1.20 +++ loncom/Attic/lonManage 2003/12/22 11:02:36 1.26 @@ -3,9 +3,9 @@ # # lonManage supports remote management of nodes in a LonCAPA cluster. # -# $Id: lonManage,v 1.20 2003/11/03 10:24:49 foxr Exp $ +# $Id: lonManage,v 1.26 2003/12/22 11:02:36 foxr Exp $ # -# $Id: lonManage,v 1.20 2003/11/03 10:24:49 foxr Exp $ +# $Id: lonManage,v 1.26 2003/12/22 11:02:36 foxr Exp $ # # Copyright Michigan State University Board of Trustees # @@ -63,8 +63,8 @@ use lib "."; use strict; # Because it's good practice. use English; # Cause I like meaningful names. use Getopt::Long; -use IO::Socket::UNIX; # To communicate with lonc. use LondConnection; +use IO::Poll qw(POLLRDNORM POLLWRNORM POLLIN POLLHUP POLLOUT); # File scoped variables: @@ -74,6 +74,15 @@ my %hostshash; # Host table as a host my $MyHost=""; # Host name to use as me. my $ForeignHostTab=""; # Name of foreign hosts table. +my $DefaultServerPort = 5663; # Default server port if standalone. +my $ServerPort; # Port used to connect to lond. + +my $TransitionTimeout = 5; # Poll timeout in seconds. + + +# LondConnection::SetDebug(10); + + # # prints out utility's command usage info. # @@ -102,6 +111,18 @@ sub Usage { If [host] is omitted, all hosts in the hosts.tab file are iterated over. +lonManage [--myname=host --hosts=table] --edit= editscript [host] + Requests lond edit the hosts or domain table (selected by + tablename) with the editing command in editscript. If + host is supplied the individual host is operated on, + otherwise, the entire cluster is operated on. + The edit file has edit request, one per line of the form: + append|newline + replace|key|newline + delete|key + The key is a loncapa hostname if editing the host file + or a domain name if editing the domain table file. + For all of the above syntaxes if --myname=host and --hosts=table are supplied (both must be present), the utility runs in standalone mode presenting itself to the world as 'host' and using the hosts.tab file @@ -110,9 +131,142 @@ USAGE } + +# +# Make a direct connection to the lond in 'host'. The port is +# gotten from the global variable: ServerPort. +# Returns: +# The connection or undef if one could not be formed. +# +sub MakeLondConnection { + my $host = shift; + + my $Connection = LondConnection->new($host, $ServerPort); + return return $Connection; +} +# +# Process the connection state machine until the connection +# becomes idle. This is used both to negotiate the initial +# connection, during which the LondConnection sequences a rather +# complex state machine and during the transaction itself +# for a simpler set of transitions. +# All we really need to be concerned with is whether or not +# we're readable or writable and the final state: +# +# Parameter: +# connection - Represents the LondConnection to be sequenced. +# timeout - Maximum time to wait for readable/writable sockets. +# in seconds. < 0 waits forever. +# Return: +# 'ok' - We got to idle ok. +# 'error:msg' - An error occured. msg describes the error. +# +sub SequenceStateMachine { + my $connection = shift; + my $timeout = shift; + + my $Socket = $connection->GetSocket; + my $returnstatus = "ok"; # optimist!!! + my $error = 0; # Used to force early loop termination + # damned perl has no break!!. + my $state = $connection->GetState; + + while(($connection->GetState ne "Idle") && (!$error)) { + # + # Figure out what the connection wants. read/write and wait for it + # or for the timeout. + # + my $wantread = $connection->WantReadable; + my $poll = new IO::Poll; + $poll->mask($Socket, => $wantread ? POLLIN : POLLOUT); + my $handlecount = $poll->poll($timeout); + if($handlecount == 0) { # no handles ready... timeout!! + $returnstatus = "error:"; + $returnstatus .= "Timeout in state $state\n"; + $error = 1; + } else { + my $done = $poll->handles(); + my $status; + $status = $wantread ? $connection->Readable : + $connection->Writable; + if($status != 0) { + $returnstatus = "error:"; + $returnstatus .= " I/O failed in state $state\n"; + $error = 1; + } + } + $state = $connection->GetState; + } + return $returnstatus; +} + +# +# This function runs through the section of the connection +# state machine that has to do with negotiating the startup +# sequence with lond. The general strategy is to loop +# until the connection state becomes idle or disconnected. +# Disconnected indicates an error or rejection of the +# connection at some point in the negotiation. +# idle indicates a connection ready for a request. +# The main loop consults the object to determine if it +# wants to be writeable or readable, waits for that +# condition on the socket (with timeout) and then issues +# the appropriate LondConnection call. Note that +# LondConnection is capable of doing everything necessary +# to get to the initial idle state. +# +# +# Parameters: +# connection - A connection that has been created with +# the remote lond. This connection should +# be in the Connected state ready to send +# the init sequence. +# +sub NegotiateStartup { + my $connection = shift; + my $returnstatus = "ok"; # Optimistic!!. + + my $state = $connection->GetState; + if($state ne "Connected") { + print "Error: Initial lond connection state: $state should be Connected\n"; + return "error"; + } + + return SequenceStateMachine($connection, $TransitionTimeout); +} +# +# Perform a transaction with the remote lond. +# Paramters: +# connection - the connection object that represents +# a LondConnection to the remote lond. +# command - The request to send to the remote system. +# Returns: +# The 'reaction' of the lond to this command. +# However if the connection to lond is lost during the transaction +# or some other error occurs, the text "error:con_lost" is returned. +# +sub PerformTransaction { + my $connection = shift; + my $command = shift; + my $retval; # What we'll returnl. + + + # Set up the connection to do the transaction then + # do the I/O until idle or error. + # + $connection->InitiateTransaction($command); + + my $status = SequenceStateMachine($connection, $TransitionTimeout); + if($status eq "ok") { + $retval = $connection->GetReply; + } else { + $retval = $status; + } + + return $retval; +} # -# Lifted from lonnet.pm - and we need to figure out a way to get it back in. -# Performas a transaction with lond via the lonc proxy server. +# Performs a transaction direct to a remote lond. # Parameter: # cmd - The text of the request. # host - The host to which the request ultimately goes. @@ -121,19 +275,23 @@ USAGE # lond/lonc etc. # sub subreply { - my ($cmd,$server)=@_; - my $peerfile="$perlvar{'lonSockDir'}/$server"; - my $client=IO::Socket::UNIX->new(Peer =>"$peerfile", - Type => SOCK_STREAM, - Timeout => 10) - or return "con_lost"; - print $client "$cmd\n"; - my $answer=<$client>; - if (!$answer) { $answer="con_lost"; } - chomp($answer); - return $answer; + my $cmd = shift; + my $host = shift; + + + my $connection = MakeLondConnection($host); + if ($connection eq undef) { + return "Connect Failed"; + } + my $reply = NegotiateStartup($connection); + if($reply ne "ok") { + return "connection negotiation failed"; + } + my $reply = PerformTransaction($connection, $cmd); + return $reply; + + } -# >>> BUGBUG <<< # # Use Getopt::Long to parse the parameters of the program. # @@ -159,74 +317,114 @@ sub subreply { sub ParseArgs { - my $pushing = ''; + my $pushing = ''; my $reinitting = ''; - + my $editing = ''; + if(!GetOptions('push=s' => \$pushing, - 'reinit=s' => \$reinitting, - 'myname=s' => \$MyHost, - 'hosts=s' => \$ForeignHostTab)) { - return (); - } + 'reinit=s' => \$reinitting, + 'edit=s' => \$editing, + 'myname=s' => \$MyHost, + 'hosts=s' => \$ForeignHostTab)) { + return (); + } # The --myname and --hosts switch must have values and # most both appear if either appears: - if(($MyHost ne "") && ($ForeignHostTab eq "")) { - return (); - } - if(($ForeignHostTab ne "") && ($MyHost eq "")) { - return (); - } - - # Require exactly one of --push and --reinit - - my $command = ''; - my $commandarg = ''; - my $paramcount = @ARGV; # Number of additional arguments. - - - if($pushing ne '') { - - # --push takes in addition a table, and an optional host: - # - if(($paramcount != 2) && ($paramcount != 1)) { - return (); # Invalid parameter count. - } - if($command ne '') { - return (); - } else { + if(($MyHost ne "") && ($ForeignHostTab eq "")) { + return (); + } + if(($ForeignHostTab ne "") && ($MyHost eq "")) { + return (); + } + + # Require exactly one of --push, --reinit, or --edit + + my $command = ''; + my $commandarg = ''; + my $paramcount = @ARGV; # Number of additional arguments. + + my $commands = 0; # Number of commands seen. + + if($pushing ne '') { + + # --push takes in addition a table, and an optional host: + # + + if(($paramcount != 2) && ($paramcount != 1)) { + return (); # Invalid parameter count. + } - $command = 'push'; - $commandarg = $pushing; + $commands++; # Count a command seen. + $command = 'push'; + $commandarg = $pushing; } - } - if ($reinitting ne '') { + if ($reinitting ne '') { # --reinit takes in addition just an optional host name - if($paramcount > 1) { - return (); + if($paramcount > 1) { + return (); + } + $commands++; # Count a command seen. + $command = 'reinit'; + $commandarg = $reinitting; } - if($command ne '') { - return (); - } else { - $command = 'reinit'; - $commandarg = $reinitting; - } - } + + # --edit takes a script file and optional host name. + # + if ($editing ne "") { + if(($paramcount != 2) && ($paramcount != 1)) { + return (); # Invalid parameter count. + } + + $commands++; # Count a command seen. + $command = 'edit'; + $commandarg = $editing; + } + + # At this point, $commands must be 1 or else we've seen + # The wrong number of command switches: + + if($commands != 1) { + return (); + } # Build the result list: - my @result = ($command, $commandarg); - my $i; - for($i = 0; $i < $paramcount; $i++) { - push(@result, $ARGV[$i]); - } + my @result = ($command, $commandarg); + my $i; + for($i = 0; $i < $paramcount; $i++) { + push(@result, $ARGV[$i]); + } return @result; } # +# Build the editor script. This function: +# - Opens the edit script file. +# - Reads each line of the edit script file +# - Replaces the ending \n with a / +# - Appends it to the EditScript variable. +# - Returns the contents of the EditScript variable. +# Parameters: +# tabletype - The type of table being built: +# hosts or domain +# scriptname - The script input file. +# +sub BuildEditScript { + my $TableType = shift; + my $ScriptName = shift; + + #Stub + + my @EditScript = ( + "$TableType\:append|". + "nscll2\:nscl\:library\:lonkashy.nscl.msu.edu\:35.8.32.89" + ); + return \@EditScript; +} # Read the loncapa configuration stuff. If ForeignHostTab is empty, # assume we are part of a loncapa cluster and read the hosts.tab # file from the config directory. Otherwise, ForeignHossTab @@ -235,20 +433,24 @@ sub ParseArgs { # sub ReadConfig { - if($ForeignHostTab eq "") { - my $perlvarref = LondConnection::read_conf('loncapa.conf'); - %perlvar = %{$perlvarref}; - my $hoststab = LondConnection::read_hosts( - "$perlvar{'lonTabDir'}/hosts.tab"); - %hostshash = %{$hoststab}; - $MyHost = $perlvar{lonHostID}; # Set hostname from vars. - } else { - my $hoststab = LondConnection::read_hosts($ForeignHostTab); - %hostshash = %{$hoststab}; - $perlvar{londPort} = 5663; - $perlvar{lonSockDir} = "/home/httpd/sockets"; - } + + if($ForeignHostTab eq "") { + my $perlvarref = LondConnection::read_conf('loncapa.conf'); + %perlvar = %{$perlvarref}; + my $hoststab = LondConnection::read_hosts( + "$perlvar{lonTabDir}/hosts.tab"); + %hostshash = %{$hoststab}; + $MyHost = $perlvar{lonHostID}; # Set hostname from vars. + $ServerPort = $perlvar{londPort}; + } else { + + LondConnection::ReadForeignConfig($MyHost, + $ForeignHostTab); + my $hoststab = LondConnection::read_hosts($ForeignHostTab); # we need to know too. + %hostshash = %{$hoststab}; + $ServerPort = $DefaultServerPort; + } } # # Determine if the target host is valid. @@ -294,23 +496,23 @@ sub Transact { my $body; my $i; - if(scalar @ARG) { - $body = shift; - $haveBody = 1; - } + if(scalar @ARG) { + $body = shift; + $haveBody = 1; + } # Construct the command to send to the server: - my $request = "encrypt\:"; # All requests are encrypted. - $request .= $command; - if($haveBody) { - $request .= "\:"; - my $bodylines = scalar @$body; - for($i = 0; $i < $bodylines; $i++) { - $request .= $$body[$i]; - } - } else { - $request .= "\n"; - } + my $request = "encrypt\:"; # All requests are encrypted. + $request .= $command; + if($haveBody) { + $request .= "\:"; + my $bodylines = scalar @$body; + for($i = 0; $i < $bodylines; $i++) { + $request .= $$body[$i]; + } + } else { + $request .= "\n"; + } # Body is now built... transact with lond.. my $answer = subreply($request, $host); @@ -334,7 +536,7 @@ sub Transact { # tablefile - name of the file containing the table to push. # host - name of the host to push this file to. # -# >>>BUGBUG<<< This belongs in lonnet.pm. +# # sub PushFile { my $tablename = shift; @@ -343,28 +545,47 @@ sub PushFile { # Open the table file: - if(!open(TABLEFILE, "<$tablefile")) { - die "ENOENT - No such file or directory $tablefile"; - } + if(!open(TABLEFILE, "<$tablefile")) { + die "ENOENT - No such file or directory $tablefile"; + } # Require that the host be valid: - if(!ValidHost($host)) { - die "EHOSTINVAL - Invalid host $host"; # Ok so I invented this 'errno'. - } + if(!ValidHost($host)) { + die "EHOSTINVAL - Invalid host $host"; # Ok so I invented this 'errno'. + } # Read in the file. If the table name is valid, push it. - my @table = ; # These files are pretty small. - close TABLEFILE; + my @table = ; # These files are pretty small. + close TABLEFILE; - if( ($tablename eq "host") || - ($tablename eq "domain")) { - print("Pushing $tablename to $host\n"); - Transact($host, "pushfile:$tablename",\@table); - } else { - die "EINVAL - Invalid parameter. tablename: $tablename must be host or domain"; - } + if( ($tablename eq "host") || + ($tablename eq "domain")) { + print("Pushing $tablename to $host\n"); + Transact($host, "pushfile:$tablename",\@table); + } else { + die "EINVAL - Invalid parameter. tablename: $tablename must be host or domain"; + } +} +# +# This function forms and executes an edit file with a +# remote lond server. We build the full transaction string +# and use Transact to perform the transaction. +# Paramters: +# host - loncapa name of host to operate on. +# body - Body of the command. We send: +# edit:$body as the command request. +# +sub EditFile { + my $host = shift; + my $body = shift; + + if(!ValidHost($host)) { + die "EHOSTINVAL - Invalid host $host"; + } + Transact($host, "edit", $body); } + # # This function is called to reinitialize a server in a remote host. # The servers that can be reinitialized are: @@ -381,23 +602,23 @@ sub PushFile { # >>>BUGBUG<<<< This belongs in lonnet.pm # sub ReinitProcess { - my $process = shift; - my $host = shift; + my $process = shift; + my $host = shift; # Ensure the host is valid: - if(!ValidHost($host)) { - die "EHOSTINVAL - Invalid host $host"; - } + if(!ValidHost($host)) { + die "EHOSTINVAL - Invalid host $host"; + } # Ensure target process selector is valid: - if(($process eq "lonc") || - ($process eq "lond")) { - print("Reinitializing $process in $host\n"); - Transact($host, "reinit:$process"); - } else { - die "EINVAL -Invalid parameter. Process $process must be lonc or lond"; - } + if(($process eq "lonc") || + ($process eq "lond")) { + print("Reinitializing $process in $host\n"); + Transact($host, "reinit:$process"); + } else { + die "EINVAL -Invalid parameter. Process $process must be lonc or lond"; + } } #--------------------------- Entry point: -------------------------- @@ -435,11 +656,11 @@ if($operation eq "push") { # push table my $tablefile = shift @params; my $host = shift @params; if($host) { - PushFile($tablename, $tablefile, $host); + PushFile($tablename, $tablefile, $host); } else { # Push to whole cluster. - foreach my $host (keys %hostshash) { - PushFile($tablename, $tablefile, $host); - } + foreach my $host (keys %hostshash) { + PushFile($tablename, $tablefile, $host); + } } } elsif($operation eq "reinit") { # reinit processname host. @@ -452,9 +673,21 @@ if($operation eq "push") { # push table ReinitProcess($process,$host); } } -} +} elsif($operation eq "edit") { # Edit a table. + my $tablename = shift @params; + my $scriptfile = shift @params; + my $host = shift @params; + my $CommandBody = BuildEditScript($tablename, $scriptfile); + if ($host) { + EditFile($host, $CommandBody); + } else { + foreach my $ClusterMember (keys %hostshash) { + EditFile($ClusterMember, $CommandBody); + } + } +} else { - Usage; + Usage; } exit 0; @@ -474,12 +707,25 @@ Usage: B Sends a HUP signal to the remote systems's lond. - B + B Requests the remote system's lond perform the same action as if it had received a HUP signal. - In the above syntax, the host above is the hosts.tab name of a host, - not the IP address of the host. + B editscript host> + Requests the remote system's lond perform an edit + on editscript supplies a set of + editing commands. Each edit command is one of : + + append|key|newline + delete|key| + replace|key|newline + + The key above is the value of the loncapa host name + in the file. + +In the above syntax, the host above is the +hosts.tab name of a host, +not the IP address of the host. =head1 DESCRIPTION @@ -490,6 +736,7 @@ Usage: =item Getopt::Long =item English =item IO::Socket::UNIX +=item LONCAPA::LondConnection =head1 KEY Subroutines.