Subversion Repositories camp_sysinfo_client_3

Rev

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

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