Subversion Repositories camp_sysinfo_client_3

Rev

Rev 257 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
174 rodolico 1
#! /usr/bin/env perl
2
 
258 rodolico 3
# Copyright (c) 2025, R. W. Rodolico
4
# All rights reserved.
5
#
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are met:
8
#
9
# 1. Redistributions of source code must retain the above copyright notice, this
10
#    list of conditions and the following disclaimer.
11
#
12
# 2. Redistributions in binary form must reproduce the above copyright notice,
13
#    this list of conditions and the following disclaimer in the documentation
14
#    and/or other materials provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 
20 rodolico 27
use warnings;
26 rodolico 28
use strict;  
233 rodolico 29
use open ':std', ':encoding(utf8)' ; # force all I/O, including disk and STD?, to use utf-8
30
use utf8;                            # Source code encoded using UTF-8, used to ensure output from modules is UTF-8, see tabDelimitedToHash
2 rodolico 31
 
32
# sysinfo
33
# Author: R. W. Rodolico
34
# Primary client portion of sysinfo system. Will collect information about its current
35
# host and create a report containing the information. This report can then be processed
36
# by process_sysinfo.pl on the collection computer.
112 rodolico 37
# output file consists of a YAML file of the form:
2 rodolico 38
#  <sysinfo3.0.0>
39
#    <diskinfo name='/dev/xvda3'>
40
#      <fstype>ext3</fstype>
41
#      <mount>/home</mount>
42
#      <size>51606140</size>
43
#      <used>331472</used>
44
#    </diskinfo>
45
#    <network name='eth0'>
46
#      <address>192.168.1.3</address>
47
#      <ip6address>fe80::216:3eff:fefb:4e10</ip6address>
48
#      <ip6networkbits>64</ip6networkbits>
49
#      <mac>00:16:3e:fb:4e:10</mac>
50
#      <mtu>1500</mtu>
51
#      <netmask>255.255.255.0</netmask>
52
#    </network>
53
#    <operatingsystem>
54
#      <codename>squeeze</codename>
55
#      <description>Debian GNU/Linux 6.0.4 (squeeze)</description>
56
#      <distribution>Debian</distribution>
57
#      <kernel>2.6.32-5-xen-686</kernel>
58
#      <os_name>Linux</os_name>
59
#      <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>
60
#      <release>6.0.4</release>
61
#    </operatingsystem>
62
#    <pci name='0000:00:00.0'>
63
#      <class>RAM memory</class>
64
#      <device>MCP55 Memory Controller</device>
65
#      <rev>a2</rev>
66
#      <sdevice>Device cb84</sdevice>
67
#      <slot>0000:00:00.0</slot>
68
#      <svendor>nVidia Corporation</svendor>
69
#      <vendor>nVidia Corporation</vendor>
70
#    </pci>
71
#    <report>
72
#      <client>Staffmasters</client>
73
#      <date>2012-05-01 03:00</date>
74
#      <version>2.0.0</version>
75
#    </report>
76
#    <software name='aptitude'>
77
#      <description>terminal-based package manager (terminal interface only)</description>
78
#      <version>0.6.3-3.2+squeeze1</version>
79
#    </software>
80
#    <system>
81
#      <cpu_speed>1800.103</cpu_speed>
82
#      <cpu_sub>i686</cpu_sub>
83
#      <cpu_type>GenuineIntel</cpu_type>
84
#      <hostname>backup.staffmasters.local</hostname>
85
#      <last_boot>1333259809</last_boot>
86
#      <memory>520852</memory>
87
#      <num_cpu>1</num_cpu>
88
#    </system>
89
#  </sysinfo3.0.0>
90
 
91
 
92
#
93
# Version 1.3 20071104
94
# added capability of e-mailing the results by itself and external configuration file
95
 
96
# Version 1.3.1 20071110
97
# added du -sk to explicitly do directory sizes in 'k'. Also, fixed some documentation
98
 
99
# Version 1.3.3 20081104
100
# modified hostname to hostname -f, and allowed user to place custom value in configuration file
101
# also, modified to go with Debian standards in preparation to creating a debian package.
102
 
103
# Version 2.0 20081208
104
# Modified to use different libraries for different OS's in preparation to porting to Windows
105
# Uses different packages based on which OS it is on.
106
 
107
# Version 3.0 20120923
108
# Major revision. Most internal intelligence pulled out and put into modules and data transfer format has been changed to YAML
109
#
110
# Base system only pulls client name, machine name and machine number, all of which can be set in the configuration file
111
# if the value is not set, it attempts various means to determine the values and, if it fails, aborts with an error message
112
#    client name -- REQUIRED, must come from configuration file
113
#    machine name --  REQUIRED, if not set via conf file, attempts hostname command (hostname -f) or module getHostName
114
#    machine number -- REQUIRED, if not set via conf file, attempts "echo `hostname -f`-clientname | md5sum" or module getSerial
115
# modules are stored in "configuration directory/modules" (/etc/sysinfo/modules on most Linux systems) and are processed in 
116
# standard sort order (case sensitive). 
117
# Module filenames may contain alpha-numeric, underscore and the period only (files containing other characters are ignored).
118
# Modules should set their exit code to 0 for success, and non-zero for failure
119
# Modules should return 0 or more tab delimited, newline terminated strings, processed as one record per line
120
# A module return string line is processed as follows:
121
#     category \t [category \t ...] \t key \t value
122
# example:
123
#    System \t num_cpu \t 1
124
#    System \t Users \t root \t /root/
125
# (note, if non-zero exit code returned, return value is assumed to be error message and is printed to STDERR) 
126
# sysinfo stores the result in a hash, using categories as the keys (case sensitive), thus, the above results in
127
# $store{'System'}{'num_cpu'} = '1';
128
# $store{'System'}{'Users'}{'root'} = '/root';
129
# upon completion, sysinfo converts the $store hash into an XML or YAML string for transfer
130
# It then sends it to the main server as defined in the conf file.
131
# NOTE: YAML is hand crafted to kill any requirements for external libraries
132
# see sub hashToYAML for details
133
 
9 rodolico 134
# Version 3.0.1 20160321
135
# Renamed to sysinfo-client to not conflict with Linux package sysinfo
136
# created installer in Perl to not rely on package managers
137
# default path for configuration file changed to /etc/camp/sysinfo-client.conf
138
# $VERSION changed to $DATA_VERSION to not conflict with $main::VERSION (script version vs data format version)
13 rodolico 139
#
140
# Version 3.1.0 20160401
141
# module and script dirs now arrays to be searched. Idea is that default
142
#    modules/scripts are in installdir/modules or installdir/scripts, and
143
#    user supplied are in /etc/scripts and /etc/modules
14 rodolico 144
# Tightened up the file systems checks, requiring all scripts and modules
145
#    be set 0700 at least, and owned by root
18 rodolico 146
# Transport layers now an array, and if one fails to send the report, the others
147
#    are tried in turn
14 rodolico 148
# Worked on logic for sendReport to give better error checking.
149
# Doing a search for the configuration file matching cwd, then /etc/camp, then /usr/local/etc/camp
21 rodolico 150
# Self documenting, ie a key for software\tsysinfo-client\version\current version is inserted
28 rodolico 151
#
152
# Version 3.1.1 20160915 RWR
153
# set use strict and use warnings, then fixed errors
37 rodolico 154
#
155
# Version 3.1.2 20160922 RWR
156
# $exitCode 1 (not applicable to this machine) does not throw warning
157
#
42 rodolico 158
# Version 3.1.3 20161010 RWR
159
# Removed extra use warnings
47 rodolico 160
#
161
# Version 3.1.4 20161023 RWR
162
# Would error out if moduledir does not exist, added a return
51 rodolico 163
#
164
# Version 3.1.5 20170327 RWR
165
# On freeBSD systems, was looking in wrong place for configuration file
59 rodolico 166
#
167
# Version 3.2.0 20180320 RWR
62 rodolico 168
# Major change in the configuration file format; All entries are loaded into 
169
# hash %configuration, so clientname is no longer $clientname, but is now
258 rodolico 170
# $config{'clientname'}
62 rodolico 171
# NOT backwards compatible
59 rodolico 172
# changed configuration to be loaded into hash (vs directly loaded into variables)
173
# added UUID to configuration file
62 rodolico 174
#
175
# Version 3.2.1 20180424 RWR
176
# Finally got a semi-stable version of this running. Fixed a bunch of bugs
177
# and appears to be working correctly.
112 rodolico 178
#
179
# Version 3.3.0 20190419 RWR
180
# Converted to use YAML config file
135 rodolico 181
#
182
# Version 3.4.0 20191111 RWR
256 rodolico 183
# adding logging with priority. logging is a hash inside of %configuration which contains the following
258 rodolico 184
# $config{ 'logging' } = {
135 rodolico 185
#    'log type'  => 'string',
186
#    'log level' => #,
187
#    'other params' => something,
188
# };
138 rodolico 189
#
190
# The default log type is cache, which builds an array of all messages passed. When the log type is changed, the cache is
191
# checked for values and, if they exist, they are dumped to the log, then removed.
192
#
135 rodolico 193
# Currently, the only log type is 'file', which has one other additional parameter, 'log path' which
194
# points to the actual log to be created. The log is NOT limited in size, so use something else to
195
# do that.
196
# log level is an integer which is compared the a priority passed to the logging function. The
197
# higher log level is set, the more verbose the log.
198
# 0 - Normal, basically logs when the program starts and ends, and any warnings.
199
# 1 - a little more information about flow
200
# 2 - Gives ending information on structures
201
# 3 - Gives a lot of info about structures when they are initialized and at the end
202
# 4 - Crazy. Dumps just about every structure every time they are changed
203
#
204
# $TESTING has been set to a binary. If true, the report is not sent via the transports, but is dumped to /tmp/sysinfo.testing.yaml
144 rodolico 205
#
206
# Version 3.4.1 20191117 RWR
207
# Added syslog as a possible option for logging.
208
#
175 rodolico 209
# Version 3.5.4 20200317 RWR
174 rodolico 210
# changed so report->version will show the version of sysinfo, not the data version
185 rodolico 211
#
212
# Version 3.5.5 20200317 RWR
213
# bug fix for bsd/opnsense
191 rodolico 214
# 
215
# Version 3.6.0 20220607 RWR
216
# Changed the way upload works. It automatically now passes an additional parameter, upload_type, which with the new upload
217
# script will choose a directory, allowing one script to handle various uploads
218
# additionally, if the key 'postRunScript' is in the config, it is assumed to be the name of a script to be run after sysinfo
219
# completes it's task. This can be used to download/update the program, add new modules, etc..
210 rodolico 220
#
221
# Version 3.6.1 20230308 RWR
222
# fixed a problem where it did not correctly find the source directory when called from a symbolic link
229 rodolico 223
#
224
# Version 3.7.0 20240517 RWR
225
# Added code to put everything from config file except for moduleDirs, transports and scriptDirs into the report. This allows 
226
# users to arbitrarily choose to send additional information by simply adding it to the config file.
233 rodolico 227
#
228
# Version 3.7.1 20240518 RWR
229
# Added code to ensure output is UTF-8
247 rodolico 230
#
231
# Version 3.7.2 20240601 RWR
232
# Added code to undefine &doit if it is already defined, ie if one transport fails and we want to try another one,
233
# we were getting a "can not redefine doit" and the transports would fail. The line added is
234
# undef &doit if exists &doit;
256 rodolico 235
#
236
# Version 4.0.0 20250331 RWR
237
# Adding support for MS Windows
238
# Removed requirement for modules to be executable
191 rodolico 239
 
112 rodolico 240
# find our location and use it for searching for libraries
241
BEGIN {
242
   use FindBin;
243
   use File::Spec;
209 rodolico 244
   use Cwd 'abs_path';
210 rodolico 245
   use File::Basename;
246
   use lib dirname( abs_path( __FILE__ ) );
154 rodolico 247
   eval( 'use YAML::Tiny;' );
248
   eval( 'use Data::Dumper;' );
112 rodolico 249
}
62 rodolico 250
 
154 rodolico 251
# contains the directory our script is in
210 rodolico 252
my $sourceDir = dirname( abs_path( __FILE__ ) );
112 rodolico 253
 
154 rodolico 254
# define the version number
255
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
258 rodolico 256
use version 0.77;
257
our $VERSION = version->declare("v4.0.0");
256 rodolico 258
our $DATA_VERSION = version->declare( 'v3.7.2' ); # used in sending the data file. sets version of XML/YAML data file
154 rodolico 259
 
260
# see https://perldoc.perl.org/Getopt/Long.html
261
use Getopt::Long;
262
# allow -vvn (ie, --verbose --verbose --dryrun)
263
Getopt::Long::Configure ("bundling");
264
 
194 rodolico 265
use sysinfoconf;
266
 
2 rodolico 267
# Following are global variables overridden if configuration file exists
268
 
135 rodolico 269
my $TESTING = 0; # if set to 1, will do everything, but will dump output to /tmp/sysinfo.testing.yaml
9 rodolico 270
 
2 rodolico 271
my $indentLevel = 2; # number of spaces to indent per level in XML or YAML
272
 
135 rodolico 273
my $reportDate = &timeStamp(); # set report date
2 rodolico 274
 
154 rodolico 275
my $periodicOverrideFile = '/tmp/sysinfo.firstrun'; # if this file exists, library.pm will tell all periodic modules to run anyway
276
my $periodic = 0; # if set to 1, will do modules which are only supposed to run weekly, monthly, etc...
277
 
278
my $version;
279
my $help;
258 rodolico 280
my $configFile; # optional path to configuration file specified via command line
281
my $configType; # optional config file type: yaml, json, or datatransport
282
my $outputType; # optional output type: yaml, json, datatransport
154 rodolico 283
 
258 rodolico 284
my $config = {
142 rodolico 285
   'logging' => { 'log type' => 'cache', 'log level' => 0 },    # if set, will point to logging
154 rodolico 286
   'moduleDirs' => ["$sourceDir/modules"], # search paths for modules
287
   'scriptDirs' => ["$sourceDir/scripts"], # search paths for scripts
59 rodolico 288
   'clientName' => '',  # Required!! Must be set in conf file (no defaults)
289
   'serialNumber' => '', # serial number of machine
290
   'UUID'         => '', # UUID of machine
154 rodolico 291
   'transports'   => {'3' => { '-name-' => 'saveLocal', 'sendScript' => 'save_local', 'output directory' => "$sourceDir/reports" }  }, # hash with various transports
61 rodolico 292
   'hostname' => &getHostName() # fully qualified host name of machine
258 rodolico 293
};
2 rodolico 294
 
295
 
13 rodolico 296
 
136 rodolico 297
#######################################################
298
#
2 rodolico 299
# sendResults( $parameters, $message, $scriptDirectory )
300
#
301
# Sends results of run to server using external script. If external
302
# script not defined, just print to STDOUT
303
#
304
# Parameters
305
#  $parameters - a hash containing the information necessary to make the transfer
306
#  $message - the message to be sent
307
#  $scriptDirectory - path (not filename) of script to be executed
308
# 
309
# $parameters contains different key/value pairs depending on the script used
310
#             for example, a stand-alone SMTP script may need a username/password,
311
#             smtp server name, port number, from and to address
312
#             while an http transfer may only need a script name
313
#             See the individual scripts to determine what parameters need to be
314
#             filled in.
315
#             The only required parameter is 'sendScript' which must contain the
316
#             name of the script to execute (and it must be located in $scriptDirectory)
317
# SCRIPT must contain one sub named doit, that accepts three parameters, the hash, 
318
#       the message, and, optionally, the script directory
319
#
320
# If script not defined, just dump to STDOUT. With a properly set up cron job, the output
321
# would then be sent via e-mail to an administrative account, possibly root
322
#
323
#######################################################
324
sub sendResults {
62 rodolico 325
   my ( $globals, $transports, $message, $scriptDirectory ) = @_;
258 rodolico 326
   &logIt( $config,  3, "Entering sendResults" );
113 rodolico 327
   foreach my $key ( sort { $a <=> $b } %$transports ) {
328
      if ( $transports->{$key}->{'sendScript'} ) {
258 rodolico 329
         &logIt( $config,  3, "Trying to find file " . $transports->{$key}->{'sendScript'} . " in " . join( "\n\t", @{$scriptDirectory} ) );
113 rodolico 330
         my $sendScript = &findFile( $transports->{$key}->{'sendScript'}, $scriptDirectory );
19 rodolico 331
         if ( $sendScript ) {
247 rodolico 332
            # check if we have doit defined from previous iteration and, if so, undefine it
333
            undef &doit if exists &doit;
18 rodolico 334
            # load the chosen script into memory
335
            require $sendScript;
19 rodolico 336
            # merge the globals in
337
            while ( my ( $gkey, $value ) = each %$globals ) { 
113 rodolico 338
               $transports->{$key}->{$gkey} = $value; 
19 rodolico 339
            }
20 rodolico 340
            # do variable substitution for any values which need it
113 rodolico 341
            foreach my $thisOne ( keys %{$transports->{$key}} ) {
258 rodolico 342
               &logIt( $config,  4, "$thisOne" );
343
               if ( $transports->{$key}->{$thisOne} =~ m/(\$config\{'hostname'\})|(\$reportDate)|(\$config\{'clientName'\})|(\$config\{'serialNumber'\})/ ) {
113 rodolico 344
                  $transports->{$key}->{$thisOne} = eval "\"$transports->{$key}->{$thisOne}\"";
20 rodolico 345
               }
346
            }
258 rodolico 347
            # add all global keys/values into transport hash
348
            $transports->{$key}->{keys %$globals} = values %$globals;
62 rodolico 349
            #print Dumper( $$transports[$key] );
20 rodolico 350
            #next;
18 rodolico 351
            # execute the "doit" sub from that script
258 rodolico 352
            &logIt( $config,  3, $message );
135 rodolico 353
            my $return = &doit( $transports->{$key}, $message );
354
            return $return if ( $return == 1 );
18 rodolico 355
         } else {
258 rodolico 356
            &logIt( $config,  0,"Could not find " . $$transports[$key]{'sendScript'} . ", trying next transport" );
18 rodolico 357
         } # if..else
358
      } # if
359
   } # foreach
360
   # if we made it here, we have not sent the report, so just return it to the user
85 rodolico 361
   # if called from a cron job, it will (hopefully) be sent to root
258 rodolico 362
   &logIt( $config,  0, 'Error, reached ' . __LINE__ . " which should not happen, message was\n$message" );
85 rodolico 363
   print $message;
16 rodolico 364
   return 1;
2 rodolico 365
}
366
 
367
#######################################################
368
#
369
# getHostName
370
#
371
# return hostname from hostname -f
372
#
373
#######################################################
374
sub getHostName {
258 rodolico 375
   &logIt( $config,  3, "Entering getHostName" );
252 rodolico 376
   my $hostname = lc $^O eq 'mswin32' ? `hostname` : `hostname -f`;
2 rodolico 377
   chomp $hostname;
378
   return $hostname;
379
}
380
 
381
#######################################################
382
#
258 rodolico 383
# validatePermission ( $file )
2 rodolico 384
#
258 rodolico 385
# Checks that file is owned by root, and has permission
386
# 0700 or less
387
# 
388
# Returns empty string on success, error message
389
# on failure
2 rodolico 390
#
391
#######################################################
392
 
258 rodolico 393
sub validatePermission {
394
   my $file = shift;
395
   # on Windows system, we can not look for ownership and exect status
396
   return '' if lc $^O eq 'mswin32'; 
397
   &logIt( $config,  3, "Entering validatePermission with $file" );
398
   return "$file - Not a file" unless -f $file;
399
   # in test mode, do not check permissions
400
   return '' if $TESTING;
401
   my $return;
402
   # must be owned by root
403
   my $owner = (stat($file))[4];
404
   # print "\tOwner = $owner\n";
405
   $return .= " - Bad Owner [$owner]" if $owner;
406
   # must not have any permissions for group or world
407
   # ie, 0700 or less
408
   my $mode = sprintf( '%04o', (stat($file))[2] & 07777 );
409
   # print "\tMode = $mode\n";
410
   $return .= " - Bad Permission [$mode]" unless $mode =~ m/0.00/;
411
   &logIt( $config, 4, "validatePermission: $file permissions check result: " . ($return ? $return : "OK") );
412
   return $return ? $file . $return : '';
413
}
2 rodolico 414
 
415
#######################################################
416
#
417
# tabDelimitedToHash ($hashRef, $tabdelim)
418
#
419
# Takes a tab delimited multi line string and adds it
420
# to a hash. The final field in each line is considered to
421
# be the value, and all prior fields are considered to be
422
# hierachial keys.
423
#
424
# Parameters
425
#     $hashref - reference (address) of a hash
426
#     $tabdelim - A tab delimited, newline terminated set of records
427
#
428
#
429
#######################################################
430
sub tabDelimitedToHash {
431
   my ($hashRef, $tabdelim) = @_;
258 rodolico 432
   &logIt( $config, 3, "Entering tabDelimitedToHash" );
233 rodolico 433
 
434
   utf8::encode( $tabdelim ); # ensure this is all utf8, convert if necessary
435
 
2 rodolico 436
   foreach my $line ( split( "\n", $tabdelim ) ) { # split on newlines, then process each line in turn
437
      $line =~ s/'/\\'/gi; # escape single quotes
28 rodolico 438
      my @fields = split( / *\t */, $line ); # get all the field values into array
2 rodolico 439
      my $theValue = pop @fields; # the last one is the value, so save it
440
      # now, we build a Perl statement that would create the assignment. The goal is
441
      # to have a string that says something like $$hashRef{'key'}{'key'} = $value;
442
      # then, eval that.
443
      my $command = '$$hashRef'; # start with the name of the dereferenced hash (parameter 1)
444
      while (my $key = shift @fields) { # while we have a key, from left to right
445
         $command .= '{' . "'$key'" . '}'; # build it as {'key'} concated to string
446
      }
447
      $command .= "='$theValue';"; # add the assignment
448
      #print STDERR "$command\n"; 
449
      eval $command; # eval the string to make the actual assignment
450
   }
451
}
452
 
258 rodolico 453
 
454
 
2 rodolico 455
#######################################################
456
#
258 rodolico 457
# getModulesFromDir( $moduleDir )
13 rodolico 458
#
258 rodolico 459
# Retrieves a list of valid module files from the specified
460
# module directory
13 rodolico 461
#
258 rodolico 462
# Parameters
463
#     $moduleDir - full path to directory containing modules
464
#
465
# Returns
466
#     Reference to an array of valid module file paths
467
#
468
# Module files must:
469
#   - Have names consisting only of alphanumerics and underscores
470
#   - Begin with an alphanumeric character
471
#   - Pass permission validation (owned by root, 0700 or less)
472
#
13 rodolico 473
#######################################################
256 rodolico 474
sub getModulesFromDir {
475
   my $moduleDir = shift;
476
   $moduleDir .= '/' unless substr( $moduleDir, -1 ) eq '/';
477
   # open the module directory
478
   return unless -d $moduleDir;
479
   opendir( my $dh, $moduleDir ) || die "Module Directory $moduleDir can not be opened: $!\n";
480
   # and get all files which are nothing but alpha-numerics and underscores (must begin with alpha-numeric)
481
   # ignore anything else, including directories
482
   # then, prepend the directory name (map), then validate they are ready to be used (validatePermissions)
483
   my @modules = grep{ ! &validatePermission( $_ ) } map { "$moduleDir$_" } grep { /^[a-zA-Z0-9][a-zA-Z0-9_]*$/ } readdir( $dh );
484
   closedir $dh;
258 rodolico 485
   &logIt( $config, 4, "getModulesFromDir: Modules found in $moduleDir: " . join(", ", @modules) );
256 rodolico 486
   return \@modules;
487
 
488
}
489
 
13 rodolico 490
#######################################################
491
#
2 rodolico 492
# ProcessModules ( $system, $moduleDir )
493
#
494
# Processes all modules in $moduleDir, adding result to $system hash
495
# 
496
# Parameters
497
#     $system - reference (address) of a hash
498
#     $moduleDir - full path to a directory containing executable scripts
499
#  
500
# Each file in the $moduleDir directory that matches the regex in the grep
501
# and is executable is run. It is assumed the script will return 0 on success
502
# or a non-zero on failure
503
# The output of the script is assumed to be a tab delimited, newline separated
504
# list of records that should be added to the hash $system. This is done by calling 
505
# &parseModule above.
506
# on failure, the returned output of the script is assumed to be an error message
507
# and is displayed on STDERR
508
#######################################################
509
sub ProcessModules {
510
   my ( $system, $moduleDir ) = @_;
258 rodolico 511
   &logIt( $config,  3, "Entering processModules using $moduleDir" );
256 rodolico 512
   foreach my $modFile ( sort @{ &getModulesFromDir( $moduleDir ) } ) { # for each valid script
513
      next unless -f $modFile; # skip any directories, symbolic links, etc...
514
      my $output = do $modFile;
258 rodolico 515
      &logIt( $config, 4, "Output of module $modFile is\n$output\n" );
256 rodolico 516
      if ( defined $output && $output ) {
517
         if ( substr( $output, 1,6) eq 'error:' ) { # we have an error
258 rodolico 518
            &logIt( $config,  1, $output );
256 rodolico 519
         } else {
520
            &tabDelimitedToHash( $system, $output );
521
         }
522
      } else {
523
         print "Script $modFile failed to compile with error\n$@\n";
13 rodolico 524
      }
258 rodolico 525
      &logIt( $config,  3, "Processing module $moduleDir$modFile");
21 rodolico 526
   } # foreach
527
   # add sysinfo-client (me) to the software list, since we're obviously installed
528
   &tabDelimitedToHash( $system, "software\tsysinfo-client\tversion\t$main::VERSION\n" );
2 rodolico 529
}
530
 
258 rodolico 531
#######################################################
532
# initReport( $config )
533
#
534
# Initialize the report structure with configuration data
535
#
536
# Parameters
537
#     $config - reference to configuration hash
538
#
539
# Returns
540
#     Reference to initialized report hash
541
#
542
# Creates the initial report structure with:
543
#   - Report metadata (version, date, client)
544
#   - System information (hostname, serial, UUID)
545
#   - Additional configuration values (excluding moduleDirs,
546
#     transports, scriptDirs, and logging)
547
#
548
#######################################################
229 rodolico 549
sub initReport {
550
   my $config = shift;
551
   my %ignore = ( # list of config keys to ignore. Using hash for fast lookup
552
         'moduleDirs' => 1,
553
         'transports' => 1,
554
         'scriptDirs' => 1,
555
         'logging'    => 1
556
      );
557
   my $report;
558
   # some presets for compatibility
559
   $report->{'report'}->{'version'} = $VERSION->normal; # global value
560
   $report->{'report'}->{'date'} = $reportDate; # global value
561
   $report->{'report'}->{'client'} = $config->{'clientName'};
562
   $report->{'system'}->{'hostname'} = $config->{'hostname'};
563
   $report->{'system'}->{'serial'} = $config->{'serialNumber'};
564
   $report->{'system'}->{'UUID'} = $config->{'UUID'};
565
   foreach my $key ( keys %$config ) {
566
      next if defined( $ignore{$key} ); # ignore anything in our ignore hash
567
      $report->{'system'}->{$key} = $config->{$key}; # simply copy to the system part of the report
568
   }
569
   return $report;
570
}
571
 
258 rodolico 572
#######################################################
573
#
574
# detectConfigType( $filename )
575
#
576
# Detects the configuration file type by examining content
577
# json and DataTransport have unique starting lines
578
# yaml will usually start with --- or key: value, but may be
579
# comments in the first few lines, so checks up to 5 lines
580
#
581
# Parameters:
582
#   $lines - array reference containing lines of configuration content
583
#
584
# Returns:
585
#   'yaml', 'json', 'datatransport', or undef
586
#
587
#######################################################
588
sub detectConfigType {
589
   my $lines = shift;
590
   # first look at first line for unique identifiers. First line comments
591
   # may contain type info
592
   if ( $lines->[0] =~ m/^.*(yaml|datatransport).*/i ) {
593
      &logIt( $config,  3, "Configuration file type hint found in first line comment" );
594
      return lc $1;
595
   }
596
   # look through first five lines, there should be some indicator
597
   for ( my $line = 0; $line < 5 && $lines->[$line]; $line++ ) {
598
      return 'yaml' if $lines->[$line] =~ /^#.*YAML/; 
599
      return 'yaml' if $lines->[$line] =~ /^---/;  # YAML document start
600
      return 'yaml' if $lines->[$line] =~ /^\s*\w+:\s*/;  # YAML key: value
601
      return 'json' if $lines->[$line] =~ /^#.*JSON/;  # skip comments
602
      return 'json' if $lines->[$line] =~ /^\s*[{\[]/;  # JSON starts with { or [
603
   }
604
   return undef;
605
}
606
 
607
#######################################################
608
#
609
# loadConfig( $confStr, $type )
610
#
611
# Load configuration file based on specified or detected type
612
# adds 'dataType' key to returned hashref indicating type used
613
#
614
# Parameters:
615
#   $confStr - configuration content as an arrayref of string
616
#   $type     - optional type: 'yaml', 'json', 'datatransport'
617
#
618
# Returns:
619
#   hashref of configuration
620
#
621
#######################################################
622
sub loadConfig {
623
   my ($confStr, $type) = @_;
624
 
625
   my $return = {};
626
 
627
   # Auto-detect if type not specified
628
   $type = &detectConfigType($confStr) unless $type;
629
 
630
   die "Cannot determine configuration file type\n" unless $type;
631
 
632
   $type = lc($type);  # normalize to lowercase
633
   $confStr = join("\n", @$confStr); # join arrayref into single string for processing
634
 
635
   if ($type eq 'datatransport') {
636
      # Try to load DataTransport module
637
      eval { require DataTransport; };
638
      if ($@) {
639
         die "DataTransport module not available: $@\n";
640
      }
641
 
642
      my $dt = DataTransport->new();
643
      $return = $dt->decode($confStr);
644
      die "Failed to read DataTransport content\n" unless $return;
645
   } elsif ($type eq 'json') {
646
      # Try to load JSON module
647
      eval { require JSON; };
648
      if ($@) {
649
         die "JSON module not available: $@\n";
650
      }
651
      $return = JSON::decode_json($confStr);
652
   } elsif ($type eq 'yaml') {
653
      # Use YAML::Tiny directly for single file loading
654
      eval { require YAML::Tiny; };
655
      if ($@) {
656
         die "YAML::Tiny module not available: $@\n";
657
      }
658
 
659
      $return = YAML::Tiny->read_string($confStr);
660
      die "Failed to read YAML content\n" unless $return;
661
      $return = $return->[0]; # only return the first document
662
   } else {
663
      die "Unknown configuration type: $type\n";
664
   }
665
   $return->{'dataType'} = $type; # store the type used
666
   return $return;
667
}
668
 
669
########################################################
670
# hashToReportString( $report, $outputType )
671
#
672
# Converts report hash to specified output format string
673
## Parameters:
674
#   $report - reference to report hash
675
#   $outputType - output format: 'yaml', 'json', 'datatransport'
676
# Returns:
677
#   string containing report in specified format
678
########################################################
679
sub hashToReportString {
680
   my ( $report, $outputType ) = @_;
681
   &logIt( $config,  3, "Entering hashToReportString with outputType $outputType" );
682
   my $output;
683
   if ( $outputType eq 'yaml' ) {
684
      eval { require YAML::Tiny; };
685
      if ($@) {
686
         die "YAML::Tiny module not available: $@\n";
687
      }
688
      $output = YAML::Tiny->new( $report )->write_string();
689
   } elsif ( $outputType eq 'json' ) {
690
      eval { require JSON; };
691
      if ($@) {
692
         die "JSON module not available: $@\n";
693
      }
694
      $output = JSON::encode_json( $report );
695
   } elsif ( $outputType eq 'datatransport' ) {
696
      eval { require DataTransport; };
697
      if ($@) {
698
         die "DataTransport module not available: $@\n";
699
      }
700
      my $dt = DataTransport->new();
701
      $output = $dt->encode( $report );
702
   } else {
703
      die "Unknown output type: $outputType\n";
704
   }
705
   return $output;
706
}
707
 
708
#######################################################
709
#
710
# help()
711
#
712
# Display help message showing command-line options
713
#
714
# Parameters
715
#     None
716
#
717
# Returns
718
#     None (prints to STDOUT)
719
#
720
# Shows program version and available command-line options:
721
#   -f, --config       : Specify configuration file path
722
#   --configtype       : Specify config format (yaml/json/datatransport)
723
#   --version          : Display version
724
#   --help             : Display help message
725
#   -p, --periodic     : Run periodic modules
726
#   -t, --test         : Test mode (output to /tmp)
727
#
728
# Note: For interactive configuration, use sysinfo-client-interactive
729
#
730
#######################################################
154 rodolico 731
sub help {
732
   use File::Basename;
256 rodolico 733
   print basename($0) . " $VERSION (data $DATA_VERSION)\n";
154 rodolico 734
   print <<END
735
$0 [options]
258 rodolico 736
 
737
For interactive configuration, use sysinfo-client-interactive instead.
738
 
154 rodolico 739
Options:
740
   --version        - display version and exit
258 rodolico 741
   --help           - display this help message
742
   -f,
743
   --config=/path/  - path to configuration file (overrides default search)
744
   --configtype=xxx - config file type: yaml, json, or datatransport (autodetects if not specified)
154 rodolico 745
   -p,
746
   --periodic       - runs modules designed to be run only weekly, monthly, etc...
256 rodolico 747
   -t,
258 rodolico 748
   --test           - If set, runs but places output in /tmp
154 rodolico 749
END
750
}
20 rodolico 751
 
154 rodolico 752
 
258 rodolico 753
# Process command line options
154 rodolico 754
GetOptions (
755
            'periodic|p'    => \$periodic,    # will do modules which are marked as periodic
258 rodolico 756
            'config|f=s'    => \$configFile,  # path to configuration file
757
            'configtype=s'  => \$configType,  # config file type: yaml, json, datatransport
758
            'outputType|o=s' => \$outputType,  # output type: xml, yaml
759
            'help'          => \$help,
154 rodolico 760
            'version'       => \$version,
258 rodolico 761
            'test|t'      => \$TESTING
154 rodolico 762
            ) or die "Error parsing command line\n";
763
 
764
if ( $help ) { &help() ; exit; }
256 rodolico 765
if ( $version ) { use File::Basename; print basename($0) . " $VERSION (data $DATA_VERSION)\n"; exit; }
154 rodolico 766
 
258 rodolico 767
# if periodic flag set, create the override file
154 rodolico 768
`touch $periodicOverrideFile` if $periodic; # tells periodic modules to run
769
 
258 rodolico 770
$configFile //= 'sysinfo-client.yaml'; # default configuration file name
771
# load the configuration file. Note, loadConfigurationFile returns an arrayref of lines
772
# then loadConfig processes that based on type
773
$config = loadConfig( loadConfigurationFile( $configFile ), $configType); 
774
 
2 rodolico 775
# oops, no client name (required) so tell them and exit
258 rodolico 776
die "No client name defined in $configFile" unless $config->{'clientName'};
2 rodolico 777
 
258 rodolico 778
# clean up some missing values
779
# output type is either what user passed on command line, or what is in config file, or defaults to yaml
780
# the config->{'dataType'} is set by loadConfig, so we can use that as last resort
781
$config->{'outputType'} //= $outputType //= $config->{'dataType'} //= 'yaml'; # default to yaml output
782
# user did not define a serial number, so make something up. Just take an md5sum of hostname-clientname
783
$config->{'serialNumber'} //= $config->{UUID} ? $config->{UUID} : `echo \`$config->{'hostname'}\`-$config->{'clientName'} | md5sum | awk '{print \$1}'`;
784
chomp $config->{'serialNumber'};
135 rodolico 785
 
258 rodolico 786
&logIt( $config,  0, 'Starting sysinfo Run' );
787
&logIt( $config,  3, "Configuration is\n" . Data::Dumper->Dump( [$config], [ qw($config) ] ) );
61 rodolico 788
 
258 rodolico 789
$TESTING //= $config->{'TESTING'} //= 0; # default to 0 if not defined
790
&logIt( $config,  0, "Testing => $TESTING" ) if $TESTING;
76 rodolico 791
 
229 rodolico 792
# hash reference that will store all info we are going to send to the server
258 rodolico 793
# initialize it with some basic info from configuration file
794
my $report = &initReport( $config );
795
&logIt( $config,  3, "Initial System\n" . Data::Dumper->Dump( [$report], [qw( $report )] ) );
76 rodolico 796
 
2 rodolico 797
# process any modules in the system
258 rodolico 798
foreach my $moduleDir ( @{$config->{'moduleDirs'}} ) {
799
   &logIt( $config,  3, "Processing modules from $moduleDir" );
800
   &ProcessModules( $report, "$moduleDir/" );
13 rodolico 801
}
258 rodolico 802
&logIt( $config,  4, "After processing modules\n" . Data::Dumper->Dump( [$report], [qw( $report )] ) );
2 rodolico 803
 
258 rodolico 804
# convert the report hash to the desired output type
805
my $out = hashToReportString( $report, $config->{'outputType'} ); # test if we can convert to output type
806
# add comment to top of file except for json
807
$out = "# sysinfo " . $VERSION->normal . " (data " . $DATA_VERSION->normal . ") $config->{outputType}\n" . $out unless $config->{'outputType'} eq 'json'; 
808
&logIt( $config,  4, 'At line number ' . __LINE__ . "\n" . $out );
135 rodolico 809
 
258 rodolico 810
# actually writing the report
135 rodolico 811
if ( $TESTING ) {
258 rodolico 812
   &logIt( $config, 0, "Sending report to sysinfo.testing.yaml" );
135 rodolico 813
   open DATA, ">/tmp/sysinfo.testing.yaml" or die "Could not write to /tmp/sysinfo.testing.yaml: $!\n";
814
   print DATA $out;
815
   close DATA;
816
} else {
258 rodolico 817
   # load some global values for use in the script, if required
818
   my $globals = { 
819
         'upload_type'  => 'sysinfo',
820
         'data version' => $DATA_VERSION->normal,
821
         'report date'  => $reportDate,
822
         'client name'  => $config->{'clientName'},
823
         'host name'    => $config->{'hostname'},
824
         'serial number'=> $config->{'serialNumber'},
825
         'UUID'         => $config->{'UUID'},
826
         'fileType'     => $config->{'outputType'}
827
         };
828
 
829
   &logIt( $config,  4, "Globals initialized\n" . Data::Dumper->Dump([$globals],[qw($globals)]) );
830
   &logIt( $config, 0, "Sending report to remote transport" );
831
   if ( my $success = &sendResults( $globals, $config->{'transports'}, $out, $config->{'scriptDirs'} ) != 1 ) {
832
      &logIt( $config,  0, "Error $success while sending report from $config->{'hostname'}" );
135 rodolico 833
   }
16 rodolico 834
}
2 rodolico 835
 
154 rodolico 836
unlink ( $periodicOverrideFile ) if -e $periodicOverrideFile;
258 rodolico 837
&logIt( $config,  0, 'Ending sysinfo Run' );
135 rodolico 838
 
258 rodolico 839
if ( $config->{'postRunScript'}{'script name'} ) {
840
   my $script = $sourceDir . '/' . $config->{'postRunScript'}{'script name'};
841
   exec ( "$script $configFile" ) if -x $script;
197 rodolico 842
}
154 rodolico 843
 
9 rodolico 844
1;