File Coverage

blib/lib/OSLV/Monitor/Backends/FreeBSD.pm
Criterion Covered Total %
statement 23 191 12.0
branch 0 74 0.0
condition 0 51 0.0
subroutine 8 12 66.6
pod 3 4 75.0
total 34 332 10.2


line stmt bran cond sub pod time code
1             package OSLV::Monitor::Backends::FreeBSD;
2              
3 1     1   123834 use 5.006;
  1         3  
4 1     1   3 use strict;
  1         10  
  1         21  
5 1     1   5 use warnings;
  1         1  
  1         39  
6 1     1   763 use JSON;
  1         10316  
  1         4  
7 1     1   548 use Clone 'clone';
  1         469  
  1         69  
8 1     1   486 use File::Slurp;
  1         40901  
  1         67  
9 1     1   7 use List::Util qw( uniq );
  1         1  
  1         51  
10 1     1   4 use Scalar::Util qw(looks_like_number);
  1         2  
  1         2706  
11              
12             =head1 NAME
13              
14             OSLV::Monitor::Backends::FreeBSD - backend for FreeBSD jails
15              
16             =head1 VERSION
17              
18             Version 0.0.4
19              
20             =cut
21              
22             our $VERSION = '0.0.4';
23              
24             =head1 SYNOPSIS
25              
26             use OSLV::Monitor::Backends::FreeBSD;
27              
28             my $backend = OSLV::Monitor::Backends::FreeBSD->new;
29              
30             my $usable=$backend->usable;
31             if ( $usable ){
32             $return_hash_ref=$backend->run;
33             }
34              
35             The stats names match those produced by "ps --libxo json".
36              
37             =head2 METHODS
38              
39             =head2 new
40              
41             Initiates the backend object.
42              
43             my $backend=OSLV::MOnitor::Backend::FreeBSD->new(
44             base_dir => $base_dir,
45             );
46              
47             The following arguments are usable.
48              
49             - base_dir :: Path to use for the base dir, where the proc
50             cache, freebsd_proc_cache.json, is is created.
51             Default :: /var/cache/oslv_monitor
52              
53             - obj :: The OSLVM::Monitor object.
54              
55             =cut
56              
57             sub new {
58 0     0 1   my ( $blank, %opts ) = @_;
59              
60 0 0         if ( !defined( $opts{base_dir} ) ) {
61 0           $opts{base_dir} = '/var/cache/oslv_monitor';
62             }
63              
64 0 0         if ( !defined( $opts{obj} ) ) {
    0          
65 0           die('$opts{obj} is undef');
66             } elsif ( ref( $opts{obj} ) ne 'OSLV::Monitor' ) {
67 0           die('ref $opts{obj} is not OSLV::Monitor');
68             }
69              
70 0           my $self = { version => 1, proc_cache => $opts{base_dir} . '/freebsd_proc_cache.json', obj => $opts{obj} };
71 0           bless $self;
72              
73 0           return $self;
74             } ## end sub new
75              
76             =head2 run
77              
78             $return_hash_ref=$backend->run;
79              
80             =cut
81              
82             sub run {
83 0     0 1   my $self = $_[0];
84              
85 0           my $data = {
86             errors => [],
87             cache_failure => 0,
88             oslvms => {},
89             has => {
90             'linux_mem_stats' => 0,
91             'rwdops' => 0,
92             'rwdbytes' => 0,
93             'rwdblocks' => 1,
94             'signals-taken' => 1,
95             'recv_sent_msgs' => 1,
96             'cows' => 1,
97             'stack-size' => 1,
98             'swaps' => 1,
99             'sock' => 0,
100             },
101             totals => {
102             'copy-on-write-faults' => 0,
103             'cpu-time' => 0,
104             'data-size' => 0,
105             'elapsed-times' => 0,
106             'involuntary-context-switches' => 0,
107             'major-faults' => 0,
108             'minor-faults' => 0,
109             'percent-cpu' => 0,
110             'percent-memory' => 0,
111             'read-blocks' => 0,
112             'received-messages' => 0,
113             'rss' => 0,
114             'sent-messages' => 0,
115             'stack-size' => 0,
116             'swaps' => 0,
117             'system-time' => 0,
118             'text-size' => 0,
119             'user-time' => 0,
120             'virtual-size' => 0,
121             'voluntary-context-switches' => 0,
122             'written-blocks' => 0,
123             'procs' => 0,
124             'signals-taken' => 0,
125             },
126             };
127              
128 0           my $proc_cache;
129 0           my $new_proc_cache = {};
130 0           my $cache_is_new = 0;
131 0 0         if ( -f $self->{proc_cache} ) {
132 0           eval {
133 0           my $raw_cache = read_file( $self->{proc_cache} );
134 0           $proc_cache = decode_json($raw_cache);
135             };
136 0 0         if ($@) {
137             push(
138 0           @{ $data->{errors} },
139 0           'reading proc cache "' . $self->{proc_cache} . '" failed... using a empty one...' . $@
140             );
141 0           $data->{cache_failure} = 1;
142 0           $proc_cache = {};
143 0           return $data;
144             }
145             } else {
146 0           $cache_is_new = 1;
147             }
148              
149 0           my $base_stats = {
150             'copy-on-write-faults' => 0,
151             'cpu-time' => 0,
152             'data-size' => 0,
153             'elapsed-times' => 0,
154             'involuntary-context-switches' => 0,
155             'major-faults' => 0,
156             'minor-faults' => 0,
157             'percent-cpu' => 0,
158             'percent-memory' => 0,
159             'read-blocks' => 0,
160             'received-messages' => 0,
161             'rss' => 0,
162             'sent-messages' => 0,
163             'stack-size' => 0,
164             'swaps' => 0,
165             'system-time' => 0,
166             'text-size' => 0,
167             'user-time' => 0,
168             'virtual-size' => 0,
169             'voluntary-context-switches' => 0,
170             'written-blocks' => 0,
171             'procs' => 0,
172             'signals-taken' => 0,
173             'ip' => [],
174             'path' => [],
175             };
176              
177             # get a list of jails for jid to name mapping
178 0           my $output = `/usr/sbin/jls -h --libxo json 2> /dev/null`;
179 0           my $jls;
180             my %jid_to_name;
181 0           my @IP_keys = ( 'ip4.addr', 'ip6.addr' );
182 0           eval { $jls = decode_json($output) };
  0            
183 0 0         if ($@) {
184 0           push( @{ $data->{errors} }, 'decoding output from "jls -h --libxo json 2> /dev/null" failed... ' . $@ );
  0            
185 0           return $data;
186             }
187 0 0 0       if ( defined($jls)
      0        
      0        
      0        
      0        
188             && ref($jls) eq 'HASH'
189             && defined( $jls->{'jail-information'} )
190             && ref( $jls->{'jail-information'} ) eq 'HASH'
191             && defined( $jls->{'jail-information'}{jail} )
192             && ref( $jls->{'jail-information'}{jail} ) eq 'ARRAY' )
193             {
194 0           foreach my $jls_jail ( @{ $jls->{'jail-information'}{jail} } ) {
  0            
195 0 0 0       if ( defined( $jls_jail->{name} ) && defined( $jls_jail->{jid} ) ) {
196 0           my $jname = $jls_jail->{name};
197              
198 0           $jid_to_name{ $jls_jail->{jid} } = $jname;
199              
200 0           $data->{oslvms}{$jname} = clone($base_stats);
201              
202             # finds each ip ifconfig shows in a jail
203 0           my $output = `ifconfig -j $jname 2> /dev/null`;
204 0           my %found_IPv4;
205             my %found_IPv6;
206 0 0         if ( $? eq 0 ) {
207 0           my @output_split = split( /\n/, $output );
208 0           my $interface;
209 0           foreach my $line (@output_split) {
210 0 0 0       if ( $line =~ /^[a-zA-Z].*\:[\ \t]+flags\=/ ) {
    0 0        
    0          
211 0           $interface = $line;
212 0           $interface =~ s/\:[\ \t]+flags.*//;
213             } elsif ( $line =~ /^[\ \t]+inet6 /
214             && defined($interface) )
215             {
216 0           $line =~ s/^[\ \t]+inet6 //;
217 0           $line =~ s/\ .*$//;
218 0           $line =~ s/\%.*$//;
219 0           $found_IPv6{$line} = $interface;
220             } elsif ( $line =~ /^[\ \t]+inet /
221             && defined($interface) )
222             {
223 0           $line =~ s/^[\ \t]+inet //;
224 0           $line =~ s/ .*$//;
225 0           $found_IPv4{$line} = $interface;
226             }
227             } ## end foreach my $line (@output_split)
228             } ## end if ( $? eq 0 )
229              
230 0           foreach my $ip_key (@IP_keys) {
231 0           my @current_IPs;
232              
233 0 0         if ( $ip_key eq 'ip4.addr' ) {
234 0           @current_IPs = keys(%found_IPv4);
235             } else {
236 0           @current_IPs = keys(%found_IPv6);
237             }
238              
239 0 0 0       if ( defined( $jls_jail->{$ip_key} )
      0        
240             && ref( $jls_jail->{$ip_key} ) eq 'ARRAY'
241             && defined( $jls_jail->{$ip_key}[0] ) )
242             {
243 0           foreach my $ip ( @{ $jls_jail->{$ip_key} } ) {
  0            
244 0 0 0       if ( ref($ip) eq '' && !defined( $found_IPv4{$ip} ) && !defined( $found_IPv6{$ip} ) ) {
      0        
245 0 0 0       if ( $ip =~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/
246             || $ip =~ /^[\:0-9a-fA-F]+$/ )
247             {
248 0           push( @current_IPs, $ip );
249             }
250             }
251             }
252             } ## end if ( defined( $jls_jail->{$ip_key} ) && ref...)
253 0           foreach my $ip (@current_IPs) {
254 0           my $ip_if;
255             my $ip_gw;
256 0           my $ip_gw_if;
257              
258 0 0 0       if ( $ip_key eq 'ip4.addr'
    0 0        
259             && defined( $found_IPv4{$ip} ) )
260             {
261 0           $ip_if = $found_IPv4{$ip};
262             } elsif ( $ip_key eq 'ip6.addr'
263             && defined( $found_IPv6{$ip} ) )
264             {
265 0           $ip_if = $found_IPv6{$ip};
266             }
267             # set the ip type flag for netstat
268 0           my $ip_flag = '-6';
269 0 0         if ( $ip_key eq 'ip4.addr' ) {
270 0           $ip_flag = '-4';
271             }
272              
273             # fetch netstat route info for the jail
274 0           my $output = `route -n -j $jname $ip_flag show default 2> /dev/null`;
275 0 0         if ( $? eq 0 ) {
276 0           my @output_split = split( /\n/, $output );
277 0           foreach my $line (@output_split) {
278 0 0         if ( $line =~ /gateway\:[\ \t]/ ) {
    0          
279 0           $line =~ s/.*gateway\:[\ \t]+//;
280 0           $line =~ s/[\ \t]*$//;
281 0           $ip_gw = $line;
282             } elsif ( $line =~ /interface:[\ \t]/ ) {
283 0           $line =~ s/.*interface\:[\ \t]+//;
284 0           $line =~ s/[\ \t]*$//;
285 0           $ip_gw_if = $line;
286             }
287             } ## end foreach my $line (@output_split)
288             } ## end if ( $? eq 0 )
289              
290             push(
291 0           @{ $data->{oslvms}{$jname}{ip} },
  0            
292             {
293             ip => $ip,
294             if => $ip_if,
295             gw => $ip_gw,
296             gw_if => $ip_gw_if,
297             }
298             );
299             } ## end foreach my $ip (@current_IPs)
300             } ## end foreach my $ip_key (@IP_keys)
301             } ## end if ( defined( $jls_jail->{name} ) && defined...)
302             } ## end foreach my $jls_jail ( @{ $jls->{'jail-information'...}})
303             } ## end if ( defined($jls) && ref($jls) eq 'HASH' ...)
304              
305             # remove possible dup paths
306 0           my @found_jails = keys( %{ $data->{oslvms} } );
  0            
307 0           foreach my $jail (@found_jails) {
308 0           my @uniq_paths = uniq( @{ $data->{oslvms}{$jail}{path} } );
  0            
309 0           $data->{oslvms}{$jail}{path} = \@uniq_paths;
310             }
311              
312 0           my @stats = (
313             'copy-on-write-faults', 'cpu-time',
314             'data-size', 'elapsed-times',
315             'involuntary-context-switches', 'major-faults',
316             'minor-faults', 'percent-cpu',
317             'percent-memory', 'read-blocks',
318             'received-messages', 'rss',
319             'sent-messages', 'stack-size',
320             'swaps', 'system-time',
321             'text-size', 'user-time',
322             'virtual-size', 'voluntary-context-switches',
323             'written-blocks', 'signals-taken',
324             );
325              
326             # values that are time stats that require additional processing
327 0           my $times = { 'cpu-time' => 1, 'system-time' => 1, 'user-time' => 1, };
328             # these are counters and differences needed computed for them
329 0           my $counters = {
330             'cpu-time' => 1,
331             'system-time' => 1,
332             'user-time' => 1,
333             'read-blocks' => 1,
334             'major-faults' => 1,
335             'involuntary-context-switches' => 1,
336             'minor-faults' => 1,
337             'received-messages' => 1,
338             'sent-messages' => 1,
339             'swaps' => 1,
340             'voluntary-context-switches' => 1,
341             'written-blocks' => 1,
342             'copy-on-write-faults' => 1,
343             'signals-taken' => 1,
344             };
345              
346 0           foreach my $jail (@found_jails) {
347 0           $output
348             = `/bin/ps ax --libxo json -o %cpu,%mem,pid,acflag,cow,dsiz,etimes,inblk,jail,majflt,minflt,msgrcv,msgsnd,nivcsw,nswap,nvcsw,oublk,rss,ssiz,systime,time,tsiz,usertime,vsz,pid,gid,uid,command,nsigs -J $jail 2> /dev/null`;
349 0           my $ps;
350 0           eval { $ps = decode_json($output); };
  0            
351 0 0         if ( !$@ ) {
352 0           foreach my $proc ( @{ $ps->{'process-information'}{process} } ) {
  0            
353 0 0         if ( $proc->{'elapsed-times'} ne '-' ) {
354             my $cache_name
355 0           = $proc->{pid} . '-' . $proc->{uid} . '-' . $proc->{gid} . '-' . $jail . '-' . $proc->{command};
356              
357 0           foreach my $stat (@stats) {
358 0           my $stat_value = $proc->{$stat};
359             # pre-process the stat if it is a time value that requires it
360 0 0         if ( $times->{$stat} ) {
361             # [days-][hours:]minutes:seconds
362 0           my $seconds = 0;
363 0           my $time = $stat_value;
364              
365 0 0         if ( $time =~ /-/ ) {
366 0           my $days = $time;
367 0           $days =~ s/\-.*$//;
368 0           $time =~ s/^.*\-//;
369 0           $seconds = $seconds + ( $days * 86400 );
370             }
371 0           my @time_split = split( /\:/, $time );
372 0 0         if ( defined( $time_split[2] ) ) {
373 0           $seconds
374             = $seconds + ( 3600 * $time_split[0] ) + ( 60 * $time_split[1] ) + $time_split[2];
375             } else {
376 0           $seconds = $seconds + ( 60 * $time_split[1] ) + $time_split[1];
377             }
378 0           $stat_value = $seconds;
379 0           $proc->{$stat} = $stat_value;
380             } ## end if ( $times->{$stat} )
381              
382 0 0         if ( looks_like_number($stat_value) ) {
383 0 0         if ( $counters->{$stat} ) {
384 0 0 0       if ( defined( $proc_cache->{$cache_name} )
385             && defined( $proc_cache->{$cache_name}{$stat} ) )
386             {
387 0           $stat_value = ( $stat_value - $proc_cache->{$cache_name}{$stat} ) / 300;
388             } else {
389 0           $stat_value = $stat_value / 300;
390             }
391             $data->{oslvms}{$jail}{$stat}
392 0           = $data->{oslvms}{$jail}{$stat} + $stat_value;
393 0           $data->{totals}{$stat} = $data->{totals}{$stat} + $stat_value;
394             } else {
395             $data->{oslvms}{$jail}{$stat}
396 0           = $data->{oslvms}{$jail}{$stat} + $stat_value;
397 0           $data->{totals}{$stat} = $data->{totals}{$stat} + $stat_value;
398             }
399             } else {
400 0           warn( '"' . $stat_value . '" for ' . $stat . ' does not appear numeric' );
401             }
402             } ## end foreach my $stat (@stats)
403              
404 0           $data->{oslvms}{$jail}{procs}++;
405 0           $data->{totals}{procs}++;
406              
407 0           $new_proc_cache->{$cache_name} = $proc;
408             } ## end if ( $proc->{'elapsed-times'} ne '-' )
409             } ## end foreach my $proc ( @{ $ps->{'process-information'...}})
410             } ## end if ( !$@ )
411             } ## end foreach my $jail (@found_jails)
412              
413             # save the proc cache for next run
414 0           eval { write_file( $self->{proc_cache}, encode_json($new_proc_cache) ); };
  0            
415 0 0         if ($@) {
416 0           push( @{ $data->{errors} }, 'saving proc cache failed, "' . $self->{proc_cache} . '"... ' . $@ );
  0            
417             }
418              
419 0 0         if ($cache_is_new) {
420 0           delete( $data->{oslvms} );
421 0           $data->{oslvms} = {};
422 0           my @total_keys = keys( %{ $data->{totals} } );
  0            
423 0           foreach my $total_key (@total_keys) {
424 0 0         if ( ref( $data->{totals}{$total_key} ) eq '' ) {
425 0           $data->{totals}{$total_key} = 0;
426             }
427             }
428             } ## end if ($cache_is_new)
429              
430 0           return $data;
431             } ## end sub run
432              
433             =head2 usable
434              
435             Dies if not usable.
436              
437             eval{ $backend->usable; };
438             if ( $@ ){
439             print 'Not usable because... '.$@."\n";
440             }
441              
442             =cut
443              
444             sub usable {
445 0     0 1   my $self = $_[0];
446              
447             # make sure it is freebsd
448 0 0         if ( $^O !~ 'freebsd' ) {
449 0           die '$^O is "' . $^O . '" and not "freebsd"';
450             }
451              
452             # make sure we can locate jls
453 0           my $cmd_bin = `/bin/sh -c 'which jls 2> /dev/null'`;
454 0 0         if ( $? != 0 ) {
455 0           die 'The command "jls" is not in the path... ' . $ENV{PATH};
456             }
457              
458 0           return 1;
459             } ## end sub usable
460              
461             sub ip_to_if {
462 0     0 0   my $self = $_[0];
463 0           my $ip = $_[1];
464              
465 0 0 0       if ( !defined($ip) || ref($ip) ne '' ) {
466 0           return undef;
467             }
468              
469 0           my $if = IO::Interface::Simple->new_from_address($ip);
470              
471 0 0         if ( !defined($if) ) {
472 0           return undef;
473             }
474              
475 0           return $if->name;
476             } ## end sub ip_to_if
477              
478             =head1 AUTHOR
479              
480             Zane C. Bowers-Hadley, C<< >>
481              
482             =head1 BUGS
483              
484             Please report any bugs or feature requests to C, or through
485             the web interface at L. I will be notified, and then you'll
486             automatically be notified of progress on your bug as I make changes.
487              
488              
489              
490              
491             =head1 SUPPORT
492              
493             You can find documentation for this module with the perldoc command.
494              
495             perldoc OSLV::Monitor
496              
497              
498             You can also look for information at:
499              
500             =over 4
501              
502             =item * RT: CPAN's request tracker (report bugs here)
503              
504             L
505              
506             =item * CPAN Ratings
507              
508             L
509              
510             =item * Search CPAN
511              
512             L
513              
514             =back
515              
516              
517             =head1 ACKNOWLEDGEMENTS
518              
519              
520             =head1 LICENSE AND COPYRIGHT
521              
522             This software is Copyright (c) 2024 by Zane C. Bowers-Hadley.
523              
524             This is free software, licensed under:
525              
526             The Artistic License 2.0 (GPL Compatible)
527              
528              
529             =cut
530              
531             1; # End of OSLV::Monitor