File Coverage

blib/lib/IS/Init.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package IS::Init;
2 14     14   95805 use strict;
  14         31  
  14         534  
3 14     14   12748 use IO::Socket;
  14         411740  
  14         64  
4 14     14   124656 use IO::Select;
  14         30776  
  14         727  
5 14     14   11546 use POSIX qw(:signal_h :errno_h :sys_wait_h);
  14         122848  
  14         131  
6 14     14   55074 use Data::Dump qw(dump);
  0            
  0            
7              
8             my $debug=$ENV{DEBUG} || 0;
9              
10             sub debug
11             {
12             warn @_ if $debug;
13             }
14              
15             BEGIN {
16             use vars qw ($VERSION);
17             $VERSION = 0.93;
18             }
19              
20             =head1 NAME
21              
22             IS::Init - Clusterwide "init", spawn cluster applications
23              
24             =head1 SYNOPSIS
25              
26             use IS::Init;
27              
28             my $init = new IS::Init;
29              
30             # spawn all apps for resource group "foo", runlevel "run"
31             $init->tell("foo","run");
32              
33             # spawn all apps for resource group "foo", runlevel "runmore"
34             # (this stops everything started by runlevel "run")
35             $init->tell("foo","runmore");
36              
37             =head1 DESCRIPTION
38              
39             This module provides basic "init" functionality, giving you a single
40             inittab-like file to manage initialization and daemon startup across a
41             cluster or collection of machines.
42              
43             =head1 USAGE
44              
45             This module's package includes a script 'isinit', which is intended to
46             be a bolt-in cluster init tool, calling IS::Init. The script is
47             called like 'init', with the addition of a new "resource group"
48             argument.
49              
50             This module is intended to be used like 'init' and 'telinit' -- the
51             first execution runs as a daemon, spawning and managing processes.
52             Later executions talk to the first, requesting it to switch to
53             different runlevels.
54              
55             The module references a configuration file, /etc/isinittab by default,
56             which is identical in format to /etc/inittab, with a new "resource
57             group" column added. This file must be replicated across all hosts in
58             the cluster by some means.
59              
60             A "resource group" is a collection of applications and physical
61             resources which together make up a coherent function. For example,
62             sendmail, /etc/sendmail.cf, and the /var/spool/mqueue directory might
63             make up a resource group. From /etc/isinittab you could spawn the
64             scripts which update sendmail.cf, mount mqueue, and then start
65             sendmail itself.
66              
67             =head1 PUBLIC METHODS
68              
69             =head2 new
70              
71             =cut
72              
73             sub new
74             {
75             my $class=shift;
76             $class = (ref $class || $class);
77             my $self={};
78             bless $self, $class;
79              
80             =pod
81              
82             The constructor accepts an optional hash containing the paths to the
83             configuration file and to the socket, like this:
84              
85             my $init = new IS::Init (
86             'config' => '/etc/isinittab',
87             'socket' => '/var/run/is/init.s'
88             'initstat' => '/var/run/is/initstat'
89             );
90              
91             =cut
92              
93             my %parms=@_;
94            
95             $self->{'config'} = $parms{'config'} || "/etc/isinittab";
96             $self->{'socket'} = "/var/run/is/init.s";
97             $self->_config();
98             $self->{'socket'} = $parms{'socket'} if $parms{'socket'};
99              
100             ($self->{'group'}, $self->{'level'}) = ("NULL", "NULL");
101              
102             =pod
103              
104             The first time this method is executed on a machine, it opens a UNIX
105             domain socket, /var/run/is/init.s by default. Subsequent executions
106             communicate with the first via this socket.
107              
108             =cut
109              
110             $self->_open_socket() || $self->_start_daemon() || die $!;
111              
112             return $self;
113             }
114              
115             =head2 tell($resource_group,$runlevel)
116              
117             This method talks to a running IS::Init daemon, telling it to switch
118             the given resource group to the given runlevel.
119              
120             All processes listed in the configuration file (normally
121             /etc/isinittab) which belong to the new runlevel will be started if
122             they aren't already running.
123              
124             All processes in the resource group which do not belong to the new
125             runlevel will be killed.
126              
127             Other resource groups will not be affected.
128              
129             =cut
130              
131             sub tell
132             {
133             my ($self,$group,$runlevel)=@_;
134             my $socket = $self->_open_socket() || die $!;
135             print $socket "$group $runlevel\n";
136             close($socket);
137             1;
138             }
139              
140             sub status
141             {
142             my $self=shift;
143             my %parm = @_;
144             my $group = $parm{'group'} if $parm{'group'};
145             my $level = $parm{'level'} if $parm{'level'};
146             my $initstat = $parm{'initstat'} if $parm{'initstat'};
147             # allow this to be called as IS::Init->status(...)
148             $self=bless({},$self) unless ref($self);
149             $self->{'initstat'} = $initstat if $initstat;
150             return "" unless $self->{'initstat'} && -f $self->{'initstat'};
151             my $startid="start";
152             my $endid="end";
153             my $out;
154             do
155             {
156             $out ="";
157             open(STATUS,"<$self->{'initstat'}") || die $!;
158             while()
159             {
160             if (/^!startid (.*)/)
161             {
162             $startid = $1;
163             next;
164             }
165             next if $startid eq "start";
166             if (/^!endid (.*)/)
167             {
168             $endid = $1;
169             last;
170             }
171             my ($sgroup,$state,$slevel) = split;
172             next unless $state;
173             if ($group)
174             {
175             next if $group ne $sgroup;
176             s/^\S+\s+(.*?)\s*$/$1/;
177             chomp;
178             }
179             if ($level)
180             {
181             next if $level ne $slevel;
182             s/^(\S+)\s+.*$/$1/;
183             chomp;
184             }
185             $out .= $_;
186             }
187              
188             } while $startid ne $endid;
189             return $out;
190             }
191              
192             sub stopall
193             {
194             my ($self)=@_;
195             my $socket = $self->_open_socket() || die $!;
196             print $socket "stopall";
197             close($socket);
198             1;
199             }
200              
201              
202             sub _open_socket
203             {
204             my $self=shift;
205             my $client = new IO::Socket::UNIX (
206             Peer => $self->{'socket'},
207             Type => SOCK_STREAM
208             );
209             return $client;
210             }
211              
212             sub _start_daemon
213             {
214             my $self=shift;
215              
216             my $child;
217             unless ($child = fork())
218             {
219             while(1)
220             {
221             unlink $self->{'socket'};
222             my $server = new IO::Socket::UNIX (
223             Local => $self->{'socket'},
224             Type => SOCK_STREAM,
225             Listen => SOMAXCONN
226             ) || die $!;
227             while(my $client = $server->accept())
228             {
229             $SIG{CHLD} = 'IGNORE';
230             debug "reading\n";
231             my $data=<$client>;
232             $data="" unless $data;
233             chomp($data=$data);
234             debug "$data\n" if $data;
235             debug "done reading, got: $data\n";
236             $self->_stopall() if $data =~ /^stopall/;
237             my ($group,$level) = split(' ',$data);
238             $self->_spawn($group,$level);
239             $self->_sigchld();
240             close($client);
241             }
242             debug "restarting socket\n";
243             }
244             }
245              
246             debug "IS::Init daemon started as PID $child\n";
247              
248             sleep 1;
249             return $child;
250             }
251              
252             sub _status
253             {
254             my ($self,$group,$level,$state) = @_;
255             $level = $self->{'status'}{$group}{'level'} unless $level;
256             $self->{'status'}{$group}{'level'}=$level;
257             $self->{'status'}{$group}{'state'}=$state;
258             return "" unless $self->{'initstat'};
259             # fetch all groups in inittab
260             my @group;
261             for my $tag (keys %{$self->{'inittab'}{'group'}})
262             {
263             my $group = $self->{'inittab'}{'group'}{$tag};
264             push @group, $group unless grep /^$group$/, @group;
265             }
266             # add the groups in status, just in case
267             for my $group (keys %{$self->{'status'}})
268             {
269             push @group, $group unless grep /^$group$/, @group;
270             }
271             my $id = rand();
272             open(STATUS,">$self->{'initstat'}") || die $!;
273             print STATUS "!startid $id\n";
274             # for my $group (keys %{$self->{'status'}})
275             for my $group (sort @group)
276             {
277             next if $group eq "NULL";
278             debug "storing status for $group\n";
279             my $state = $self->{'status'}{$group}{'state'} || "stopped";
280             my $level = $self->{'status'}{$group}{'level'} || "";
281             printf STATUS ("%-15s %-15s %-15s\n", $group, $state, $level);
282             }
283             print STATUS "!endid $id\n";
284             # print STATUS dump($self);
285             close STATUS;
286             }
287              
288             sub _stopall
289             {
290             my $self=shift;
291             for my $group (keys %{$self->{'status'}})
292             {
293             $self->_status($group,'',"stopping");
294             }
295             for my $tag (keys %{$self->{'pid'}})
296             {
297             $self->_kill($tag);
298             }
299             for my $group (keys %{$self->{'status'}})
300             {
301             $self->_status($group,'',"stopped");
302             }
303             exit(0);
304             }
305              
306             sub _config
307             {
308             my $self=shift;
309             $self->{'inittab'}={};
310             open(INITTAB,"<$self->{'config'}") || die $!;
311             while()
312             {
313             next if /^#/;
314             next if /^\s*$/;
315             chomp;
316             my ($group,$tag,$level,$mode,$cmd) = split(':',$_,5);
317             debug "inittab $group|$tag|$level|$mode|$cmd\n";
318             if ($mode eq "socket")
319             {
320             $self->{'socket'} = $cmd;
321             next;
322             }
323             if ($mode eq "initstat")
324             {
325             $self->{'initstat'} = $cmd;
326             next;
327             }
328             next if /^:::/;
329             $self->{'inittab'}{'group'}{$tag} = $group;
330             my @level;
331             if ($level =~/,/)
332             {
333             @level = split(',',$level);
334             debug dump(@level). "\n";
335             }
336             else
337             {
338             @level = split('',$level);
339             }
340             debug "final levels @level\n";
341             $self->{'inittab'}{'levels'}{$tag} = \@level;
342             $self->{'inittab'}{'mode'}{$tag} = $mode;
343             $self->{'inittab'}{'cmd'}{$tag} = $cmd;
344             }
345             }
346              
347             # starts and stops processes according to new runlevel
348             sub _spawn
349             {
350             my ($self,$newgroup,$newlevel)=@_;
351             ($newgroup,$newlevel)=($self->{'group'},$self->{'level'})
352             unless $newgroup && ($newlevel || (defined($newlevel) && $newlevel == 0));
353             ($self->{'group'},$self->{'level'}) = ($newgroup,$newlevel);
354             $self->_status($newgroup,$newlevel,"start");
355             $self->_config();
356             my @activetags;
357             my $testres="";
358             for my $tag (keys %{$self->{'inittab'}{'group'}})
359             {
360             debug "checking $tag\n";
361             my $group=$self->{'inittab'}{'group'}{$tag};
362             my @level=@{$self->{'inittab'}{'levels'}{$tag}};
363             my $mode=$self->{'inittab'}{'mode'}{$tag};
364             my $cmd=$self->{'inittab'}{'cmd'}{$tag};
365             next if $mode eq "off";
366             push @activetags, $tag;
367             next unless $group eq $newgroup;
368              
369             debug "$group $tag has levels @level\n";
370              
371             # if this line is for our newly commanded runlevel
372             if(grep /^$newlevel$/, @level)
373             {
374             # start processes in new runlevel
375             debug "starting $newgroup $newlevel\n";
376              
377             # bail if already started in another runlevel
378             next if $self->{'pid'}{$tag};
379              
380             if ($mode eq "wait")
381             {
382             # set a placeholder to keep us from running $tag again
383             $self->{'pid'}{$tag} = "wait";
384             debug "wait system($cmd)\n";
385             # XXX process start
386             system($cmd);
387             next;
388             }
389              
390             if ($mode eq "test")
391             {
392             # set a placeholder to keep us from running $tag again
393             $self->{'pid'}{$tag} = "test";
394             debug "test system($cmd)\n";
395             # XXX process start
396             system($cmd);
397             my $rc = $? >> 8;
398             $testres = "fail" if $rc;
399             next;
400             }
401              
402             if ($mode eq "respawn")
403             {
404             # start timing and counting
405             $self->{'time'}{$tag}=time() unless $self->{'time'}{$tag};
406             $self->{'counter'}{$tag}=0 unless $self->{'counter'}{$tag};
407             if($self->{'time'}{$tag} < time() - 10)
408             {
409             # it's been a while; restarting timing and counting
410             $self->{'time'}{$tag}=time();
411             $self->{'counter'}{$tag}=0;
412             }
413             # skip this inittab entry if we're in jail
414             next unless time() >= $self->{'time'}{$tag};
415             # let it respawn no more than 5 times in 10 seconds
416             if ($self->{'counter'}{$tag} >= 5)
417             {
418             warn "$0: $tag respawning too rapidly -- sleeping 60 seconds\n";
419             # go to jail
420             $self->{'time'}{$tag}=time() + 60;
421             $self->{'counter'}{$tag}=0;
422             next;
423             }
424             $self->{'counter'}{$tag}++;
425             }
426              
427             # we only get here if tag is respawn or once
428             if (my $pid = fork())
429             {
430             # parent
431             debug "$pid forked\n";
432             # build index so we can find pid from tag
433             $self->{'pid'}{$tag} = $pid;
434             # build reverse index so we can find tag from pid
435             $self->{'tag'}{$self->{'pid'}{$tag}}=$tag;
436             next;
437             }
438             # child
439             # sleep 1;
440             debug "exec $cmd\n";
441             # XXX process start
442             exec($cmd);
443             }
444             else
445             {
446             # stop processes in old runlevel
447             next unless $self->{'pid'}{$tag};
448             $self->_kill($tag);
449             }
450              
451             }
452              
453             # stop processes which are no longer in inittab
454             for my $tag (keys %{$self->{'pid'}})
455             {
456             next if grep /^$tag$/, @activetags;
457             $self->_kill($tag);
458             }
459              
460             my $state = $testres || "run";
461             $self->_status($newgroup,$newlevel,$state);
462             }
463              
464             sub _kill
465             {
466             my $self = shift;
467             my $tag = shift;
468             if
469             (
470             $self->{'pid'}{$tag} eq "wait" ||
471             $self->{'pid'}{$tag} eq "test"
472             )
473             {
474             delete $self->{'pid'}{$tag};
475             return;
476             }
477             return unless $self->{'pid'}{$tag};
478             debug "killing $self->{'pid'}{$tag}\n";
479             # XXX process kill start
480             kill(15,$self->{'pid'}{$tag});
481             for(my $i=1;$i <= 16; $i*=2)
482             {
483             last unless $self->{'pid'}{$tag};
484             last unless kill(0,$self->{'pid'}{$tag});
485             sleep $i;
486             }
487             return unless $self->{'pid'}{$tag};
488             while(kill(0,$self->{'pid'}{$tag}))
489             {
490             # XXX process kill hard
491             debug "hard kill $self->{'pid'}{$tag}\n";
492             kill(9,$self->{'pid'}{$tag});
493             }
494             # XXX process kill done
495             debug "killed $self->{'pid'}{$tag}\n";
496              
497             delete $self->{'pid'}{$tag};
498             }
499              
500             sub _sigchld
501             {
502             my $self=shift;
503             my $pid = waitpid(-1, &WNOHANG);
504             if ($pid == -1)
505             {
506             # nothing exited -- ignore
507             $SIG{CHLD} = sub {$self->_sigchld()};
508             return;
509             }
510             unless (kill(0,$pid) == 0)
511             {
512             # still running -- false alarm
513             $SIG{CHLD} = sub {$self->_sigchld()};
514             return;
515             }
516             # $pid exited
517             debug "$pid exited\n";
518             # XXX pid exited (do we get here for every kill? what about system()?)
519             my $tag = $self->{'tag'}{$pid};
520             # why not just always delete $self->{'pid'}{$tag} here?
521             delete $self->{'pid'}{$tag} if $self->{'inittab'}{'mode'}{$tag} eq 'respawn';
522             # reread isinittab
523             $self->_spawn();
524             $SIG{CHLD} = sub {$self->_sigchld()};
525             }
526              
527             =head1 BUGS
528              
529             =head1 AUTHOR
530              
531             Steve Traugott
532             CPAN ID: STEVEGT
533             stevegt@TerraLuna.Org
534             http://www.stevegt.com
535              
536             =head1 COPYRIGHT
537              
538             Copyright (c) 2001 Steve Traugott. All rights reserved.
539             This program is free software; you can redistribute
540             it and/or modify it under the same terms as Perl itself.
541              
542             The full text of the license can be found in the
543             LICENSE file included with this module.
544              
545             =head1 SEE ALSO
546              
547             perl(1).
548              
549             =cut
550              
551             1; #this line is important and will help the module return a true value
552              
553             __END__