Handle hostnames with upper-case letters
[webmin.git] / status / status-lib.pl
1 # status-lib.pl
2 # Functions for getting the status of services
3
4 BEGIN { push(@INC, ".."); };
5 use WebminCore;
6 &init_config();
7 %access = &get_module_acl();
8 use Socket;
9
10 $services_dir = "$module_config_directory/services";
11 $cron_cmd = "$module_config_directory/monitor.pl";
12
13 $oldstatus_file = "$module_config_directory/oldstatus";
14 $fails_file = "$module_config_directory/fails";
15
16 $templates_dir = "$module_config_directory/templates";
17
18 %monitor_os_support = ( 'traffic' => { 'os_support' => '*-linux freebsd' },
19                       );
20
21 @monitor_statuses = ( 'up', 'down', 'un', 'webmin', 'timed', 'isdown' );
22
23 # list_services()
24 # Returns a list of all services this module knows how to get status on.
25 # If this is the first time the function is called a default set of services
26 # will be setup.
27 sub list_services
28 {
29 my (%mod, @rv);
30 if (!-d $services_dir) {
31         # setup initial services
32         mkdir($module_config_directory, 0700);
33         mkdir($services_dir, 0700);
34         system("cp services/* $services_dir");
35         }
36 map { $mod{$_}++ } &list_modules();
37 opendir(DIR, $services_dir);
38 while($f = readdir(DIR)) {
39         next if ($f !~ /^(.*)\.serv$/);
40         local $serv = &get_service($1);
41         next if (!$serv || !$serv->{'type'} || !$serv->{'id'});
42         if ($serv->{'depends'}) {
43                 local $d;
44                 map { $d++ if (!$mod{$_}) } split(/\s+/, $serv->{'depends'});
45                 push(@rv, $serv) if (!$d);
46                 }
47         else {
48                 push(@rv, $serv);
49                 }
50         }
51 closedir(DIR);
52 return @rv;
53 }
54
55 # get_service(id)
56 sub get_service
57 {
58 local %serv;
59 &read_file("$services_dir/$_[0].serv", \%serv);
60 $serv{'fails'} = 1 if (!defined($serv{'fails'}));
61 $serv{'_file'} = "$services_dir/$_[0].serv";
62 if (!defined($serv{'notify'})) {
63         $serv{'notify'} = 'email pager snmp';
64         }
65 $serv{'remote'} = "*" if (!$serv{'remote'} && !$serv{'groups'});
66 return $_[0] ne $serv{'id'} ? undef : \%serv;
67 }
68
69 # save_service(&serv)
70 sub save_service
71 {
72 mkdir($services_dir, 0755) if (!-d $services_dir);
73 &lock_file("$services_dir/$_[0]->{'id'}.serv");
74 &write_file("$services_dir/$_[0]->{'id'}.serv", $_[0]);
75 &unlock_file("$services_dir/$_[0]->{'id'}.serv");
76 }
77
78 # delete_service(serv)
79 sub delete_service
80 {
81 &lock_file("$services_dir/$_[0]->{'id'}.serv");
82 unlink("$services_dir/$_[0]->{'id'}.serv");
83 &unlock_file("$services_dir/$_[0]->{'id'}.serv");
84 }
85
86 # expand_remotes(&service)
87 # Given a service with direct and group remote hosts, returns a list of the
88 # names of all actual hosts (* means local)
89 sub expand_remotes
90 {
91 local @remote;
92 push(@remote, split(/\s+/, $_[0]->{'remote'}));
93 local @groupnames = split(/\s+/, $_[0]->{'groups'});
94 if (@groupnames) {
95         &foreign_require("servers");
96         local @groups = &servers::list_all_groups();
97         foreach my $g (@groupnames) {
98                 local ($group) = grep { $_->{'name'} eq $g } @groups;
99                 if ($group) {
100                         push(@remote, @{$group->{'members'}});
101                         }
102                 }
103         }
104 return &unique(@remote);
105 }
106
107 # service_status(&service, [from-cgi])
108 # Gets the status of a service, possibly on another server. If called in
109 # an array content, the status of all hosts for this monitor are returned.
110 sub service_status
111 {
112 local $t = $_[0]->{'type'};
113 local @rv;
114 foreach $r (&expand_remotes($_[0])) {
115         local $rv;
116         local $main::error_must_die = 1;
117         eval {
118                 local $SIG{'ALRM'} = sub { die "status alarm\n" };
119                 alarm(60);      # wait at most 60 secs for a result
120                 if ($r ne "*") {
121                         # Make a remote call to another webmin server
122                         &remote_error_setup(\&remote_error);
123                         $remote_error_msg = undef;
124                         &remote_foreign_require($r, 'status', 'status-lib.pl')
125                                 if (!$done_remote_status{$r}++);
126                         local $webmindown = $s->{'type'} eq 'alive' ? 0 : -2;
127                         if ($remote_error_msg) {
128                                 $rv = { 'up' => $webmindown,
129                                          'desc' => "$text{'mon_webmin'} : $remote_error_msg" };
130                                 }
131                         else {
132                                 local %s = %{$_[0]};
133                                 $s{'remote'} = '*';
134                                 $s{'groups'} = undef;
135                                 ($rv) = &remote_foreign_call($r, 'status',
136                                             'service_status', \%s, $_[1]);
137                                 if ($remote_error_msg) {
138                                         $rv = { 'up' => $webmindown, 'desc' =>
139                                             "$text{'mon_webmin'} : $remote_error_msg" };
140                                         }
141                                 }
142                         }
143                 elsif ($t =~ /^(\S+)::(\S+)$/) {
144                         # Call to another module
145                         local ($mod, $mtype) = ($1, $2);
146                         &foreign_require($mod, "status_monitor.pl");
147                         $rv = &foreign_call($mod, "status_monitor_status",
148                                             $mtype, $_[0], $_[1]);
149                         }
150                 else {
151                         # Just include and use the local monitor library
152                         do "${t}-monitor.pl" if (!$done_monitor{$t}++);
153                         local $func = "get_${t}_status";
154                         $rv = &$func($_[0],
155                                      $_[0]->{'clone'} ? $_[0]->{'clone'} : $t,
156                                      $_[1]);
157                         }
158                 alarm(0);
159                 };
160         if ($@ =~ /status alarm/) {
161                 push(@rv, { 'up' => -3,
162                             'remote' => $r });
163                 }
164         elsif ($@) {
165                 # A real error happened
166                 die $@;
167                 }
168         else {
169                 $rv->{'remote'} = $r;
170                 push(@rv, $rv);
171                 }
172         }
173 return wantarray ? @rv : $rv[0];
174 }
175
176 sub remote_error
177 {
178 $remote_error_msg = join("", @_);
179 }
180
181 # list_modules()
182 # Returns a list of all modules available on this system
183 sub list_modules
184 {
185 return map { $_->{'dir'} } grep { &check_os_support($_) }
186         &get_all_module_infos();
187 }
188
189 # list_handlers()
190 # Returns a list of the module's monitor type handlers, and those
191 # defined in other modules.
192 sub list_handlers
193 {
194 local ($f, @rv);
195 opendir(DIR, ".");
196 while($f = readdir(DIR)) {
197         if ($f =~ /^(\S+)-monitor\.pl$/) {
198                 local $m = $1;
199                 local $oss = $monitor_os_support{$m};
200                 next if ($oss && !&check_os_support($oss));
201                 push(@rv, [ $m, $text{"type_$m"} ]);
202                 }
203         }
204 closedir(DIR);
205 local $m;
206 foreach $m (&get_all_module_infos()) {
207         local $mdir = defined(&module_root_directory) ?
208                 &module_root_directory($m->{'dir'}) :
209                 "$root_directory/$m->{'dir'}";
210         if (-r "$mdir/status_monitor.pl" &&
211             &check_os_support($m)) {
212                 &foreign_require($m->{'dir'}, "status_monitor.pl");
213                 local @mms = &foreign_call($m->{'dir'}, "status_monitor_list");
214                 push(@rv, map { [ $m->{'dir'}."::".$_->[0], $_->[1] ] } @mms);
215                 }
216         }
217 return @rv;
218 }
219
220 # depends_check(&service, [module]+)
221 sub depends_check
222 {
223 return if ($_[0]->{'id'});      # only check for new services
224 if ($_[0]->{'remote'}) {
225         # Check on the remote server
226         foreach $m (@_[1..$#_]) {
227                 &remote_foreign_check($_[0]->{'remote'}, $m, 1) ||
228                         &error(&text('depends_remote', "<tt>$m</tt>",
229                                      "<tt>$_[0]->{'remote'}</tt>"));
230                 }
231         }
232 else {
233         # Check on this server
234         foreach $m (@_[1..$#_]) {
235                 local %minfo = &get_module_info($m);
236                 %minfo || &error(&text('depends_mod', "<tt>$m</tt>"));
237                 &check_os_support(\%minfo, undef, undef, 1) ||
238                         &error(&text('depends_os', "<tt>$minfo{'desc'}</tt>"));
239                 }
240         $_[0]->{'depends'} = join(" ", @_[1..$#_]);
241         }
242 }
243
244 # find_named_process(regexp)
245 sub find_named_process
246 {
247 foreach $p (&proc::list_processes()) {
248         $p->{'args'} =~ s/\s.*$//; $p->{'args'} =~ s/[\[\]]//g;
249         if ($p->{'args'} =~ /$_[0]/) {
250                 return $p;
251                 }
252         }
253 return undef;
254 }
255
256 # smtp_command(handle, command)
257 sub smtp_command
258 {
259 local ($m, $c) = @_;
260 print $m $c;
261 local $r = <$m>;
262 if ($r !~ /^[23]\d+/) {
263         &error(&text('sched_esmtpcmd', "<tt>$c</tt>", "<tt>$r</tt>"));
264         }
265 }
266
267 # setup_cron_job()
268 # Create a cron job based on the module's configuration
269 sub setup_cron_job
270 {
271 &lock_file($cron_cmd);
272 &foreign_require("cron");
273 local ($j, $job);
274 foreach $j (&cron::list_cron_jobs()) {
275         $job = $j if ($j->{'user'} eq 'root' && $j->{'command'} eq $cron_cmd);
276         }
277 if ($job) {
278         &lock_file(&cron::cron_file($job));
279         &cron::delete_cron_job($job);
280         &unlock_file(&cron::cron_file($job));
281         unlink($cron_cmd);
282         }
283 if ($config{'sched_mode'}) {
284         # Create the program that cron calls
285         &cron::create_wrapper($cron_cmd, $module_name, "monitor.pl");
286
287         # Setup the actual cron job
288         local $njob;
289         $njob = { 'user' => 'root', 'active' => 1,
290                   'hours' => '*', 'days' => '*',
291                   'months' => '*', 'weekdays' => '*',
292                   'command' => $cron_cmd };
293         if ($config{'sched_period'} == 0) {
294                 $njob->{'mins'} = &make_interval(60);
295                 }
296         elsif ($config{'sched_period'} == 1) {
297                 $njob->{'hours'} = &make_interval(24);
298                 $njob->{'mins'} = 0;
299                 }
300         elsif ($config{'sched_period'} == 2) {
301                 $njob->{'days'} = &make_interval(31, 1);
302                 $njob->{'hours'} = $njob->{'mins'} = 0;
303                 }
304         elsif ($config{'sched_period'} == 3) {
305                 $njob->{'months'} = &make_interval(12, 1);
306                 $njob->{'days'} = 1;
307                 $njob->{'hours'} = $njob->{'mins'} = 0;
308                 }
309         &lock_file(&cron::cron_file($njob));
310         &cron::create_cron_job($njob);
311         &unlock_file(&cron::cron_file($njob));
312         }
313 &unlock_file($cron_cmd);
314 }
315
316 # make_interval(length, offset2)
317 sub make_interval
318 {
319 local (@rv, $i);
320 for($i=$config{'sched_offset'}+$_[1]; $i<$_[0]; $i+=$config{'sched_int'}) {
321         push(@rv,$i);
322         }
323 return join(",", @rv);
324 }
325
326 # expand_oldstatus(oldstatus, &serv)
327 # Converts an old-status string like *=1 foo.com=2 into a hash. If the string
328 # contains just one number, it is assumed to be for just the first remote host
329 sub expand_oldstatus
330 {
331 local ($o, $serv) = @_;
332 local @remotes = split(/\s+/, $serv->{'remote'});
333 if ($o =~ /^\-?(\d+)$/) {
334         return { $remotes[0] => $o };
335         }
336 else {
337         local %rv;
338         foreach my $hs (split(/\s+/, $o)) {
339                 local ($h, $s) = split(/=/, $hs);
340                 $rv{$h} = $s;
341                 }
342         return \%rv;
343         }
344 }
345
346 # nice_remotes(&monitor, [max])
347 sub nice_remotes
348 {
349 local ($s, $max) = @_;
350 $max ||= 3;
351 local @remotes = map { $_ eq "*" ? $text{'index_local'}
352                                  : &html_escape($_) }
353                      split(/\s+/, $s->{'remote'});
354 foreach my $g (split(/\s+/, $s->{'groups'})) {
355         push(@remotes, &text('index_group', $g));
356         }
357 return @remotes > $max ? join(", ", @remotes[0..$max]).", ..."
358                        : join(", ", @remotes);
359 }
360
361 sub group_desc
362 {
363 local ($group) = @_;
364 local $mems = scalar(@{$group->{'members'}});
365 return $group->{'name'}." (".
366        &text($mems == 0 ? 'mon_empty' :
367              $mems == 1 ? 'mon_onemem' : 'mon_members', $mems).")";
368 }
369
370 # list_notification_modes()
371 # Returns a list of available notifcation modes (like email, sms, etc..)
372 sub list_notification_modes
373 {
374 local @rv = ( "email" );
375 if ($config{'pager_cmd'} && $config{'sched_pager'}) {
376         push(@rv, "pager");
377         }
378 if ($config{'snmp_server'}) {
379         local $gotmod = 0;
380         eval "use Net::SNMP";
381         $gotmod++ if (!$@);
382         eval "use SNMP_Session";
383         $gotmod++ if (!$@);
384         push(@rv, "snmp") if ($gotmod);
385         }
386 if ($config{'sched_carrier'} && $config{'sched_sms'}) {
387         push(@rv, "sms");
388         }
389 return @rv;
390 }
391
392 # list_sms_carriers()
393 # Returns a list of information about carriers to whom we can send SMS
394 sub list_sms_carriers
395 {
396 return ( { 'id' => 'tmobile',
397            'desc' => 'T-Mobile',
398            'domain' => 'tmomail.net' },
399          { 'id' => 'cingular',
400            'desc' => 'AT&T',
401            'domain' => 'txt.att.net' },
402          { 'id' => 'oldcingular',
403            'desc' => 'Cingular',
404            'domain' => 'cingularme.com',
405            'alpha' => 1 },
406          { 'id' => 'verizon',
407            'desc' => 'Verizon',
408            'domain' => 'vtext.com' },
409          { 'id' => 'sprint',
410            'desc' => 'Sprint PCS',
411            'domain' => 'messaging.sprintpcs.com' },
412          { 'id' => 'nextel',
413            'desc' => 'Nextel',
414            'domain' => 'messaging.nextel.com' },
415          { 'id' => 'alltel',
416            'desc' => 'Alltel',
417            'domain' => 'message.alltel.com' },
418          { 'id' => 'boost',
419            'desc' => 'Boost Mobile',
420            'domain' => 'myboostmobile.com' },
421          { 'id' => 'virgin',
422            'desc' => 'Virgin Mobile',
423            'domain' => 'vmobl.com' },
424          { 'id' => 'cbell',
425            'desc' => 'Cincinnati Bell',
426            'domain' => 'gocbw.com' },
427          { 'id' => 'tcom',
428            'desc' => 'T-COM',
429            'domain' => 'sms.t-online.de' },
430          { 'id' => 'vodafone',
431            'desc' => 'Vodafone UK',
432            'domain' => 'vodafone.net' },
433          { 'id' => 'vodafonejapan',
434            'desc' => 'Vodafone Japan',
435            'domain' => 't.vodafone.ne.jp' },
436          { 'id' => 'bellcanada',
437            'desc' => 'Bell Canada',
438            'domain' => 'txt.bellmobility.ca' },
439          { 'id' => 'bellsouth',
440            'desc' => 'Bell South',
441            'domain' => 'sms.bellsouth.com' },
442          { 'id' => 'cellularone',
443            'desc' => 'Cellular One',
444            'domain' => 'mobile.celloneusa.com' },
445          { 'id' => 'o2',
446            'desc' => 'O2',
447            'domain' => 'mmail.co.uk' },
448          { 'id' => 'rogers',
449            'desc' => 'Rogers Canada',
450            'domain' => 'pcs.rogers.com' },
451          { 'id' => 'skytel',
452            'desc' => 'Skytel',
453            'domain' => 'skytel.com' },
454         );
455 }
456
457 # list_templates()
458 # Returns a list of hash refs, one for each email template
459 sub list_templates
460 {
461 opendir(DIR, $templates_dir) || return ( );
462 local @rv;
463 foreach my $f (readdir(DIR)) {
464         if ($f =~ /^\d+$/) {
465                 push(@rv, &get_template($f));
466                 }
467         }
468 closedir(DIR);
469 return @rv;
470 }
471
472 # get_template(id)
473 # Returns the hash ref for a specific template, by ID
474 sub get_template
475 {
476 local ($id) = @_;
477 local %tmpl;
478 &read_file("$templates_dir/$id", \%tmpl) || return undef;
479 $tmpl{'id'} = $id;
480 $tmpl{'file'} = "$templates_dir/$id";
481 $tmpl{'email'} =~ s/\\n/\n/g;
482 $tmpl{'email'} =~ s/\\\\/\\/g;
483 return \%tmpl;
484 }
485
486 # save_template(&template)
487 # Creates or saves an email template. Also does locking.
488 sub save_template
489 {
490 local ($tmpl) = @_;
491 $tmpl->{'id'} ||= time().$$;
492 $tmpl->{'file'} = "$templates_dir/$tmpl->{'id'}";
493 local %write = %$tmpl;
494 $write{'email'} =~ s/\\/\\\\/g;
495 $write{'email'} =~ s/\n/\\n/g;
496 if (!-d $templates_dir) {
497         &make_dir($templates_dir, 0755);
498         }
499 &lock_file($tmpl->{'file'});
500 &write_file($tmpl->{'file'}, \%write);
501 &unlock_file($tmpl->{'file'});
502 }
503
504 # delete_template(&template)
505 # Removes an existing template. Also does locking.
506 sub delete_template
507 {
508 local ($tmpl) = @_;
509 &unlink_logged($tmpl->{'file'});
510 }
511
512 1;
513