Handle hostnames with upper-case letters
[webmin.git] / system-status / system-status-lib.pl
1 # Functions for collecting general system info
2
3 BEGIN { push(@INC, ".."); };
4 eval "use WebminCore;";
5 &init_config();
6 $systeminfo_cron_cmd = "$module_config_directory/systeminfo.pl";
7 $collected_info_file = "$module_config_directory/info";
8 $historic_info_dir = "$module_config_directory/history";
9
10 # collect_system_info()
11 # Returns a hash reference containing system information
12 sub collect_system_info
13 {
14 my $info = { };
15
16 if (&foreign_check("proc")) {
17         # CPU and memory
18         &foreign_require("proc", "proc-lib.pl");
19         if (defined(&proc::get_cpu_info)) {
20                 my @c = &proc::get_cpu_info();
21                 $info->{'load'} = \@c;
22                 }
23         my @procs = &proc::list_processes();
24         $info->{'procs'} = scalar(@procs);
25         if (defined(&proc::get_memory_info)) {
26                 my @m = &proc::get_memory_info();
27                 $info->{'mem'} = \@m;
28                 if ($m[0] > 128*1024*1024 && $gconfig{'os_type'} eq 'freebsd') {
29                         # Some Webmin versions overstated memory by a factor
30                         # of 1k on FreeBSD - fix it
31                         $m[0] /= 1024;
32                         $m[1] /= 1024;
33                         }
34                 }
35
36         # CPU and kernel
37         my ($r, $m, $o) = &proc::get_kernel_info();
38         $info->{'kernel'} = { 'version' => $r,
39                               'arch' => $m,
40                               'os' => $o };
41         }
42
43 # Disk space on local filesystems
44 if (&foreign_check("mount")) {
45         &foreign_require("mount");
46         ($info->{'disk_total'}, $info->{'disk_free'}) =
47                 &mount::local_disk_space();
48         }
49
50 # Available package updates
51 if (&foreign_installed("package-updates") && $config{'collect_pkgs'}) {
52         &foreign_require("package-updates");
53         my @poss = &package_updates::list_possible_updates(2, 1);
54         $info->{'poss'} = \@poss;
55         }
56
57 # CPU and drive temps
58 my @cpu = &get_current_cpu_temps();
59 $info->{'cputemps'} = \@cpu if (@cpu);
60 my @drive = &get_current_drive_temps();
61 $info->{'drivetemps'} = \@drive if (@drive);
62
63 # IO input and output
64 if ($gconfig{'os_type'} =~ /-linux$/) {
65         local $out = &backquote_command("vmstat 1 2 2>/dev/null");
66         if (!$?) {
67                 local @lines = split(/\r?\n/, $out);
68                 local @w = split(/\s+/, $lines[$#lines]);
69                 shift(@w) if ($w[0] eq '');
70                 if ($w[8] =~ /^\d+$/ && $w[9] =~ /^\d+$/) {
71                         # Blocks in and out
72                         $info->{'io'} = [ $w[8], $w[9] ];
73
74                         # CPU user, kernel, idle, io, vm
75                         $info->{'cpu'} = [ @w[12..16] ];
76                         }
77                 }
78         }
79
80 return $info;
81 }
82
83 # get_collected_info()
84 # Returns the most recently collected system information, or the current info
85 sub get_collected_info
86 {
87 my $infostr = $config{'collect_interval'} eq 'none' ? undef :
88                         &read_file_contents($collected_info_file);
89 if ($infostr) {
90         my $info = &unserialise_variable($infostr);
91         if (ref($info) eq 'HASH' && keys(%$info) > 0) {
92                 return $info;
93                 }
94         }
95 return &collect_system_info();
96 }
97
98 # save_collected_info(&info)
99 # Save information collected on schedule
100 sub save_collected_info
101 {
102 my ($info) = @_;
103 &open_tempfile(INFO, ">$collected_info_file");
104 &print_tempfile(INFO, &serialise_variable($info));
105 &close_tempfile(INFO);
106 }
107
108 # refresh_possible_packages(&newpackages)
109 # Refresh regularly collected info on available packages
110 sub refresh_possible_packages
111 {
112 my ($pkgs) = @_;
113 my %pkgs = map { $_, 1 } @$pkgs;
114 my $info = &get_collected_info();
115 if ($info->{'poss'} && &foreign_installed("package-updates")) {
116         &foreign_require("package-updates");
117         my @poss = &package_updates::list_possible_updates(2);
118         $info->{'poss'} = \@poss;
119         }
120 &save_collected_info($info);
121 }
122
123 # add_historic_collected_info(&info, time)
124 # Add to the collected info log files the current CPU load, memory uses, swap
125 # use, disk use and other info we might want to graph
126 sub add_historic_collected_info
127 {
128 my ($info, $time) = @_;
129 if (!-d $historic_info_dir) {
130         &make_dir($historic_info_dir, 0700);
131         }
132 my @stats;
133 push(@stats, [ "load", $info->{'load'}->[0] ]) if ($info->{'load'});
134 push(@stats, [ "load5", $info->{'load'}->[1] ]) if ($info->{'load'});
135 push(@stats, [ "load15", $info->{'load'}->[2] ]) if ($info->{'load'});
136 push(@stats, [ "procs", $info->{'procs'} ]) if ($info->{'procs'});
137 if ($info->{'mem'}) {
138         push(@stats, [ "memused",
139                        ($info->{'mem'}->[0]-$info->{'mem'}->[1])*1024,
140                        $info->{'mem'}->[0]*1024 ]);
141         if ($info->{'mem'}->[2]) {
142                 push(@stats, [ "swapused",
143                               ($info->{'mem'}->[2]-$info->{'mem'}->[3])*1024,
144                               $info->{'mem'}->[2]*1024 ]);
145                 }
146         }
147 if ($info->{'disk_total'}) {
148         push(@stats, [ "diskused",
149                        $info->{'disk_total'}-$info->{'disk_free'},
150                        $info->{'disk_total'} ]);
151         }
152
153 # Get network traffic counts since last run
154 if (&foreign_check("net") && $gconfig{'os_type'} =~ /-linux$/) {
155         # Get the current byte count
156         my $rxtotal = 0;
157         my $txtotal = 0;
158         if ($config{'collect_ifaces'}) {
159                 # From module config
160                 @ifaces = split(/\s+/, $config{'collect_ifaces'});
161                 }
162         else {
163                 # Get list from net module
164                 &foreign_require("net");
165                 foreach my $i (&net::active_interfaces()) {
166                         if ($i->{'virtual'} eq '' &&
167                             $i->{'name'} =~ /^(eth|ppp|wlan|ath|wlan)/) {
168                                 push(@ifaces, $i->{'name'});
169                                 }
170                         }
171                 }
172         my $ifaces = join(" ", @ifaces);
173         foreach my $iname (@ifaces) {
174                 &clean_language();
175                 my $out = &backquote_command(
176                         "ifconfig ".quotemeta($iname)." 2>/dev/null");
177                 &reset_environment();
178                 my $rx = $out =~ /RX\s+bytes:\s*(\d+)/i ? $1 : undef;
179                 my $tx = $out =~ /TX\s+bytes:\s*(\d+)/i ? $1 : undef;
180                 $rxtotal += $rx;
181                 $txtotal += $tx;
182                 }
183
184         # Work out the diff since the last run, if we have it
185         my %netcounts;
186         if (&read_file("$historic_info_dir/netcounts", \%netcounts) &&
187             $netcounts{'rx'} && $netcounts{'tx'} &&
188             $netcounts{'ifaces'} eq $ifaces &&
189             $rxtotal >= $netcounts{'rx'} && $txtotal >= $netcounts{'tx'}) {
190                 my $secs = ($now - $netcounts{'now'}) * 1.0;
191                 if ($secs) {
192                         my $rxscaled = ($rxtotal - $netcounts{'rx'}) / $secs;
193                         my $txscaled = ($txtotal - $netcounts{'tx'}) / $secs;
194                         if ($rxscaled >= $netcounts{'rx_max'}) {
195                                 $netcounts{'rx_max'} = $rxscaled;
196                                 }
197                         if ($txscaled >= $netcounts{'tx_max'}) {
198                                 $netcounts{'tx_max'} = $txscaled;
199                                 }
200                         push(@stats, [ "rx",$rxscaled, $netcounts{'rx_max'} ]);
201                         push(@stats, [ "tx",$txscaled, $netcounts{'tx_max'} ]);
202                         }
203                 }
204
205         # Save the last counts
206         $netcounts{'rx'} = $rxtotal;
207         $netcounts{'tx'} = $txtotal;
208         $netcounts{'now'} = $now;
209         $netcounts{'ifaces'} = $ifaces;
210         &write_file("$historic_info_dir/netcounts", \%netcounts);
211         }
212
213 # Get drive temperatures
214 my ($temptotal, $tempcount);
215 foreach my $t (@{$info->{'drivetemps'}}) {
216         $temptotal += $t->{'temp'};
217         $tempcount++;
218         }
219 if ($temptotal) {
220         push(@stats, [ "drivetemp", $temptotal / $tempcount ]);
221         }
222
223 # Get CPU temperature
224 my ($temptotal, $tempcount);
225 foreach my $t (@{$info->{'cputemps'}}) {
226         $temptotal += $t->{'temp'};
227         $tempcount++;
228         }
229 if ($temptotal) {
230         push(@stats, [ "cputemp", $temptotal / $tempcount ]);
231         }
232
233 # Get IO blocks
234 if ($info->{'io'}) {
235         push(@stats, [ "bin", $info->{'io'}->[0] ]);
236         push(@stats, [ "bout", $info->{'io'}->[1] ]);
237         }
238
239 # Get CPU user and IO time
240 if ($info->{'cpu'}) {
241         push(@stats, [ "cpuuser", $info->{'cpu'}->[0] ]);
242         push(@stats, [ "cpukernel", $info->{'cpu'}->[1] ]);
243         push(@stats, [ "cpuidle", $info->{'cpu'}->[2] ]);
244         push(@stats, [ "cpuio", $info->{'cpu'}->[3] ]);
245         }
246
247 # Write to the file
248 foreach my $stat (@stats) {
249         open(HISTORY, ">>$historic_info_dir/$stat->[0]");
250         print HISTORY $time," ",$stat->[1],"\n";
251         close(HISTORY);
252         }
253
254 # Update the file storing the max possible value for each variable
255 my %maxpossible;
256 &read_file("$historic_info_dir/maxes", \%maxpossible);
257 foreach my $stat (@stats) {
258         if ($stat->[2] && $stat->[2] > $maxpossible{$stat->[0]}) {
259                 $maxpossible{$stat->[0]} = $stat->[2];
260                 }
261         }
262 &write_file("$historic_info_dir/maxes", \%maxpossible);
263 }
264
265 # list_historic_collected_info(stat, [start], [end])
266 # Returns an array of times and values for some stat, within the given
267 # time period
268 sub list_historic_collected_info
269 {
270 my ($stat, $start, $end) = @_;
271 my @rv;
272 my $last_time;
273 my $now = time();
274 open(HISTORY, "$historic_info_dir/$stat");
275 while(<HISTORY>) {
276         chop;
277         my ($time, $value) = split(" ", $_);
278         next if ($time < $last_time ||  # No time travel or future data
279                  $time > $now);
280         if ((!defined($start) || $time >= $start) &&
281             (!defined($end) || $time <= $end)) {
282                 push(@rv, [ $time, $value ]);
283                 }
284         if (defined($end) && $time > $end) {
285                 last;   # Past the end point
286                 }
287         $last_time = $time;
288         }
289 close(HISTORY);
290 return @rv;
291 }
292
293 # list_all_historic_collected_info([start], [end])
294 # Returns a hash mapping stats to data within some time period
295 sub list_all_historic_collected_info
296 {
297 my ($start, $end) = @_;
298 foreach my $f (&list_historic_stats()) {
299         my @rv = &list_historic_collected_info($f, $start, $end);
300         $all{$f} = \@rv;
301         }
302 closedir(HISTDIR);
303 return \%all;
304 }
305
306 # get_historic_maxes()
307 # Returns a hash reference from stats to the max possible values ever seen
308 sub get_historic_maxes
309 {
310 my %maxpossible;
311 &read_file("$historic_info_dir/maxes", \%maxpossible);
312 return \%maxpossible;
313 }
314
315 # get_historic_first_last(stat)
316 # Returns the Unix time for the first and last stats recorded
317 sub get_historic_first_last
318 {
319 my ($stat) = @_;
320 open(HISTORY, "$historic_info_dir/$stat") || return (undef, undef);
321 my $first = <HISTORY>;
322 $first || return (undef, undef);
323 chop($first);
324 my ($firsttime, $firstvalue) = split(" ", $first);
325 seek(HISTORY, 2, -256) || seek(HISTORY, 0, 0);
326 while(<HISTORY>) {
327         $last = $_;
328         }
329 close(HISTORY);
330 chop($last);
331 my ($lasttime, $lastvalue) = split(" ", $last);
332 return ($firsttime, $lasttime);
333 }
334
335 # list_historic_stats()
336 # Returns a list of variables on which we have stats
337 sub list_historic_stats
338 {
339 my @rv;
340 opendir(HISTDIR, $historic_info_dir);
341 foreach my $f (readdir(HISTDIR)) {
342         if ($f =~ /^[a-z]+[0-9]*$/ && $f ne "maxes" && $f ne "procmailpos" &&
343             $f ne "netcounts") {
344                 push(@rv, $f);
345                 }
346         }
347 closedir(HISTDIR);
348 return @rv;
349 }
350
351 # setup_collectinfo_job()
352 # Creates or updates the Webmin function cron job, based on the interval
353 # set in the module config
354 sub setup_collectinfo_job
355 {
356 &foreign_require("webmincron");
357 my $step = $config{'collect_interval'};
358 if ($step ne 'none') {
359         # Setup webmin cron (removing old classic cron job)
360         $step ||= 5;
361         my $cron = { 'module' => $module_name,
362                      'func' => 'scheduled_collect_system_info',
363                      'interval' => $step * 60,
364                    };
365         &webmincron::create_webmin_cron($cron, $systeminfo_cron_cmd);
366         }
367 else {
368         # Delete webmin cron
369         my $cron = &webmincron::find_webmin_cron($module_name,
370                                          'scheduled_collect_system_info');
371         if ($cron) {
372                 &webmincron::delete_webmin_cron($cron);
373                 }
374         }
375 }
376
377 # get_current_drive_temps()
378 # Returns a list of hashes, containing device and temp keys
379 sub get_current_drive_temps
380 {
381 my @rv;
382 if (!$config{'collect_notemp'} &&
383     &foreign_installed("smart-status")) {
384         &foreign_require("smart-status");
385         foreach my $d (&smart_status::list_smart_disks_partitions()) {
386                 my $st = &smart_status::get_drive_status($d->{'device'}, $d);
387                 foreach my $a (@{$st->{'attribs'}}) {
388                         if ($a->[0] =~ /^Temperature\s+Celsius$/i &&
389                             $a->[1] > 0) {
390                                 push(@rv, { 'device' => $d->{'device'},
391                                             'temp' => int($a->[1]) });
392                                 }
393                         }
394                 }
395         }
396 return @rv;
397 }
398
399 # get_current_cpu_temps()
400 # Returns a list of hashes containing core and temp keys
401 sub get_current_cpu_temps
402 {
403 my @rv;
404 if (!$config{'collect_notemp'} &&
405     $gconfig{'os_type'} =~ /-linux$/ && &has_command("sensors")) {
406         &open_execute_command(SENSORS, "sensors </dev/null 2>/dev/null", 1);
407         while(<SENSORS>) {
408                 if (/Core\s+(\d+):\s+([\+\-][0-9\.]+)/) {
409                         push(@rv, { 'core' => $1,
410                                     'temp' => $2 });
411                         }
412                 elsif (/CPU:\s+([\+\-][0-9\.]+)/) {
413                         push(@rv, { 'core' => 0,
414                                     'temp' => $1 });
415                         }
416                 }
417         close(SENSORS);
418         }
419 return @rv;
420 }
421
422 # scheduled_collect_system_info()
423 # Called by Webmin Cron to collect system info
424 sub scheduled_collect_system_info
425 {
426 my $start = time();
427
428 # Make sure we are not already running
429 if (&test_lock($collected_info_file)) {
430         print STDERR "scheduled_collect_system_info : Already running\n";
431         return;
432         }
433
434 # Don't diff collected file
435 $gconfig{'logfiles'} = 0;
436 $gconfig{'logfullfiles'} = 0;
437 $WebminCore::gconfig{'logfiles'} = 0;
438 $WebminCore::gconfig{'logfullfiles'} = 0;
439 $no_log_file_changes = 1;
440 &lock_file($collected_info_file);
441
442 $info = &collect_system_info();
443 if ($info) {
444         &save_collected_info($info);
445         &add_historic_collected_info($info, $start);
446         }
447 &unlock_file($collected_info_file);
448 }
449
450 1;
451