Subversion Repositories computer_asset_manager_v1

Rev

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

Rev Author Line No. Line
1 rodolico 1
#! /usr/bin/perl -w
2
 
3
# process_sysinfo.pl
4
# Author: R. W. Rodolico
5
# Part of sysinfo package. This application takes the output from sysinfo.pl as input, parsing
6
# it and populating a database (see sysinfo.sql) with the results.
7
# Application will bypass anything up to the line beginning with [sysinfo version], allowing
8
# the output of sysinfo.pl to be e-mailed to a central server, where this app will correctly parse
9
# it.
10
# Application also has limited sensing capabilities, and will respond to certain conditions with a
11
# message on STDOUT indicating conditions, such as disk usage above an alarm value, new Operating Systems
12
# installed, etc... The goal is to allow this program to be called by a cron job, and the result (if any)
13
# returned in the cron specified e-mail.
14
# OVERVIEW:
15
#    The input file contains two types of information, both beginning with a tag surrounded by square
16
#    brackets. The single line data has the value for that tag immediately following the tag, ie:
17
#       [tagname]value
18
#    Multiline data has a beginning tag, then data following on a line by line basis, until the next
19
#    tag (denoted by a tag name surrounded by square brackets) is reached. Every line between the first
20
#    and subsequent tag are considered data for the previous tag, ie:
21
#       [tagname1]
22
#       value 1
23
#       value 2
24
#       [tagname2]
25
#    thus, value 1, and value 2 are data for tagname1. Multiline data is stored in its own hash, and handled
26
#    by separate routines.
27
#    See bottom of this app for main routine; subroutines are in between here and the beginning of the main
28
#    code
29
 
30
# Required Libraries:
31
#      GenericSQL        - Home Grown MySQL access routine, included with package
32
#      GenericTemplates  - Home Grown routine, included with package
33
#      Logging           - Home Grown logging routine, included with package
34
#      Date (uses format and parse)
35
 
36
# Suggested Uses: See process_sysinfo.sh. This is an example that looks for all messages in a directory
37
#                 (in this case, a maildir directory), processes each e-mail, then moves the e-mail
38
#                 to the Processed folder. The advantage to this is that it can be called by a nightly
39
#                 cron job, with any warnings e-mailed back to the sysadmin
40
 
41
# version 0.10 20071103
42
#      Ready for distribution. Database fairly normalized.
43
# version 0.11 20071104
44
#      Bug fix, and set up for "uninstalled" software
45
# version 0.12 20071104
46
#      Adjusted so it will ignore blank package lines, and will handle directories_to_watch
47
# version 0.13 20071120
48
#      Fixed problem where a file that was NOT a valid sysinfo file would cause a run-away
49
# version 1.00 20071206
50
#  Modified for new database format
51
# version 1.01 20071208
52
# Modified for report_date to be a date/time stamp instead of just the date
53
# version 2.0.0b 20081208
54
# converted to read XML data. Also uses older style data, but converts it to standard hash to mimic xml
55
# Adds requirement for libxml-simple-perl
56
# version 2.0.1 20090416
57
# Bug fix
58
# version 3.0.0 20121002
59
# completely changed i/o format. NOT BACKWARDS COMPATIBLE
60
# this will NOT read v2 or 1 files. It will only read v3.x files
61
# and greater. Uses YAML for data input
62
#
63
# YOU MUST have perl module YAML::XS installed
64
# Under Debian:
65
# apt-get install libyaml-libyaml-perl
66
 
67
 
68
my $VERSION = '3.0.0';
69
 
70
# only required if reports are sent from process_sysinfo. Not the norm.
71
my $iMailResults;
72
my $mailTo;
73
my $mailCC;
74
my $mailBCC;
75
my $mailServer;
76
my $mailServerPort;
77
my $mailFrom;
78
my $SENDMAIL;
79
my $DiskUsageAlert = 90; # will generate a warning if any disk has more than this percent capacity used
80
 
81
# information for the database
82
#my $DSN = 'DBI:mysql:camp'; # and, set up the database access
83
#my $DB_USER = 'test';
84
#my $DB_PASS = 'test';
85
 
86
#my $LIBRARIES = '/home/www/common-cgi/'; # where the extra libraries are stored
87
 
88
# global variables (not configuration information)
89
my $dbh; # global variable for database handle
90
my @warnings; # variable to hold errors and warnings
91
my $FATAL = ''; # in case of a fatal error, this variable will hold the reason
92
 
93
 
94
# Globals that hold information after parsing by readAndParseInput
95
my $datafileVersion;# store the data file version here.
96
my $reportDate;      # global for the report date
97
my $clientID;
98
my $computerID;
99
my $clientName;
100
my $computerName;
101
 
102
 
103
 
104
#my %info;            # stores any single line tag/value pair
105
#my $ipAddresses;     # stores IP's for this machine
106
#my @diskInfo;        # stores the disk info
107
#my @packages;        # stores package info
108
#my @directoriesToWatch; # stores directories to watch
109
#my @pciInfo;        # stores pci info
110
 
111
 
112
# safe check for equality. Handles undefined
113
sub checkEquals {
114
   my ($first, $second) = @_;
115
   return ($first eq $second) if (defined $first) and (defined $second);
116
   return 1 if (! defined $first) and (! defined $second); # both undefined, so equal
117
   return 0;
118
}
119
 
120
# standard find and load configuration file
121
sub loadConfigurationFile {
122
   my $configuration_file = shift;
123
 
124
   use File::Basename;
125
   use Cwd qw(realpath);
126
 
127
   my $filename  = realpath($0); # get my real path
128
   my $directories;
129
   my $suffix;
130
   #print "$configuration_file\n";
131
   $configuration_file = $filename unless $configuration_file;
132
   #print "$configuration_file\n";
133
 
134
   if ( $configuration_file !~ m/\// ) { # no path information
135
      ($filename, $directories, $suffix) = fileparse($filename,qr/\.[^.]*/); # break filename apart
136
      #print "No Path Given\n";
137
   } else {
138
      ($filename, $directories, $suffix) = fileparse($configuration_file,qr/\.[^.]*/); # break filename apart
139
      $configuration_file = '';
140
      #print "Path included\n";
141
   }
142
   unless (-e $directories . ($configuration_file ? $configuration_file : $filename) . '.conf' ) {
143
      $lookingIn = $directories;
144
      while ($lookingIn) {
145
         $lookingIn =~ m/^(.*\/)[^\/]+\//;
146
         $lookingIn = $1;
147
         #print "$lookingIn\n";
148
         if (-e $lookingIn . ($configuration_file ? $configuration_file : $filename) . '.conf' ) {
149
            $directories = $lookingIn;
150
            $lookingIn = '';
151
         }
152
      }
153
   }
154
   $configuration_file = $directories . ($configuration_file ? $configuration_file : $filename) . '.conf'; # add the .conf
155
   open CONFFILE, "<$configuration_file" or die "Can not open configuration file $configuration_file";
156
   my $confFileContents = join( '', <CONFFILE> );
157
   close CONFFILE;
158
   return $confFileContents;
159
}
160
 
161
# just a nice place to format any warnings/errors. Just prepend the client and computer name
162
sub createLogMessage {
163
   my $message = shift;
164
   $message = "$clientName - $computerName: " . $message;
165
   return $message;
166
}
167
 
168
# generic routine to send an e-mail
169
sub sendmessage {
170
   my ( $from, $to, $subject, $message, $cc, $bcc, $server, $port ) = @_;
171
 
172
   open SENDMAIL, "|$SENDMAIL" or die "Could not open sendmail";
173
   print SENDMAIL "From: $from\nTo: $to\nSubject: $subject\n";
174
   print SENDMAIL "cc: $cc\n" if $cc;
175
   print SENDMAIL "bcc: $bcc\n" if $bcc;
176
   print SENDMAIL "$message\n";
177
   print SENDMAIL ".\n";
178
   close SENDMAIL;
179
}
180
 
181
# simply used to get an attrib_id. If it does not exit, will create it
182
 
183
sub getAttributeID {
184
   my ($attributeName ) = @_;
185
   my $sql = qq/select attrib_id from attrib where name = $attributeName and removed_date is null/;
186
   my $result = &GenericSQL::getOneValue($dbh,$sql);
187
   unless ( $result ) {
188
      my $insertSQL = qq/insert into attrib (name,added_date) values ($attributeName,$reportDate)/;
189
      &GenericSQL::doSQL( $dbh,$insertSQL );
190
      $result = &GenericSQL::getOneValue($dbh,$sql);
191
      push @warnings, "Added a new attribute type [$attributeName]";
192
   }
193
   return $result;
194
}
195
 
196
sub fixDatabaseValue {
197
   # just return NULL if the parameter is invalid
198
   return 'NULL' unless defined $_[0];
199
 
200
   my ($value,$alwaysQuote) = @_;
201
   # remove leading and trailing blank spaces
202
   $value =~ s/^ +//gi;
203
   $value =~ s/ +$//gi;
204
   if ($alwaysQuote or ($value !~ m/^\d+$/)) { # Not a numeric value
205
      $value = &GenericSQL::fixStringValue($dbh, $value); # so get it ready for SQL (ie, put quotes around it, etc...
206
   }
207
   #$value = "'$value'" if $alwaysQuote && $value ;
208
   #print "AlwaysQuote [$alwaysQuote], value [$value]\n";
209
   return $value;
210
}
211
 
212
sub checkAndUpdateAttribute {
213
   my ($ID,$attribute,$value ) = @_;
214
   unless ($attribute && $value) {
215
      push @warnings, "Error: attempt to use null value for [$attribute], value [$value] for ID $ID in checkAndUPdateAttribute";
216
      return 0;
217
   }
218
   $value = &fixDatabaseValue($value, 1); # we want to always quote the value on this particular one
219
   #print "\tcheckAndUpdateAttribute:attribute/value = [$attribute][$value]\n";
220
   $attribute = &fixDatabaseValue( $attribute );
221
   my $attrib_id = &getAttributeID( $attribute, $reportDate );
222
   my $sql = qq/
223
      select device_attrib.value
224
      from device_attrib join attrib using (attrib_id)
225
      where device_attrib.device_id = $ID
226
            and device_attrib.removed_date is null
227
            and attrib.attrib_id = $attrib_id
228
      /;
229
   $result = &GenericSQL::getOneValue($dbh,$sql);
230
   $result = &fixDatabaseValue( $result, 1 ); # we must do this since we are comparing to $value which has had this done
231
   if ( $result ) { # got it, now see if it compares ok
232
      if ( $value ne $result ) { # nope, this has changed. Note, must use fixDatabaseValue for escapes already in $value
233
         # first, set the removed_date to now on the old part
234
         #die "[$reportDate][$ID][$attrib_id]\n";
235
         #print "\tresult = [$result], value = [$value]\n";
236
         $sql = qq/
237
            update device_attrib
238
            set removed_date = $reportDate
239
            where device_id = $ID
240
                  and attrib_id = $attrib_id
241
                  and removed_date is null
242
         /;
243
         &GenericSQL::doSQL( $dbh, $sql );
244
         undef $result; # this will force the insert in the next block of code
245
      } # if $result ne $value
246
   } # if ($result)
247
   unless ( $result ) { # we have no valid entry for this attribute
248
      $sql = qq/
249
         insert into device_attrib(device_id,attrib_id,value,added_date)
250
            values ($ID,$attrib_id,$value,$reportDate)
251
         /;
252
      &GenericSQL::doSQL( $dbh, $sql );
253
      return 1;
254
   }
255
   return 0;
256
}
257
 
258
sub ReadAndParseYAML {
259
   use YAML::XS;
260
   my ( $line, $version ) = @_;
261
   $datafileVersion = $version; # update the global variable
262
   # get the rest of the file into memory for processing
263
   # it is possible that the line contains some trailing trash, so we assume
264
   # if the originating program was nice, it put an ellipses at the end (...)
265
   # thus, we read the entire file into memory UNLESS we are stopped earlier
266
   # by a line which has three "dots" at the absolute beginning and nothing else 
267
   # the line
268
   # NOTE: for some reason, some mailers will reduce ... to .., so we're treating ..
269
   # followed by any number of spaces and dots the same. THIS IS NOT valid YAML, but
270
   # you could, in theory, end the line with ^....    .......     .....$ where the ^ 
271
   # indicates beginning of line and the $ indicates the end of the line
272
   while ( my $temp = <STDIN> ) {
273
      $line .= $temp;
274
      last if $temp =~ m/^\.\.[ .]*$/; # found YAML end of document marker
275
   }
276
   return Load( $line );
277
}
278
 
279
# main subroutine that reads and parses the input
280
# it will place the appropriate values into the arrays/hashes
281
sub readAndParseInput {
282
   while ( $line = <STDIN> ) {
283
      if ( $line =~ m/^#\s*sysinfo:\s*([0-9.]+)\s*yaml/i ) { # YAML document with leading comment
284
         return &ReadAndParseYAML( $line, $1 );
285
      }
286
   }
287
   # if we reach this point, we never found the correct header, so it is a format we don't recognize
288
   # so, set error code and just return nothing
289
   $FATAL = 'Invalid Report File';
290
   return;
291
}
292
 
293
# tries to figure out the client. If the client does not exist, will create a null record
294
# for them. Stores result in $client_id (reading $clientName)
295
sub getClientID {
296
   # let's see if the client exists
297
   $client = &fixDatabaseValue($clientName);
298
   $sql = qq/select client_id from client where name = $client and removed_date is null/;
299
   my $client_id = &GenericSQL::getOneValue( $dbh, $sql );
300
   unless ($client_id) { # no entry, check the alias table
301
      $sql = qq/select client_id from client_alias where alias=$client and removed_date is null/;
302
      $client_id = &GenericSQL::getOneValue( $dbh, $sql );
303
   }
304
   # the following has been changed to simply return a message
305
   unless ( $client_id ) { # nope, client does not exist, so add them
306
      $device = &fixDatabaseValue($computerName);
307
      $sql = qq/select report_date from unknown_entry where client_name = $client and device_name = $device/;
308
      #$report = &GenericSQL::getOneValue( $dbh, $sql );
309
      #print STDERR "Report Date $report\n";
310
      if ($report = &GenericSQL::getOneValue( $dbh, $sql )) {
311
         $FATAL = "New Client detected, but entry already made. You must update Camp before this can be processed";
312
      } else {
313
         $sql = qq/insert into unknown_entry(client_name,device_name,report_date) values ($client, $device, $reportDate)/;
314
         &GenericSQL::doSQL( $dbh, $sql );
315
         $FATAL = "Warning, new client $client found with new device $device. You must update Camp before this report can be processed\n";
316
      }
317
   }
318
   return $client_id;
319
}
320
 
321
# get a device type id from the device table. Create it if it does not exist
322
sub getDeviceTypeID {
323
   my $typeDescription = &GenericSQL::fixStringValue( $dbh, shift );
324
   my $reportDate = shift;
325
   $sql = qq/select device_type_id from device_type where name = $typeDescription and removed_date is null/;
326
   my $id = &GenericSQL::getOneValue( $dbh, $sql );
327
   unless ($id) {
328
      my $sql_insert = qq/insert into device_type ( name,added_date ) values ($typeDescription, $reportDate) /;
329
      &GenericSQL::doSQL( $dbh, $sql_insert );
330
      $id = &GenericSQL::getOneValue( $dbh, $sql );
331
   }
332
   return $id;
333
}
334
 
335
# gets the computer ID. If the computerID does not exist, creates it.
336
# returns the id of the computer.
337
sub getComputerID {
338
   # ok, does this computer name exist (each computer name per site must be unique)
339
   $computer = &fixDatabaseValue($computerName);
340
   my $sql = qq/
341
      select device_id
342
      from device join site using (site_id)
343
      where site.client_id = $clientID
344
            and device.name = $computer
345
            and device.removed_date is null
346
         /;
347
   my $computer_id = &GenericSQL::getOneValue( $dbh, $sql ); # actually, result of query above
348
   unless ( $computer_id ) { # didn't find it. Let's see if it is in the alias table
349
      $sql = qq/select device_id from device_alias join device using (device_id) join site using (site_id) where device_alias.alias = $computer and site.client_id = $clientID/;
350
      $computer_id = &GenericSQL::getOneValue( $dbh, $sql );
351
   }
352
   # changed to just give a warning
353
   unless ( $computer_id ) { # nope, computer does not exist so create it
354
      $client = &fixDatabaseValue($clientName);
355
      $sql = qq/select report_date from unknown_entry where client_name = $client and device_name = $computer and processed_date is null/;
356
      #$report = &GenericSQL::getOneValue( $dbh, $sql );
357
      #print STDERR "Report Date $report\n";
358
      if ($report = &GenericSQL::getOneValue( $dbh, $sql )) {
359
         $FATAL = "New Device detected, but entry already made. You must update Camp before this can be processed";
360
      } else {
361
         $sql = qq/insert into unknown_entry(client_name,device_name,report_date) values ($client, $computer, $reportDate)/;
362
         &GenericSQL::doSQL( $dbh, $sql );
363
         $FATAL = "Warning, new device $computer found associated with client $client. You must update Camp before this report can be processed\n";
364
      }
365
   }
366
   return $computer_id;
367
}
368
 
369
# checks for a duplicate report, ie one that has already been run.
370
# the only thing that always changes is disk space usage, so just look to see
371
# if this computer has a report already for this date/time
372
sub recordReport {
373
   my $sql = qq/select count(*) from sysinfo_report where device_id = $computerID and report_date = $reportDate/;
374
   if (&GenericSQL::getOneValue( $dbh, $sql ) > 0) {
375
      my $thisDevice = &GenericSQL::getOneValue( $dbh, "select name from device where device_id = $computerID" );
376
      $FATAL = qq/Duplicate Report for $thisDevice (id $computerID) on $reportDate/;
377
      return;
378
   }
379
   $version = &fixDatabaseValue($datafileVersion);
380
   # if we made it this far, we are ok, so just add the report id
381
   $sql = qq/insert into sysinfo_report(device_id,version,report_date,added_date) values ($computerID,$version,$reportDate,now())/;
382
   &GenericSQL::doSQL($dbh,$sql);
383
   $sql = qq/select sysinfo_report_id from sysinfo_report where device_id = $computerID and report_date = $reportDate/;
384
   return &GenericSQL::getOneValue( $dbh, $sql );
385
}
386
 
387
# gets operating system ID. If it does not exist, creates it
388
sub getOSID {
389
   my ($osHash) = shift;
390
   my $os_id;
391
   my $osName = &fixDatabaseValue($$osHash{'os_name'});
392
   my $kernel = &fixDatabaseValue($$osHash{'kernel'});
393
   my $distro_name = &fixDatabaseValue($$osHash{'distribution'});
394
   my $release = &fixDatabaseValue($$osHash{'release'});
395
   my $version = &fixDatabaseValue($$osHash{'os_version'});
396
   my $description = &fixDatabaseValue($$osHash{'description'});
397
   my $codename = &fixDatabaseValue($$osHash{'codename'});
398
 
399
   $sql = qq/select operating_system_id from operating_system
400
            where name = $osName
401
               and kernel = $kernel
402
               and distro = $distro_name
403
               and distro_release = $release /;
404
   unless ( $os_id = &GenericSQL::getOneValue( $dbh, $sql ) ) {
405
      $sql = qq/insert into operating_system (name,version,kernel,distro,distro_description,distro_release,distro_codename, added_date) values
406
               ($osName,$version,$kernel,$distro_name,$description,$release,$codename, $reportDate)/;
407
      &GenericSQL::doSQL( $dbh, $sql );
408
      $sql = qq/select operating_system_id from operating_system
409
            where name = $osName
410
               and kernel = $kernel
411
               and distro = $distro_name
412
               and distro_release = $release /;
413
      $os_id = &GenericSQL::getOneValue( $dbh, $sql );
414
   }
415
   return $os_id;
416
}
417
 
418
 
419
# simply verifies some attributes of the computer
420
sub updateComputerMakeup {
421
   my ($systemHash) = @_;
422
   #print "[$$systemHash{'memory'}]\n";
423
   &checkAndUpdateAttribute($computerID,'Memory',$$systemHash{'memory'});
424
   #print "[$$systemHash{'num_cpu'}]\n";
425
   &checkAndUpdateAttribute($computerID,'Number of CPUs',$$systemHash{'num_cpu'});
426
   #die;
427
   &checkAndUpdateAttribute($computerID,'CPU Type',$$systemHash{'cpu_type'});
428
   &checkAndUpdateAttribute($computerID,'CPU SubType',$$systemHash{'cpu_sub'});
429
   &checkAndUpdateAttribute($computerID,'CPU Speed',$$systemHash{'cpu_speed'});
430
}
431
 
432
sub updateOS {
433
   my ($osHash) = @_;
434
   # verify the operating system
435
   my $os_id = &getOSID($osHash, $reportDate);
436
   $sql = qq/select operating_system_id from device_operating_system where device_id = $computerID and removed_date is null/;
437
   $registeredOS = &GenericSQL::getOneValue( $dbh, $sql );
438
   unless ($registeredOS && $registeredOS eq $os_id ) {
439
      if ( $registeredOS ) { #we have the same computer, but a new OS???
440
         $sql = qq/update device_operating_system set removed_date = $reportDate where device_id = $computerID and removed_date is null/;
441
         &GenericSQL::doSQL( $dbh, $sql);
442
         push @warnings, &createLogMessage("Computer $computerName has a new OS" );
443
         $os_id = &getOSID($osHash, $reportDate);
444
      }
445
      $sql = qq/insert into device_operating_system( device_id,operating_system_id,added_date) values ($computerID,$os_id,$reportDate)/;
446
      &GenericSQL::doSQL( $dbh, $sql );
447
   }
448
}
449
 
450
sub dateToMySQL {
451
   my $date = shift;
452
   # print "Date In $date\n";
453
   $date =~ s/'//g; # some of the older reports put quotes around this
454
   return &fixDatabaseValue($date) if $date =~ m/\d{4}[-\/]\d{2}[-\/]\d{2} \d{2}:\d{2}/;  # this is already in the correct format
455
 
456
   my ($ss,$mm,$hh,$day,$month,$year,$zone);
457
   unless  ( $date =~ m/^\d+$/ ) { # If it is not a unix time stamp
458
      $date = str2time($date); # try to parse it
459
   }
460
   return '' unless defined $date && $date; # bail if date is not defined or zero
461
   # standard processing of a date
462
   ($ss,$mm,$hh,$day,$month,$year,$zone) = localtime($date);
463
   $year += 1900;
464
   ++$month;
465
   # printf( "Answer Is: %4d-%02d-%02d %02d:%02d\n", $year,$month,$day,$hh,$mm);
466
   return &fixDatabaseValue(sprintf( '%4d-%02d-%02d %02d:%02d', $year,$month,$day,$hh,$mm));
467
}
468
 
469
# every time we get a report, we need to see if the computer was rebooted
470
# if last reboot date is not the same as what our report shows, we will
471
# remove the existing entry, then add a new one
472
sub updateBootTime {
473
   my ($systemHash) = @_;
474
   my $lastReboot;
475
   if ($$systemHash{'last_boot'}) {
476
      #print "Checking Boot Time\n";
477
      if ($lastReboot = &dateToMySQL($$systemHash{'last_boot'})) {
478
         my $sql = qq/select computer_uptime_id from computer_uptime where device_id = $computerID and last_reboot = $lastReboot/;
479
         unless ( &GenericSQL::getOneValue( $dbh, $sql ) ) {
480
            push @warnings, &createLogMessage("Computer was rebooted at $lastReboot");
481
            my $sql_insert = qq/update computer_uptime set removed_date = $reportDate where device_id = $computerID and removed_date is null/;
482
            &GenericSQL::doSQL( $dbh, $sql_insert );
483
            $sql_insert = qq/insert into computer_uptime (device_id,added_date,last_reboot) values ($computerID,$reportDate,$lastReboot)/;
484
            &GenericSQL::doSQL( $dbh, $sql_insert );
485
         }
486
      } else {
487
         push @warnings, &createLogMessage('Invalid reboot time [' . $$systemHash{'last_boot'} . ']');
488
      }
489
   } else {
490
      push @warnings, &createLogMessage('No Boot time given');
491
   }
492
}
493
 
494
# routine will check for all IP addresses reported and check against those recorded in the
495
# database. It will remove any no longer in the database, and add any new ones
496
sub doIPAddresses {
497
   my ( $networkHash ) = @_;
498
   # delete $$networkHash{'lo'}; # we don't process lo
499
   # first, remove any interfaces that no longer exist
500
   my $interfaces = join ',', (map { &fixDatabaseValue($_) } keys %$networkHash); # get a list of interfaces being passed in
501
   if ( $interfaces ) {
502
      my $sql = qq/update network set removed_date = $reportDate where device_id = $computerID and removed_date is null and interface not in ($interfaces)/;
503
      &GenericSQL::doSQL( $dbh, $sql );
504
   }
505
   # let's get all remaining network information
506
   $sql = qq/select network_id,interface,address,netmask,ip6,ip6net,mac,mtu from network where device_id = $computerID and removed_date is null/;
507
   my $sth = &GenericSQL::startQuery( $dbh, $sql );
508
   while (my $thisRow = &GenericSQL::getNextRow($sth)) {
509
      if ( defined $$thisRow{'interface'} ) { # pre 2.0 versions did not have an interface object
510
         # long drawn out thing to check if they are the same
511
         if ( &checkEquals($$networkHash{$$thisRow{'interface'}}{'address'}, $$thisRow{'address'}) && 
512
              &checkEquals($$networkHash{$$thisRow{'interface'}}{'ip6address'}, $$thisRow{'ip6'}) &&
513
              &checkEquals($$networkHash{$$thisRow{'interface'}}{'ip6networkbits'}, $$thisRow{'ip6net'}) &&  
514
              &checkEquals($$networkHash{$$thisRow{'interface'}}{'mac'}, $$thisRow{'mac'}) &&
515
              &checkEquals($$networkHash{$$thisRow{'interface'}}{'mtu'}, $$thisRow{'mtu'}) && 
516
              &checkEquals($$networkHash{$$thisRow{'interface'}}{'netmask'}, $$thisRow{'netmask'}) ) {
517
            # they are the same, so just mark it off the list
518
            delete $$networkHash{$$thisRow{'interface'}};
519
         } else { # it has changed, so invalidate the current line in the database
520
            $sql = qq/update network set removed_date = $reportDate where network_id = $$thisRow{'network_id'}/;
521
            &GenericSQL::doSQL( $dbh, $sql ); 
522
         }
523
      } else { # the database is still using pre 2.0 values, so we must see if we need to upgrade this
524
         if ($datafileVersion =~ m/^[23]/) { # version 2.x, or 3.x so we will need to update this record
525
            # in this case, we are going to just "remove" all current entries and reload them below.
526
            # this code will only be run once for each machine that needs to conver to the new format
527
            $sql = qq/update network set removed_date = $reportDate where removed_date is null and device_id = $computerID/;
528
            last;
529
         }
530
      }
531
   }
532
   # at this point, the only items left are either new or have changed, so just insert them.
533
   foreach my $device ( keys %$networkHash ) {
534
      $sql = qq/insert into network (device_id,added_date,interface,address,netmask,ip6,ip6net,mtu,mac) values /;
535
      $sql .= '( ' . join(',',
536
                           $computerID,
537
                           $reportDate,
538
                           &fixDatabaseValue($device),
539
                           &fixDatabaseValue($$networkHash{$device}{'address'}),
540
                           &fixDatabaseValue($$networkHash{$device}{'netmask'}),
541
                           &fixDatabaseValue($$networkHash{$device}{'ip6address'}),
542
                           &fixDatabaseValue($$networkHash{$device}{'ip6networkbits'}),
543
                           &fixDatabaseValue($$networkHash{$device}{'mtu'}),
544
                           &fixDatabaseValue($$networkHash{$device}{'mac'})
545
                           ) .
546
              ')';
547
      &GenericSQL::doSQL( $dbh, $sql );
548
      push @warnings,&createLogMessage("Network Device $device was added/modified");
549
   }
550
} # sub doIPAddresses
551
 
552
 
553
sub processDisks {
554
   my ($diskHash) = @_;
555
   #print Data::Dumper->Dump([$diskHash],['$diskHash']);
556
   #print "Upon entry, we have " . (scalar keys %$diskHash) . " Items in hash\n";
557
   # first, see if there are any alerts
558
   foreach my $partition (keys %$diskHash) {
559
      if ($$diskHash{$partition}{'size'}) {
560
         my $usedPercent = sprintf('%4.2f', ($$diskHash{$partition}{'used'}/$$diskHash{$partition}{'size'}) * 100);
561
         push @warnings, &createLogMessage("Partition $partition at $usedPercent%% capacity") if $usedPercent > $DiskUsageAlert;
562
      }
563
   }
564
   # now, remove any that are no longer reported
565
   my $temp = join ',', (map { &fixDatabaseValue($_) } keys %$diskHash); # get a list of interfaces being passed in
566
   my $sql = qq/select disk_info_id from disk_info where removed_date is null and device_id = $computerID and disk_device not in ($temp)/;
567
   #print "\n$sql\n";
568
#   die;
569
   my @idsToDelete = &GenericSQL::getArrayOfValues( $dbh, $sql );
570
#   print '[' . join ('][', @idsToDelete) . "]\n";
571
   foreach my $id ( @idsToDelete ) {
572
      next unless $id;
573
      push @warnings,&createLogMessage("Disk Partition removed");
574
      $sql = qq/update disk_info set removed_date = $reportDate where removed_date is null and disk_info_id = $id/;
575
      &GenericSQL::doSQL( $dbh, $sql );
576
      $sql = qq/update disk_space set removed_date = $reportDate where removed_date is null and disk_info_id = $id/;
577
      &GenericSQL::doSQL( $dbh, $sql );
578
   }
579
   # now, we have a "clean" database
580
   # do a query to retrieve all disk entries for this device
581
  $sql = qq/select disk_info.disk_info_id,disk_space_id,disk_device,filesystem,mount_point,capacity 
582
            from disk_info join disk_space using (disk_info_id) 
583
            where disk_space.removed_date is null and disk_info.removed_date is null and device_id = $computerID/;
584
   my $sth = &GenericSQL::startQuery( $dbh, $sql );
585
   #print "Before we start processing, we have " . (scalar keys %$diskHash) . " Items in hash\n";
586
   while (my $thisDBRow = &GenericSQL::getNextRow($sth)) {
587
      my $thisHashRow = $$diskHash{$$thisDBRow{'disk_device'}} ; # just for convenience 
588
      # Always invalidate the disk space entry. We'll either add a new row, or it is changed too much
589
      $sql = "update disk_space set removed_date = $reportDate where removed_date is null and disk_info_id = " . $$thisDBRow{'disk_info_id'};
590
      &GenericSQL::doSQL( $dbh, $sql );
591
      # we know this exists in both
592
      #print "\n\n" . $$thisDBRow{'disk_device'} . "\n";
593
      #print Data::Dumper->Dump([$thisDBRow],['thisRow']);
594
      #print Data::Dumper->Dump([$thisHashRow],['thisHashRow']);
595
      #print $$thisHashRow{'fstype'} . "\n";
596
      #print $$thisHashRow{'size'} . "\n";
597
      #print $$thisHashRow{'mount'} . "\n";
598
 
599
      # is it a partition, or a directory to watch. This is defined as a directory to watch does not contain a size,
600
      # mount point or file system type.
601
      my $diskPartition = (exists ($$thisHashRow{'fstype'}) && exists ($$thisHashRow{'size'}) && exists ($$thisHashRow{'mount'}) );
602
      # now, determine if we need to update the disk_info for some reason
603
      # this condition is based upon two types of entries
604
      # Type #1 (top) is a standard partition, so we see if fstype, mount and capacity are the same
605
      # type #2 (after ||) us a "directory to watch" (with no fstype, size or mount)
606
      if ( $diskPartition ) { # it is a partition, so check if something has changed in the entry
607
         #print "\n\nDevice: [" . $$thisDBRow{'disk_device'} . "] is a partition\n";
608
         #print "thisHashRow fstype [" . $$thisHashRow{'fstype'} . "]\n";
609
         #print "thisHashRow size  [" . $$thisHashRow{'size'} . "]\n";
610
         #print "thisHashRow mount  [" . $$thisHashRow{'mount'} . "]\n";
611
         #print "thisRow filesystem [" . 
612
         unless ( &checkEquals($$thisHashRow{'fstype'}, $$thisDBRow{'filesystem'}) and
613
                  &checkEquals($$thisHashRow{'mount'}, $$thisDBRow{'mount_point'}) and
614
                  &checkEquals($$thisHashRow{'size'}, $$thisDBRow{'capacity'}) ) {
615
            # yes, a change. If we just remove this entry, the add loop (below) will set it as a new device
616
            $sql = "update disk_info set removed_date = $reportDate where disk_info_id = " . $$thisDBRow{'disk_info_id'};
617
            &GenericSQL::doSQL( $dbh, $sql );
618
            #print "$sql\n";
619
            next;
620
         }
621
      }
622
      $usedSpace = $$diskHash{$$thisDBRow{'disk_device'}}{'used'};
623
      #print "\tupdating entry, looking at disk has => $$thisDBRow{'disk_device'} with space $usedSpace\n";
624
      $sql = "insert into disk_space (disk_info_id,space_used,added_date) values ";
625
      $sql .= '(' . join (',', ($$thisDBRow{'disk_info_id'}, &fixDatabaseValue($usedSpace), $reportDate)) . ')';
626
      &GenericSQL::doSQL( $dbh, $sql );
627
      # and delete the hash entry so we don't process it as a change
628
      delete $$diskHash{$$thisDBRow{'disk_device'}};
629
   }
630
   # at this point, all we have left are additions and changes
631
   foreach my $partition ( keys %$diskHash ) {
632
      $sql = 'insert into disk_info(device_id,added_date,disk_device,filesystem,mount_point,capacity) values ';
633
      $sql .= '(' . join( ',', ( $computerID,
634
                                 $reportDate, 
635
                                 &fixDatabaseValue($partition), 
636
                                 &fixDatabaseValue($$diskHash{$partition}{'fstype'}),
637
                                 &fixDatabaseValue($$diskHash{$partition}{'mount'}),
638
                                 &fixDatabaseValue($$diskHash{$partition}{'size'})
639
                               )
640
                        ) . ')';
641
      &GenericSQL::doSQL($dbh, $sql);
642
      $sql = "select disk_info_id from disk_info where removed_date is null and device_id = $computerID and disk_device = " . &fixDatabaseValue($partition);
643
      $temp = &GenericSQL::getOneValue( $dbh, $sql );
644
      $sql = 'insert into disk_space(disk_info_id,added_date,space_used) values (';
645
      $sql .= join( ',', ($temp, $reportDate, fixDatabaseValue($$diskHash{$partition}{'used'}))) . ')';
646
      &GenericSQL::doSQL( $dbh, $sql );
647
   }
648
}
649
 
650
# routine to ensure the hardware returned as PCI hardware is in the attributes area
651
sub processPCI {
652
   my  ($pciHash) = @_;
653
   # print "Entering processPCI\n";
654
   #print Data::Dumper->Dump([$pciHash],[$key]);
655
   return unless $pciHash && keys %$pciHash;
656
 
657
   #my %attributeMappings = ('class' => 'Class', # v2 database has these items, but we want to have a pretty name
658
   #                         'device' => 'Device Name',
659
   #                         'sdevice' => 'Subsystem Device',
660
   #                         'svendor' => 'Subsystem Vendor',
661
   #                         'vendor' => 'Vendor',
662
   #                         'name'   => 'Name',
663
   #                         'slot' => 'Slot'
664
   #                        );
665
 
666
   # The two keys we'll check for uniquness are device.name and device_type with a key value of 'slot'. If these
667
   # are the same, we assume this is the same record
668
 
669
   # print Data::Dumper->Dump([$pciHash]);
670
 
671
   my $key;
672
   # normalize the data
673
   foreach $key ( keys %$pciHash ) {
674
      unless ( defined ($$pciHash{$key}{'slot'}) ) { # doesn't have a slot field
675
         my $slotField = '';
676
         my $test = $$pciHash{$key};
677
         foreach $subkey ( keys %$test) { # scan through all keys and see if there is something with a "slot looking" value in it
678
            $slotField = $key if $$test{$subkey} =~ m/^[0-9a-f:.]+$/;
679
         }
680
         if ( $slotField ) {
681
            $$pciHash{$key}{$subkey}{'slot'} = $$pciHash{$key}{$subkey}{$slotField};
682
         } else {
683
            $$pciHash{$key}{'slot'} = 'Unknown';
684
         }
685
      }
686
      # Each entry must have a name. Use 'device' if it doesn't exist
687
      $$pciHash{$key}{'name'} = $$pciHash{$key}{'device'} unless defined($$pciHash{$key}{'name'}) && $$pciHash{$key}{'name'};
688
      $$pciHash{$key}{'name'} = $$pciHash{$key}{'sdevice'} unless defined($$pciHash{$key}{'name'}) && $$pciHash{$key}{'name'};
689
      $$pciHash{$key}{'name'} =~ s/^ +//; 
690
      unless ( $$pciHash{$key}{'name'} ) {
691
         push @warnings, &createLogMessage("No name given for one or more PCI devices at normalize, Computer ID: [$computerID], Report Date: [$reportDate]");
692
         return;
693
      }
694
      # Following is what will actually be put in the device table, ie device.name
695
      $$pciHash{$key}{'keyFieldValue'} = $$pciHash{$key}{'slot'} . ' - ' . $$pciHash{$key}{'name'};
696
   }
697
   # at this point, we should have a slot and a name field in all pci devices
698
 
699
   # print Data::Dumper->Dump([$pciHash]);
700
   # die;
701
   # Get list of all PCI cards in database for this computer
702
   my @toDelete;
703
   $sql = qq/select device_id,
704
                     device.name name
705
               from device join device_type using (device_type_id) 
706
               where device_type.name = 'PCI Card' 
707
                     and device.removed_date is null
708
                     and device.part_of = $computerID/;
709
   my $sth = &GenericSQL::startQuery( $dbh, $sql );
710
   while (my $thisRow = &GenericSQL::getNextRow($sth)) { # for each row in the database
711
      my $deleteMe = $$thisRow{'device_id'}; # assume we will delete it
712
      foreach $key (keys %$pciHash ) { # look for it in the hash
713
         #print "Checking [$$pciHash{$key}{'name'}] eq [$$thisRow{'name'}]\n";
714
         #print "         [$$pciHash{$key}{'slot'}] eq [$$thisRow{'slot'}]\n\n";
715
         if (
716
               ($$pciHash{$key}{'keyFieldValue'} eq $$thisRow{'name'})
717
               &&
718
               ! defined ($$pciHash{$key}{'device_id'})               # this keeps us from ignoring a card when two are installed
719
            ) { # it is in the database and in pciHash
720
            $deleteMe = ''; # so let's keep it
721
            $$pciHash{$key}{'device_id'} = $$thisRow{'device_id'}; # and mark it as there
722
            #print "\tfound equality at $$thisRow{'device_id'}\n";
723
            last; # and exit the foreach loop
724
         }
725
      }
726
      push @toDelete, $deleteMe if $deleteMe; # if we did not find it, mark for deletion
727
   }
728
   # remove stale items from the database
729
   if (@toDelete) {
730
      my $toDelete = join ",", @toDelete; # this is a list of device_id's
731
      push @warnings, &createLogMessage( scalar(@toDelete) . " PCI Devices removed");
732
      # remove from the device_attrib table
733
      $sql = qq/update device_attrib set removed_date = $reportDate where device_id in ($toDelete)/;
734
      # print "$sql\n";
735
      &GenericSQL::doSQL($dbh, $sql);
736
      # and from the device table itself
737
      $sql = qq/update device set removed_date = $reportDate where device_id in ($toDelete)/;
738
      &GenericSQL::doSQL($dbh, $sql);
739
   }
740
   undef @toDelete; # don't need this anymore
741
 
742
   my $added = 0;
743
   my $updated = 0;
744
   # now, we have either inserts or updates
745
   foreach $key (keys %$pciHash) {
746
      unless ( $$pciHash{$key}{'device_id'} ) { # we did not find it in the database, so it is an insert
747
         my $thisKey = &fixDatabaseValue($$pciHash{$key}{'keyFieldValue'});
748
         $sql = qq/insert into device (site_id,device_type_id,name,part_of,added_date) 
749
                   select site_id,device_type.device_type_id, $thisKey, device_id, $reportDate
750
                   from device,device_type 
751
                   where device.device_id = $computerID 
752
                         and device_type.name = 'PCI Card'/;
753
         &GenericSQL::doSQL($dbh, $sql);
754
         # get the inserted key
755
         $$pciHash{$key}{'device_id'} = &GenericSQL::getOneValue($dbh, qq/select max(device_id) from device where part_of = $computerID and name = $thisKey and added_date = $reportDate/);
756
         $added++;
757
      } # unless
758
      my $thisEntry = $$pciHash{$key};
759
      $value = 0;
760
      foreach my $subkey ( keys %$thisEntry ) {
761
#         $test = $attributeMappings{$subkey} ? $attributeMappings{$subkey} : $subkey;
762
         # print "checking $subkey [$$thisEntry{$subkey}]\n";
763
         $value += &checkAndUpdateAttribute($$pciHash{$key}{'device_id'}, 
764
                                            $attributeMappings{$subkey} ? $attributeMappings{$subkey} : $subkey, 
765
                                            $$thisEntry{$subkey} ) 
766
                                            unless ($subkey eq 'device_id') or ($subkey eq 'keyFieldValue');
767
      }
768
      $updated++ if $value;
769
   }
770
   push @warnings, &createLogMessage("$added PCI Devices added") if $added;
771
   push @warnings, &createLogMessage("$updated PCI Devices modified") if $updated;
772
}
773
 
774
 
775
# figure out the software_id and software_version_id of a package. Adds the package/version if
776
# it doesn't exist in the database
777
sub getSoftwareID {
778
   my ( $packageName,$versionInfo,$description ) = @_;
779
   #print "In getSoftwareID, paramters are [$packageName][$versionInfo][$description]\n";
780
   #return;
781
   # escape and quote the values for SQL
782
   $packageName = &GenericSQL::fixStringValue($dbh, $packageName );
783
   $versionInfo = &GenericSQL::fixStringValue($dbh, $versionInfo );
784
   # does the package exist?
785
   my $sql = qq/select software_id from software where package_name = $packageName and removed_date is null/;
786
   my $result = &GenericSQL::getOneValue( $dbh, $sql );
787
   unless ( $result ) { # NO, package doesn't exist, so add it to the database
788
      $description = &GenericSQL::fixStringValue($dbh, $description );
789
      $sql = qq/insert into software (package_name,description, added_date) values ($packageName,$description, $reportDate)/;
790
      &GenericSQL::doSQL( $dbh, $sql );
791
      $sql = qq/select software_id from software where package_name = $packageName and removed_date is null/;
792
      $result = &GenericSQL::getOneValue( $dbh, $sql );
793
   }
794
   # does this version number exist?
795
   $sql = qq/select software_version_id from software_version where version = $versionInfo and removed_date is null/;
796
   my $version = &GenericSQL::getOneValue( $dbh, $sql );
797
   unless ( $version ) { # nope, so add it
798
      $sql = qq/insert into software_version ( version,added_date ) values ($versionInfo,$reportDate)/;
799
      &GenericSQL::doSQL( $dbh, $sql );
800
      $sql = qq/select software_version_id from software_version where version = $versionInfo and removed_date is null/;
801
      $version = &GenericSQL::getOneValue( $dbh, $sql );
802
   }
803
   return ($result,$version);
804
}
805
 
806
# process each package. We will only add entries if a package has changed, either version number
807
# added, or deleted. Deleted packages are not handled well right now.
808
sub processPackages {
809
   my  ($softwareHash) = @_;
810
   my %softwareIDs;
811
   my $count;
812
   # since we go by software and version id's, let's just precalculate them
813
   foreach my $package (keys %$softwareHash) {
814
      # this will also insert the package and/or version in the software or software_version tables
815
      ($$softwareHash{$package}{'softwareid'},$$softwareHash{$package}{'versionid'}) = 
816
                         &getSoftwareID( $package, $$softwareHash{$package}{'version'}, $$softwareHash{$package}{'description'}, $reportDate );
817
      # this is just a shortcut for when we need to query
818
      #$$softwareHash{$package}{'complexkey'} = $$softwareHash{$package}{'softwareid'} . '-' . $$softwareHash{$package}{'versionid'};
819
      #push @installedPackages,$$softwareHash{$package}{'softwareid'};
820
      $softwareIDs{$$softwareHash{$package}{'softwareid'}} = $$softwareHash{$package}{'versionid'};
821
   }
822
   # remove any software for this machine that no longer exists
823
   my $temp = join( ',', grep { /^\d+$/ } keys %softwareIDs); # make sure we only have numerics
824
   my $sql = "update installed_packages set removed_date = $reportDate where device_id = $computerID and removed_date is null and software_id not in ($temp)";
825
   &GenericSQL::doSQL( $dbh, $sql);
826
   # ok, at this point, all software in the database exists in the computer
827
   # now, lets see if there are any modified versions or something
828
   $sql = qq/select installed_packages_id,software_id,software_version_id from installed_packages where device_id = $computerID and removed_date is null/;
829
   my $sth = &GenericSQL::startQuery( $dbh, $sql );
830
   #print "Before we start processing, we have " . (scalar keys %$diskHash) . " Items in hash\n";
831
   while (my $thisRow = &GenericSQL::getNextRow($sth)) {
832
      # if the version is the same, just do the next one
833
      if ( $softwareIDs{$$thisRow{'software_id'}} == $$thisRow{'software_version_id'}) {
834
         delete $softwareIDs{$$thisRow{'software_id'}};
835
      } else { # we have a change. We simply remove the entry and let the "add new packages" section take care of it
836
         $sql = qq/update installed_packages set removed_date = $reportDate where installed_packages_id = $$thisRow{'installed_packages_id'}/;
837
         &GenericSQL::doSQL( $dbh, $sql);
838
      }
839
   }
840
   # at this point, the only items left in $softwareIDs are the packages that have changed or been added
841
   $count = 0;
842
   foreach my $softwareID ( keys %softwareIDs ) {
843
      $count++;
844
      $sql = qq/insert into installed_packages( device_id,software_id,software_version_id,added_date ) values 
845
                ($computerID,$softwareID,$softwareIDs{$softwareID},$reportDate)/;
846
      &GenericSQL::doSQL( $dbh, $sql);
847
   }
848
   push @warnings, &createLogMessage("$count Software Packages changed or added") if $count;
849
}
850
 
851
###############################################################################
852
#            BEGIN MAIN ROUTINE
853
###############################################################################
854
BEGIN{
855
   # load the configuration file
856
   eval ( &loadConfigurationFile );
857
   push @INC, $LIBRARIES;
858
}
859
 
860
use strict;
861
no strict 'vars';
862
#use Data::Dumper;
863
use GenericSQL; # generic, home grown MySQL access routines
864
#use GenericTemplates;
865
#use Logging; # generic, home grown logging routines
866
use Date::Format; # allows us to format our dates nicely
867
use Date::Parse; # VERY KEWL, parses out a huge number of date formats
868
 
869
$dbh = DBI->connect( $DSN, $DB_USER , $DB_PASS ) or die $DBI::errstr; # try to connect to db first
870
 
871
# read the input, parse it into useable information
872
my $data = &readAndParseInput;
873
#print Data::Dumper->Dump([$data]);
874
#die; 
875
$reportDate = &dateToMySQL($$data{'report'}{'date'});
876
$clientName = $$data{'report'}{'client'};
877
$FATAL = 'No client name' unless $clientName;
878
$computerName = $$data{'system'}{'hostname'} unless $FATAL;
879
$FATAL = 'No computer name' unless $computerName;
880
# print STDERR "[$computerName]\n";
881
 
882
 
883
# try to figure out who the client is, creating if necessary
884
$clientID = &getClientID( ) unless $FATAL;
885
# try to figure out the computer ID, creating an entry if necessary
886
$computerID = &getComputerID( ) unless $FATAL;
887
# Ok, we have enough info, now let's make sure we aren't re-runing a report and record the current one.
888
my $reportID = &recordReport( ) unless $FATAL;
889
# we will simply verify memory, cpu, etc...
890
&updateComputerMakeup($$data{'system'}) unless $FATAL;
891
# check if the operating system has changed
892
&updateOS( $$data{'operatingsystem'} ) unless $FATAL;
893
# see if the machine has been rebooted and, if so, record it
894
&updateBootTime ($$data{'system'}) unless $FATAL;
895
# see what IP's this machine has
896
&doIPAddresses($$data{'network'}) unless $FATAL;
897
# Look at the disk usage, and report if they are above limits
898
&processDisks($$data{'diskinfo'}) unless $FATAL;
899
# and also if any hardware has changed
900
&processPCI($$data{'pci'}) unless $FATAL;
901
# see if any software packages have changed
902
&processPackages($$data{'software'}) unless $FATAL;
903
if ($FATAL) { # we had a fatal error, so just return it
904
   print "ERROR: $FATAL\n";
905
   exit 0;
906
}
907
# ok, work is done. If there are any values in $warnings, they should be either printed or e-mailed
908
# to whoever the sysadmin is.
909
if (@warnings) {
910
   my $warnings = join ("\n", @warnings);
911
   if ($iMailResults) {
912
      &sendmail( $mailFrom, $mailTo, 'Process Sysinfo Warnings', $warnings, $mailCC, $mailBCC, $mailServer, $mailServerPort );
913
   } else {
914
      print "$warnings\n";
915
   }
916
}
917
 
918
exit 1;