Rev 257 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
#! /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:
# <sysinfo3.0.0>
# <diskinfo name='/dev/xvda3'>
# <fstype>ext3</fstype>
# <mount>/home</mount>
# <size>51606140</size>
# <used>331472</used>
# </diskinfo>
# <network name='eth0'>
# <address>192.168.1.3</address>
# <ip6address>fe80::216:3eff:fefb:4e10</ip6address>
# <ip6networkbits>64</ip6networkbits>
# <mac>00:16:3e:fb:4e:10</mac>
# <mtu>1500</mtu>
# <netmask>255.255.255.0</netmask>
# </network>
# <operatingsystem>
# <codename>squeeze</codename>
# <description>Debian GNU/Linux 6.0.4 (squeeze)</description>
# <distribution>Debian</distribution>
# <kernel>2.6.32-5-xen-686</kernel>
# <os_name>Linux</os_name>
# <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>
# <release>6.0.4</release>
# </operatingsystem>
# <pci name='0000:00:00.0'>
# <class>RAM memory</class>
# <device>MCP55 Memory Controller</device>
# <rev>a2</rev>
# <sdevice>Device cb84</sdevice>
# <slot>0000:00:00.0</slot>
# <svendor>nVidia Corporation</svendor>
# <vendor>nVidia Corporation</vendor>
# </pci>
# <report>
# <client>Staffmasters</client>
# <date>2012-05-01 03:00</date>
# <version>2.0.0</version>
# </report>
# <software name='aptitude'>
# <description>terminal-based package manager (terminal interface only)</description>
# <version>0.6.3-3.2+squeeze1</version>
# </software>
# <system>
# <cpu_speed>1800.103</cpu_speed>
# <cpu_sub>i686</cpu_sub>
# <cpu_type>GenuineIntel</cpu_type>
# <hostname>backup.staffmasters.local</hostname>
# <last_boot>1333259809</last_boot>
# <memory>520852</memory>
# <num_cpu>1</num_cpu>
# </system>
# </sysinfo3.0.0>
#
# 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 <<END
$0 [options]
For interactive configuration, use sysinfo-client-interactive instead.
Options:
--version - display version and exit
--help - display this help message
-f,
--config=/path/ - path to configuration file (overrides default search)
--configtype=xxx - config file type: yaml, json, or datatransport (autodetects if not specified)
-p,
--periodic - runs modules designed to be run only weekly, monthly, etc...
-t,
--test - If set, runs but places output in /tmp
END
}
# Process command line options
GetOptions (
'periodic|p' => \$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;