#! /usr/bin/env perl # Copyright (c) 2025, R. W. Rodolico # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use warnings; use strict; use open ':std', ':encoding(utf8)' ; # force all I/O, including disk and STD?, to use utf-8 use utf8; # Source code encoded using UTF-8, used to ensure output from modules is UTF-8, see tabDelimitedToHash # sysinfo # Author: R. W. Rodolico # Primary client portion of sysinfo system. Will collect information about its current # host and create a report containing the information. This report can then be processed # by process_sysinfo.pl on the collection computer. # output file consists of a YAML file of the form: # # # ext3 # /home # 51606140 # 331472 # # #
192.168.1.3
# fe80::216:3eff:fefb:4e10 # 64 # 00:16:3e:fb:4e:10 # 1500 # 255.255.255.0 #
# # squeeze # Debian GNU/Linux 6.0.4 (squeeze) # Debian # 2.6.32-5-xen-686 # Linux # 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 # 6.0.4 # # # RAM memory # MCP55 Memory Controller # a2 # Device cb84 # 0000:00:00.0 # nVidia Corporation # nVidia Corporation # # # Staffmasters # 2012-05-01 03:00 # 2.0.0 # # # terminal-based package manager (terminal interface only) # 0.6.3-3.2+squeeze1 # # # 1800.103 # i686 # GenuineIntel # backup.staffmasters.local # 1333259809 # 520852 # 1 # #
# # Version 1.3 20071104 # added capability of e-mailing the results by itself and external configuration file # Version 1.3.1 20071110 # added du -sk to explicitly do directory sizes in 'k'. Also, fixed some documentation # Version 1.3.3 20081104 # modified hostname to hostname -f, and allowed user to place custom value in configuration file # also, modified to go with Debian standards in preparation to creating a debian package. # Version 2.0 20081208 # Modified to use different libraries for different OS's in preparation to porting to Windows # Uses different packages based on which OS it is on. # Version 3.0 20120923 # Major revision. Most internal intelligence pulled out and put into modules and data transfer format has been changed to YAML # # Base system only pulls client name, machine name and machine number, all of which can be set in the configuration file # if the value is not set, it attempts various means to determine the values and, if it fails, aborts with an error message # client name -- REQUIRED, must come from configuration file # machine name -- REQUIRED, if not set via conf file, attempts hostname command (hostname -f) or module getHostName # machine number -- REQUIRED, if not set via conf file, attempts "echo `hostname -f`-clientname | md5sum" or module getSerial # modules are stored in "configuration directory/modules" (/etc/sysinfo/modules on most Linux systems) and are processed in # standard sort order (case sensitive). # Module filenames may contain alpha-numeric, underscore and the period only (files containing other characters are ignored). # Modules should set their exit code to 0 for success, and non-zero for failure # Modules should return 0 or more tab delimited, newline terminated strings, processed as one record per line # A module return string line is processed as follows: # category \t [category \t ...] \t key \t value # example: # System \t num_cpu \t 1 # System \t Users \t root \t /root/ # (note, if non-zero exit code returned, return value is assumed to be error message and is printed to STDERR) # sysinfo stores the result in a hash, using categories as the keys (case sensitive), thus, the above results in # $store{'System'}{'num_cpu'} = '1'; # $store{'System'}{'Users'}{'root'} = '/root'; # upon completion, sysinfo converts the $store hash into an XML or YAML string for transfer # It then sends it to the main server as defined in the conf file. # NOTE: YAML is hand crafted to kill any requirements for external libraries # see sub hashToYAML for details # Version 3.0.1 20160321 # Renamed to sysinfo-client to not conflict with Linux package sysinfo # created installer in Perl to not rely on package managers # default path for configuration file changed to /etc/camp/sysinfo-client.conf # $VERSION changed to $DATA_VERSION to not conflict with $main::VERSION (script version vs data format version) # # Version 3.1.0 20160401 # module and script dirs now arrays to be searched. Idea is that default # modules/scripts are in installdir/modules or installdir/scripts, and # user supplied are in /etc/scripts and /etc/modules # Tightened up the file systems checks, requiring all scripts and modules # be set 0700 at least, and owned by root # Transport layers now an array, and if one fails to send the report, the others # are tried in turn # Worked on logic for sendReport to give better error checking. # Doing a search for the configuration file matching cwd, then /etc/camp, then /usr/local/etc/camp # Self documenting, ie a key for software\tsysinfo-client\version\current version is inserted # # Version 3.1.1 20160915 RWR # set use strict and use warnings, then fixed errors # # Version 3.1.2 20160922 RWR # $exitCode 1 (not applicable to this machine) does not throw warning # # Version 3.1.3 20161010 RWR # Removed extra use warnings # # Version 3.1.4 20161023 RWR # Would error out if moduledir does not exist, added a return # # Version 3.1.5 20170327 RWR # On freeBSD systems, was looking in wrong place for configuration file # # Version 3.2.0 20180320 RWR # Major change in the configuration file format; All entries are loaded into # hash %configuration, so clientname is no longer $clientname, but is now # $config{'clientname'} # NOT backwards compatible # changed configuration to be loaded into hash (vs directly loaded into variables) # added UUID to configuration file # # Version 3.2.1 20180424 RWR # Finally got a semi-stable version of this running. Fixed a bunch of bugs # and appears to be working correctly. # # Version 3.3.0 20190419 RWR # Converted to use YAML config file # # Version 3.4.0 20191111 RWR # adding logging with priority. logging is a hash inside of %configuration which contains the following # $config{ 'logging' } = { # 'log type' => 'string', # 'log level' => #, # 'other params' => something, # }; # # The default log type is cache, which builds an array of all messages passed. When the log type is changed, the cache is # checked for values and, if they exist, they are dumped to the log, then removed. # # Currently, the only log type is 'file', which has one other additional parameter, 'log path' which # points to the actual log to be created. The log is NOT limited in size, so use something else to # do that. # log level is an integer which is compared the a priority passed to the logging function. The # higher log level is set, the more verbose the log. # 0 - Normal, basically logs when the program starts and ends, and any warnings. # 1 - a little more information about flow # 2 - Gives ending information on structures # 3 - Gives a lot of info about structures when they are initialized and at the end # 4 - Crazy. Dumps just about every structure every time they are changed # # $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 # # Version 3.4.1 20191117 RWR # Added syslog as a possible option for logging. # # Version 3.5.4 20200317 RWR # changed so report->version will show the version of sysinfo, not the data version # # Version 3.5.5 20200317 RWR # bug fix for bsd/opnsense # # Version 3.6.0 20220607 RWR # Changed the way upload works. It automatically now passes an additional parameter, upload_type, which with the new upload # script will choose a directory, allowing one script to handle various uploads # additionally, if the key 'postRunScript' is in the config, it is assumed to be the name of a script to be run after sysinfo # completes it's task. This can be used to download/update the program, add new modules, etc.. # # Version 3.6.1 20230308 RWR # fixed a problem where it did not correctly find the source directory when called from a symbolic link # # Version 3.7.0 20240517 RWR # Added code to put everything from config file except for moduleDirs, transports and scriptDirs into the report. This allows # users to arbitrarily choose to send additional information by simply adding it to the config file. # # Version 3.7.1 20240518 RWR # Added code to ensure output is UTF-8 # # Version 3.7.2 20240601 RWR # Added code to undefine &doit if it is already defined, ie if one transport fails and we want to try another one, # we were getting a "can not redefine doit" and the transports would fail. The line added is # undef &doit if exists &doit; # # Version 4.0.0 20250331 RWR # Adding support for MS Windows # Removed requirement for modules to be executable # find our location and use it for searching for libraries BEGIN { use FindBin; use File::Spec; use Cwd 'abs_path'; use File::Basename; use lib dirname( abs_path( __FILE__ ) ); eval( 'use YAML::Tiny;' ); eval( 'use Data::Dumper;' ); } # contains the directory our script is in my $sourceDir = dirname( abs_path( __FILE__ ) ); # define the version number # see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod use version 0.77; our $VERSION = version->declare("v4.0.0"); our $DATA_VERSION = version->declare( 'v3.7.2' ); # used in sending the data file. sets version of XML/YAML data file # see https://perldoc.perl.org/Getopt/Long.html use Getopt::Long; # allow -vvn (ie, --verbose --verbose --dryrun) Getopt::Long::Configure ("bundling"); use sysinfoconf; # Following are global variables overridden if configuration file exists my $TESTING = 0; # if set to 1, will do everything, but will dump output to /tmp/sysinfo.testing.yaml my $indentLevel = 2; # number of spaces to indent per level in XML or YAML my $reportDate = &timeStamp(); # set report date my $periodicOverrideFile = '/tmp/sysinfo.firstrun'; # if this file exists, library.pm will tell all periodic modules to run anyway my $periodic = 0; # if set to 1, will do modules which are only supposed to run weekly, monthly, etc... my $version; my $help; my $configFile; # optional path to configuration file specified via command line my $configType; # optional config file type: yaml, json, or datatransport my $outputType; # optional output type: yaml, json, datatransport my $config = { 'logging' => { 'log type' => 'cache', 'log level' => 0 }, # if set, will point to logging 'moduleDirs' => ["$sourceDir/modules"], # search paths for modules 'scriptDirs' => ["$sourceDir/scripts"], # search paths for scripts 'clientName' => '', # Required!! Must be set in conf file (no defaults) 'serialNumber' => '', # serial number of machine 'UUID' => '', # UUID of machine 'transports' => {'3' => { '-name-' => 'saveLocal', 'sendScript' => 'save_local', 'output directory' => "$sourceDir/reports" } }, # hash with various transports 'hostname' => &getHostName() # fully qualified host name of machine }; ####################################################### # # sendResults( $parameters, $message, $scriptDirectory ) # # Sends results of run to server using external script. If external # script not defined, just print to STDOUT # # Parameters # $parameters - a hash containing the information necessary to make the transfer # $message - the message to be sent # $scriptDirectory - path (not filename) of script to be executed # # $parameters contains different key/value pairs depending on the script used # for example, a stand-alone SMTP script may need a username/password, # smtp server name, port number, from and to address # while an http transfer may only need a script name # See the individual scripts to determine what parameters need to be # filled in. # The only required parameter is 'sendScript' which must contain the # name of the script to execute (and it must be located in $scriptDirectory) # SCRIPT must contain one sub named doit, that accepts three parameters, the hash, # the message, and, optionally, the script directory # # If script not defined, just dump to STDOUT. With a properly set up cron job, the output # would then be sent via e-mail to an administrative account, possibly root # ####################################################### sub sendResults { my ( $globals, $transports, $message, $scriptDirectory ) = @_; &logIt( $config, 3, "Entering sendResults" ); foreach my $key ( sort { $a <=> $b } %$transports ) { if ( $transports->{$key}->{'sendScript'} ) { &logIt( $config, 3, "Trying to find file " . $transports->{$key}->{'sendScript'} . " in " . join( "\n\t", @{$scriptDirectory} ) ); my $sendScript = &findFile( $transports->{$key}->{'sendScript'}, $scriptDirectory ); if ( $sendScript ) { # check if we have doit defined from previous iteration and, if so, undefine it undef &doit if exists &doit; # load the chosen script into memory require $sendScript; # merge the globals in while ( my ( $gkey, $value ) = each %$globals ) { $transports->{$key}->{$gkey} = $value; } # do variable substitution for any values which need it foreach my $thisOne ( keys %{$transports->{$key}} ) { &logIt( $config, 4, "$thisOne" ); if ( $transports->{$key}->{$thisOne} =~ m/(\$config\{'hostname'\})|(\$reportDate)|(\$config\{'clientName'\})|(\$config\{'serialNumber'\})/ ) { $transports->{$key}->{$thisOne} = eval "\"$transports->{$key}->{$thisOne}\""; } } # add all global keys/values into transport hash $transports->{$key}->{keys %$globals} = values %$globals; #print Dumper( $$transports[$key] ); #next; # execute the "doit" sub from that script &logIt( $config, 3, $message ); my $return = &doit( $transports->{$key}, $message ); return $return if ( $return == 1 ); } else { &logIt( $config, 0,"Could not find " . $$transports[$key]{'sendScript'} . ", trying next transport" ); } # if..else } # if } # foreach # if we made it here, we have not sent the report, so just return it to the user # if called from a cron job, it will (hopefully) be sent to root &logIt( $config, 0, 'Error, reached ' . __LINE__ . " which should not happen, message was\n$message" ); print $message; return 1; } ####################################################### # # getHostName # # return hostname from hostname -f # ####################################################### sub getHostName { &logIt( $config, 3, "Entering getHostName" ); my $hostname = lc $^O eq 'mswin32' ? `hostname` : `hostname -f`; chomp $hostname; return $hostname; } ####################################################### # # validatePermission ( $file ) # # Checks that file is owned by root, and has permission # 0700 or less # # Returns empty string on success, error message # on failure # ####################################################### sub validatePermission { my $file = shift; # on Windows system, we can not look for ownership and exect status return '' if lc $^O eq 'mswin32'; &logIt( $config, 3, "Entering validatePermission with $file" ); return "$file - Not a file" unless -f $file; # in test mode, do not check permissions return '' if $TESTING; my $return; # must be owned by root my $owner = (stat($file))[4]; # print "\tOwner = $owner\n"; $return .= " - Bad Owner [$owner]" if $owner; # must not have any permissions for group or world # ie, 0700 or less my $mode = sprintf( '%04o', (stat($file))[2] & 07777 ); # print "\tMode = $mode\n"; $return .= " - Bad Permission [$mode]" unless $mode =~ m/0.00/; &logIt( $config, 4, "validatePermission: $file permissions check result: " . ($return ? $return : "OK") ); return $return ? $file . $return : ''; } ####################################################### # # tabDelimitedToHash ($hashRef, $tabdelim) # # Takes a tab delimited multi line string and adds it # to a hash. The final field in each line is considered to # be the value, and all prior fields are considered to be # hierachial keys. # # Parameters # $hashref - reference (address) of a hash # $tabdelim - A tab delimited, newline terminated set of records # # ####################################################### sub tabDelimitedToHash { my ($hashRef, $tabdelim) = @_; &logIt( $config, 3, "Entering tabDelimitedToHash" ); utf8::encode( $tabdelim ); # ensure this is all utf8, convert if necessary foreach my $line ( split( "\n", $tabdelim ) ) { # split on newlines, then process each line in turn $line =~ s/'/\\'/gi; # escape single quotes my @fields = split( / *\t */, $line ); # get all the field values into array my $theValue = pop @fields; # the last one is the value, so save it # now, we build a Perl statement that would create the assignment. The goal is # to have a string that says something like $$hashRef{'key'}{'key'} = $value; # then, eval that. my $command = '$$hashRef'; # start with the name of the dereferenced hash (parameter 1) while (my $key = shift @fields) { # while we have a key, from left to right $command .= '{' . "'$key'" . '}'; # build it as {'key'} concated to string } $command .= "='$theValue';"; # add the assignment #print STDERR "$command\n"; eval $command; # eval the string to make the actual assignment } } ####################################################### # # getModulesFromDir( $moduleDir ) # # Retrieves a list of valid module files from the specified # module directory # # Parameters # $moduleDir - full path to directory containing modules # # Returns # Reference to an array of valid module file paths # # Module files must: # - Have names consisting only of alphanumerics and underscores # - Begin with an alphanumeric character # - Pass permission validation (owned by root, 0700 or less) # ####################################################### sub getModulesFromDir { my $moduleDir = shift; $moduleDir .= '/' unless substr( $moduleDir, -1 ) eq '/'; # open the module directory return unless -d $moduleDir; opendir( my $dh, $moduleDir ) || die "Module Directory $moduleDir can not be opened: $!\n"; # and get all files which are nothing but alpha-numerics and underscores (must begin with alpha-numeric) # ignore anything else, including directories # then, prepend the directory name (map), then validate they are ready to be used (validatePermissions) my @modules = grep{ ! &validatePermission( $_ ) } map { "$moduleDir$_" } grep { /^[a-zA-Z0-9][a-zA-Z0-9_]*$/ } readdir( $dh ); closedir $dh; &logIt( $config, 4, "getModulesFromDir: Modules found in $moduleDir: " . join(", ", @modules) ); return \@modules; } ####################################################### # # ProcessModules ( $system, $moduleDir ) # # Processes all modules in $moduleDir, adding result to $system hash # # Parameters # $system - reference (address) of a hash # $moduleDir - full path to a directory containing executable scripts # # Each file in the $moduleDir directory that matches the regex in the grep # and is executable is run. It is assumed the script will return 0 on success # or a non-zero on failure # The output of the script is assumed to be a tab delimited, newline separated # list of records that should be added to the hash $system. This is done by calling # &parseModule above. # on failure, the returned output of the script is assumed to be an error message # and is displayed on STDERR ####################################################### sub ProcessModules { my ( $system, $moduleDir ) = @_; &logIt( $config, 3, "Entering processModules using $moduleDir" ); foreach my $modFile ( sort @{ &getModulesFromDir( $moduleDir ) } ) { # for each valid script next unless -f $modFile; # skip any directories, symbolic links, etc... my $output = do $modFile; &logIt( $config, 4, "Output of module $modFile is\n$output\n" ); if ( defined $output && $output ) { if ( substr( $output, 1,6) eq 'error:' ) { # we have an error &logIt( $config, 1, $output ); } else { &tabDelimitedToHash( $system, $output ); } } else { print "Script $modFile failed to compile with error\n$@\n"; } &logIt( $config, 3, "Processing module $moduleDir$modFile"); } # foreach # add sysinfo-client (me) to the software list, since we're obviously installed &tabDelimitedToHash( $system, "software\tsysinfo-client\tversion\t$main::VERSION\n" ); } ####################################################### # initReport( $config ) # # Initialize the report structure with configuration data # # Parameters # $config - reference to configuration hash # # Returns # Reference to initialized report hash # # Creates the initial report structure with: # - Report metadata (version, date, client) # - System information (hostname, serial, UUID) # - Additional configuration values (excluding moduleDirs, # transports, scriptDirs, and logging) # ####################################################### sub initReport { my $config = shift; my %ignore = ( # list of config keys to ignore. Using hash for fast lookup 'moduleDirs' => 1, 'transports' => 1, 'scriptDirs' => 1, 'logging' => 1 ); my $report; # some presets for compatibility $report->{'report'}->{'version'} = $VERSION->normal; # global value $report->{'report'}->{'date'} = $reportDate; # global value $report->{'report'}->{'client'} = $config->{'clientName'}; $report->{'system'}->{'hostname'} = $config->{'hostname'}; $report->{'system'}->{'serial'} = $config->{'serialNumber'}; $report->{'system'}->{'UUID'} = $config->{'UUID'}; foreach my $key ( keys %$config ) { next if defined( $ignore{$key} ); # ignore anything in our ignore hash $report->{'system'}->{$key} = $config->{$key}; # simply copy to the system part of the report } return $report; } ####################################################### # # detectConfigType( $filename ) # # Detects the configuration file type by examining content # json and DataTransport have unique starting lines # yaml will usually start with --- or key: value, but may be # comments in the first few lines, so checks up to 5 lines # # Parameters: # $lines - array reference containing lines of configuration content # # Returns: # 'yaml', 'json', 'datatransport', or undef # ####################################################### sub detectConfigType { my $lines = shift; # first look at first line for unique identifiers. First line comments # may contain type info if ( $lines->[0] =~ m/^.*(yaml|datatransport).*/i ) { &logIt( $config, 3, "Configuration file type hint found in first line comment" ); return lc $1; } # look through first five lines, there should be some indicator for ( my $line = 0; $line < 5 && $lines->[$line]; $line++ ) { return 'yaml' if $lines->[$line] =~ /^#.*YAML/; return 'yaml' if $lines->[$line] =~ /^---/; # YAML document start return 'yaml' if $lines->[$line] =~ /^\s*\w+:\s*/; # YAML key: value return 'json' if $lines->[$line] =~ /^#.*JSON/; # skip comments return 'json' if $lines->[$line] =~ /^\s*[{\[]/; # JSON starts with { or [ } return undef; } ####################################################### # # loadConfig( $confStr, $type ) # # Load configuration file based on specified or detected type # adds 'dataType' key to returned hashref indicating type used # # Parameters: # $confStr - configuration content as an arrayref of string # $type - optional type: 'yaml', 'json', 'datatransport' # # Returns: # hashref of configuration # ####################################################### sub loadConfig { my ($confStr, $type) = @_; my $return = {}; # Auto-detect if type not specified $type = &detectConfigType($confStr) unless $type; die "Cannot determine configuration file type\n" unless $type; $type = lc($type); # normalize to lowercase $confStr = join("\n", @$confStr); # join arrayref into single string for processing if ($type eq 'datatransport') { # Try to load DataTransport module eval { require DataTransport; }; if ($@) { die "DataTransport module not available: $@\n"; } my $dt = DataTransport->new(); $return = $dt->decode($confStr); die "Failed to read DataTransport content\n" unless $return; } elsif ($type eq 'json') { # Try to load JSON module eval { require JSON; }; if ($@) { die "JSON module not available: $@\n"; } $return = JSON::decode_json($confStr); } elsif ($type eq 'yaml') { # Use YAML::Tiny directly for single file loading eval { require YAML::Tiny; }; if ($@) { die "YAML::Tiny module not available: $@\n"; } $return = YAML::Tiny->read_string($confStr); die "Failed to read YAML content\n" unless $return; $return = $return->[0]; # only return the first document } else { die "Unknown configuration type: $type\n"; } $return->{'dataType'} = $type; # store the type used return $return; } ######################################################## # hashToReportString( $report, $outputType ) # # Converts report hash to specified output format string ## Parameters: # $report - reference to report hash # $outputType - output format: 'yaml', 'json', 'datatransport' # Returns: # string containing report in specified format ######################################################## sub hashToReportString { my ( $report, $outputType ) = @_; &logIt( $config, 3, "Entering hashToReportString with outputType $outputType" ); my $output; if ( $outputType eq 'yaml' ) { eval { require YAML::Tiny; }; if ($@) { die "YAML::Tiny module not available: $@\n"; } $output = YAML::Tiny->new( $report )->write_string(); } elsif ( $outputType eq 'json' ) { eval { require JSON; }; if ($@) { die "JSON module not available: $@\n"; } $output = JSON::encode_json( $report ); } elsif ( $outputType eq 'datatransport' ) { eval { require DataTransport; }; if ($@) { die "DataTransport module not available: $@\n"; } my $dt = DataTransport->new(); $output = $dt->encode( $report ); } else { die "Unknown output type: $outputType\n"; } return $output; } ####################################################### # # help() # # Display help message showing command-line options # # Parameters # None # # Returns # None (prints to STDOUT) # # Shows program version and available command-line options: # -f, --config : Specify configuration file path # --configtype : Specify config format (yaml/json/datatransport) # --version : Display version # --help : Display help message # -p, --periodic : Run periodic modules # -t, --test : Test mode (output to /tmp) # # Note: For interactive configuration, use sysinfo-client-interactive # ####################################################### sub help { use File::Basename; print basename($0) . " $VERSION (data $DATA_VERSION)\n"; print < \$periodic, # will do modules which are marked as periodic 'config|f=s' => \$configFile, # path to configuration file 'configtype=s' => \$configType, # config file type: yaml, json, datatransport 'outputType|o=s' => \$outputType, # output type: xml, yaml 'help' => \$help, 'version' => \$version, 'test|t' => \$TESTING ) or die "Error parsing command line\n"; if ( $help ) { &help() ; exit; } if ( $version ) { use File::Basename; print basename($0) . " $VERSION (data $DATA_VERSION)\n"; exit; } # if periodic flag set, create the override file `touch $periodicOverrideFile` if $periodic; # tells periodic modules to run $configFile //= 'sysinfo-client.yaml'; # default configuration file name # load the configuration file. Note, loadConfigurationFile returns an arrayref of lines # then loadConfig processes that based on type $config = loadConfig( loadConfigurationFile( $configFile ), $configType); # oops, no client name (required) so tell them and exit die "No client name defined in $configFile" unless $config->{'clientName'}; # clean up some missing values # output type is either what user passed on command line, or what is in config file, or defaults to yaml # the config->{'dataType'} is set by loadConfig, so we can use that as last resort $config->{'outputType'} //= $outputType //= $config->{'dataType'} //= 'yaml'; # default to yaml output # user did not define a serial number, so make something up. Just take an md5sum of hostname-clientname $config->{'serialNumber'} //= $config->{UUID} ? $config->{UUID} : `echo \`$config->{'hostname'}\`-$config->{'clientName'} | md5sum | awk '{print \$1}'`; chomp $config->{'serialNumber'}; &logIt( $config, 0, 'Starting sysinfo Run' ); &logIt( $config, 3, "Configuration is\n" . Data::Dumper->Dump( [$config], [ qw($config) ] ) ); $TESTING //= $config->{'TESTING'} //= 0; # default to 0 if not defined &logIt( $config, 0, "Testing => $TESTING" ) if $TESTING; # hash reference that will store all info we are going to send to the server # initialize it with some basic info from configuration file my $report = &initReport( $config ); &logIt( $config, 3, "Initial System\n" . Data::Dumper->Dump( [$report], [qw( $report )] ) ); # process any modules in the system foreach my $moduleDir ( @{$config->{'moduleDirs'}} ) { &logIt( $config, 3, "Processing modules from $moduleDir" ); &ProcessModules( $report, "$moduleDir/" ); } &logIt( $config, 4, "After processing modules\n" . Data::Dumper->Dump( [$report], [qw( $report )] ) ); # convert the report hash to the desired output type my $out = hashToReportString( $report, $config->{'outputType'} ); # test if we can convert to output type # add comment to top of file except for json $out = "# sysinfo " . $VERSION->normal . " (data " . $DATA_VERSION->normal . ") $config->{outputType}\n" . $out unless $config->{'outputType'} eq 'json'; &logIt( $config, 4, 'At line number ' . __LINE__ . "\n" . $out ); # actually writing the report if ( $TESTING ) { &logIt( $config, 0, "Sending report to sysinfo.testing.yaml" ); open DATA, ">/tmp/sysinfo.testing.yaml" or die "Could not write to /tmp/sysinfo.testing.yaml: $!\n"; print DATA $out; close DATA; } else { # load some global values for use in the script, if required my $globals = { 'upload_type' => 'sysinfo', 'data version' => $DATA_VERSION->normal, 'report date' => $reportDate, 'client name' => $config->{'clientName'}, 'host name' => $config->{'hostname'}, 'serial number'=> $config->{'serialNumber'}, 'UUID' => $config->{'UUID'}, 'fileType' => $config->{'outputType'} }; &logIt( $config, 4, "Globals initialized\n" . Data::Dumper->Dump([$globals],[qw($globals)]) ); &logIt( $config, 0, "Sending report to remote transport" ); if ( my $success = &sendResults( $globals, $config->{'transports'}, $out, $config->{'scriptDirs'} ) != 1 ) { &logIt( $config, 0, "Error $success while sending report from $config->{'hostname'}" ); } } unlink ( $periodicOverrideFile ) if -e $periodicOverrideFile; &logIt( $config, 0, 'Ending sysinfo Run' ); if ( $config->{'postRunScript'}{'script name'} ) { my $script = $sourceDir . '/' . $config->{'postRunScript'}{'script name'}; exec ( "$script $configFile" ) if -x $script; } 1;