--- loncom/Attic/lonManage 2003/08/12 09:58:49 1.2 +++ loncom/Attic/lonManage 2003/11/04 11:52:06 1.25 @@ -3,9 +3,9 @@ # # lonManage supports remote management of nodes in a LonCAPA cluster. # -# $Id: lonManage,v 1.2 2003/08/12 09:58:49 foxr Exp $ +# $Id: lonManage,v 1.25 2003/11/04 11:52:06 foxr Exp $ # -# $Id: lonManage,v 1.2 2003/08/12 09:58:49 foxr Exp $ +# $Id: lonManage,v 1.25 2003/11/04 11:52:06 foxr Exp $ # # Copyright Michigan State University Board of Trustees # @@ -33,57 +33,590 @@ # it is a command line tool. The following command line syntax (usage) # is supported: # -# lonManage -push newfile host +# lonManage -push newfile [host] # Push to the lonTabs directory. Note that # must be one of: -# hosts (hosts.tab) +# host (hosts.tab) # domain (domain.tab) # -# lonManage -reinit lonc host +# lonManage -reinit lonc [host] # Sends a HUP signal to the remote systems's lond. # -# lonmanage -reinit lond host +# lonmanage -reinit lond [host] # 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. -# -# $Log: lonManage,v $ -# Revision 1.2 2003/08/12 09:58:49 foxr -# Add usage and skeleton documentation. +# not the IP address of the host +# +# If [host] is not supplied, every host in the client's hosts.tab +# table is iterated through and procesed.. # # -sub Usage {} { + + +# Modules required: + +use lib "."; + +use strict; # Because it's good practice. +use English; # Cause I like meaningful names. +use Getopt::Long; +use LondConnection; +use IO::Poll qw(POLLRDNORM POLLWRNORM POLLIN POLLHUP POLLOUT); + +# File scoped variables: + +my %perlvar; # Perl variable defs from apache config. +my %hostshash; # Host table as a host indexed hash. + +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. +# +sub Usage { print "Usage:"; print < newfile host + lonManage [--myname=host --hosts=table] --push= newfile [host] Push to the lonTabs directory. Note that must be one of: - hosts (hosts.tab) + host (hosts.tab) domain (domain.tab) - lonManage -reinit lonc host - Sends a HUP signal to the remote systems's lond. - - lonmanage -reinit lond host - Requests the remote system's lond perform the same action as if - it had received a HUP signal. + lonManage [--myname=host --hosts=table] --reinit=lonc [host] + Causes lonc in the remote system to reread hosts.tab and + adjust the set of clients that are being maintained to match + the new file. + + + lonManage [--myname=host --hosts=table] --reinit=lond [host] + Causes lond in the remote system to reread the hosts.tab file + and adjust the set of servers to match changes in that file. In the above syntax, the host above is the hosts.tab name of a host, not the IP address of the host. + + If [host] is omitted, all hosts in the hosts.tab file are iterated + over. + + 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 + specified in the --hosts switch. USAGE } # -# If command parsing failed, then print 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); + $poll->poll($timeout); + my $done = $poll->handles(); + if(scalar($done) == 0) { # no handles ready... timeout!! + $returnstatus = "error:"; + $returnstatus .= "Timeout in state $state\n"; + $error = 1; + } else { + 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; +} +# +# Performs a transaction direct to a remote lond. +# Parameter: +# cmd - The text of the request. +# host - The host to which the request ultimately goes. +# Returns: +# The text of the reply from the lond or con_lost if not able to contact +# lond/lonc etc. +# +sub subreply { + 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; + + + # 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; +} +# >>> BUGBUG <<< +# +# Use Getopt::Long to parse the parameters of the program. +# +# Return value is a list consisting of: +# A 'command' which is one of: +# push - table push requested. +# reinit - reinit requested. +# Additional parameters as follows: +# for push: Tablename, hostname +# for reinit: Appname hostname +# +# This function does not validation of the parameters of push and +# reinit. +# +# returns a list. The first element of the list is the operation name +# (e.g. reinit or push). The second element is the switch parameter. +# for push, this is the table name, for reinit, this is the process name. +# Additional elements of the list are the command argument. The count of +# command arguments is validated, but not their semantics. +# +# returns an empty list if the parse fails. +# + + +sub ParseArgs { + my $pushing = ''; + my $reinitting = ''; + + if(!GetOptions('push=s' => \$pushing, + 'reinit=s' => \$reinitting, + '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 { + + $command = 'push'; + $commandarg = $pushing; + } + } + + if ($reinitting ne '') { + + # --reinit takes in addition just an optional host name + + if($paramcount > 1) { + return (); + } + if($command ne '') { + return (); + } else { + $command = 'reinit'; + $commandarg = $reinitting; + } + } + + # Build the result list: + + my @result = ($command, $commandarg); + my $i; + for($i = 0; $i < $paramcount; $i++) { + push(@result, $ARGV[$i]); + } + + return @result; +} +# +# 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 +# is the name of an alternate configuration file to read in +# standalone mode. +# +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. + $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. +# This is done by reading the current hosts.tab file. +# For the host to be valid, it must be inthe file. +# +# Parameters: +# host - Name of host to check on. +# Returns: +# true if host is valid. +# false if host is invalid. +# +sub ValidHost { + my $host = shift; + + + return defined $hostshash{$host}; + +} + + + +# +# Performs a transaction with lonc. +# By the time this is called, the transaction has already been +# validated by the caller. +# +# Parameters: +# +# host - hosts.tab name of the host whose lonc we'll be talking to. +# command - The base command we'll be asking lond to execute. +# body - [optional] If supplied, this is a command body that is a ref. +# to an array of lines that will be appended to the +# command. +# +# NOTE: +# The command will be done as an encrypted operation. +# +sub Transact { + my $host = shift; + my $command = shift; + my $haveBody= 0; + my $body; + my $i; + + 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"; + } + # Body is now built... transact with lond.. + + my $answer = subreply($request, $host); -if(0) { + print "$answer\n"; + +} +# +# Called to push a file to the remote system. +# The only legal files to push are hosts.tab and domain.tab. +# Security is somewhat improved by +# +# - Requiring the user run as root. +# - Connecting with lonc rather than lond directly ensuring this is a loncapa +# host +# - We must appear in the remote host's hosts.tab file. +# - The host must appear in our hosts.tab file. +# +# Parameters: +# tablename - must be one of hosts or domain. +# 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; + my $tablefile = shift; + my $host = shift; + + # Open the table file: + + 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'. + } + # Read in the file. If the table name is valid, push it. + + 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"; + } +} +# +# This function is called to reinitialize a server in a remote host. +# The servers that can be reinitialized are: +# - lonc - The lonc client process. +# - lond - The lond daemon. +# NOTE: +# Reinitialization in this case means re-scanning the hosts table, +# starting new lond/lonc's as approprate and stopping existing lonc/lond's. +# +# Parameters: +# process - The name of the process to reinit (lonc or lond). +# host - The host in which this reinit will happen. +# +# >>>BUGBUG<<<< This belongs in lonnet.pm +# +sub ReinitProcess { + my $process = shift; + my $host = shift; + + # Ensure the host is valid: + + 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"; + } +} +#--------------------------- Entry point: -------------------------- + + + +# Parse the parameters +# If command parsing failed, then print usage: + +my @params = ParseArgs; +my $nparam = @params; + +if($nparam == 0) { + Usage; + exit -1; +} +# +# Next, ensure we are running as EID root. +# +if ($EUID != 0) { + die "ENOPRIV - No privilege for requested operation" +} + +# +# Read the configuration file. +# + +ReadConfig; # Read the configuration info (incl.hosts). + +# Based on the operation requested invoke the appropriate function: + +my $operation = shift @params; + +if($operation eq "push") { # push tablename filename host + my $tablename = shift @params; + my $tablefile = shift @params; + my $host = shift @params; + if($host) { + PushFile($tablename, $tablefile, $host); + } else { # Push to whole cluster. + foreach my $host (keys %hostshash) { + PushFile($tablename, $tablefile, $host); + } + } + +} elsif($operation eq "reinit") { # reinit processname host. + my $process = shift @params; + my $host = shift @params; + if ($host) { + ReinitProcess($process, $host); + } else { # Reinit whole cluster. + foreach my $host (keys %hostshash) { + ReinitProcess($process,$host); + } + } +} +else { Usage; } +exit 0; =head1 NAME lonManage - Command line utility for remote management of lonCAPA @@ -92,16 +625,16 @@ if(0) { =head1 SYNOPSIS Usage: - B newfile host> + B newfile host> Push to the lonTabs directory. Note that must be one of: hosts (hosts.tab) domain (domain.tab) - B + 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. @@ -113,6 +646,13 @@ Usage: =head1 PREREQUISITES +=item strict +=item Getopt::Long +=item English +=item IO::Socket::UNIX + +=head1 KEY Subroutines. + =head1 CATEGORIES Command line utility