174 |
rodolico |
1 |
#! /usr/bin/env perl
|
|
|
2 |
|
20 |
rodolico |
3 |
use warnings;
|
26 |
rodolico |
4 |
use strict;
|
2 |
rodolico |
5 |
|
|
|
6 |
# sysinfo
|
|
|
7 |
# Author: R. W. Rodolico
|
|
|
8 |
# Primary client portion of sysinfo system. Will collect information about its current
|
|
|
9 |
# host and create a report containing the information. This report can then be processed
|
|
|
10 |
# by process_sysinfo.pl on the collection computer.
|
112 |
rodolico |
11 |
# output file consists of a YAML file of the form:
|
2 |
rodolico |
12 |
# <sysinfo3.0.0>
|
|
|
13 |
# <diskinfo name='/dev/xvda3'>
|
|
|
14 |
# <fstype>ext3</fstype>
|
|
|
15 |
# <mount>/home</mount>
|
|
|
16 |
# <size>51606140</size>
|
|
|
17 |
# <used>331472</used>
|
|
|
18 |
# </diskinfo>
|
|
|
19 |
# <network name='eth0'>
|
|
|
20 |
# <address>192.168.1.3</address>
|
|
|
21 |
# <ip6address>fe80::216:3eff:fefb:4e10</ip6address>
|
|
|
22 |
# <ip6networkbits>64</ip6networkbits>
|
|
|
23 |
# <mac>00:16:3e:fb:4e:10</mac>
|
|
|
24 |
# <mtu>1500</mtu>
|
|
|
25 |
# <netmask>255.255.255.0</netmask>
|
|
|
26 |
# </network>
|
|
|
27 |
# <operatingsystem>
|
|
|
28 |
# <codename>squeeze</codename>
|
|
|
29 |
# <description>Debian GNU/Linux 6.0.4 (squeeze)</description>
|
|
|
30 |
# <distribution>Debian</distribution>
|
|
|
31 |
# <kernel>2.6.32-5-xen-686</kernel>
|
|
|
32 |
# <os_name>Linux</os_name>
|
|
|
33 |
# <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>
|
|
|
34 |
# <release>6.0.4</release>
|
|
|
35 |
# </operatingsystem>
|
|
|
36 |
# <pci name='0000:00:00.0'>
|
|
|
37 |
# <class>RAM memory</class>
|
|
|
38 |
# <device>MCP55 Memory Controller</device>
|
|
|
39 |
# <rev>a2</rev>
|
|
|
40 |
# <sdevice>Device cb84</sdevice>
|
|
|
41 |
# <slot>0000:00:00.0</slot>
|
|
|
42 |
# <svendor>nVidia Corporation</svendor>
|
|
|
43 |
# <vendor>nVidia Corporation</vendor>
|
|
|
44 |
# </pci>
|
|
|
45 |
# <report>
|
|
|
46 |
# <client>Staffmasters</client>
|
|
|
47 |
# <date>2012-05-01 03:00</date>
|
|
|
48 |
# <version>2.0.0</version>
|
|
|
49 |
# </report>
|
|
|
50 |
# <software name='aptitude'>
|
|
|
51 |
# <description>terminal-based package manager (terminal interface only)</description>
|
|
|
52 |
# <version>0.6.3-3.2+squeeze1</version>
|
|
|
53 |
# </software>
|
|
|
54 |
# <system>
|
|
|
55 |
# <cpu_speed>1800.103</cpu_speed>
|
|
|
56 |
# <cpu_sub>i686</cpu_sub>
|
|
|
57 |
# <cpu_type>GenuineIntel</cpu_type>
|
|
|
58 |
# <hostname>backup.staffmasters.local</hostname>
|
|
|
59 |
# <last_boot>1333259809</last_boot>
|
|
|
60 |
# <memory>520852</memory>
|
|
|
61 |
# <num_cpu>1</num_cpu>
|
|
|
62 |
# </system>
|
|
|
63 |
# </sysinfo3.0.0>
|
|
|
64 |
|
|
|
65 |
|
|
|
66 |
#
|
|
|
67 |
# Version 1.3 20071104
|
|
|
68 |
# added capability of e-mailing the results by itself and external configuration file
|
|
|
69 |
|
|
|
70 |
# Version 1.3.1 20071110
|
|
|
71 |
# added du -sk to explicitly do directory sizes in 'k'. Also, fixed some documentation
|
|
|
72 |
|
|
|
73 |
# Version 1.3.3 20081104
|
|
|
74 |
# modified hostname to hostname -f, and allowed user to place custom value in configuration file
|
|
|
75 |
# also, modified to go with Debian standards in preparation to creating a debian package.
|
|
|
76 |
|
|
|
77 |
# Version 2.0 20081208
|
|
|
78 |
# Modified to use different libraries for different OS's in preparation to porting to Windows
|
|
|
79 |
# Uses different packages based on which OS it is on.
|
|
|
80 |
|
|
|
81 |
# Version 3.0 20120923
|
|
|
82 |
# Major revision. Most internal intelligence pulled out and put into modules and data transfer format has been changed to YAML
|
|
|
83 |
#
|
|
|
84 |
# Base system only pulls client name, machine name and machine number, all of which can be set in the configuration file
|
|
|
85 |
# if the value is not set, it attempts various means to determine the values and, if it fails, aborts with an error message
|
|
|
86 |
# client name -- REQUIRED, must come from configuration file
|
|
|
87 |
# machine name -- REQUIRED, if not set via conf file, attempts hostname command (hostname -f) or module getHostName
|
|
|
88 |
# machine number -- REQUIRED, if not set via conf file, attempts "echo `hostname -f`-clientname | md5sum" or module getSerial
|
|
|
89 |
# modules are stored in "configuration directory/modules" (/etc/sysinfo/modules on most Linux systems) and are processed in
|
|
|
90 |
# standard sort order (case sensitive).
|
|
|
91 |
# Module filenames may contain alpha-numeric, underscore and the period only (files containing other characters are ignored).
|
|
|
92 |
# Modules should set their exit code to 0 for success, and non-zero for failure
|
|
|
93 |
# Modules should return 0 or more tab delimited, newline terminated strings, processed as one record per line
|
|
|
94 |
# A module return string line is processed as follows:
|
|
|
95 |
# category \t [category \t ...] \t key \t value
|
|
|
96 |
# example:
|
|
|
97 |
# System \t num_cpu \t 1
|
|
|
98 |
# System \t Users \t root \t /root/
|
|
|
99 |
# (note, if non-zero exit code returned, return value is assumed to be error message and is printed to STDERR)
|
|
|
100 |
# sysinfo stores the result in a hash, using categories as the keys (case sensitive), thus, the above results in
|
|
|
101 |
# $store{'System'}{'num_cpu'} = '1';
|
|
|
102 |
# $store{'System'}{'Users'}{'root'} = '/root';
|
|
|
103 |
# upon completion, sysinfo converts the $store hash into an XML or YAML string for transfer
|
|
|
104 |
# It then sends it to the main server as defined in the conf file.
|
|
|
105 |
# NOTE: YAML is hand crafted to kill any requirements for external libraries
|
|
|
106 |
# see sub hashToYAML for details
|
|
|
107 |
|
9 |
rodolico |
108 |
# Version 3.0.1 20160321
|
|
|
109 |
# Renamed to sysinfo-client to not conflict with Linux package sysinfo
|
|
|
110 |
# created installer in Perl to not rely on package managers
|
|
|
111 |
# default path for configuration file changed to /etc/camp/sysinfo-client.conf
|
|
|
112 |
# $VERSION changed to $DATA_VERSION to not conflict with $main::VERSION (script version vs data format version)
|
13 |
rodolico |
113 |
#
|
|
|
114 |
# Version 3.1.0 20160401
|
|
|
115 |
# module and script dirs now arrays to be searched. Idea is that default
|
|
|
116 |
# modules/scripts are in installdir/modules or installdir/scripts, and
|
|
|
117 |
# user supplied are in /etc/scripts and /etc/modules
|
14 |
rodolico |
118 |
# Tightened up the file systems checks, requiring all scripts and modules
|
|
|
119 |
# be set 0700 at least, and owned by root
|
18 |
rodolico |
120 |
# Transport layers now an array, and if one fails to send the report, the others
|
|
|
121 |
# are tried in turn
|
14 |
rodolico |
122 |
# Worked on logic for sendReport to give better error checking.
|
|
|
123 |
# Doing a search for the configuration file matching cwd, then /etc/camp, then /usr/local/etc/camp
|
21 |
rodolico |
124 |
# Self documenting, ie a key for software\tsysinfo-client\version\current version is inserted
|
28 |
rodolico |
125 |
#
|
|
|
126 |
# Version 3.1.1 20160915 RWR
|
|
|
127 |
# set use strict and use warnings, then fixed errors
|
37 |
rodolico |
128 |
#
|
|
|
129 |
# Version 3.1.2 20160922 RWR
|
|
|
130 |
# $exitCode 1 (not applicable to this machine) does not throw warning
|
|
|
131 |
#
|
42 |
rodolico |
132 |
# Version 3.1.3 20161010 RWR
|
|
|
133 |
# Removed extra use warnings
|
47 |
rodolico |
134 |
#
|
|
|
135 |
# Version 3.1.4 20161023 RWR
|
|
|
136 |
# Would error out if moduledir does not exist, added a return
|
51 |
rodolico |
137 |
#
|
|
|
138 |
# Version 3.1.5 20170327 RWR
|
|
|
139 |
# On freeBSD systems, was looking in wrong place for configuration file
|
59 |
rodolico |
140 |
#
|
|
|
141 |
# Version 3.2.0 20180320 RWR
|
62 |
rodolico |
142 |
# Major change in the configuration file format; All entries are loaded into
|
|
|
143 |
# hash %configuration, so clientname is no longer $clientname, but is now
|
|
|
144 |
# $configuration{'clientname'}
|
|
|
145 |
# NOT backwards compatible
|
59 |
rodolico |
146 |
# changed configuration to be loaded into hash (vs directly loaded into variables)
|
|
|
147 |
# added UUID to configuration file
|
62 |
rodolico |
148 |
#
|
|
|
149 |
# Version 3.2.1 20180424 RWR
|
|
|
150 |
# Finally got a semi-stable version of this running. Fixed a bunch of bugs
|
|
|
151 |
# and appears to be working correctly.
|
112 |
rodolico |
152 |
#
|
|
|
153 |
# Version 3.3.0 20190419 RWR
|
|
|
154 |
# Converted to use YAML config file
|
135 |
rodolico |
155 |
#
|
|
|
156 |
# Version 3.4.0 20191111 RWR
|
|
|
157 |
# adding logging with priority. logging is a hash inside of %cvonfiguration which contains the following
|
|
|
158 |
# $configuration{ 'logging' } = {
|
|
|
159 |
# 'log type' => 'string',
|
|
|
160 |
# 'log level' => #,
|
|
|
161 |
# 'other params' => something,
|
|
|
162 |
# };
|
138 |
rodolico |
163 |
#
|
|
|
164 |
# The default log type is cache, which builds an array of all messages passed. When the log type is changed, the cache is
|
|
|
165 |
# checked for values and, if they exist, they are dumped to the log, then removed.
|
|
|
166 |
#
|
135 |
rodolico |
167 |
# Currently, the only log type is 'file', which has one other additional parameter, 'log path' which
|
|
|
168 |
# points to the actual log to be created. The log is NOT limited in size, so use something else to
|
|
|
169 |
# do that.
|
|
|
170 |
# log level is an integer which is compared the a priority passed to the logging function. The
|
|
|
171 |
# higher log level is set, the more verbose the log.
|
|
|
172 |
# 0 - Normal, basically logs when the program starts and ends, and any warnings.
|
|
|
173 |
# 1 - a little more information about flow
|
|
|
174 |
# 2 - Gives ending information on structures
|
|
|
175 |
# 3 - Gives a lot of info about structures when they are initialized and at the end
|
|
|
176 |
# 4 - Crazy. Dumps just about every structure every time they are changed
|
|
|
177 |
#
|
|
|
178 |
# $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 |
179 |
#
|
|
|
180 |
# Version 3.4.1 20191117 RWR
|
|
|
181 |
# Added syslog as a possible option for logging.
|
|
|
182 |
#
|
175 |
rodolico |
183 |
# Version 3.5.4 20200317 RWR
|
174 |
rodolico |
184 |
# changed so report->version will show the version of sysinfo, not the data version
|
185 |
rodolico |
185 |
#
|
|
|
186 |
# Version 3.5.5 20200317 RWR
|
|
|
187 |
# bug fix for bsd/opnsense
|
191 |
rodolico |
188 |
#
|
|
|
189 |
# Version 3.6.0 20220607 RWR
|
|
|
190 |
# Changed the way upload works. It automatically now passes an additional parameter, upload_type, which with the new upload
|
|
|
191 |
# script will choose a directory, allowing one script to handle various uploads
|
|
|
192 |
# additionally, if the key 'postRunScript' is in the config, it is assumed to be the name of a script to be run after sysinfo
|
|
|
193 |
# completes it's task. This can be used to download/update the program, add new modules, etc..
|
|
|
194 |
|
|
|
195 |
|
112 |
rodolico |
196 |
# find our location and use it for searching for libraries
|
|
|
197 |
BEGIN {
|
|
|
198 |
use FindBin;
|
|
|
199 |
use File::Spec;
|
209 |
rodolico |
200 |
use Cwd 'abs_path';
|
|
|
201 |
use lib abs_path( File::Spec->catdir($FindBin::Bin) );
|
154 |
rodolico |
202 |
eval( 'use YAML::Tiny;' );
|
|
|
203 |
eval( 'use Data::Dumper;' );
|
112 |
rodolico |
204 |
}
|
62 |
rodolico |
205 |
|
154 |
rodolico |
206 |
# contains the directory our script is in
|
209 |
rodolico |
207 |
my $sourceDir = abs_path( File::Spec->catdir($FindBin::Bin) );
|
112 |
rodolico |
208 |
|
154 |
rodolico |
209 |
# define the version number
|
|
|
210 |
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
|
|
|
211 |
use version;
|
191 |
rodolico |
212 |
our $VERSION = version->declare("v3.6.0");
|
154 |
rodolico |
213 |
our $DATA_VERSION = version->declare( 'v3.0.0' ); # used in sending the data file. sets version of XML/YAML data file
|
|
|
214 |
|
|
|
215 |
# see https://perldoc.perl.org/Getopt/Long.html
|
|
|
216 |
use Getopt::Long;
|
|
|
217 |
# allow -vvn (ie, --verbose --verbose --dryrun)
|
|
|
218 |
Getopt::Long::Configure ("bundling");
|
|
|
219 |
|
194 |
rodolico |
220 |
use sysinfoconf;
|
|
|
221 |
|
2 |
rodolico |
222 |
# Following are global variables overridden if configuration file exists
|
|
|
223 |
|
135 |
rodolico |
224 |
my $TESTING = 0; # if set to 1, will do everything, but will dump output to /tmp/sysinfo.testing.yaml
|
9 |
rodolico |
225 |
|
2 |
rodolico |
226 |
my $indentLevel = 2; # number of spaces to indent per level in XML or YAML
|
|
|
227 |
|
135 |
rodolico |
228 |
my $reportDate = &timeStamp(); # set report date
|
2 |
rodolico |
229 |
|
154 |
rodolico |
230 |
my $interactive = 0; # if set to 1, will go into interactive mode and output to local file
|
|
|
231 |
my $periodicOverrideFile = '/tmp/sysinfo.firstrun'; # if this file exists, library.pm will tell all periodic modules to run anyway
|
|
|
232 |
my $periodic = 0; # if set to 1, will do modules which are only supposed to run weekly, monthly, etc...
|
|
|
233 |
|
|
|
234 |
my $version;
|
|
|
235 |
my $help;
|
|
|
236 |
|
59 |
rodolico |
237 |
my %configuration = (
|
142 |
rodolico |
238 |
'logging' => { 'log type' => 'cache', 'log level' => 0 }, # if set, will point to logging
|
154 |
rodolico |
239 |
'moduleDirs' => ["$sourceDir/modules"], # search paths for modules
|
|
|
240 |
'scriptDirs' => ["$sourceDir/scripts"], # search paths for scripts
|
59 |
rodolico |
241 |
'clientName' => '', # Required!! Must be set in conf file (no defaults)
|
|
|
242 |
'serialNumber' => '', # serial number of machine
|
|
|
243 |
'UUID' => '', # UUID of machine
|
154 |
rodolico |
244 |
'transports' => {'3' => { '-name-' => 'saveLocal', 'sendScript' => 'save_local', 'output directory' => "$sourceDir/reports" } }, # hash with various transports
|
61 |
rodolico |
245 |
'hostname' => &getHostName() # fully qualified host name of machine
|
154 |
rodolico |
246 |
);
|
2 |
rodolico |
247 |
|
|
|
248 |
|
13 |
rodolico |
249 |
|
136 |
rodolico |
250 |
#######################################################
|
|
|
251 |
#
|
2 |
rodolico |
252 |
# sendResults( $parameters, $message, $scriptDirectory )
|
|
|
253 |
#
|
|
|
254 |
# Sends results of run to server using external script. If external
|
|
|
255 |
# script not defined, just print to STDOUT
|
|
|
256 |
#
|
|
|
257 |
# Parameters
|
|
|
258 |
# $parameters - a hash containing the information necessary to make the transfer
|
|
|
259 |
# $message - the message to be sent
|
|
|
260 |
# $scriptDirectory - path (not filename) of script to be executed
|
|
|
261 |
#
|
|
|
262 |
# $parameters contains different key/value pairs depending on the script used
|
|
|
263 |
# for example, a stand-alone SMTP script may need a username/password,
|
|
|
264 |
# smtp server name, port number, from and to address
|
|
|
265 |
# while an http transfer may only need a script name
|
|
|
266 |
# See the individual scripts to determine what parameters need to be
|
|
|
267 |
# filled in.
|
|
|
268 |
# The only required parameter is 'sendScript' which must contain the
|
|
|
269 |
# name of the script to execute (and it must be located in $scriptDirectory)
|
|
|
270 |
# SCRIPT must contain one sub named doit, that accepts three parameters, the hash,
|
|
|
271 |
# the message, and, optionally, the script directory
|
|
|
272 |
#
|
|
|
273 |
# If script not defined, just dump to STDOUT. With a properly set up cron job, the output
|
|
|
274 |
# would then be sent via e-mail to an administrative account, possibly root
|
|
|
275 |
#
|
|
|
276 |
#######################################################
|
|
|
277 |
sub sendResults {
|
62 |
rodolico |
278 |
my ( $globals, $transports, $message, $scriptDirectory ) = @_;
|
135 |
rodolico |
279 |
&logIt( 3, "Entering sendResults" );
|
113 |
rodolico |
280 |
foreach my $key ( sort { $a <=> $b } %$transports ) {
|
|
|
281 |
if ( $transports->{$key}->{'sendScript'} ) {
|
135 |
rodolico |
282 |
&logIt( 3, "Trying to find file " . $transports->{$key}->{'sendScript'} . " in " . join( "\n\t", @{$scriptDirectory} ) );
|
113 |
rodolico |
283 |
my $sendScript = &findFile( $transports->{$key}->{'sendScript'}, $scriptDirectory );
|
19 |
rodolico |
284 |
if ( $sendScript ) {
|
18 |
rodolico |
285 |
# load the chosen script into memory
|
|
|
286 |
require $sendScript;
|
19 |
rodolico |
287 |
# merge the globals in
|
|
|
288 |
while ( my ( $gkey, $value ) = each %$globals ) {
|
113 |
rodolico |
289 |
$transports->{$key}->{$gkey} = $value;
|
19 |
rodolico |
290 |
}
|
20 |
rodolico |
291 |
# do variable substitution for any values which need it
|
113 |
rodolico |
292 |
foreach my $thisOne ( keys %{$transports->{$key}} ) {
|
135 |
rodolico |
293 |
&logIt( 4, "$thisOne" );
|
113 |
rodolico |
294 |
if ( $transports->{$key}->{$thisOne} =~ m/(\$configuration\{'hostname'\})|(\$reportDate)|(\$configuration\{'clientName'\})|(\$configuration\{'serialNumber'\})/ ) {
|
|
|
295 |
$transports->{$key}->{$thisOne} = eval "\"$transports->{$key}->{$thisOne}\"";
|
20 |
rodolico |
296 |
}
|
|
|
297 |
}
|
|
|
298 |
|
62 |
rodolico |
299 |
#%$transports{$key}{keys %$globals} = values %$globals;
|
|
|
300 |
#print Dumper( $$transports[$key] );
|
20 |
rodolico |
301 |
#next;
|
18 |
rodolico |
302 |
# execute the "doit" sub from that script
|
135 |
rodolico |
303 |
&logIt( 3, $message );
|
|
|
304 |
my $return = &doit( $transports->{$key}, $message );
|
|
|
305 |
return $return if ( $return == 1 );
|
18 |
rodolico |
306 |
} else {
|
135 |
rodolico |
307 |
&logIt( 0,"Could not find " . $$transports[$key]{'sendScript'} . ", trying next transport" );
|
18 |
rodolico |
308 |
} # if..else
|
|
|
309 |
} # if
|
|
|
310 |
} # foreach
|
|
|
311 |
# if we made it here, we have not sent the report, so just return it to the user
|
85 |
rodolico |
312 |
# if called from a cron job, it will (hopefully) be sent to root
|
135 |
rodolico |
313 |
&logIt( 0, 'Error, reached ' . __LINE__ . " which should not happen, message was\n$message" );
|
85 |
rodolico |
314 |
print $message;
|
16 |
rodolico |
315 |
return 1;
|
2 |
rodolico |
316 |
}
|
|
|
317 |
|
|
|
318 |
#######################################################
|
|
|
319 |
#
|
|
|
320 |
# getHostName
|
|
|
321 |
#
|
|
|
322 |
# return hostname from hostname -f
|
|
|
323 |
#
|
|
|
324 |
#######################################################
|
|
|
325 |
sub getHostName {
|
135 |
rodolico |
326 |
&logIt( 3, "Entering getHostName" );
|
28 |
rodolico |
327 |
my $hostname = `hostname -f`;
|
2 |
rodolico |
328 |
chomp $hostname;
|
|
|
329 |
return $hostname;
|
|
|
330 |
}
|
|
|
331 |
|
|
|
332 |
#######################################################
|
|
|
333 |
#
|
18 |
rodolico |
334 |
# escapeForYAML
|
2 |
rodolico |
335 |
#
|
18 |
rodolico |
336 |
# Escapes values put into YAML report
|
2 |
rodolico |
337 |
#
|
112 |
rodolico |
338 |
# DEPRECATED AS OF VERSION 3.3.0
|
|
|
339 |
# uses YAML::Tiny
|
|
|
340 |
#
|
2 |
rodolico |
341 |
#######################################################
|
112 |
rodolico |
342 |
#sub escapeForYAML {
|
|
|
343 |
# my $value = shift;
|
|
|
344 |
# $value =~ s/'/\\'/gi; # escape single quotes
|
|
|
345 |
# $value =~ s/"/\\"/gi; # escape double quotes
|
|
|
346 |
# # pound sign indicates start of a comment and thus loses part
|
|
|
347 |
# # of strings. Surrounding it by double quotes in next statement
|
|
|
348 |
# # allows
|
|
|
349 |
# $value = '"' . $value . '"' if ( $value =~ m/[#:]/ );
|
|
|
350 |
# return $value;
|
|
|
351 |
#}
|
2 |
rodolico |
352 |
|
|
|
353 |
#######################################################
|
|
|
354 |
#
|
|
|
355 |
# hashToYAML( $hashRef, $indent )
|
|
|
356 |
#
|
|
|
357 |
# Converts a hash to a YAML string
|
|
|
358 |
#
|
|
|
359 |
# NOTE: This routine recursively calls itself for every level
|
|
|
360 |
# in the hash
|
|
|
361 |
#
|
|
|
362 |
# Parameters
|
|
|
363 |
# $hashref - reference (address) of a hash
|
|
|
364 |
# $indent - current indent level, defaults to 0
|
|
|
365 |
#
|
|
|
366 |
# Even though there are some very good libraries that do this
|
|
|
367 |
# I chose to hand-code it so sysinfo can be run with no libraries
|
|
|
368 |
# loaded. I chose to NOT do a full implementation, so special chars
|
|
|
369 |
# that would normally be escaped are not in here.
|
|
|
370 |
# However, I followed all the RFC for the values that were given, so
|
|
|
371 |
# assume any YAML reader can parse this
|
|
|
372 |
# NOTE: YAML appears to give a resulting file 1/3 smaller than the above
|
|
|
373 |
# XML, and compresses down in like manner
|
|
|
374 |
#
|
112 |
rodolico |
375 |
# DEPRECATED AS OF VERSION 3.3.0
|
|
|
376 |
# uses YAML::Tiny
|
|
|
377 |
#
|
2 |
rodolico |
378 |
#######################################################
|
112 |
rodolico |
379 |
#sub hashToYAML {
|
|
|
380 |
# my ($hashRef, $indent) = @_;
|
|
|
381 |
# $indent = 0 unless $indent; # default to 0 if not defined
|
|
|
382 |
#
|
|
|
383 |
# my $output; # where the output is stored
|
|
|
384 |
# foreach my $key ( keys %$hashRef ) { # for each key in the current reference
|
|
|
385 |
# print "Looking at $key\n" if $TESTING > 3;
|
|
|
386 |
# # see http://www.perlmonks.org/?node_id=175651 for isa function
|
|
|
387 |
# if ( UNIVERSAL::isa( $$hashRef{$key}, 'HASH' ) ) { # is the value another hash?
|
|
|
388 |
# # NOTE: unlike xml, indentation is NOT optional in YAML, so the following line verifies $indentlevel is non-zero
|
|
|
389 |
# # and, if it is, uses a default 3 character indentation
|
|
|
390 |
# $output .= (' ' x $indent ) . &escapeForYAML($key) . ":\n" . # key, plus colon, plus newline
|
|
|
391 |
# &hashToYAML( $$hashRef{$key}, $indent+($indentLevel ? $indentLevel : 3) ) . # add results of recursive call
|
|
|
392 |
# "\n";
|
|
|
393 |
# } elsif ( UNIVERSAL::isa( $$hashRef{$key}, 'ARRAY' ) ) { # is it an array? ignore it
|
|
|
394 |
# } else { # it is a scalar, so just do <key>value</key>
|
|
|
395 |
# $output .= (' ' x $indent ) . &escapeForYAML($key) . ': ' . &escapeForYAML($$hashRef{$key}) . "\n";
|
|
|
396 |
# }
|
|
|
397 |
# }
|
|
|
398 |
# return $output;
|
|
|
399 |
#}
|
2 |
rodolico |
400 |
|
|
|
401 |
|
|
|
402 |
#######################################################
|
|
|
403 |
#
|
|
|
404 |
# tabDelimitedToHash ($hashRef, $tabdelim)
|
|
|
405 |
#
|
|
|
406 |
# Takes a tab delimited multi line string and adds it
|
|
|
407 |
# to a hash. The final field in each line is considered to
|
|
|
408 |
# be the value, and all prior fields are considered to be
|
|
|
409 |
# hierachial keys.
|
|
|
410 |
#
|
|
|
411 |
# Parameters
|
|
|
412 |
# $hashref - reference (address) of a hash
|
|
|
413 |
# $tabdelim - A tab delimited, newline terminated set of records
|
|
|
414 |
#
|
|
|
415 |
#
|
|
|
416 |
#######################################################
|
|
|
417 |
sub tabDelimitedToHash {
|
|
|
418 |
my ($hashRef, $tabdelim) = @_;
|
135 |
rodolico |
419 |
&logIt( 3, "Entering tabDelimitedToHash" );
|
2 |
rodolico |
420 |
foreach my $line ( split( "\n", $tabdelim ) ) { # split on newlines, then process each line in turn
|
|
|
421 |
$line =~ s/'/\\'/gi; # escape single quotes
|
28 |
rodolico |
422 |
my @fields = split( / *\t */, $line ); # get all the field values into array
|
2 |
rodolico |
423 |
my $theValue = pop @fields; # the last one is the value, so save it
|
|
|
424 |
# now, we build a Perl statement that would create the assignment. The goal is
|
|
|
425 |
# to have a string that says something like $$hashRef{'key'}{'key'} = $value;
|
|
|
426 |
# then, eval that.
|
|
|
427 |
my $command = '$$hashRef'; # start with the name of the dereferenced hash (parameter 1)
|
|
|
428 |
while (my $key = shift @fields) { # while we have a key, from left to right
|
|
|
429 |
$command .= '{' . "'$key'" . '}'; # build it as {'key'} concated to string
|
|
|
430 |
}
|
|
|
431 |
$command .= "='$theValue';"; # add the assignment
|
|
|
432 |
#print STDERR "$command\n";
|
|
|
433 |
eval $command; # eval the string to make the actual assignment
|
|
|
434 |
}
|
|
|
435 |
}
|
|
|
436 |
|
|
|
437 |
#######################################################
|
|
|
438 |
#
|
13 |
rodolico |
439 |
# validatePermission ( $file )
|
|
|
440 |
#
|
|
|
441 |
# Checks that file is owned by root, and has permission
|
|
|
442 |
# 0700 or less
|
|
|
443 |
#
|
|
|
444 |
# Returns empty string on success, error message
|
|
|
445 |
# on failure
|
|
|
446 |
#
|
|
|
447 |
#######################################################
|
|
|
448 |
|
|
|
449 |
sub validatePermission {
|
|
|
450 |
my $file = shift;
|
135 |
rodolico |
451 |
&logIt( 3, "Entering validatePermission with $file" );
|
14 |
rodolico |
452 |
my $return;
|
13 |
rodolico |
453 |
# must be owned by root
|
28 |
rodolico |
454 |
my $owner = (stat($file))[4];
|
13 |
rodolico |
455 |
$return .= " - Bad Owner [$owner]" if $owner;
|
|
|
456 |
# must not have any permissions for group or world
|
|
|
457 |
# ie, 0700 or less
|
28 |
rodolico |
458 |
my $mode = (stat($file))[2];
|
13 |
rodolico |
459 |
$mode = sprintf( '%04o', $mode & 07777 );
|
|
|
460 |
$return .= " - Bad Permission [$mode]" unless $mode =~ m/0.00/;
|
|
|
461 |
return $return ? $file . $return : '';
|
|
|
462 |
}
|
|
|
463 |
|
|
|
464 |
#######################################################
|
|
|
465 |
#
|
2 |
rodolico |
466 |
# ProcessModules ( $system, $moduleDir )
|
|
|
467 |
#
|
|
|
468 |
# Processes all modules in $moduleDir, adding result to $system hash
|
|
|
469 |
#
|
|
|
470 |
# Parameters
|
|
|
471 |
# $system - reference (address) of a hash
|
|
|
472 |
# $moduleDir - full path to a directory containing executable scripts
|
|
|
473 |
#
|
|
|
474 |
# Each file in the $moduleDir directory that matches the regex in the grep
|
|
|
475 |
# and is executable is run. It is assumed the script will return 0 on success
|
|
|
476 |
# or a non-zero on failure
|
|
|
477 |
# The output of the script is assumed to be a tab delimited, newline separated
|
|
|
478 |
# list of records that should be added to the hash $system. This is done by calling
|
|
|
479 |
# &parseModule above.
|
|
|
480 |
# on failure, the returned output of the script is assumed to be an error message
|
|
|
481 |
# and is displayed on STDERR
|
|
|
482 |
#######################################################
|
|
|
483 |
sub ProcessModules {
|
|
|
484 |
my ( $system, $moduleDir ) = @_;
|
135 |
rodolico |
485 |
&logIt( 3, "Entering processModules" );
|
2 |
rodolico |
486 |
# open the module directory
|
47 |
rodolico |
487 |
return unless -d $moduleDir;
|
2 |
rodolico |
488 |
opendir( my $dh, $moduleDir ) || die "Module Directory $moduleDir can not be opened: $!\n";
|
|
|
489 |
# and get all files which are executable and contain nothing but alpha-numerics and underscores (must begin with alpha-numeric)
|
|
|
490 |
my @modules = grep { /^[a-zA-Z0-9][a-zA-Z0-9_]+$/ && -x "$moduleDir/$_" } readdir( $dh );
|
|
|
491 |
closedir $dh;
|
28 |
rodolico |
492 |
foreach my $modFile ( sort @modules ) { # for each valid script
|
14 |
rodolico |
493 |
if ( my $error = &validatePermission( "$moduleDir$modFile" ) ) {
|
13 |
rodolico |
494 |
print STDERR "Not Processed: $error\n";
|
|
|
495 |
next;
|
|
|
496 |
}
|
135 |
rodolico |
497 |
&logIt( 3, "Processing module $moduleDir$modFile");
|
2 |
rodolico |
498 |
my $output = qx/$moduleDir$modFile $moduleDir/; # execute it and grab the output
|
|
|
499 |
my $exitCode = $? >> 8; # process the exitCode
|
37 |
rodolico |
500 |
# exitCode 0 - processed normally
|
|
|
501 |
# exitCode 1 - not applicable to this machine
|
|
|
502 |
if ( $exitCode && $exitCode > 1) { # if non-zero, error, so show an error message
|
2 |
rodolico |
503 |
warn "Error in $moduleDir$modFile, [$output]\n";
|
135 |
rodolico |
504 |
&logIt( 0, "Error in $moduleDir$modFile, [$output]" );
|
2 |
rodolico |
505 |
} else { # otherwise, call tabDelimitedToHash to save the data
|
|
|
506 |
&tabDelimitedToHash( $system, $output );
|
21 |
rodolico |
507 |
} # if
|
|
|
508 |
} # foreach
|
|
|
509 |
# add sysinfo-client (me) to the software list, since we're obviously installed
|
|
|
510 |
&tabDelimitedToHash( $system, "software\tsysinfo-client\tversion\t$main::VERSION\n" );
|
2 |
rodolico |
511 |
}
|
|
|
512 |
|
154 |
rodolico |
513 |
sub getDMIDecode {
|
|
|
514 |
my ( $key, $type ) = @_;
|
|
|
515 |
my $command = 'dmidecode ';
|
|
|
516 |
$command .= "-t $type " if $type;
|
|
|
517 |
$command .= " | grep -i '$key'";
|
|
|
518 |
my $value = `$command`;
|
|
|
519 |
chomp $value;
|
|
|
520 |
if ( $value =~ m/:\s*(.*)\s*$/ ) {
|
|
|
521 |
return $1;
|
|
|
522 |
} else {
|
|
|
523 |
return '';
|
|
|
524 |
}
|
|
|
525 |
}
|
|
|
526 |
|
|
|
527 |
sub interactiveConfig {
|
|
|
528 |
my $config = shift;
|
|
|
529 |
$config->{'moduleDirs'} = $config->{'moduleDirs'}[0];
|
|
|
530 |
$config->{'scriptDirs'} = $config->{'scriptDirs'}[0];
|
|
|
531 |
$config->{'UUID'} = getDMIDecode( 'uuid', 'system' ) unless $config->{'UUID'};
|
|
|
532 |
$config->{'serialNumber'} = getDMIDecode( 'serial number', 'system' ) unless $config->{'serialNumber'};
|
|
|
533 |
|
|
|
534 |
my %menu = (
|
|
|
535 |
1 => {'prompt' => 'Host Name', 'key' => 'hostname' },
|
|
|
536 |
2 => {'prompt' => 'Client Name', 'key' => 'clientName' },
|
|
|
537 |
3 => {'prompt' => 'Serial Number', 'key' => 'serialNumber' },
|
|
|
538 |
4 => {'prompt' => 'UUID', 'key' => 'UUID' },
|
|
|
539 |
5 => {'prompt' => 'Modules Directory', 'key' => 'moduleDirs' },
|
|
|
540 |
6 => {'prompt' => 'Scripts Directory', 'key' => 'scriptDirs' }
|
|
|
541 |
);
|
|
|
542 |
my $choice = 'quit';
|
|
|
543 |
while ( $choice ) {
|
|
|
544 |
foreach my $menuItem ( sort keys %menu ) {
|
|
|
545 |
print "$menuItem\. " . $menu{$menuItem}{'prompt'} . ': ' . $config->{$menu{$menuItem}{'key'}} . "\n";
|
20 |
rodolico |
546 |
}
|
154 |
rodolico |
547 |
print "Enter Menu Item to change, or press Enter to proceed ";
|
|
|
548 |
$choice = <>;
|
|
|
549 |
chomp $choice;
|
|
|
550 |
last unless $choice;
|
|
|
551 |
print $menu{$choice}{'prompt'} . ' [' . $config->{$menu{$choice}{'key'}} . '] : ';
|
|
|
552 |
my $value = <>;
|
|
|
553 |
chomp $value;
|
|
|
554 |
$config->{$menu{$choice}{'key'}} = $value if ($value);
|
|
|
555 |
}
|
|
|
556 |
$config->{'moduleDirs'} = [ $config->{'moduleDirs'} ];
|
|
|
557 |
$config->{'scriptDirs'} = [ $config->{'scriptDirs'} ];
|
|
|
558 |
return $config;
|
20 |
rodolico |
559 |
}
|
|
|
560 |
|
154 |
rodolico |
561 |
# simple display if --help is passed
|
|
|
562 |
sub help {
|
|
|
563 |
use File::Basename;
|
|
|
564 |
print basename($0) . " $VERSION\n";
|
|
|
565 |
print <<END
|
|
|
566 |
$0 [options]
|
|
|
567 |
Options:
|
|
|
568 |
-i,
|
|
|
569 |
--interactive - do not read configuration file
|
|
|
570 |
--version - display version and exit
|
|
|
571 |
-c,
|
|
|
572 |
--client='xxx' - Client name for interactive mode
|
|
|
573 |
-s,
|
|
|
574 |
--serial='xxx' - Serial Number for interactive mode
|
|
|
575 |
-h,
|
|
|
576 |
--hostname='xxx' - override hostname
|
|
|
577 |
-m,
|
|
|
578 |
--modules=/path/ - override path to modules
|
|
|
579 |
--scripts=/path/ - override path to scripts
|
|
|
580 |
-p,
|
|
|
581 |
--periodic - runs modules designed to be run only weekly, monthly, etc...
|
|
|
582 |
END
|
|
|
583 |
}
|
20 |
rodolico |
584 |
|
154 |
rodolico |
585 |
|
|
|
586 |
# handle any command line parameters that may have been passed in
|
|
|
587 |
|
|
|
588 |
GetOptions (
|
|
|
589 |
'interactive|i' => \$interactive, # ask questions instead of using config file
|
|
|
590 |
'periodic|p' => \$periodic, # will do modules which are marked as periodic
|
|
|
591 |
'help|h' => \$help,
|
|
|
592 |
'version' => \$version,
|
|
|
593 |
'client|c=s' => \$configuration{clientName},
|
|
|
594 |
'serial|s=s' => \$configuration{serialNumber},
|
|
|
595 |
'hostname=s' => \$configuration{hostname},
|
|
|
596 |
'modules|m=s' => \$configuration{moduleDirs},
|
|
|
597 |
'scripts=s' => \$configuration{scriptDirs},
|
|
|
598 |
) or die "Error parsing command line\n";
|
|
|
599 |
|
|
|
600 |
|
|
|
601 |
if ( $help ) { &help() ; exit; }
|
|
|
602 |
if ( $version ) { use File::Basename; print basename($0) . " $VERSION\n"; exit; }
|
|
|
603 |
|
|
|
604 |
if ( $interactive ) {
|
|
|
605 |
%configuration = %{ &interactiveConfig( \%configuration ) };
|
|
|
606 |
} else {
|
|
|
607 |
# load the configuration file
|
194 |
rodolico |
608 |
%configuration = %{ &loadConfigurationFile( \$configurationFile, @confFileSearchPath) };
|
154 |
rodolico |
609 |
}
|
|
|
610 |
|
|
|
611 |
`touch $periodicOverrideFile` if $periodic; # tells periodic modules to run
|
|
|
612 |
|
|
|
613 |
#die Dumper (\%configuration );
|
135 |
rodolico |
614 |
|
2 |
rodolico |
615 |
# user did not define a serial number, so make something up
|
59 |
rodolico |
616 |
$configuration{'serialNumber'} = '' unless $configuration{'serialNumber'};
|
2 |
rodolico |
617 |
# oops, no client name (required) so tell them and exit
|
61 |
rodolico |
618 |
die "No client name defined in $configurationFile" unless $configuration{'clientName'};
|
2 |
rodolico |
619 |
|
135 |
rodolico |
620 |
&logIt( 0, 'Starting sysinfo Run' );
|
|
|
621 |
&logIt( 3, "Configuration is\n" . Data::Dumper->Dump( [\%configuration], [ qw($configuration) ] ) );
|
|
|
622 |
|
61 |
rodolico |
623 |
$TESTING = $configuration{'TESTING'} if defined $configuration{'TESTING'};
|
|
|
624 |
|
135 |
rodolico |
625 |
&logIt( 0, "Testing => $TESTING" ) if $TESTING;
|
76 |
rodolico |
626 |
|
|
|
627 |
|
2 |
rodolico |
628 |
my $System; # hash reference that will store all info we are going to send to the server
|
|
|
629 |
# some defaults.
|
174 |
rodolico |
630 |
$System->{'report'}->{'version'} = $VERSION->normal;
|
135 |
rodolico |
631 |
$System->{'report'}->{'date'} = $reportDate;
|
|
|
632 |
$System->{'report'}->{'client'} = $configuration{'clientName'};
|
|
|
633 |
$System->{'system'}->{'hostname'} = $configuration{'hostname'};
|
|
|
634 |
$System->{'system'}->{'serial'} = $configuration{'serialNumber'};
|
|
|
635 |
$System->{'system'}->{'UUID'} = $configuration{'UUID'};
|
2 |
rodolico |
636 |
|
135 |
rodolico |
637 |
&logIt( 3, "Initial System\n" . Data::Dumper->Dump( [$System], [qw( $System )] ) );
|
|
|
638 |
|
2 |
rodolico |
639 |
# process any modules in the system
|
59 |
rodolico |
640 |
foreach my $moduleDir ( @{$configuration{'moduleDirs'}} ) {
|
135 |
rodolico |
641 |
&logIt( 3, "Processing modules from $moduleDir" );
|
13 |
rodolico |
642 |
&ProcessModules( $System, "$moduleDir/" );
|
|
|
643 |
}
|
2 |
rodolico |
644 |
|
135 |
rodolico |
645 |
&logIt( 4, "After processing modules\n" . Data::Dumper->Dump( [$System], [qw( $System )] ) );
|
|
|
646 |
|
154 |
rodolico |
647 |
my $out = sprintf( "#sysinfo: %s YAML\n", $VERSION->normal ) . &Dump( $System );
|
2 |
rodolico |
648 |
|
135 |
rodolico |
649 |
&logIt( 4, 'At line number ' . __LINE__ . "\n" . Data::Dumper->Dump([$System],[qw($System)]) );
|
2 |
rodolico |
650 |
|
19 |
rodolico |
651 |
# load some global values for use in the script, if required
|
|
|
652 |
my $globals = {
|
191 |
rodolico |
653 |
'upload_type' => 'sysinfo',
|
154 |
rodolico |
654 |
'data version' => $DATA_VERSION->normal,
|
19 |
rodolico |
655 |
'report date' => $reportDate,
|
59 |
rodolico |
656 |
'client name' => $configuration{'clientName'},
|
|
|
657 |
'host name' => $configuration{'hostname'},
|
61 |
rodolico |
658 |
'serial number'=> $configuration{'serialNumber'},
|
|
|
659 |
'UUID' => $configuration{'UUID'}
|
19 |
rodolico |
660 |
};
|
62 |
rodolico |
661 |
|
135 |
rodolico |
662 |
&logIt( 4, "Globals initialized\n" . Data::Dumper->Dump([$globals],[qw($globals)]) );
|
|
|
663 |
|
|
|
664 |
if ( $TESTING ) {
|
|
|
665 |
open DATA, ">/tmp/sysinfo.testing.yaml" or die "Could not write to /tmp/sysinfo.testing.yaml: $!\n";
|
|
|
666 |
print DATA $out;
|
|
|
667 |
close DATA;
|
|
|
668 |
} else {
|
|
|
669 |
# and send the results to the server
|
|
|
670 |
if ( my $success = &sendResults( $globals, $configuration{'transports'}, $out, $configuration{'scriptDirs'} ) != 1 ) {
|
|
|
671 |
&logIt( 0, "Error $success while sending report from $configuration{'hostname'}" );
|
|
|
672 |
}
|
16 |
rodolico |
673 |
}
|
2 |
rodolico |
674 |
|
154 |
rodolico |
675 |
unlink ( $periodicOverrideFile ) if -e $periodicOverrideFile;
|
135 |
rodolico |
676 |
&logIt( 0, 'Ending sysinfo Run' );
|
|
|
677 |
|
197 |
rodolico |
678 |
if ( $configuration{'postRunScript'}{'script name'} ) {
|
|
|
679 |
my $script = $sourceDir . '/' . $configuration{'postRunScript'}{'script name'};
|
|
|
680 |
exec ( "$script $configurationFile" ) if -x $script;
|
|
|
681 |
}
|
154 |
rodolico |
682 |
|
9 |
rodolico |
683 |
1;
|