Subversion Repositories camp_sysinfo_client_3

Rev

Rev 257 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 257 Rev 258
Line 1... Line 1...
1
#! /usr/bin/env perl
1
#! /usr/bin/env perl
2
 
2
 
-
 
3
# Copyright (c) 2025, R. W. Rodolico
-
 
4
# All rights reserved.
-
 
5
#
-
 
6
# Redistribution and use in source and binary forms, with or without
-
 
7
# modification, are permitted provided that the following conditions are met:
-
 
8
#
-
 
9
# 1. Redistributions of source code must retain the above copyright notice, this
-
 
10
#    list of conditions and the following disclaimer.
-
 
11
#
-
 
12
# 2. Redistributions in binary form must reproduce the above copyright notice,
-
 
13
#    this list of conditions and the following disclaimer in the documentation
-
 
14
#    and/or other materials provided with the distribution.
-
 
15
#
-
 
16
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
 
17
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
 
18
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
 
19
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-
 
20
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
 
21
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
 
22
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
 
23
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
 
24
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
 
25
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
 
26
 
3
use warnings;
27
use warnings;
4
use strict;  
28
use strict;  
5
use open ':std', ':encoding(utf8)' ; # force all I/O, including disk and STD?, to use utf-8
29
use open ':std', ':encoding(utf8)' ; # force all I/O, including disk and STD?, to use utf-8
6
use utf8;                            # Source code encoded using UTF-8, used to ensure output from modules is UTF-8, see tabDelimitedToHash
30
use utf8;                            # Source code encoded using UTF-8, used to ensure output from modules is UTF-8, see tabDelimitedToHash
7
 
31
 
Line 141... Line 165...
141
# On freeBSD systems, was looking in wrong place for configuration file
165
# On freeBSD systems, was looking in wrong place for configuration file
142
#
166
#
143
# Version 3.2.0 20180320 RWR
167
# Version 3.2.0 20180320 RWR
144
# Major change in the configuration file format; All entries are loaded into 
168
# Major change in the configuration file format; All entries are loaded into 
145
# hash %configuration, so clientname is no longer $clientname, but is now
169
# hash %configuration, so clientname is no longer $clientname, but is now
146
# $configuration{'clientname'}
170
# $config{'clientname'}
147
# NOT backwards compatible
171
# NOT backwards compatible
148
# changed configuration to be loaded into hash (vs directly loaded into variables)
172
# changed configuration to be loaded into hash (vs directly loaded into variables)
149
# added UUID to configuration file
173
# added UUID to configuration file
150
#
174
#
151
# Version 3.2.1 20180424 RWR
175
# Version 3.2.1 20180424 RWR
Line 155... Line 179...
155
# Version 3.3.0 20190419 RWR
179
# Version 3.3.0 20190419 RWR
156
# Converted to use YAML config file
180
# Converted to use YAML config file
157
#
181
#
158
# Version 3.4.0 20191111 RWR
182
# Version 3.4.0 20191111 RWR
159
# adding logging with priority. logging is a hash inside of %configuration which contains the following
183
# adding logging with priority. logging is a hash inside of %configuration which contains the following
160
# $configuration{ 'logging' } = {
184
# $config{ 'logging' } = {
161
#    'log type'  => 'string',
185
#    'log type'  => 'string',
162
#    'log level' => #,
186
#    'log level' => #,
163
#    'other params' => something,
187
#    'other params' => something,
164
# };
188
# };
165
#
189
#
Line 227... Line 251...
227
# contains the directory our script is in
251
# contains the directory our script is in
228
my $sourceDir = dirname( abs_path( __FILE__ ) );
252
my $sourceDir = dirname( abs_path( __FILE__ ) );
229
 
253
 
230
# define the version number
254
# define the version number
231
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
255
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
-
 
256
use version 0.77;
232
use version 0.77; our $VERSION = version->declare("v4.0.0");
257
our $VERSION = version->declare("v4.0.0");
233
our $DATA_VERSION = version->declare( 'v3.7.2' ); # used in sending the data file. sets version of XML/YAML data file
258
our $DATA_VERSION = version->declare( 'v3.7.2' ); # used in sending the data file. sets version of XML/YAML data file
234
 
259
 
235
# see https://perldoc.perl.org/Getopt/Long.html
260
# see https://perldoc.perl.org/Getopt/Long.html
236
use Getopt::Long;
261
use Getopt::Long;
237
# allow -vvn (ie, --verbose --verbose --dryrun)
262
# allow -vvn (ie, --verbose --verbose --dryrun)
Line 245... Line 270...
245
 
270
 
246
my $indentLevel = 2; # number of spaces to indent per level in XML or YAML
271
my $indentLevel = 2; # number of spaces to indent per level in XML or YAML
247
 
272
 
248
my $reportDate = &timeStamp(); # set report date
273
my $reportDate = &timeStamp(); # set report date
249
 
274
 
250
my $interactive = 0; # if set to 1, will go into interactive mode and output to local file
-
 
251
my $periodicOverrideFile = '/tmp/sysinfo.firstrun'; # if this file exists, library.pm will tell all periodic modules to run anyway
275
my $periodicOverrideFile = '/tmp/sysinfo.firstrun'; # if this file exists, library.pm will tell all periodic modules to run anyway
252
my $periodic = 0; # if set to 1, will do modules which are only supposed to run weekly, monthly, etc...
276
my $periodic = 0; # if set to 1, will do modules which are only supposed to run weekly, monthly, etc...
253
 
277
 
254
my $version;
278
my $version;
255
my $help;
279
my $help;
-
 
280
my $configFile; # optional path to configuration file specified via command line
-
 
281
my $configType; # optional config file type: yaml, json, or datatransport
-
 
282
my $outputType; # optional output type: yaml, json, datatransport
256
 
283
 
257
my %configuration = (
284
my $config = {
258
   'logging' => { 'log type' => 'cache', 'log level' => 0 },    # if set, will point to logging
285
   'logging' => { 'log type' => 'cache', 'log level' => 0 },    # if set, will point to logging
259
   'moduleDirs' => ["$sourceDir/modules"], # search paths for modules
286
   'moduleDirs' => ["$sourceDir/modules"], # search paths for modules
260
   'scriptDirs' => ["$sourceDir/scripts"], # search paths for scripts
287
   'scriptDirs' => ["$sourceDir/scripts"], # search paths for scripts
261
   'clientName' => '',  # Required!! Must be set in conf file (no defaults)
288
   'clientName' => '',  # Required!! Must be set in conf file (no defaults)
262
   'serialNumber' => '', # serial number of machine
289
   'serialNumber' => '', # serial number of machine
263
   'UUID'         => '', # UUID of machine
290
   'UUID'         => '', # UUID of machine
264
   'transports'   => {'3' => { '-name-' => 'saveLocal', 'sendScript' => 'save_local', 'output directory' => "$sourceDir/reports" }  }, # hash with various transports
291
   'transports'   => {'3' => { '-name-' => 'saveLocal', 'sendScript' => 'save_local', 'output directory' => "$sourceDir/reports" }  }, # hash with various transports
265
   'hostname' => &getHostName() # fully qualified host name of machine
292
   'hostname' => &getHostName() # fully qualified host name of machine
266
);
293
};
267
 
294
 
268
 
295
 
269
 
296
 
270
#######################################################
297
#######################################################
271
#
298
#
Line 294... Line 321...
294
# would then be sent via e-mail to an administrative account, possibly root
321
# would then be sent via e-mail to an administrative account, possibly root
295
#
322
#
296
#######################################################
323
#######################################################
297
sub sendResults {
324
sub sendResults {
298
   my ( $globals, $transports, $message, $scriptDirectory ) = @_;
325
   my ( $globals, $transports, $message, $scriptDirectory ) = @_;
299
   &logIt( \%configuration,  3, "Entering sendResults" );
326
   &logIt( $config,  3, "Entering sendResults" );
300
   foreach my $key ( sort { $a <=> $b } %$transports ) {
327
   foreach my $key ( sort { $a <=> $b } %$transports ) {
301
      if ( $transports->{$key}->{'sendScript'} ) {
328
      if ( $transports->{$key}->{'sendScript'} ) {
302
         &logIt( \%configuration,  3, "Trying to find file " . $transports->{$key}->{'sendScript'} . " in " . join( "\n\t", @{$scriptDirectory} ) );
329
         &logIt( $config,  3, "Trying to find file " . $transports->{$key}->{'sendScript'} . " in " . join( "\n\t", @{$scriptDirectory} ) );
303
         my $sendScript = &findFile( $transports->{$key}->{'sendScript'}, $scriptDirectory );
330
         my $sendScript = &findFile( $transports->{$key}->{'sendScript'}, $scriptDirectory );
304
         if ( $sendScript ) {
331
         if ( $sendScript ) {
305
            # check if we have doit defined from previous iteration and, if so, undefine it
332
            # check if we have doit defined from previous iteration and, if so, undefine it
306
            undef &doit if exists &doit;
333
            undef &doit if exists &doit;
307
            # load the chosen script into memory
334
            # load the chosen script into memory
Line 310... Line 337...
310
            while ( my ( $gkey, $value ) = each %$globals ) { 
337
            while ( my ( $gkey, $value ) = each %$globals ) { 
311
               $transports->{$key}->{$gkey} = $value; 
338
               $transports->{$key}->{$gkey} = $value; 
312
            }
339
            }
313
            # do variable substitution for any values which need it
340
            # do variable substitution for any values which need it
314
            foreach my $thisOne ( keys %{$transports->{$key}} ) {
341
            foreach my $thisOne ( keys %{$transports->{$key}} ) {
315
               &logIt( \%configuration,  4, "$thisOne" );
342
               &logIt( $config,  4, "$thisOne" );
316
               if ( $transports->{$key}->{$thisOne} =~ m/(\$configuration\{'hostname'\})|(\$reportDate)|(\$configuration\{'clientName'\})|(\$configuration\{'serialNumber'\})/ ) {
343
               if ( $transports->{$key}->{$thisOne} =~ m/(\$config\{'hostname'\})|(\$reportDate)|(\$config\{'clientName'\})|(\$config\{'serialNumber'\})/ ) {
317
                  $transports->{$key}->{$thisOne} = eval "\"$transports->{$key}->{$thisOne}\"";
344
                  $transports->{$key}->{$thisOne} = eval "\"$transports->{$key}->{$thisOne}\"";
318
               }
345
               }
319
            }
346
            }
320
            
347
            # add all global keys/values into transport hash
321
            #%$transports{$key}{keys %$globals} = values %$globals;
348
            $transports->{$key}->{keys %$globals} = values %$globals;
322
            #print Dumper( $$transports[$key] );
349
            #print Dumper( $$transports[$key] );
323
            #next;
350
            #next;
324
            # execute the "doit" sub from that script
351
            # execute the "doit" sub from that script
325
            &logIt( \%configuration,  3, $message );
352
            &logIt( $config,  3, $message );
326
            my $return = &doit( $transports->{$key}, $message );
353
            my $return = &doit( $transports->{$key}, $message );
327
            return $return if ( $return == 1 );
354
            return $return if ( $return == 1 );
328
         } else {
355
         } else {
329
            &logIt( \%configuration,  0,"Could not find " . $$transports[$key]{'sendScript'} . ", trying next transport" );
356
            &logIt( $config,  0,"Could not find " . $$transports[$key]{'sendScript'} . ", trying next transport" );
330
         } # if..else
357
         } # if..else
331
      } # if
358
      } # if
332
   } # foreach
359
   } # foreach
333
   # if we made it here, we have not sent the report, so just return it to the user
360
   # if we made it here, we have not sent the report, so just return it to the user
334
   # if called from a cron job, it will (hopefully) be sent to root
361
   # if called from a cron job, it will (hopefully) be sent to root
335
   &logIt( \%configuration,  0, 'Error, reached ' . __LINE__ . " which should not happen, message was\n$message" );
362
   &logIt( $config,  0, 'Error, reached ' . __LINE__ . " which should not happen, message was\n$message" );
336
   print $message;
363
   print $message;
337
   return 1;
364
   return 1;
338
}
365
}
339
 
366
 
340
#######################################################
367
#######################################################
Line 343... Line 370...
343
#
370
#
344
# return hostname from hostname -f
371
# return hostname from hostname -f
345
#
372
#
346
#######################################################
373
#######################################################
347
sub getHostName {
374
sub getHostName {
348
   &logIt( \%configuration,  3, "Entering getHostName" );
375
   &logIt( $config,  3, "Entering getHostName" );
349
   my $hostname = lc $^O eq 'mswin32' ? `hostname` : `hostname -f`;
376
   my $hostname = lc $^O eq 'mswin32' ? `hostname` : `hostname -f`;
350
   chomp $hostname;
377
   chomp $hostname;
351
   return $hostname;
378
   return $hostname;
352
}
379
}
353
 
380
 
354
#######################################################
381
#######################################################
355
#
382
#
356
# escapeForYAML
-
 
357
#
-
 
358
# Escapes values put into YAML report
383
# validatePermission ( $file )
359
#
384
#
-
 
385
# Checks that file is owned by root, and has permission
-
 
386
# 0700 or less
-
 
387
# 
360
# DEPRECATED AS OF VERSION 3.3.0
388
# Returns empty string on success, error message
361
# uses YAML::Tiny
389
# on failure
362
#
390
#
363
#######################################################
391
#######################################################
364
#sub escapeForYAML {
-
 
365
#   my $value = shift;
-
 
366
#   $value =~ s/'/\\'/gi; # escape single quotes
-
 
367
#   $value =~ s/"/\\"/gi; # escape double quotes
-
 
368
#   # pound sign indicates start of a comment and thus loses part
-
 
369
#   # of strings. Surrounding it by double quotes in next statement
-
 
370
#   # allows 
-
 
371
#   $value = '"' . $value . '"' if ( $value =~ m/[#:]/ );
-
 
372
#   return $value;
-
 
373
#}
-
 
374
 
-
 
375
#######################################################
-
 
376
#
-
 
377
# hashToYAML( $hashRef, $indent )
-
 
378
#
-
 
379
# Converts a hash to a YAML string
-
 
380
#
-
 
381
# NOTE: This routine recursively calls itself for every level
-
 
382
#       in the hash
-
 
383
#
-
 
384
# Parameters
-
 
385
#     $hashref - reference (address) of a hash
-
 
386
#     $indent  - current indent level, defaults to 0
-
 
387
#
-
 
388
# Even though there are some very good libraries that do this
-
 
389
# I chose to hand-code it so sysinfo can be run with no libraries
-
 
390
# loaded. I chose to NOT do a full implementation, so special chars
-
 
391
# that would normally be escaped are not in here. 
-
 
392
# However, I followed all the RFC for the values that were given, so
-
 
393
# assume any YAML reader can parse this
-
 
394
# NOTE: YAML appears to give a resulting file 1/3 smaller than the above
-
 
395
#       XML, and compresses down in like manner
-
 
396
#
-
 
397
# DEPRECATED AS OF VERSION 3.3.0
-
 
398
# uses YAML::Tiny
-
 
399
#
-
 
400
#######################################################
-
 
401
#sub hashToYAML {
-
 
402
#   my ($hashRef, $indent) = @_;
-
 
403
#   $indent = 0 unless $indent; # default to 0 if not defined
-
 
404
#   
-
 
405
#   my $output; # where the output is stored
-
 
406
#   foreach my $key ( keys %$hashRef ) { # for each key in the current reference
-
 
407
#      print "Looking at $key\n" if $TESTING > 3;
-
 
408
#      # see http://www.perlmonks.org/?node_id=175651 for isa function
-
 
409
#      if ( UNIVERSAL::isa( $$hashRef{$key}, 'HASH' ) ) { # is the value another hash?
-
 
410
#            # NOTE: unlike xml, indentation is NOT optional in YAML, so the following line verifies $indentlevel is non-zero
-
 
411
#            #       and, if it is, uses a default 3 character indentation
-
 
412
#            $output .= (' ' x $indent ) . &escapeForYAML($key) . ":\n" . # key, plus colon, plus newline
-
 
413
#                    &hashToYAML( $$hashRef{$key}, $indent+($indentLevel ? $indentLevel : 3) ) . # add results of recursive call
-
 
414
#                    "\n";
-
 
415
#      } elsif ( UNIVERSAL::isa( $$hashRef{$key}, 'ARRAY' ) ) { # is it an array? ignore it
-
 
416
#      } else { # it is a scalar, so just do <key>value</key>
-
 
417
#         $output .= (' ' x $indent ) . &escapeForYAML($key) . ': ' . &escapeForYAML($$hashRef{$key}) . "\n";
-
 
418
#      }
-
 
419
#   }
-
 
420
#   return $output;
-
 
421
#}
-
 
422
 
392
 
-
 
393
sub validatePermission {
-
 
394
   my $file = shift;
-
 
395
   # on Windows system, we can not look for ownership and exect status
-
 
396
   return '' if lc $^O eq 'mswin32'; 
-
 
397
   &logIt( $config,  3, "Entering validatePermission with $file" );
-
 
398
   return "$file - Not a file" unless -f $file;
-
 
399
   # in test mode, do not check permissions
-
 
400
   return '' if $TESTING;
-
 
401
   my $return;
-
 
402
   # must be owned by root
-
 
403
   my $owner = (stat($file))[4];
-
 
404
   # print "\tOwner = $owner\n";
-
 
405
   $return .= " - Bad Owner [$owner]" if $owner;
-
 
406
   # must not have any permissions for group or world
-
 
407
   # ie, 0700 or less
-
 
408
   my $mode = sprintf( '%04o', (stat($file))[2] & 07777 );
-
 
409
   # print "\tMode = $mode\n";
-
 
410
   $return .= " - Bad Permission [$mode]" unless $mode =~ m/0.00/;
-
 
411
   &logIt( $config, 4, "validatePermission: $file permissions check result: " . ($return ? $return : "OK") );
-
 
412
   return $return ? $file . $return : '';
-
 
413
}
423
 
414
 
424
#######################################################
415
#######################################################
425
#
416
#
426
# tabDelimitedToHash ($hashRef, $tabdelim)
417
# tabDelimitedToHash ($hashRef, $tabdelim)
427
#
418
#
Line 436... Line 427...
436
#
427
#
437
#
428
#
438
#######################################################
429
#######################################################
439
sub tabDelimitedToHash {
430
sub tabDelimitedToHash {
440
   my ($hashRef, $tabdelim) = @_;
431
   my ($hashRef, $tabdelim) = @_;
441
   &logIt( \%configuration,  3, "Entering tabDelimitedToHash" );
432
   &logIt( $config, 3, "Entering tabDelimitedToHash" );
442
   
433
   
443
   utf8::encode( $tabdelim ); # ensure this is all utf8, convert if necessary
434
   utf8::encode( $tabdelim ); # ensure this is all utf8, convert if necessary
444
 
435
 
445
   foreach my $line ( split( "\n", $tabdelim ) ) { # split on newlines, then process each line in turn
436
   foreach my $line ( split( "\n", $tabdelim ) ) { # split on newlines, then process each line in turn
446
      $line =~ s/'/\\'/gi; # escape single quotes
437
      $line =~ s/'/\\'/gi; # escape single quotes
Line 457... Line 448...
457
      #print STDERR "$command\n"; 
448
      #print STDERR "$command\n"; 
458
      eval $command; # eval the string to make the actual assignment
449
      eval $command; # eval the string to make the actual assignment
459
   }
450
   }
460
}
451
}
461
 
452
 
-
 
453
 
-
 
454
 
462
#######################################################
455
#######################################################
463
#
456
#
464
# validatePermission ( $file )
457
# getModulesFromDir( $moduleDir )
465
#
458
#
466
# Checks that file is owned by root, and has permission
459
# Retrieves a list of valid module files from the specified
-
 
460
# module directory
-
 
461
#
467
# 0700 or less
462
# Parameters
-
 
463
#     $moduleDir - full path to directory containing modules
468
# 
464
#
-
 
465
# Returns
469
# Returns empty string on success, error message
466
#     Reference to an array of valid module file paths
-
 
467
#
470
# on failure
468
# Module files must:
-
 
469
#   - Have names consisting only of alphanumerics and underscores
-
 
470
#   - Begin with an alphanumeric character
-
 
471
#   - Pass permission validation (owned by root, 0700 or less)
471
#
472
#
472
#######################################################
473
#######################################################
473
 
-
 
474
sub validatePermission {
-
 
475
   my $file = shift;
-
 
476
   # on Windows system, we can not look for ownership and exect status
-
 
477
   return '' if lc $^O eq 'mswin32'; 
-
 
478
   &logIt( \%configuration,  3, "Entering validatePermission with $file" );
-
 
479
   return "$file - Not a file" unless -f $file;
-
 
480
   # in test mode, do not check permissions
-
 
481
   return '' if $TESTING;
-
 
482
   my $return;
-
 
483
   # must be owned by root
-
 
484
   my $owner = (stat($file))[4];
-
 
485
   # print "\tOwner = $owner\n";
-
 
486
   $return .= " - Bad Owner [$owner]" if $owner;
-
 
487
   # must not have any permissions for group or world
-
 
488
   # ie, 0700 or less
-
 
489
   my $mode = sprintf( '%04o', (stat($file))[2] & 07777 );
-
 
490
   # print "\tMode = $mode\n";
-
 
491
   $return .= " - Bad Permission [$mode]" unless $mode =~ m/0.00/;
-
 
492
   return $return ? $file . $return : '';
-
 
493
}
-
 
494
 
-
 
495
sub getModulesFromDir {
474
sub getModulesFromDir {
496
   my $moduleDir = shift;
475
   my $moduleDir = shift;
497
   $moduleDir .= '/' unless substr( $moduleDir, -1 ) eq '/';
476
   $moduleDir .= '/' unless substr( $moduleDir, -1 ) eq '/';
498
   # open the module directory
477
   # open the module directory
499
   return unless -d $moduleDir;
478
   return unless -d $moduleDir;
Line 501... Line 480...
501
   # and get all files which are nothing but alpha-numerics and underscores (must begin with alpha-numeric)
480
   # and get all files which are nothing but alpha-numerics and underscores (must begin with alpha-numeric)
502
   # ignore anything else, including directories
481
   # ignore anything else, including directories
503
   # then, prepend the directory name (map), then validate they are ready to be used (validatePermissions)
482
   # then, prepend the directory name (map), then validate they are ready to be used (validatePermissions)
504
   my @modules = grep{ ! &validatePermission( $_ ) } map { "$moduleDir$_" } grep { /^[a-zA-Z0-9][a-zA-Z0-9_]*$/ } readdir( $dh );
483
   my @modules = grep{ ! &validatePermission( $_ ) } map { "$moduleDir$_" } grep { /^[a-zA-Z0-9][a-zA-Z0-9_]*$/ } readdir( $dh );
505
   closedir $dh;
484
   closedir $dh;
-
 
485
   &logIt( $config, 4, "getModulesFromDir: Modules found in $moduleDir: " . join(", ", @modules) );
506
   return \@modules;
486
   return \@modules;
507
   
487
   
508
}
488
}
509
 
489
 
510
#######################################################
490
#######################################################
Line 526... Line 506...
526
# on failure, the returned output of the script is assumed to be an error message
506
# on failure, the returned output of the script is assumed to be an error message
527
# and is displayed on STDERR
507
# and is displayed on STDERR
528
#######################################################
508
#######################################################
529
sub ProcessModules {
509
sub ProcessModules {
530
   my ( $system, $moduleDir ) = @_;
510
   my ( $system, $moduleDir ) = @_;
531
   &logIt( \%configuration,  3, "Entering processModules" );
511
   &logIt( $config,  3, "Entering processModules using $moduleDir" );
532
   foreach my $modFile ( sort @{ &getModulesFromDir( $moduleDir ) } ) { # for each valid script
512
   foreach my $modFile ( sort @{ &getModulesFromDir( $moduleDir ) } ) { # for each valid script
533
      next unless -f $modFile; # skip any directories, symbolic links, etc...
513
      next unless -f $modFile; # skip any directories, symbolic links, etc...
534
      my $output = do $modFile;
514
      my $output = do $modFile;
535
      &logIt( \%configuration, 4, "Output of module $modFile is\n$output\n" );
515
      &logIt( $config, 4, "Output of module $modFile is\n$output\n" );
536
      if ( defined $output && $output ) {
516
      if ( defined $output && $output ) {
537
         if ( substr( $output, 1,6) eq 'error:' ) { # we have an error
517
         if ( substr( $output, 1,6) eq 'error:' ) { # we have an error
538
            &logIt( \%configuration,  1, $output );
518
            &logIt( $config,  1, $output );
539
         } else {
519
         } else {
540
            &tabDelimitedToHash( $system, $output );
520
            &tabDelimitedToHash( $system, $output );
541
         }
521
         }
542
      } else {
522
      } else {
543
         print "Script $modFile failed to compile with error\n$@\n";
523
         print "Script $modFile failed to compile with error\n$@\n";
544
      }
524
      }
545
      &logIt( \%configuration,  3, "Processing module $moduleDir$modFile");
525
      &logIt( $config,  3, "Processing module $moduleDir$modFile");
546
   } # foreach
526
   } # foreach
547
   # add sysinfo-client (me) to the software list, since we're obviously installed
527
   # add sysinfo-client (me) to the software list, since we're obviously installed
548
   &tabDelimitedToHash( $system, "software\tsysinfo-client\tversion\t$main::VERSION\n" );
528
   &tabDelimitedToHash( $system, "software\tsysinfo-client\tversion\t$main::VERSION\n" );
549
}
529
}
550
 
530
 
551
sub getDMIDecode {
-
 
552
   my ( $key, $type ) = @_;
-
 
553
   my $command = 'dmidecode ';
-
 
554
   $command .= "-t $type " if $type;
531
#######################################################
555
   $command .= " | grep -i '$key'";
-
 
556
   my $value = `$command`;
532
# initReport( $config )
557
   chomp $value;
-
 
558
   if ( $value =~ m/:\s*(.*)\s*$/ ) {
-
 
559
      return $1;
-
 
560
   } else {
-
 
561
      return '';
-
 
562
   }
-
 
563
}
533
#
-
 
534
# Initialize the report structure with configuration data
564
 
535
#
565
sub interactiveConfig {
536
# Parameters
566
   my $config = shift;
-
 
567
   $config->{'moduleDirs'} = $config->{'moduleDirs'}[0];
-
 
568
   $config->{'scriptDirs'} = $config->{'scriptDirs'}[0];
537
#     $config - reference to configuration hash
569
   $config->{'UUID'} = getDMIDecode( 'uuid', 'system' ) unless $config->{'UUID'};
-
 
570
   $config->{'serialNumber'} = getDMIDecode( 'serial number', 'system' ) unless $config->{'serialNumber'};
-
 
571
   
538
#
572
   my %menu = (
539
# Returns
573
      1 => {'prompt' => 'Host Name', 'key' => 'hostname' },
-
 
574
      2 => {'prompt' => 'Client Name', 'key' => 'clientName' },
-
 
575
      3 => {'prompt' => 'Serial Number', 'key' => 'serialNumber' },
-
 
576
      4 => {'prompt' => 'UUID', 'key' => 'UUID' },
540
#     Reference to initialized report hash
577
      5 => {'prompt' => 'Modules Directory', 'key' => 'moduleDirs' },
-
 
578
      6 => {'prompt' => 'Scripts Directory', 'key' => 'scriptDirs' }
-
 
579
   );
541
#
580
   my $choice = 'quit';
-
 
581
   while ( $choice ) {
-
 
582
      foreach my $menuItem ( sort keys %menu ) {
542
# Creates the initial report structure with:
583
         print "$menuItem\. " . $menu{$menuItem}{'prompt'} . ': ' . $config->{$menu{$menuItem}{'key'}} . "\n";
-
 
584
      }
-
 
585
      print "Enter Menu Item to change, or press Enter to proceed ";
543
#   - Report metadata (version, date, client)
586
      $choice = <>;
-
 
587
      chomp $choice;
-
 
588
      last unless $choice;
-
 
589
      print $menu{$choice}{'prompt'} . ' [' . $config->{$menu{$choice}{'key'}} . '] : ';
-
 
590
      my $value = <>;
-
 
591
      chomp $value;
-
 
592
      $config->{$menu{$choice}{'key'}} = $value if ($value);
544
#   - System information (hostname, serial, UUID)
593
   }
-
 
594
   $config->{'moduleDirs'} = [ $config->{'moduleDirs'} ];
545
#   - Additional configuration values (excluding moduleDirs,
595
   $config->{'scriptDirs'} = [ $config->{'scriptDirs'} ];
546
#     transports, scriptDirs, and logging)
596
   return $config;
-
 
597
}
547
#
598
 
-
 
599
# Initialize the report with some stuff from the program itself and the configuration file
548
#######################################################
600
sub initReport {
549
sub initReport {
601
   my $config = shift;
550
   my $config = shift;
602
   my %ignore = ( # list of config keys to ignore. Using hash for fast lookup
551
   my %ignore = ( # list of config keys to ignore. Using hash for fast lookup
603
         'moduleDirs' => 1,
552
         'moduleDirs' => 1,
604
         'transports' => 1,
553
         'transports' => 1,
Line 618... Line 567...
618
      $report->{'system'}->{$key} = $config->{$key}; # simply copy to the system part of the report
567
      $report->{'system'}->{$key} = $config->{$key}; # simply copy to the system part of the report
619
   }
568
   }
620
   return $report;
569
   return $report;
621
}
570
}
622
 
571
 
-
 
572
#######################################################
-
 
573
#
-
 
574
# detectConfigType( $filename )
-
 
575
#
-
 
576
# Detects the configuration file type by examining content
-
 
577
# json and DataTransport have unique starting lines
-
 
578
# yaml will usually start with --- or key: value, but may be
-
 
579
# comments in the first few lines, so checks up to 5 lines
-
 
580
#
-
 
581
# Parameters:
-
 
582
#   $lines - array reference containing lines of configuration content
-
 
583
#
-
 
584
# Returns:
-
 
585
#   'yaml', 'json', 'datatransport', or undef
-
 
586
#
-
 
587
#######################################################
-
 
588
sub detectConfigType {
-
 
589
   my $lines = shift;
-
 
590
   # first look at first line for unique identifiers. First line comments
-
 
591
   # may contain type info
-
 
592
   if ( $lines->[0] =~ m/^.*(yaml|datatransport).*/i ) {
-
 
593
      &logIt( $config,  3, "Configuration file type hint found in first line comment" );
-
 
594
      return lc $1;
-
 
595
   }
-
 
596
   # look through first five lines, there should be some indicator
-
 
597
   for ( my $line = 0; $line < 5 && $lines->[$line]; $line++ ) {
-
 
598
      return 'yaml' if $lines->[$line] =~ /^#.*YAML/; 
-
 
599
      return 'yaml' if $lines->[$line] =~ /^---/;  # YAML document start
-
 
600
      return 'yaml' if $lines->[$line] =~ /^\s*\w+:\s*/;  # YAML key: value
-
 
601
      return 'json' if $lines->[$line] =~ /^#.*JSON/;  # skip comments
-
 
602
      return 'json' if $lines->[$line] =~ /^\s*[{\[]/;  # JSON starts with { or [
-
 
603
   }
-
 
604
   return undef;
-
 
605
}
-
 
606
 
-
 
607
#######################################################
-
 
608
#
-
 
609
# loadConfig( $confStr, $type )
-
 
610
#
-
 
611
# Load configuration file based on specified or detected type
-
 
612
# adds 'dataType' key to returned hashref indicating type used
-
 
613
#
-
 
614
# Parameters:
-
 
615
#   $confStr - configuration content as an arrayref of string
-
 
616
#   $type     - optional type: 'yaml', 'json', 'datatransport'
-
 
617
#
-
 
618
# Returns:
-
 
619
#   hashref of configuration
-
 
620
#
-
 
621
#######################################################
-
 
622
sub loadConfig {
-
 
623
   my ($confStr, $type) = @_;
-
 
624
   
-
 
625
   my $return = {};
-
 
626
 
623
# simple display if --help is passed
627
   # Auto-detect if type not specified
-
 
628
   $type = &detectConfigType($confStr) unless $type;
-
 
629
   
-
 
630
   die "Cannot determine configuration file type\n" unless $type;
-
 
631
   
-
 
632
   $type = lc($type);  # normalize to lowercase
-
 
633
   $confStr = join("\n", @$confStr); # join arrayref into single string for processing
-
 
634
 
-
 
635
   if ($type eq 'datatransport') {
-
 
636
      # Try to load DataTransport module
-
 
637
      eval { require DataTransport; };
-
 
638
      if ($@) {
-
 
639
         die "DataTransport module not available: $@\n";
-
 
640
      }
-
 
641
      
-
 
642
      my $dt = DataTransport->new();
-
 
643
      $return = $dt->decode($confStr);
-
 
644
      die "Failed to read DataTransport content\n" unless $return;
-
 
645
   } elsif ($type eq 'json') {
-
 
646
      # Try to load JSON module
-
 
647
      eval { require JSON; };
-
 
648
      if ($@) {
-
 
649
         die "JSON module not available: $@\n";
-
 
650
      }
-
 
651
      $return = JSON::decode_json($confStr);
-
 
652
   } elsif ($type eq 'yaml') {
-
 
653
      # Use YAML::Tiny directly for single file loading
-
 
654
      eval { require YAML::Tiny; };
-
 
655
      if ($@) {
-
 
656
         die "YAML::Tiny module not available: $@\n";
-
 
657
      }
-
 
658
      
-
 
659
      $return = YAML::Tiny->read_string($confStr);
-
 
660
      die "Failed to read YAML content\n" unless $return;
-
 
661
      $return = $return->[0]; # only return the first document
-
 
662
   } else {
-
 
663
      die "Unknown configuration type: $type\n";
-
 
664
   }
-
 
665
   $return->{'dataType'} = $type; # store the type used
-
 
666
   return $return;
-
 
667
}
-
 
668
 
-
 
669
########################################################
-
 
670
# hashToReportString( $report, $outputType )
-
 
671
#
-
 
672
# Converts report hash to specified output format string
-
 
673
## Parameters:
-
 
674
#   $report - reference to report hash
-
 
675
#   $outputType - output format: 'yaml', 'json', 'datatransport'
-
 
676
# Returns:
-
 
677
#   string containing report in specified format
-
 
678
########################################################
-
 
679
sub hashToReportString {
-
 
680
   my ( $report, $outputType ) = @_;
-
 
681
   &logIt( $config,  3, "Entering hashToReportString with outputType $outputType" );
-
 
682
   my $output;
-
 
683
   if ( $outputType eq 'yaml' ) {
-
 
684
      eval { require YAML::Tiny; };
-
 
685
      if ($@) {
-
 
686
         die "YAML::Tiny module not available: $@\n";
-
 
687
      }
-
 
688
      $output = YAML::Tiny->new( $report )->write_string();
-
 
689
   } elsif ( $outputType eq 'json' ) {
-
 
690
      eval { require JSON; };
-
 
691
      if ($@) {
-
 
692
         die "JSON module not available: $@\n";
-
 
693
      }
-
 
694
      $output = JSON::encode_json( $report );
-
 
695
   } elsif ( $outputType eq 'datatransport' ) {
-
 
696
      eval { require DataTransport; };
-
 
697
      if ($@) {
-
 
698
         die "DataTransport module not available: $@\n";
-
 
699
      }
-
 
700
      my $dt = DataTransport->new();
-
 
701
      $output = $dt->encode( $report );
-
 
702
   } else {
-
 
703
      die "Unknown output type: $outputType\n";
-
 
704
   }
-
 
705
   return $output;
-
 
706
}
-
 
707
 
-
 
708
#######################################################
-
 
709
#
-
 
710
# help()
-
 
711
#
-
 
712
# Display help message showing command-line options
-
 
713
#
-
 
714
# Parameters
-
 
715
#     None
-
 
716
#
-
 
717
# Returns
-
 
718
#     None (prints to STDOUT)
-
 
719
#
-
 
720
# Shows program version and available command-line options:
-
 
721
#   -f, --config       : Specify configuration file path
-
 
722
#   --configtype       : Specify config format (yaml/json/datatransport)
-
 
723
#   --version          : Display version
-
 
724
#   --help             : Display help message
-
 
725
#   -p, --periodic     : Run periodic modules
-
 
726
#   -t, --test         : Test mode (output to /tmp)
-
 
727
#
-
 
728
# Note: For interactive configuration, use sysinfo-client-interactive
-
 
729
#
-
 
730
#######################################################
624
sub help {
731
sub help {
625
   use File::Basename;
732
   use File::Basename;
626
   print basename($0) . " $VERSION (data $DATA_VERSION)\n";
733
   print basename($0) . " $VERSION (data $DATA_VERSION)\n";
627
   print <<END
734
   print <<END
628
$0 [options]
735
$0 [options]
-
 
736
 
-
 
737
For interactive configuration, use sysinfo-client-interactive instead.
-
 
738
 
629
Options:
739
Options:
630
   -i,
-
 
631
   --interactive    - do not read configuration file
-
 
632
   --version        - display version and exit
740
   --version        - display version and exit
633
   -c,
-
 
634
   --client='xxx'   - Client name for interactive mode
741
   --help           - display this help message
635
   -s,
742
   -f,
636
   --serial='xxx'   - Serial Number for interactive mode
-
 
637
   -h,
-
 
638
   --hostname='xxx' - override hostname
-
 
639
   -m,
-
 
640
   --modules=/path/ - override path to modules
743
   --config=/path/  - path to configuration file (overrides default search)
641
   --scripts=/path/ - override path to scripts
744
   --configtype=xxx - config file type: yaml, json, or datatransport (autodetects if not specified)
642
   -p,
745
   -p,
643
   --periodic       - runs modules designed to be run only weekly, monthly, etc...
746
   --periodic       - runs modules designed to be run only weekly, monthly, etc...
644
   -t,
747
   -t,
645
   --test           - If non-zero, runs but places output in /tmp
748
   --test           - If set, runs but places output in /tmp
646
END
749
END
647
}
750
}
648
 
751
 
649
 
752
 
650
# handle any command line parameters that may have been passed in
753
# Process command line options
651
 
-
 
652
GetOptions (
754
GetOptions (
653
            'interactive|i' => \$interactive, # ask questions instead of using config file
-
 
654
            'periodic|p'    => \$periodic,    # will do modules which are marked as periodic
755
            'periodic|p'    => \$periodic,    # will do modules which are marked as periodic
-
 
756
            'config|f=s'    => \$configFile,  # path to configuration file
-
 
757
            'configtype=s'  => \$configType,  # config file type: yaml, json, datatransport
-
 
758
            'outputType|o=s' => \$outputType,  # output type: xml, yaml
655
            'help|h'        => \$help,
759
            'help'          => \$help,
656
            'version'       => \$version,
760
            'version'       => \$version,
657
            'client|c=s'    => \$configuration{clientName},
-
 
658
            'serial|s=s'    => \$configuration{serialNumber},
-
 
659
            'hostname=s'    => \$configuration{hostname},
-
 
660
            'modules|m=s'   => \$configuration{moduleDirs},
-
 
661
            'scripts=s'     => \$configuration{scriptDirs},
-
 
662
            'test|t=s'      => \$TESTING
761
            'test|t'      => \$TESTING
663
            ) or die "Error parsing command line\n";
762
            ) or die "Error parsing command line\n";
664
 
763
 
665
                  
-
 
666
if ( $help ) { &help() ; exit; }
764
if ( $help ) { &help() ; exit; }
667
if ( $version ) { use File::Basename; print basename($0) . " $VERSION (data $DATA_VERSION)\n"; exit; }
765
if ( $version ) { use File::Basename; print basename($0) . " $VERSION (data $DATA_VERSION)\n"; exit; }
668
 
766
 
669
if ( $interactive ) {
-
 
670
   %configuration = %{ &interactiveConfig( \%configuration ) };
-
 
671
} else {
-
 
672
   # load the configuration file
767
# if periodic flag set, create the override file
673
   %configuration = %{ &loadConfigurationFile( \$configurationFile, @confFileSearchPath) };
-
 
674
}
-
 
675
 
-
 
676
`touch $periodicOverrideFile` if $periodic; # tells periodic modules to run
768
`touch $periodicOverrideFile` if $periodic; # tells periodic modules to run
677
 
769
 
678
#die Dumper (\%configuration );
770
$configFile //= 'sysinfo-client.yaml'; # default configuration file name
679
 
-
 
-
 
771
# load the configuration file. Note, loadConfigurationFile returns an arrayref of lines
680
# user did not define a serial number, so make something up
772
# then loadConfig processes that based on type
681
$configuration{'serialNumber'} = '' unless $configuration{'serialNumber'};
773
$config = loadConfig( loadConfigurationFile( $configFile ), $configType); 
-
 
774
 
682
# oops, no client name (required) so tell them and exit
775
# oops, no client name (required) so tell them and exit
683
die "No client name defined in $configurationFile" unless $configuration{'clientName'};
776
die "No client name defined in $configFile" unless $config->{'clientName'};
684
 
777
 
685
&logIt( \%configuration,  0, 'Starting sysinfo Run' );
778
# clean up some missing values
686
&logIt( \%configuration,  3, "Configuration is\n" . Data::Dumper->Dump( [\%configuration], [ qw($configuration) ] ) );
779
# output type is either what user passed on command line, or what is in config file, or defaults to yaml
-
 
780
# the config->{'dataType'} is set by loadConfig, so we can use that as last resort
-
 
781
$config->{'outputType'} //= $outputType //= $config->{'dataType'} //= 'yaml'; # default to yaml output
-
 
782
# user did not define a serial number, so make something up. Just take an md5sum of hostname-clientname
-
 
783
$config->{'serialNumber'} //= $config->{UUID} ? $config->{UUID} : `echo \`$config->{'hostname'}\`-$config->{'clientName'} | md5sum | awk '{print \$1}'`;
-
 
784
chomp $config->{'serialNumber'};
687
 
785
 
-
 
786
&logIt( $config,  0, 'Starting sysinfo Run' );
688
$TESTING = $configuration{'TESTING'} if defined $configuration{'TESTING'};
787
&logIt( $config,  3, "Configuration is\n" . Data::Dumper->Dump( [$config], [ qw($config) ] ) );
689
 
788
 
-
 
789
$TESTING //= $config->{'TESTING'} //= 0; # default to 0 if not defined
690
&logIt( \%configuration,  0, "Testing => $TESTING" ) if $TESTING;
790
&logIt( $config,  0, "Testing => $TESTING" ) if $TESTING;
691
 
791
 
692
# hash reference that will store all info we are going to send to the server
792
# hash reference that will store all info we are going to send to the server
-
 
793
# initialize it with some basic info from configuration file
693
my $System = &initReport( \%configuration );
794
my $report = &initReport( $config );
694
 
-
 
695
&logIt( \%configuration,  3, "Initial System\n" . Data::Dumper->Dump( [$System], [qw( $System )] ) );
795
&logIt( $config,  3, "Initial System\n" . Data::Dumper->Dump( [$report], [qw( $report )] ) );
696
 
796
 
697
# process any modules in the system
797
# process any modules in the system
698
foreach my $moduleDir ( @{$configuration{'moduleDirs'}} ) {
798
foreach my $moduleDir ( @{$config->{'moduleDirs'}} ) {
699
   &logIt( \%configuration,  3, "Processing modules from $moduleDir" );
799
   &logIt( $config,  3, "Processing modules from $moduleDir" );
700
   &ProcessModules( $System, "$moduleDir/" );
800
   &ProcessModules( $report, "$moduleDir/" );
701
}
801
}
702
 
-
 
703
&logIt( \%configuration,  4, "After processing modules\n" . Data::Dumper->Dump( [$System], [qw( $System )] ) );
802
&logIt( $config,  4, "After processing modules\n" . Data::Dumper->Dump( [$report], [qw( $report )] ) );
704
 
-
 
705
my $out =  sprintf( "#sysinfo: %s (data: %s) YAML\n", $VERSION, $DATA_VERSION ) . &Dump( $System );
-
 
706
 
-
 
707
&logIt( \%configuration,  4, 'At line number ' . __LINE__ . "\n" . Data::Dumper->Dump([$System],[qw($System)]) );
-
 
708
 
803
 
709
# load some global values for use in the script, if required
804
# convert the report hash to the desired output type
710
my $globals = { 
-
 
711
      'upload_type'  => 'sysinfo',
-
 
712
      'data version' => $DATA_VERSION->normal,
-
 
713
      'report date'  => $reportDate,
-
 
714
      'client name'  => $configuration{'clientName'},
805
my $out = hashToReportString( $report, $config->{'outputType'} ); # test if we can convert to output type
715
      'host name'    => $configuration{'hostname'},
806
# add comment to top of file except for json
716
      'serial number'=> $configuration{'serialNumber'},
807
$out = "# sysinfo " . $VERSION->normal . " (data " . $DATA_VERSION->normal . ") $config->{outputType}\n" . $out unless $config->{'outputType'} eq 'json'; 
717
      'UUID'         => $configuration{'UUID'}
-
 
718
      };
-
 
719
 
-
 
720
&logIt( \%configuration,  4, "Globals initialized\n" . Data::Dumper->Dump([$globals],[qw($globals)]) );
808
&logIt( $config,  4, 'At line number ' . __LINE__ . "\n" . $out );
721
 
809
 
-
 
810
# actually writing the report
722
if ( $TESTING ) {
811
if ( $TESTING ) {
723
   &logIt( \%configuration, 0, "Sending report to sysinfo.testing.yaml" );
812
   &logIt( $config, 0, "Sending report to sysinfo.testing.yaml" );
724
   open DATA, ">/tmp/sysinfo.testing.yaml" or die "Could not write to /tmp/sysinfo.testing.yaml: $!\n";
813
   open DATA, ">/tmp/sysinfo.testing.yaml" or die "Could not write to /tmp/sysinfo.testing.yaml: $!\n";
725
   print DATA $out;
814
   print DATA $out;
726
   close DATA;
815
   close DATA;
727
} else {
816
} else {
-
 
817
   # load some global values for use in the script, if required
-
 
818
   my $globals = { 
-
 
819
         'upload_type'  => 'sysinfo',
-
 
820
         'data version' => $DATA_VERSION->normal,
728
   # and send the results to the server
821
         'report date'  => $reportDate,
-
 
822
         'client name'  => $config->{'clientName'},
-
 
823
         'host name'    => $config->{'hostname'},
-
 
824
         'serial number'=> $config->{'serialNumber'},
-
 
825
         'UUID'         => $config->{'UUID'},
-
 
826
         'fileType'     => $config->{'outputType'}
-
 
827
         };
-
 
828
 
-
 
829
   &logIt( $config,  4, "Globals initialized\n" . Data::Dumper->Dump([$globals],[qw($globals)]) );
729
   &logIt( \%configuration, 0, "Sending report to remote transport" );
830
   &logIt( $config, 0, "Sending report to remote transport" );
730
   if ( my $success = &sendResults( $globals, $configuration{'transports'}, $out, $configuration{'scriptDirs'} ) != 1 ) {
831
   if ( my $success = &sendResults( $globals, $config->{'transports'}, $out, $config->{'scriptDirs'} ) != 1 ) {
731
      &logIt( \%configuration,  0, "Error $success while sending report from $configuration{'hostname'}" );
832
      &logIt( $config,  0, "Error $success while sending report from $config->{'hostname'}" );
732
   }
833
   }
733
}
834
}
734
 
835
 
735
unlink ( $periodicOverrideFile ) if -e $periodicOverrideFile;
836
unlink ( $periodicOverrideFile ) if -e $periodicOverrideFile;
736
&logIt( \%configuration,  0, 'Ending sysinfo Run' );
837
&logIt( $config,  0, 'Ending sysinfo Run' );
737
 
838
 
738
if ( $configuration{'postRunScript'}{'script name'} ) {
839
if ( $config->{'postRunScript'}{'script name'} ) {
739
   my $script = $sourceDir . '/' . $configuration{'postRunScript'}{'script name'};
840
   my $script = $sourceDir . '/' . $config->{'postRunScript'}{'script name'};
740
   exec ( "$script $configurationFile" ) if -x $script;
841
   exec ( "$script $configFile" ) if -x $script;
741
}
842
}
742
 
843
 
743
1;
844
1;