Subversion Repositories camp_sysinfo_client_3

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
16 rodolico 1
#!/usr/bin/env perl
20 rodolico 2
use warnings;
26 rodolico 3
use strict;  
2 rodolico 4
 
5
# sysinfo
6
# Author: R. W. Rodolico
7
# Primary client portion of sysinfo system. Will collect information about its current
8
# host and create a report containing the information. This report can then be processed
9
# by process_sysinfo.pl on the collection computer.
10
# output file consists of an XML file of the form:
11
#  <sysinfo3.0.0>
12
#    <diskinfo name='/dev/xvda3'>
13
#      <fstype>ext3</fstype>
14
#      <mount>/home</mount>
15
#      <size>51606140</size>
16
#      <used>331472</used>
17
#    </diskinfo>
18
#    <network name='eth0'>
19
#      <address>192.168.1.3</address>
20
#      <ip6address>fe80::216:3eff:fefb:4e10</ip6address>
21
#      <ip6networkbits>64</ip6networkbits>
22
#      <mac>00:16:3e:fb:4e:10</mac>
23
#      <mtu>1500</mtu>
24
#      <netmask>255.255.255.0</netmask>
25
#    </network>
26
#    <operatingsystem>
27
#      <codename>squeeze</codename>
28
#      <description>Debian GNU/Linux 6.0.4 (squeeze)</description>
29
#      <distribution>Debian</distribution>
30
#      <kernel>2.6.32-5-xen-686</kernel>
31
#      <os_name>Linux</os_name>
32
#      <os_version>Linux version 2.6.32-5-xen-686 (Debian 2.6.32-41) (ben@decadent.org.uk) (gcc version 4.3.5 (Debian 4.3.5-4) ) #1 SMP Mon Jan 16 19:46:09 UTC 2012</os_version>
33
#      <release>6.0.4</release>
34
#    </operatingsystem>
35
#    <pci name='0000:00:00.0'>
36
#      <class>RAM memory</class>
37
#      <device>MCP55 Memory Controller</device>
38
#      <rev>a2</rev>
39
#      <sdevice>Device cb84</sdevice>
40
#      <slot>0000:00:00.0</slot>
41
#      <svendor>nVidia Corporation</svendor>
42
#      <vendor>nVidia Corporation</vendor>
43
#    </pci>
44
#    <report>
45
#      <client>Staffmasters</client>
46
#      <date>2012-05-01 03:00</date>
47
#      <version>2.0.0</version>
48
#    </report>
49
#    <software name='aptitude'>
50
#      <description>terminal-based package manager (terminal interface only)</description>
51
#      <version>0.6.3-3.2+squeeze1</version>
52
#    </software>
53
#    <system>
54
#      <cpu_speed>1800.103</cpu_speed>
55
#      <cpu_sub>i686</cpu_sub>
56
#      <cpu_type>GenuineIntel</cpu_type>
57
#      <hostname>backup.staffmasters.local</hostname>
58
#      <last_boot>1333259809</last_boot>
59
#      <memory>520852</memory>
60
#      <num_cpu>1</num_cpu>
61
#    </system>
62
#  </sysinfo3.0.0>
63
 
64
 
65
#
66
# Version 1.3 20071104
67
# added capability of e-mailing the results by itself and external configuration file
68
 
69
# Version 1.3.1 20071110
70
# added du -sk to explicitly do directory sizes in 'k'. Also, fixed some documentation
71
 
72
# Version 1.3.3 20081104
73
# modified hostname to hostname -f, and allowed user to place custom value in configuration file
74
# also, modified to go with Debian standards in preparation to creating a debian package.
75
 
76
# Version 2.0 20081208
77
# Modified to use different libraries for different OS's in preparation to porting to Windows
78
# Uses different packages based on which OS it is on.
79
 
80
# Version 3.0 20120923
81
# Major revision. Most internal intelligence pulled out and put into modules and data transfer format has been changed to YAML
82
#
83
# Base system only pulls client name, machine name and machine number, all of which can be set in the configuration file
84
# if the value is not set, it attempts various means to determine the values and, if it fails, aborts with an error message
85
#    client name -- REQUIRED, must come from configuration file
86
#    machine name --  REQUIRED, if not set via conf file, attempts hostname command (hostname -f) or module getHostName
87
#    machine number -- REQUIRED, if not set via conf file, attempts "echo `hostname -f`-clientname | md5sum" or module getSerial
88
# modules are stored in "configuration directory/modules" (/etc/sysinfo/modules on most Linux systems) and are processed in 
89
# standard sort order (case sensitive). 
90
# Module filenames may contain alpha-numeric, underscore and the period only (files containing other characters are ignored).
91
# Modules should set their exit code to 0 for success, and non-zero for failure
92
# Modules should return 0 or more tab delimited, newline terminated strings, processed as one record per line
93
# A module return string line is processed as follows:
94
#     category \t [category \t ...] \t key \t value
95
# example:
96
#    System \t num_cpu \t 1
97
#    System \t Users \t root \t /root/
98
# (note, if non-zero exit code returned, return value is assumed to be error message and is printed to STDERR) 
99
# sysinfo stores the result in a hash, using categories as the keys (case sensitive), thus, the above results in
100
# $store{'System'}{'num_cpu'} = '1';
101
# $store{'System'}{'Users'}{'root'} = '/root';
102
# upon completion, sysinfo converts the $store hash into an XML or YAML string for transfer
103
# It then sends it to the main server as defined in the conf file.
104
# NOTE: YAML is hand crafted to kill any requirements for external libraries
105
# see sub hashToYAML for details
106
 
9 rodolico 107
# Version 3.0.1 20160321
108
# Renamed to sysinfo-client to not conflict with Linux package sysinfo
109
# created installer in Perl to not rely on package managers
110
# default path for configuration file changed to /etc/camp/sysinfo-client.conf
111
# $VERSION changed to $DATA_VERSION to not conflict with $main::VERSION (script version vs data format version)
13 rodolico 112
#
113
# Version 3.1.0 20160401
114
# module and script dirs now arrays to be searched. Idea is that default
115
#    modules/scripts are in installdir/modules or installdir/scripts, and
116
#    user supplied are in /etc/scripts and /etc/modules
14 rodolico 117
# Tightened up the file systems checks, requiring all scripts and modules
118
#    be set 0700 at least, and owned by root
18 rodolico 119
# Transport layers now an array, and if one fails to send the report, the others
120
#    are tried in turn
14 rodolico 121
# Worked on logic for sendReport to give better error checking.
122
# Doing a search for the configuration file matching cwd, then /etc/camp, then /usr/local/etc/camp
21 rodolico 123
# Self documenting, ie a key for software\tsysinfo-client\version\current version is inserted
28 rodolico 124
#
125
# Version 3.1.1 20160915 RWR
126
# set use strict and use warnings, then fixed errors
37 rodolico 127
#
128
# Version 3.1.2 20160922 RWR
129
# $exitCode 1 (not applicable to this machine) does not throw warning
130
#
42 rodolico 131
# Version 3.1.3 20161010 RWR
132
# Removed extra use warnings
47 rodolico 133
#
134
# Version 3.1.4 20161023 RWR
135
# Would error out if moduledir does not exist, added a return
51 rodolico 136
#
137
# Version 3.1.5 20170327 RWR
138
# On freeBSD systems, was looking in wrong place for configuration file
9 rodolico 139
 
2 rodolico 140
# Following are global variables overridden if configuration file exists
141
 
142
my $TESTING = 0; # level's 0 (none) to 3 defined and increase verbosity while decreasing functionality
51 rodolico 143
our $VERSION = '3.1.5';
9 rodolico 144
 
2 rodolico 145
my $indentLevel = 2; # number of spaces to indent per level in XML or YAML
146
 
147
$indentLevel = 3 if $TESTING;
148
if ($TESTING) {
149
   use Data::Dumper;
150
}
151
 
13 rodolico 152
# paths to search for configuration file
51 rodolico 153
my @confFileSearchPath = ( '.', '/etc/camp/sysinfo-client', '/etc/camp', '/usr/local/etc/camp/sysinfo-client' );
2 rodolico 154
 
13 rodolico 155
my $configurationFile = 'sysinfo-client.conf'; # name of the configuration file
2 rodolico 156
 
157
# default values may be overridden in conf file
14 rodolico 158
my @moduleDirs;
159
my @scriptDirs;
2 rodolico 160
my $reportDate = &getReportDate; # set report date
161
 
162
# defined only in configuration file
163
my $clientName; # Required!! Must be set in conf file (no defaults)
13 rodolico 164
my $hostname = &getHostName; # set hostname to default to hostname -f, unless overridden in conf file
2 rodolico 165
my $serialNumber; # defined in configuration file, or later in here if not defined there
18 rodolico 166
my $transports; # hash which can optionally use a script to send the report to the server
2 rodolico 167
 
9 rodolico 168
my $DATA_VERSION = '3.0.0'; # used in sending the data file. sets version of XML/YAML data file
2 rodolico 169
 
13 rodolico 170
 
171
 
2 rodolico 172
#######################################################
173
#
13 rodolico 174
# findFile( $filename, @directories )
175
#
176
# Locates a file by searching sequentially in one or more
177
# directories, returning the first one found
178
# 
179
# Returns '' if not found
180
#
181
#######################################################
182
 
183
sub findFile {
184
   my ( $filename, @directories ) = @_;
28 rodolico 185
   for ( my $i = 0; $i < scalar( @directories ); $i++ ) {
186
      my $confFile = $directories[$i] . '/' . $filename;
13 rodolico 187
      return $confFile if ( -f $confFile );
188
   }
189
   return '';
190
}
191
 
192
 
193
#######################################################
194
#
2 rodolico 195
# loadConfigurationFile($confFile)
196
#
197
# Loads configuration file defined by $configurationFile, and dies if not available
198
# Reads entire contents into memory where it is eval'd in main program
199
# Parameters: configuration file fully path/file name
200
# NOTE: conf file must be a valid Perl file
201
#
202
#######################################################
203
 
204
sub loadConfigurationFile {
14 rodolico 205
   my ( $fileName, @searchPath ) = @_;
13 rodolico 206
   my $confFile;
14 rodolico 207
   if ( $confFile = &findFile( $fileName, @searchPath ) ) {
13 rodolico 208
      open CONFFILE, "<$confFile" or die "Can not open configuration file $confFile: $!\n";
209
      my $confFileContents = join( '', <CONFFILE> ); # just slurp it into memory
210
      close CONFFILE;
211
      return ($confFileContents);
212
   }
14 rodolico 213
   die "Can not find $fileName in any of " . join( "\n\t", @searchPath ) . "\n";
2 rodolico 214
}
215
 
216
#######################################################
217
#
218
# sendResults( $parameters, $message, $scriptDirectory )
219
#
220
# Sends results of run to server using external script. If external
221
# script not defined, just print to STDOUT
222
#
223
# Parameters
224
#  $parameters - a hash containing the information necessary to make the transfer
225
#  $message - the message to be sent
226
#  $scriptDirectory - path (not filename) of script to be executed
227
# 
228
# $parameters contains different key/value pairs depending on the script used
229
#             for example, a stand-alone SMTP script may need a username/password,
230
#             smtp server name, port number, from and to address
231
#             while an http transfer may only need a script name
232
#             See the individual scripts to determine what parameters need to be
233
#             filled in.
234
#             The only required parameter is 'sendScript' which must contain the
235
#             name of the script to execute (and it must be located in $scriptDirectory)
236
# SCRIPT must contain one sub named doit, that accepts three parameters, the hash, 
237
#       the message, and, optionally, the script directory
238
#
239
# If script not defined, just dump to STDOUT. With a properly set up cron job, the output
240
# would then be sent via e-mail to an administrative account, possibly root
241
#
242
#######################################################
243
sub sendResults {
19 rodolico 244
   my ( $globals, $parameters, $message, @scriptDirectory ) = @_;
18 rodolico 245
   foreach my $key ( sort { $a <=> $b } keys %$parameters ) {
19 rodolico 246
      if ( $$parameters{$key}{'sendScript'} ) {
247
         # print "Trying to find file " . $$parameters{$key}{'sendScript'} . " in " . join( "\n\t", @scriptDirectory ) . "\n";
248
         my $sendScript = &findFile( $$parameters{$key}{'sendScript'}, @scriptDirectory );
249
         if ( $sendScript ) {
18 rodolico 250
            # load the chosen script into memory
251
            require $sendScript;
19 rodolico 252
            # merge the globals in
253
            while ( my ( $gkey, $value ) = each %$globals ) { 
254
               $$parameters{$key}{$gkey} = $value; 
255
            }
20 rodolico 256
            # do variable substitution for any values which need it
257
            foreach my $thisOne ( keys %{$$parameters{$key}} ) {
258
               #print "$thisOne\n";
259
               if ( $$parameters{$key}{$thisOne} =~ m/(\$hostname)|(\$reportDate)|(\$clientName)|(\$serialNumber)/ ) {
260
                  $$parameters{$key}{$thisOne} = eval "\"$$parameters{$key}{$thisOne}\"";
261
               }
262
            }
263
 
19 rodolico 264
            #%$parameters{$key}{keys %$globals} = values %$globals;
265
            #print Dumper( $$parameters{$key} );
20 rodolico 266
            #next;
18 rodolico 267
            # execute the "doit" sub from that script
28 rodolico 268
            my $return = &doit( $$parameters{$key}, $message );
18 rodolico 269
            return $return if ( $return == 1 );
270
         } else {
271
            print "Could not find " . $$parameters{$key}{'sendScript'} . ", trying next transport\n";
272
         } # if..else
273
      } # if
274
   } # foreach
275
   # if we made it here, we have not sent the report, so just return it to the user
276
   print $message;
16 rodolico 277
   return 1;
2 rodolico 278
}
279
 
280
#######################################################
281
#
282
# getReportDate
283
#
284
# return current system date as YYYY-MM-DD HH:MM:SS
285
#
286
#######################################################
287
sub getReportDate {
28 rodolico 288
   my ($second, $minute, $hour, $dayOfMonth, $month, $year) = localtime();
2 rodolico 289
   return sprintf( "%4u-%02u-%02u %02u:%02u:%02u", $year+1900, $month+1, $dayOfMonth, $hour, $minute, $second );
290
}
291
 
292
#######################################################
293
#
294
# getHostName
295
#
296
# return hostname from hostname -f
297
#
298
#######################################################
299
sub getHostName {
28 rodolico 300
   my $hostname = `hostname -f`;
2 rodolico 301
   chomp $hostname;
302
   return $hostname;
303
}
304
 
305
#######################################################
306
#
18 rodolico 307
# escapeForYAML
2 rodolico 308
#
18 rodolico 309
# Escapes values put into YAML report
2 rodolico 310
#
311
#######################################################
312
sub escapeForYAML {
313
   my $value = shift;
314
   $value =~ s/'/\\'/gi; # escape single quotes
315
   $value =~ s/"/\\"/gi; # escape double quotes
316
   # pound sign indicates start of a comment and thus loses part
317
   # of strings. Surrounding it by double quotes in next statement
318
   # allows 
319
   $value = '"' . $value . '"' if ( $value =~ m/[#:]/ );
320
   return $value;
321
}
322
 
323
#######################################################
324
#
325
# hashToYAML( $hashRef, $indent )
326
#
327
# Converts a hash to a YAML string
328
#
329
# NOTE: This routine recursively calls itself for every level
330
#       in the hash
331
#
332
# Parameters
333
#     $hashref - reference (address) of a hash
334
#     $indent  - current indent level, defaults to 0
335
#
336
# Even though there are some very good libraries that do this
337
# I chose to hand-code it so sysinfo can be run with no libraries
338
# loaded. I chose to NOT do a full implementation, so special chars
339
# that would normally be escaped are not in here. 
340
# However, I followed all the RFC for the values that were given, so
341
# assume any YAML reader can parse this
342
# NOTE: YAML appears to give a resulting file 1/3 smaller than the above
343
#       XML, and compresses down in like manner
344
#
345
#######################################################
346
sub hashToYAML {
347
   my ($hashRef, $indent) = @_;
348
   $indent = 0 unless $indent; # default to 0 if not defined
349
 
350
   my $output; # where the output is stored
351
   foreach my $key ( keys %$hashRef ) { # for each key in the current reference
352
      print "Looking at $key\n" if $TESTING > 3;
353
      # see http://www.perlmonks.org/?node_id=175651 for isa function
354
      if ( UNIVERSAL::isa( $$hashRef{$key}, 'HASH' ) ) { # is the value another hash?
355
            # NOTE: unlike xml, indentation is NOT optional in YAML, so the following line verifies $indentlevel is non-zero
356
            #       and, if it is, uses a default 3 character indentation
357
            $output .= (' ' x $indent ) . &escapeForYAML($key) . ":\n" . # key, plus colon, plus newline
358
                    &hashToYAML( $$hashRef{$key}, $indent+($indentLevel ? $indentLevel : 3) ) . # add results of recursive call
359
                    "\n";
360
      } elsif ( UNIVERSAL::isa( $$hashRef{$key}, 'ARRAY' ) ) { # is it an array? ignore it
361
      } else { # it is a scalar, so just do <key>value</key>
362
         $output .= (' ' x $indent ) . &escapeForYAML($key) . ': ' . &escapeForYAML($$hashRef{$key}) . "\n";
363
      }
364
   }
365
   return $output;
366
}
367
 
368
 
369
#######################################################
370
#
371
# tabDelimitedToHash ($hashRef, $tabdelim)
372
#
373
# Takes a tab delimited multi line string and adds it
374
# to a hash. The final field in each line is considered to
375
# be the value, and all prior fields are considered to be
376
# hierachial keys.
377
#
378
# Parameters
379
#     $hashref - reference (address) of a hash
380
#     $tabdelim - A tab delimited, newline terminated set of records
381
#
382
#
383
#######################################################
384
sub tabDelimitedToHash {
385
   my ($hashRef, $tabdelim) = @_;
386
   foreach my $line ( split( "\n", $tabdelim ) ) { # split on newlines, then process each line in turn
387
      $line =~ s/'/\\'/gi; # escape single quotes
28 rodolico 388
      my @fields = split( / *\t */, $line ); # get all the field values into array
2 rodolico 389
      my $theValue = pop @fields; # the last one is the value, so save it
390
      # now, we build a Perl statement that would create the assignment. The goal is
391
      # to have a string that says something like $$hashRef{'key'}{'key'} = $value;
392
      # then, eval that.
393
      my $command = '$$hashRef'; # start with the name of the dereferenced hash (parameter 1)
394
      while (my $key = shift @fields) { # while we have a key, from left to right
395
         $command .= '{' . "'$key'" . '}'; # build it as {'key'} concated to string
396
      }
397
      $command .= "='$theValue';"; # add the assignment
398
      #print STDERR "$command\n"; 
399
      eval $command; # eval the string to make the actual assignment
400
   }
401
}
402
 
403
#######################################################
404
#
13 rodolico 405
# validatePermission ( $file )
406
#
407
# Checks that file is owned by root, and has permission
408
# 0700 or less
409
# 
410
# Returns empty string on success, error message
411
# on failure
412
#
413
#######################################################
414
 
415
sub validatePermission {
416
   my $file = shift;
14 rodolico 417
   my $return;
13 rodolico 418
   # must be owned by root
28 rodolico 419
   my $owner = (stat($file))[4];
13 rodolico 420
   $return .= " - Bad Owner [$owner]" if $owner;
421
   # must not have any permissions for group or world
422
   # ie, 0700 or less
28 rodolico 423
   my $mode = (stat($file))[2];
13 rodolico 424
   $mode = sprintf( '%04o', $mode & 07777 );
425
   $return .= " - Bad Permission [$mode]" unless $mode =~ m/0.00/;
426
   return $return ? $file . $return : '';
427
}
428
 
429
#######################################################
430
#
2 rodolico 431
# ProcessModules ( $system, $moduleDir )
432
#
433
# Processes all modules in $moduleDir, adding result to $system hash
434
# 
435
# Parameters
436
#     $system - reference (address) of a hash
437
#     $moduleDir - full path to a directory containing executable scripts
438
#  
439
# Each file in the $moduleDir directory that matches the regex in the grep
440
# and is executable is run. It is assumed the script will return 0 on success
441
# or a non-zero on failure
442
# The output of the script is assumed to be a tab delimited, newline separated
443
# list of records that should be added to the hash $system. This is done by calling 
444
# &parseModule above.
445
# on failure, the returned output of the script is assumed to be an error message
446
# and is displayed on STDERR
447
#######################################################
448
sub ProcessModules {
449
   my ( $system, $moduleDir ) = @_;
450
   # open the module directory
47 rodolico 451
   return unless -d $moduleDir;
2 rodolico 452
   opendir( my $dh, $moduleDir ) || die "Module Directory $moduleDir can not be opened: $!\n";
453
   # and get all files which are executable and contain nothing but alpha-numerics and underscores (must begin with alpha-numeric)
454
   my @modules = grep { /^[a-zA-Z0-9][a-zA-Z0-9_]+$/ && -x "$moduleDir/$_" } readdir( $dh );
455
   closedir $dh;
28 rodolico 456
   foreach my $modFile ( sort @modules ) { # for each valid script
14 rodolico 457
      if ( my $error = &validatePermission( "$moduleDir$modFile" ) ) {
13 rodolico 458
         print STDERR "Not Processed: $error\n";
459
         next;
460
      }
2 rodolico 461
      print "Processing module $moduleDir$modFile\n" if $TESTING > 2;
462
      my $output = qx/$moduleDir$modFile $moduleDir/; # execute it and grab the output
463
      my $exitCode = $? >> 8; # process the exitCode
37 rodolico 464
      # exitCode 0 - processed normally
465
      # exitCode 1 - not applicable to this machine
466
      if ( $exitCode && $exitCode > 1) { # if non-zero, error, so show an error message
2 rodolico 467
         warn "Error in $moduleDir$modFile, [$output]\n";
468
      } else { # otherwise, call tabDelimitedToHash to save the data
469
         &tabDelimitedToHash( $system, $output );
21 rodolico 470
      } # if
471
   } # foreach
472
   # add sysinfo-client (me) to the software list, since we're obviously installed
473
   &tabDelimitedToHash( $system, "software\tsysinfo-client\tversion\t$main::VERSION\n" );
2 rodolico 474
}
475
 
20 rodolico 476
sub processParameters {
477
   while ( my $parameter = shift ) {
478
      if ( $parameter eq '-v' ) {
479
         print "$main::VERSION\n";
480
         exit;
481
      }
482
   } # while
483
}
484
 
485
&processParameters( @ARGV );
486
 
14 rodolico 487
# load the configuration file
13 rodolico 488
 
14 rodolico 489
#die "Searching for $configurationFile in = \n" . join( "\n", @confFileSearchPath ) . "\n";
13 rodolico 490
eval ( &loadConfigurationFile( $configurationFile, @confFileSearchPath) );
2 rodolico 491
# user did not define a serial number, so make something up
18 rodolico 492
$serialNumber = '' unless $serialNumber;
2 rodolico 493
# oops, no client name (required) so tell them and exit
494
die "You must configure this package in $configurationFile" unless $clientName;
495
 
496
my $System; # hash reference that will store all info we are going to send to the server
497
# some defaults.
9 rodolico 498
$$System{'report'}{'version'} = $DATA_VERSION;
2 rodolico 499
$$System{'report'}{'date'} = $reportDate;
500
$$System{'report'}{'client'} = $clientName;
501
$$System{'system'}{'hostname'} = $hostname;
502
$$System{'system'}{'serial'} = $serialNumber;
503
 
504
# process any modules in the system
28 rodolico 505
foreach my $moduleDir ( @moduleDirs ) {
13 rodolico 506
   &ProcessModules( $System, "$moduleDir/" );
507
}
2 rodolico 508
 
509
# now, everything ins in $System, so convert it to the proper output format
20 rodolico 510
my $out =  "#sysinfo: $VERSION YAML\n---\n" . &hashToYAML( $System ) . "...\n";
2 rodolico 511
 
512
print Data::Dumper->Dump([$System],['System']) if $TESTING>3;
513
 
19 rodolico 514
# load some global values for use in the script, if required
515
my $globals = { 
516
      'data version' => $DATA_VERSION,
517
      'report date'  => $reportDate,
518
      'client name'  => $clientName,
519
      'host name'    => $hostname,
520
      'serial number'=> $serialNumber
521
      };
14 rodolico 522
 
2 rodolico 523
# and send the results to the server
19 rodolico 524
if ( my $success = &sendResults( $globals, $transports, $out, @scriptDirs ) != 1 ) {
16 rodolico 525
   print "Error $success while sending report from $hostname\n";
526
}
2 rodolico 527
 
9 rodolico 528
1;