1: #!/usr/bin/perl
2: # The LearningOnline Network with CAPA
3: #
4: # lonManage supports remote management of nodes in a LonCAPA cluster.
5: #
6: # $Id: lonManage,v 1.23 2003/11/04 11:23:37 foxr Exp $
7: #
8: # $Id: lonManage,v 1.23 2003/11/04 11:23:37 foxr Exp $
9: #
10: # Copyright Michigan State University Board of Trustees
11: #
12: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
13: ## LON-CAPA is free software; you can redistribute it and/or modify
14: # it under the terms of the GNU General Public License as published by
15: # the Free Software Foundation; either version 2 of the License, or
16: # (at your option) any later version.
17: #
18: # LON-CAPA is distributed in the hope that it will be useful,
19: # but WITHOUT ANY WARRANTY; without even the implied warranty of
20: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21: # GNU General Public License for more details.
22: #
23: # You should have received a copy of the GNU General Public License
24: # along with LON-CAPA; if not, write to the Free Software
25: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26: #
27: # /home/httpd/html/adm/gpl.txt
28: #
29: # http://www.lon-capa.org/
30: #
31: #
32: # lonManage supports management of remot nodes in a lonCAPA cluster.
33: # it is a command line tool. The following command line syntax (usage)
34: # is supported:
35: #
36: # lonManage -push <tablename> newfile [host]
37: # Push <tablename> to the lonTabs directory. Note that
38: # <tablename> must be one of:
39: # host (hosts.tab)
40: # domain (domain.tab)
41: #
42: # lonManage -reinit lonc [host]
43: # Sends a HUP signal to the remote systems's lond.
44: #
45: # lonmanage -reinit lond [host]
46: # Requests the remote system's lond perform the same action as if
47: # it had received a HUP signal.
48: #
49: # In the above syntax, the host above is the hosts.tab name of a host,
50: # not the IP address of the host
51: #
52: # If [host] is not supplied, every host in the client's hosts.tab
53: # table is iterated through and procesed..
54: #
55: #
56:
57:
58:
59: # Modules required:
60:
61: use lib ".";
62:
63: use strict; # Because it's good practice.
64: use English; # Cause I like meaningful names.
65: use Getopt::Long;
66: use LondConnection;
67: use IO::Poll qw(POLLRDNORM POLLWRNORM POLLIN POLLHUP POLLOUT);
68:
69: # File scoped variables:
70:
71: my %perlvar; # Perl variable defs from apache config.
72: my %hostshash; # Host table as a host indexed hash.
73:
74: my $MyHost=""; # Host name to use as me.
75: my $ForeignHostTab=""; # Name of foreign hosts table.
76:
77: my $DefaultServerPort = 5663; # Default server port if standalone.
78: my $ServerPort; # Port used to connect to lond.
79:
80: my $TransitionTimeout = 5; # Poll timeout in seconds.
81:
82:
83: LondConnection::SetDebug(10);
84:
85:
86: #
87: # prints out utility's command usage info.
88: #
89: sub Usage {
90: print "Usage:";
91: print <<USAGE;
92: lonManage [--myname=host --hosts=table] --push=<tablename> newfile [host]
93: Push <tablename> to the lonTabs directory. Note that
94: <tablename> must be one of:
95: host (hosts.tab)
96: domain (domain.tab)
97:
98: lonManage [--myname=host --hosts=table] --reinit=lonc [host]
99: Causes lonc in the remote system to reread hosts.tab and
100: adjust the set of clients that are being maintained to match
101: the new file.
102:
103:
104: lonManage [--myname=host --hosts=table] --reinit=lond [host]
105: Causes lond in the remote system to reread the hosts.tab file
106: and adjust the set of servers to match changes in that file.
107:
108: In the above syntax, the host above is the hosts.tab name of a host,
109: not the IP address of the host.
110:
111: If [host] is omitted, all hosts in the hosts.tab file are iterated
112: over.
113:
114: For all of the above syntaxes if --myname=host and --hosts=table are
115: supplied (both must be present), the utility runs in standalone mode
116: presenting itself to the world as 'host' and using the hosts.tab file
117: specified in the --hosts switch.
118: USAGE
119:
120:
121: }
122:
123: #
124: # Make a direct connection to the lond in 'host'. The port is
125: # gotten from the global variable: ServerPort.
126: # Returns:
127: # The connection or undef if one could not be formed.
128: #
129: sub MakeLondConnection {
130: my $host = shift;
131:
132: my $Connection = LondConnection->new($host, $ServerPort);
133: return return $Connection;
134: }
135: #
136: # This function runs through the section of the connection
137: # state machine that has to do with negotiating the startup
138: # sequence with lond. The general strategy is to loop
139: # until the connection state becomes idle or disconnected.
140: # Disconnected indicates an error or rejection of the
141: # connection at some point in the negotiation.
142: # idle indicates a connection ready for a request.
143: # The main loop consults the object to determine if it
144: # wants to be writeable or readable, waits for that
145: # condition on the socket (with timeout) and then issues
146: # the appropriate LondConnection call. Note that
147: # LondConnection is capable of doing everything necessary
148: # to get to the initial idle state.
149: #
150: #
151: # Parameters:
152: # connection - A connection that has been created with
153: # the remote lond. This connection should
154: # be in the Connected state ready to send
155: # the init sequence.
156: #
157: sub NegotiateStartup {
158: my $connection = shift;
159: my $returnstatus = "ok"; # Optimistic!!.
160:
161: my $state = $connection->GetState;
162: if($state ne "Connected") {
163: print "Error: Initial lond connection state: $state should be Connected\n";
164: return "error";
165: }
166: my $Socket = $connection->GetSocket; # This is a IO:Socket::INET object.
167:
168: # Ready now to enter the main loop:
169: #
170: my $error = 0;
171: while (($connection->GetState ne "Idle") && (!$error)) {
172: #
173: # Wait for the socket to get into the appropriate state:
174: #
175: my $wantread = $connection->WantReadable;
176: my $poll = new IO::Poll;
177: $poll->mask($Socket => $wantread ? POLLIN : POLLOUT);
178: $poll->poll($TransitionTimeout);
179: my $done = $poll->handles();
180: if(scalar($done) == 0) { # Timeout!!!
181: print "Error: Timeout in state : $state negotiating connection\n";
182: $returnstatus = "error";
183: $error = 1;
184: } else {
185: my $status;
186: $status = $wantread ? $connection->Readable : $connection->Writable;
187: if ($status != 0) {
188: print "Error: I/O failed in state : $state negotiating connection\n";
189: $returnstatus = "error";
190: $error = 1;
191: }
192: }
193: }
194:
195:
196: return $returnstatus;
197: }
198: sub PerformTransaction {
199: my $connection = shift;
200: my $command = shift;
201:
202: return "ok";
203: }
204: #
205: # Performs a transaction direct to a remote lond.
206: # Parameter:
207: # cmd - The text of the request.
208: # host - The host to which the request ultimately goes.
209: # Returns:
210: # The text of the reply from the lond or con_lost if not able to contact
211: # lond/lonc etc.
212: #
213: sub subreply {
214: my $cmd = shift;
215: my $host = shift;
216:
217:
218: my $connection = MakeLondConnection($host);
219: if ($connection eq undef) {
220: return "Connect Failed";
221: }
222: my $reply = NegotiateStartup($connection);
223: if($reply ne "ok") {
224: return "connection negotiation failed";
225: }
226: print "Connection negotiated\n";
227: my $reply = PerformTransaction($connection, $cmd);
228: return $reply;
229:
230:
231: # my ($cmd,$server)=@_;
232: # my $peerfile="$perlvar{'lonSockDir'}/$server";
233: # my $client=IO::Socket::UNIX->new(Peer =>"$peerfile",
234: # Type => SOCK_STREAM,
235: # Timeout => 10)
236: # or return "con_lost";
237: # print $client "$cmd\n";
238: # my $answer=<$client>;
239: # if (!$answer) { $answer="con_lost"; }
240: # chomp($answer);
241: # return $answer;
242: }
243: # >>> BUGBUG <<<
244: #
245: # Use Getopt::Long to parse the parameters of the program.
246: #
247: # Return value is a list consisting of:
248: # A 'command' which is one of:
249: # push - table push requested.
250: # reinit - reinit requested.
251: # Additional parameters as follows:
252: # for push: Tablename, hostname
253: # for reinit: Appname hostname
254: #
255: # This function does not validation of the parameters of push and
256: # reinit.
257: #
258: # returns a list. The first element of the list is the operation name
259: # (e.g. reinit or push). The second element is the switch parameter.
260: # for push, this is the table name, for reinit, this is the process name.
261: # Additional elements of the list are the command argument. The count of
262: # command arguments is validated, but not their semantics.
263: #
264: # returns an empty list if the parse fails.
265: #
266:
267:
268: sub ParseArgs {
269: my $pushing = '';
270: my $reinitting = '';
271:
272: if(!GetOptions('push=s' => \$pushing,
273: 'reinit=s' => \$reinitting,
274: 'myname=s' => \$MyHost,
275: 'hosts=s' => \$ForeignHostTab)) {
276: return ();
277: }
278: # The --myname and --hosts switch must have values and
279: # most both appear if either appears:
280:
281: if(($MyHost ne "") && ($ForeignHostTab eq "")) {
282: return ();
283: }
284: if(($ForeignHostTab ne "") && ($MyHost eq "")) {
285: return ();
286: }
287:
288: # Require exactly one of --push and --reinit
289:
290: my $command = '';
291: my $commandarg = '';
292: my $paramcount = @ARGV; # Number of additional arguments.
293:
294:
295: if($pushing ne '') {
296:
297: # --push takes in addition a table, and an optional host:
298: #
299: if(($paramcount != 2) && ($paramcount != 1)) {
300: return (); # Invalid parameter count.
301: }
302: if($command ne '') {
303: return ();
304: } else {
305:
306: $command = 'push';
307: $commandarg = $pushing;
308: }
309: }
310:
311: if ($reinitting ne '') {
312:
313: # --reinit takes in addition just an optional host name
314:
315: if($paramcount > 1) {
316: return ();
317: }
318: if($command ne '') {
319: return ();
320: } else {
321: $command = 'reinit';
322: $commandarg = $reinitting;
323: }
324: }
325:
326: # Build the result list:
327:
328: my @result = ($command, $commandarg);
329: my $i;
330: for($i = 0; $i < $paramcount; $i++) {
331: push(@result, $ARGV[$i]);
332: }
333:
334: return @result;
335: }
336: #
337: # Read the loncapa configuration stuff. If ForeignHostTab is empty,
338: # assume we are part of a loncapa cluster and read the hosts.tab
339: # file from the config directory. Otherwise, ForeignHossTab
340: # is the name of an alternate configuration file to read in
341: # standalone mode.
342: #
343: sub ReadConfig {
344:
345: if($ForeignHostTab eq "") {
346: my $perlvarref = LondConnection::read_conf('loncapa.conf');
347: %perlvar = %{$perlvarref};
348: my $hoststab = LondConnection::read_hosts(
349: "$perlvar{'lonTabDir'}/hosts.tab");
350: %hostshash = %{$hoststab};
351: $MyHost = $perlvar{lonHostID}; # Set hostname from vars.
352: $ServerPort = $perlvar{londPort};
353: } else {
354:
355: LondConnection::ReadForeignConfig($MyHost, $ForeignHostTab);
356: my $hoststab = LondConnection::read_hosts($ForeignHostTab); # we need to know too.
357: %hostshash = %{$hoststab};
358: $ServerPort = $DefaultServerPort;
359: }
360:
361: }
362: #
363: # Determine if the target host is valid.
364: # This is done by reading the current hosts.tab file.
365: # For the host to be valid, it must be inthe file.
366: #
367: # Parameters:
368: # host - Name of host to check on.
369: # Returns:
370: # true if host is valid.
371: # false if host is invalid.
372: #
373: sub ValidHost {
374: my $host = shift;
375:
376:
377: return defined $hostshash{$host};
378:
379: }
380:
381:
382:
383: #
384: # Performs a transaction with lonc.
385: # By the time this is called, the transaction has already been
386: # validated by the caller.
387: #
388: # Parameters:
389: #
390: # host - hosts.tab name of the host whose lonc we'll be talking to.
391: # command - The base command we'll be asking lond to execute.
392: # body - [optional] If supplied, this is a command body that is a ref.
393: # to an array of lines that will be appended to the
394: # command.
395: #
396: # NOTE:
397: # The command will be done as an encrypted operation.
398: #
399: sub Transact {
400: my $host = shift;
401: my $command = shift;
402: my $haveBody= 0;
403: my $body;
404: my $i;
405:
406: if(scalar @ARG) {
407: $body = shift;
408: $haveBody = 1;
409: }
410: # Construct the command to send to the server:
411:
412: my $request = "encrypt\:"; # All requests are encrypted.
413: $request .= $command;
414: if($haveBody) {
415: $request .= "\:";
416: my $bodylines = scalar @$body;
417: for($i = 0; $i < $bodylines; $i++) {
418: $request .= $$body[$i];
419: }
420: } else {
421: $request .= "\n";
422: }
423: # Body is now built... transact with lond..
424:
425: my $answer = subreply($request, $host);
426:
427: print "$answer\n";
428:
429: }
430: #
431: # Called to push a file to the remote system.
432: # The only legal files to push are hosts.tab and domain.tab.
433: # Security is somewhat improved by
434: #
435: # - Requiring the user run as root.
436: # - Connecting with lonc rather than lond directly ensuring this is a loncapa
437: # host
438: # - We must appear in the remote host's hosts.tab file.
439: # - The host must appear in our hosts.tab file.
440: #
441: # Parameters:
442: # tablename - must be one of hosts or domain.
443: # tablefile - name of the file containing the table to push.
444: # host - name of the host to push this file to.
445: #
446: # >>>BUGBUG<<< This belongs in lonnet.pm.
447: #
448: sub PushFile {
449: my $tablename = shift;
450: my $tablefile = shift;
451: my $host = shift;
452:
453: # Open the table file:
454:
455: if(!open(TABLEFILE, "<$tablefile")) {
456: die "ENOENT - No such file or directory $tablefile";
457: }
458:
459: # Require that the host be valid:
460:
461: if(!ValidHost($host)) {
462: die "EHOSTINVAL - Invalid host $host"; # Ok so I invented this 'errno'.
463: }
464: # Read in the file. If the table name is valid, push it.
465:
466: my @table = <TABLEFILE>; # These files are pretty small.
467: close TABLEFILE;
468:
469: if( ($tablename eq "host") ||
470: ($tablename eq "domain")) {
471: print("Pushing $tablename to $host\n");
472: Transact($host, "pushfile:$tablename",\@table);
473: } else {
474: die "EINVAL - Invalid parameter. tablename: $tablename must be host or domain";
475: }
476: }
477: #
478: # This function is called to reinitialize a server in a remote host.
479: # The servers that can be reinitialized are:
480: # - lonc - The lonc client process.
481: # - lond - The lond daemon.
482: # NOTE:
483: # Reinitialization in this case means re-scanning the hosts table,
484: # starting new lond/lonc's as approprate and stopping existing lonc/lond's.
485: #
486: # Parameters:
487: # process - The name of the process to reinit (lonc or lond).
488: # host - The host in which this reinit will happen.
489: #
490: # >>>BUGBUG<<<< This belongs in lonnet.pm
491: #
492: sub ReinitProcess {
493: my $process = shift;
494: my $host = shift;
495:
496: # Ensure the host is valid:
497:
498: if(!ValidHost($host)) {
499: die "EHOSTINVAL - Invalid host $host";
500: }
501: # Ensure target process selector is valid:
502:
503: if(($process eq "lonc") ||
504: ($process eq "lond")) {
505: print("Reinitializing $process in $host\n");
506: Transact($host, "reinit:$process");
507: } else {
508: die "EINVAL -Invalid parameter. Process $process must be lonc or lond";
509: }
510: }
511: #--------------------------- Entry point: --------------------------
512:
513:
514:
515: # Parse the parameters
516: # If command parsing failed, then print usage:
517:
518: my @params = ParseArgs;
519: my $nparam = @params;
520:
521: if($nparam == 0) {
522: Usage;
523: exit -1;
524: }
525: #
526: # Next, ensure we are running as EID root.
527: #
528: if ($EUID != 0) {
529: die "ENOPRIV - No privilege for requested operation"
530: }
531:
532: #
533: # Read the configuration file.
534: #
535:
536: ReadConfig; # Read the configuration info (incl.hosts).
537:
538: # Based on the operation requested invoke the appropriate function:
539:
540: my $operation = shift @params;
541:
542: if($operation eq "push") { # push tablename filename host
543: my $tablename = shift @params;
544: my $tablefile = shift @params;
545: my $host = shift @params;
546: if($host) {
547: PushFile($tablename, $tablefile, $host);
548: } else { # Push to whole cluster.
549: foreach my $host (keys %hostshash) {
550: PushFile($tablename, $tablefile, $host);
551: }
552: }
553:
554: } elsif($operation eq "reinit") { # reinit processname host.
555: my $process = shift @params;
556: my $host = shift @params;
557: if ($host) {
558: ReinitProcess($process, $host);
559: } else { # Reinit whole cluster.
560: foreach my $host (keys %hostshash) {
561: ReinitProcess($process,$host);
562: }
563: }
564: }
565: else {
566: Usage;
567: }
568: exit 0;
569:
570: =head1 NAME
571: lonManage - Command line utility for remote management of lonCAPA
572: cluster nodes.
573:
574: =head1 SYNOPSIS
575:
576: Usage:
577: B<lonManage --push=<tablename> newfile host>
578: Push <tablename> to the lonTabs directory. Note that
579: <tablename> must be one of:
580: hosts (hosts.tab)
581: domain (domain.tab)
582:
583: B<lonManage --reinit=lonc host>
584: Sends a HUP signal to the remote systems's lond.
585:
586: B<lonmanage --reinit=lond host>
587: Requests the remote system's lond perform the same action as if
588: it had received a HUP signal.
589:
590: In the above syntax, the host above is the hosts.tab name of a host,
591: not the IP address of the host.
592:
593:
594: =head1 DESCRIPTION
595:
596: =head1 PREREQUISITES
597:
598: =item strict
599: =item Getopt::Long
600: =item English
601: =item IO::Socket::UNIX
602:
603: =head1 KEY Subroutines.
604:
605: =head1 CATEGORIES
606: Command line utility
607:
608: =cut
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>