Subversion Repositories computer_asset_manager_v1

Rev

Rev 36 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

<?php

/*
 * Requires php-pear and libyaml under Debian Wheezy
 *
 * apt-get install php-pear php5-dev libyaml-dev libyaml-0-2 libyaml-0-2-dbg
 * pecl install yaml
 * echo 'extension=yaml.so' >> /etc/php5/cli/php.ini
 * echo 'extension=yaml.so' >> /etc/apache2/cli/php.ini
 *
 * V0.9 20160203 RWR
 * Changed to use yaml for the configuration file. Had to take the
 * anonymous functions which parsed the different file types and convert
 * them to simpl eval() blocks.
 * 
 * V0.9.2 20160217 RWR
 * Fixed issue where if a device was marked as part of a Xen machine, it
 * was being deleted by pci updates.
 * Fixed issue where serial number was not being updated
 *
 * v0.9.3 20200218 RWR
 * Fixed invalid table name (attrib_device vs device_attrib) and set up to read a new section, attributes
 * 
*/

require 'library.php';
$VERSION='0.9.3';


class sysinfo {
   private $report; // this will hold an entire report
   private $messages = array();
   private $FATAL = false;
   private $parsingArray;   // this is a required array that teaches how to properly parse the input

   // following are used internally to track important information
   private $clientID;   // id in database
   private $clientName; // name from report
   private $machineID;  // id in database
   private $machineName;// name in report
   private $reportDate; // unix timestamp of report
   private $reportDateSQL; // used for inserts and compares in the database
   private $serialNumber; // the serial number, which is assumed to be unique
   private $fileContents;
   private $processDuplicates = false; // used for testing; allows duplicate reports
   private $returnCode = 0; // how we returned from this
   private $errors = array( 1 => 'Could not process file, not xml, yaml or ini',
                            2 => 'Invalid report, does not have one or more of report date, client name or computer name',
                            3 => 'Invalid Report, invalid machine name',
                            4 => 'Duplicate Report',
                            5 => 'New Report, must be added to CAMP',
                            6 => 'Report is in queue for addition after updates to CAMP',
                            7 => 'Too many entries match criteria of name, client and serial number'
                          );
   
   /*
    * $parsingArray contains one or more rows, each of which have three rows, starttag, endtag and eval
    * starttag and endtag are regex's which match the beginning and end of a document (ie, --- and ... for yaml)
    * eval is an anonymous function which will evaluate a particular type of document, returning it as an
    * array which we store in $report
    */
   
   function __construct( $parsingArray ) {
      $this->parsingArray = $parsingArray;
   }

   public function processReport () {
      $this->validateReport(); // validate the report appears to be ok
      // prepend the name of the report to messages BEFORE any error messages that may have appeared
      array_unshift( $this->messages,
            "Report on $this->reportDateSQL, client $this->clientName, system $this->machineName ($this->fileType)");
      // now, we do every step in turn. If they set the FATAL flag, we exit.
      if ( $this->FATAL ) return $this->returnCode;
      $this->getMachineID();
      if ( empty( $this->machineID ) or $this->FATAL ) return $this->returnCode;
      $this->checkDuplicate();
      if ( $this->FATAL) return $this->returnCode;
      $this->updateComputerMakeup();
      $this->updateOS();
      $this->updateBootTime();
      $this->doIPAddresses();
      $this->processPCI();
      $this->processSoftwarePackages();
      $this->processXen();
      $this->processAttributes();
      // at this point, we don't really need the report title information
      // so we will remove it before recordReport has a chance to put it
      // in database
      array_shift( $this->messages );
      // now record the report
      $this->recordReport();
      return $this->returnCode;;
   } // function processReport

   /* 
    * Name: loadFromFile
    * Parameters: filename (path to load file from)
    * Returns: true if on success, false if not
    * Modifies: 
    *    report - added messages on why the report is not valid
    *    FATAL    - set to true if any of the required values do not exist
    */

   public function loadFromFile ( $filename ) {
      // load file into memory. Additionally, remove any \r's from it (leaving \n's for newlines)
      $this->fileContents = str_replace("\r","", file_get_contents( $filename ) );
      // try to parse the body out
      $this->getBody();
      if ( isset( $this->report ) ) {
         if ( $this->fileType == 'xml' ) $this->fixXML();
         if ( $this->fileType == 'ini' ) {
            $this->messages[] = 'We do not process ini files, but storing it instead';
         }
         return $this->fileType;
      } else { // we don't know what it is
         $this->FATAL = true;
         $this->messages[] = "Unable to parse [$filename] using YAML, XML or INI";
         return false;
      }
      //unset( $this->fileContents ); // free up memory
   } // function loadFromFile
   

   // once the file has been loaded, we need to figure out what kind of file it is
   // then use the function embedded in $parsingArray to import it.
   private function getBody ( ) {
      foreach ( $this->parsingArray as $key => $regexes ) {
         $matches;
         $pattern = $regexes['startTag'] . '.*' . $regexes['endTag'];
         if ( preg_match( "/$pattern/ms", $this->fileContents, $matches ) ) {
            $this->fileType = $key;
            $this->fileContents = $matches[0];
            $body = $this->fileContents;
            $this->report = eval( $regexes['eval'] );
            if ( $this->report ) return true;
         } // if
      } // foreach
      return false;
   }

   /*
    * Name: fixXML
    * Parameters: None
    * Returns: nothing
    * Modifies: $this->report
    * YAML creates the entries as ['network']['interface name']
    * XML  creates the entries as ["network"][x][name]=>
    * We will simply copy the XML information into a duplicate
    * in YAML style
    */
   private function fixXML() {
       $oldInfo = $this->report['network'];
       unset ($this->report['network']);
       foreach ( $oldInfo as $entry ) {
          #$this->messages[] = "Duplicating network entry for $entry[name]";
          foreach ($entry as $key => $value ) {
             $this->report['network'][$entry['name']][$key] = $value;
             #$this->messages[] = "  Adding  $value as value for network.name.$key";
          } // inner foreach
       } // outer foreach
   } // fixXML
      

   private function processINI() {
      return false;
   }
   
   public function dumpMessages () {
      $return = '';
      if ( $this->messages ) {
         $return = implode( "\n", $this->messages );
         $return .= "\n";
      }
      return $return;
   }
   
   /* 
    * Name: validateReport
    * Parameters: none
    * Returns: true if valid, false if not
    * Modifies: 
    *    messages - added messages on why the report is not valid
    *    FATAL    - set to true if any of the required values do not exist
    */

      public function validateReport () {
      /*
       * A report is considered valid if it has a date, client name and hostname
       * No check is made at this time for validity of that data
       */
      if ( empty( $this->report['report']['date']) ) {
         $this->messages[] = 'No report date';
      } else {
         $this->reportDate = strtotime( $this->report['report']['date'] ); // store in Unix timestamp
         $this->reportDateSQL = strftime( '%F %T', $this->reportDate ); // save a copy of the date in SQL format
         if ( empty( $this->reportDate ) )
            $this->messages[] = "Unable to parse report date [$this->report['report']['date']]";
      }
         
      if ( empty( $this->report['report']['client'] ) ) {
         $this->messages[] = 'No client name';
      } else {
         $this->clientName = $this->report['report']['client'];
      }

      if ( empty ( $this->report['system']['hostname'] ) ) {
         $this->messages[] = 'No Computer Name';
      } else {
         $this->machineName = $this->report['system']['hostname'];
      }
      $this->FATAL = ! empty( $this->messages );
      
      // serial number is fairly new, so it is not a critical error
      // it will be set in getMachineID if we have it here, but not there
      if ( empty ( $this->report['system']['serial'] ) ) {
         $this->messages[] = 'No Serial Number';
      } else {
         $this->serialNumber = $this->report['system']['serial'];
      }
      
   } // function validateReport


   /* 
    * Name: getMachineID
    * Parameters: none
    * Returns: true if found, false if not
    * Modifies: 
    *    clientID - index value of client in database
    *    machineID - index value of machine in database
    */
   private function getMachineID () {
      $this->clientID = getClientID( $this->clientName );
      if ( $this->clientID ) {
         try {
            $this->machineID = getMachineID( $this->machineName, $this->clientID, $this->serialNumber );
         }
         catch (Exception $e) {
            $this->messages[] = "Duplicate entries found when trying to process Machine [$this->machineName], Client [$this->clientID], Serial [$this->serialNumber]";
            $this->FATAL = true;
            $this->returnCode = 7;
            return false;
         }
      }
      if ( empty( $this->machineID ) ) {
         $this->messages[] = $this->makeUnknown( $this->clientName, $this->machineName, $this->reportDateSQL );
         return false;
      }
      return true;
   } // function getMachineID
   
   
   
   /* 
    * checks for a duplicate report, ie one that has already been run.
    * if this computer has a report already for this date/time
    */ 
   private function checkDuplicate() {
      $count = getOneDBValue("select count(*) from sysinfo_report where device_id = $this->machineID and report_date = '$this->reportDateSQL'");
      if ( $this->processDuplicates ) { $count = 0; } // for testing
      if ( $count ) {
         $this->messages[] = "Duplicate Report for $this->machineName (id $this->machineID) on $this->reportDateSQL";
         $this->returnCode = 4;
         $this->FATAL = true;
         return false;
      }
      return true;
   } // recordReport
   
   
   /* 
    * Creates an entry in the sysinfo_report table
    */ 
   private function recordReport() {
      $version = $this->report['report']['version'];
      $messages = makeSafeSQLValue( $this->dumpMessages() );
      // if we made it this far, we are ok, so just add the report id
      queryDatabaseExtended("insert into sysinfo_report(device_id,version,report_date, notes,added_date) values ($this->machineID ,'$version','$this->reportDateSQL',$messages,now())");
      $this->reportID = getOneDBValue("select max( sysinfo_report_id ) from sysinfo_report where device_id = $this->machineID and report_date = '$this->reportDateSQL'");
      return true;
   } // recordReport
   
   
   function makeUnknown( $clientName, $machineName, $reportDate ) {
      $result = getOneDBValue("select report_date from unknown_entry where client_name = '$clientName' and device_name = '$machineName'");
      if ( empty( $result ) ) {
         queryDatabaseExtended( "insert into unknown_entry(client_name,device_name,report_date) values ('$clientName', '$machineName', '$reportDate')");
         $this->returnCode = 5;
         return "New entry, client=$clientName, device=$machineName. You must update Camp before this report can be processed";
      } else {
         $this->returnCode = 6;
         return "New entry detected, but entry already made. You must update Camp before this can be processed";
      }
   }
   
   // simply used to get an attrib_id. If it does not exist, will create it

   private function getAttributeID ( $attributeName, $reportDate ) {
      $attributeName = makeSafeSQLValue( $attributeName );
      $result = getOneDBValue( "select attrib_id from attrib where name = $attributeName and removed_date is null" );
      if ( !isset( $result) ) {
         $result = queryDatabaseExtended( "insert into attrib (name,added_date) values ($attributeName,'$reportDate')");
         $this->messages[] = "Added a new attribute type [$attributeName]";
         $result = $result['insert_id'];
      }
      return $result;
   }

   /*
    * checks to values to see if they are equal
    * If they are both numeric, and $fuzzyValue is non-zero, will determine
    * if $value2 is within $value1 +/- percentage of $fuzzyValue.
    * Thus, if $fuzzyValue is 0.10, they are "equal" as long as $value1 is 
    * within $value2 +/- 10%
    */
   private function sloppyEqual ( $value1, $value2, $fuzzyValue ) {
      return 
         ( is_numeric( $value1 ) and is_numeric( $value2 ) and $fuzzyValue and
            $value1 <= $value2 + $value2 * $fuzzyValue and
            $value1 >= $value2 - $value2 * $fuzzyValue 
         )
         or
         $value1 == $value2;
   }
            

   /*
    * Checks an attribute from the attrib_deviceurtes table. If the value has
    * changed, sets the old one's removed_date to this report date, then adds
    * new record for new value.
    * If value is not off by more than $slop, will not update. Useful for memory and speed on
    * virtual devices where the values may be slightly different at different readings
    * If the record does not exist, simply creates it
    */
   public function checkAndUpdateAttribute( $attribute, $value, $slop = 0 ) {
      if ( !isset($attribute) || !isset($value ) ) {
         $this->messages[] = "Error: attempt to use null value for [$attribute], value [$value] for ID $this->machineID in checkAndUPdateAttribute";
         return false;
      }
      $attrib_id = $this->getAttributeID( $attribute, $this->reportDateSQL );
      $result = getOneDBValue( "select attrib_device.value
         from attrib_device join attrib using (attrib_id)
         where attrib_device.device_id = $this->machineID
               and attrib_device.removed_date is null
               and attrib.attrib_id = $attrib_id
         ");
      if ( isset( $result ) && ! $this->sloppyEqual( $value, $result, $slop ) ) { # got it, now see if it compares ok
         $this->messages[] = "New value [$value] for $attribute for device $this->machineID, voiding previous";
         $value = makeSafeSQLValue($value); # we want to always quote the value on this particular one
         queryDatabaseExtended( "update attrib_device
                                    set removed_date = '$this->reportDateSQL'
                                 where device_id = $this->machineID
                                       and attrib_id = $attrib_id
                                       and removed_date is null");
         unset( $result ); # this will force the insert in the next block of code
      } # if ($result)
      if ( ! isset( $result ) ) { # we have no valid entry for this attribute
         //$this->messages[] = "In checkAndUpdateAttribute, adding new record with insert into attrib_device(device_id,attrib_id,value,added_date) values ($this->machineID,$attrib_id,$value,$this->reportDateSQL)";
         queryDatabaseExtended( "insert into attrib_device(device_id,attrib_id,value,added_date)
                                 values ($this->machineID,$attrib_id,$value,'$this->reportDateSQL')");
         return true;
      }
      return false;
   } // checkAndUpdateAttribute
   
   # simply verifies some attributes of the computer
   private function updateComputerMakeup() {
      $this->checkAndUpdateAttribute('Memory',        $this->report['system']['memory'], 0.30); // memory can be plus/minus 30%
      $this->checkAndUpdateAttribute('Number of CPUs',$this->report['system']['num_cpu']);
      $this->checkAndUpdateAttribute('CPU Type',      $this->report['system']['cpu_type']);
      $this->checkAndUpdateAttribute('CPU SubType',   $this->report['system']['cpu_sub']);
      $this->checkAndUpdateAttribute('CPU Speed',     $this->report['system']['cpu_speed'], 0.30 ); // cpu speed can be plus/minus 30%
      // validate serial number and update if necessary
      if ( $this->serialNumber ) {
         $dbSerial = getOneDBValue( "select serial from device where device_id = $this->machineID" );
         if ( ! isset ( $dbSerial ) or $dbSerial != $this->serialNumber ) {
            $this->messages[] = "Updating serial number to $this->serialNumber";
            queryDatabaseExtended( "update device set serial = '$this->serialNumber' where device_id = $this->machineID" );
         }
      }
   } // updateComputerMakeup
   
   /* several report entries are simply designed to add/update the attributes table (attrib_device), so we'll just go through them
    * one by one
    */
   private function processAttributes() {
      foreach ( $this->report['attributes'] as $key => $value ) {
         $this->checkAndUpdateAttribute( $key, $value );
      }
   } // processAttributes


   // This is kind of funky, because column = null does not work, it must be column is null, so we have to look for it.
   function makeSQLEquals ( $field, $value ) {
      return "$field " . ( $value == 'null' ? 'is null' : '=' . $value );
   }
         
   
   private function updateOS () {
      // find os
      $osName =         makeSafeSQLValue( isset($this->report['operatingsystem']['os_name'])         ? $this->report['operatingsystem']['os_name'] : '');
      $kernel =         makeSafeSQLValue( isset($this->report['operatingsystem']['kernel'])          ? $this->report['operatingsystem']['kernel'] : '');
      $distro_name =    makeSafeSQLValue( isset($this->report['operatingsystem']['os_distribution']) ? $this->report['operatingsystem']['distribution'] : '');
      $release =        makeSafeSQLValue( isset($this->report['operatingsystem']['os_release'])      ? $this->report['operatingsystem']['release'] : '');
      $version =        makeSafeSQLValue( isset($this->report['operatingsystem']['os_version'])      ? $this->report['operatingsystem']['os_version'] : '');
      $description =    makeSafeSQLValue( isset($this->report['operatingsystem']['description'])     ? $this->report['operatingsystem']['description'] : '');
      $codename =       makeSafeSQLValue( isset($this->report['operatingsystem']['codename'])        ? $this->report['operatingsystem']['codename'] : '');
      
      $osID = getOneDBValue( "select operating_system_id from operating_system
               where " . $this->makeSQLEquals( 'name', $osName ) . "
                  and " . $this->makeSQLEquals( 'kernel', $kernel ) . "
                  and " . $this->makeSQLEquals( 'distro', $distro_name ) . "
                  and " . $this->makeSQLEquals( 'distro_release', $release ) );
      if ( !isset( $osID ) ) {
         $this->messages[] = "Creating a new OS with name = $osName, kernel = $kernel, distro = $distro_name,  distro_release = $release";
         $result = queryDatabaseExtended( "insert into operating_system (name,version,kernel,distro,distro_description,distro_release,distro_codename, added_date) values
                                          ($osName,$version,$kernel,$distro_name,$description,$release,$codename, '$this->reportDateSQL')" );
         $osID = $result['insert_id'];
      }

      # verify the operating system
      $registeredOS = getOneDBValue( "select operating_system_id from device_operating_system where device_id = $this->machineID and removed_date is null" );
      if ( ! $registeredOS || $registeredOS != $osID ) {
         if ( $registeredOS ) { #we have the same computer, but a new OS???
            queryDatabaseExtended( "update device_operating_system set removed_date = '$this->reportDateSQL' where device_id = $this->machineID and removed_date is null");
            $this->messages[] = "Computer $this->machineName has a new OS";
         }
         queryDatabaseExtended( "insert into device_operating_system( device_id,operating_system_id,added_date) values ($this->machineID,$osID,'$this->reportDateSQL')");
      }
   }
   
   /* every time we get a report, we need to see if the computer was rebooted
    * There is some slop in the last reboot date, so we assume if it is less than
    * $fuzzyRebootTimes, it was not rebooted. $fuzzyRebootTimes is calculated in
    * seconds. Since most of our machines take a minimum of 5 minutes to reboot
    * we'll go with that (300), though it would be just as good to go with a day
    * (86400) since we only run this report daily
    * When one is found, we remove the old report and add a new one.
    */
   private function updateBootTime() {
      $fuzzyRebootTimes = 300; // this allows slop of this many seconds before we assume a machine has been rebooted, ie
      $lastReboot;
      if ( isset( $this->report['system']['last_boot'] ) ) {
         $lastReboot = strftime( '%F %T',$this->report['system']['last_boot'] ); // convert unix timestamp to sql value
         if ( isset( $lastReboot ) ) {
            //if ( ! getOneDBValue( "select computer_uptime_id from computer_uptime where device_id = $this->machineID and last_reboot = '$lastReboot'" ) ) {
            if ( ! getOneDBValue( "select computer_uptime_id from computer_uptime where abs(TIME_TO_SEC(timediff( last_reboot, '$lastReboot' ))) < 3600 and device_id = $this->machineID" ) ) {
               $this->messages[] = "Computer was rebooted at $lastReboot";
               queryDatabaseExtended( "update computer_uptime set removed_date = '$this->reportDateSQL' where device_id = $this->machineID and removed_date is null" );
               queryDatabaseExtended( "insert into computer_uptime (device_id,added_date,last_reboot) values ($this->machineID,'$this->reportDateSQL','$lastReboot')");
            }
         } else {
            $this->messages[] = 'Invalid reboot time [' . $this->Report['system']['last_boot'] . ']';
         }
      } else {
         $this->messages[] = 'No Boot time given';
      }
   }


   // checks if the report and the database values are the same
   private function checkInterfaceEquals( $report, $database ) {
      //var_dump( $report );
      //var_dump( $database );
      return ( 
         $report['address'] == $database['address'] and
         $report['netmask'] == $database['netmask'] and
         $report['ip6address'] == $database['ip6'] and
         $report['ip6networkbits'] == $database['ip6net'] and
         $report['mtu'] == $database['mtu'] and
         $report['mac'] == $database['mac'] 
         );
   } // checkInterfaceEquals


   /*
    * routine will check for all IP addresses reported and check against those recorded in the
    * database. It will remove any no longer in the database, and add any new ones
    */
   private function doIPAddresses () {
      /*
       * get all interfaces from the database
       * remove any interfaces from database which are not in report
       * add any interfaces from report which are not in database or which have changed
       */
      $network = $this->report['network']; // make a copy so we don't modify the original report
      // we won't process lo
      unset ($network['lo']);
      // at this point, we could get rid of tun's also, if we wanted to
      
      // let's ensure that all rows in report have the minimum required data
      foreach ( array_keys( $network ) as $interface ) {
         foreach ( array( 'address','netmask','ip6address','ip6networkbits','mtu','mac' ) as $required ) {
            if ( ! isset( $network[$interface][$required] ) ) { 
               $network[$interface][$required]  = NULL; 
            }
         } // checking all keys
      } // checking all report rows
      // var_dump( $network );   

      // get current information on all active interfaces in the database
      $dbInterfaces = queryDatabaseExtended( "select network_id,interface,address,netmask,ip6,ip6net,mac,mtu from network where device_id = $this->machineID and removed_date is null" );
      //print "select network_id,interface,address,netmask,ip6,ip6net,mac,mtu from network where device_id = $this->machineID and removed_date is null\n";
      //var_dump($dbInterfaces);

      // now, get rid of any interfaces which are no longer in the database
      // get a list of interfaces being passed in by report as a comma delimited list for processing.
      foreach ( array_keys( $network) as $temp ) {
         $interfaces[] = "'$temp'";
      }
      $interfaces = implode( ',', $interfaces );
      if ( $interfaces ) { // are there any? Then simply remove anything not in their set.
         queryDatabaseExtended( "update network set removed_date = '$this->reportDateSQL' where device_id = $this->machineID and removed_date is null and interface not in ($interfaces)");
         // print "update network set removed_date = '$this->reportDateSQL' where device_id = $this->machineID and removed_date is null and interface not in ($interfaces)\n";
         // reload the database results to exclude the ones we removed above
         $dbInterfaces = queryDatabaseExtended( "select network_id,interface,address,netmask,ip6,ip6net,mac,mtu from network where device_id = $this->machineID and removed_date is null" );
         // print "select network_id,interface,address,netmask,ip6,ip6net,mac,mtu from network where device_id = $this->machineID and removed_date is null\n";
      } // if
      // at this point, the database is a subset of the report, ie there are no entries in the database which are not also in the report.
      // let's remove the ones which are the same from both sets.
      // we can take a shortcut since the database is a subset of the report, ie we can remove entries from the report as we process
      if ( $dbInterfaces ) { // do we have anything in the database?
         foreach ( $dbInterfaces['data'] as $row => $value ) {
            // print "checking if " . $network[$value['interface']] . " equals $value\n";
            if ( $this->checkInterfaceEquals( $network[$value['interface']], $value ) ) { // compare the two entries
               // print "\tIt Does, so deleting it from updates\n";
               unset( $network[$value['interface']] );
            } else { // they are not equal. We will simply void out the existing one and it will treat the report entry as a new entry.
               // print "\tit is an update, so setting to removed in database\n";
               queryDatabaseExtended( "update network set removed_date = '$this->reportDateSQL' where device_id = $this->machineID and removed_date is null and interface = '" . $value['interface'] . "'");
               // print "update network set removed_date = '$this->reportDateSQL' where device_id = $this->machineID and removed_date is null and interface = '" . $value['interface'] . "'\n";
            } // if..else
         } // foreach
      }
      // finally, we should have only new entries, or entries which have changed and have been voided
      // a new row should be created for anything left.
      foreach ( $network as $interface => $values ) {
         //var_dump( $values );
         $sql = implode ( ',', array( 
                              $this->machineID,
                              makeSafeSQLValue($this->reportDateSQL),
                              makeSafeSQLValue($interface),
                              makeSafeSQLValue($values['address']),
                              makeSafeSQLValue($values['netmask']),
                              makeSafeSQLValue($values['ip6address']),
                              makeSafeSQLValue($values['ip6networkbits']),
                              makeSafeSQLValue($values['mtu']),
                              makeSafeSQLValue($values['mac'])
                              )
                         );
         $sql = "insert into network (device_id,added_date,interface,address,netmask,ip6,ip6net,mtu,mac) values (" . $sql . ")";
         //$this->messages[] = $sql;
         queryDatabaseExtended( $sql );
         $this->messages[] = "Network Device $interface was added/modified";
      }
   } // doIPAddresses
   

   /*
    * function looks through every element of $report, for a subkey
    * named 'keyfield'. It turns those into a string appropriate for
    * inclusion in a query as 
    * where fieldname in ( $keylist )
    * For example, with values a, b, c, 54, q'
    * returns 'a','b','c','54','q\''
    * where $keylist is generated by this function
    * returns null if no values found
    */ 
   function getKeyList ( $report ) {
      $keys = array();
      $keyset = null;
      // every row should have a key 'keyfield'.Since we don't know 
      // what they are, be sure to make sure they are SQL Safe
      foreach ( $report as $key => $value ) {
         $keys[] = makeSafeSQLValue( $value['keyfield'] );
      }
      return $keys ? implode( ",\n", $keys ) : null;
   }

   /*
    * function checks all keys in $first and $second to see if they
    * match. If they do, returns true, else returns false
    * ignores any keys passed in as array $ignore
    */
   function rowMatches ( $first, $second, $ignore = array() ) {
      foreach ( $first as $fkey => $fValue ) {
         if ( in_array( $fkey, $ignore ) )
            continue;
         if ( $fValue == $second[$fkey] )
            continue;
         return false;
      }
      return true;
   } // rowMatches

   /* Generic function that will 
    * delete items in database not found in report
    * Clean up report to contain only those items which are to be added
    * Return a copy of the report.
    * 
    * NOTE: since we are deleting items not found in the report, we will then
    * subsequently add them. This is the update process since we never actually
    * "delete" anything from the database.
    * 
    * Requires the report, and report must have a key for each entry named
    * 'keyfield' which the query below may use (as special tag <keys>
    * Also uses the following queries. If a query does not exist, that
    * process is ignored
    * 'remove old' - should have an entry as <device_id> which will have the current
    *       machine ID placed in it
    * 'delete' - <ids> which will be replaced with a list of keys from the 
    *         report.
    * 'find' - should return a column named 'id' which is a primary key on the
    *          table (used by delete query), and a column named 'keyfield'
    *          which matches the same one in the report. Any other fields in
    *          the query are checked against the report, and the report and
    *          database entry are only considered matching if all query fields
    *          match a report entry.
    * 
    * Before passing in report, you should go through the entries and create
    * a new one called 'keyfield' for each entry which will be a unique key
    * for an entry. For example, software packages may be the package name
    * and the version.
    */
   private function AddDeleteUpdate ( $report, $queries, $reportLabel, $testing=false ) {

      if ( $testing ) {
         print "*****************Starting\n"; 
         print_r( $report ); 
         print "*****************Starting\n";
      }
      $database = null;
      $keySet = $this->getKeyList( $report );
      if ( is_null( $keySet ) ) // bail if we don't have any data
         return null;
         
      // find values which are in database but not in report and delete them from database
      if ( $queries['remove old'] ) {
         $queries['remove old'] = str_replace( '<keys>', $keySet, $queries['remove old'] );
         $queries['remove old'] = str_replace( '<device_id>', $this->machineID, $queries['remove old'] );
         if ( $testing )
            print "Remove Query***************************\n" . $queries['remove old'] . "\n***********************************\n";
         else {
            $database = queryDatabaseExtended( $queries['remove old'] );
            if ( $database and $database['affected_rows'] ) 
               $this->messages[] = $database['affected_rows'] . " $reportLabel removed from database because they were removed for superceded";
         } // else
      } // if

      // now, let's get all the values left in the database
      if ( $queries['find'] ) {
         $queries['find'] = str_replace( '<keys>', $keySet, $queries['find'] );
         $queries['find'] = str_replace( '<device_id>', $this->machineID, $queries['find'] );
         if ( $testing )
            print "Find Query***************************\n" . $queries['find'] . "\n***********************************\n";
         else
            $database = queryDatabaseExtended( $queries['find'] );
      }
      if ( $database ) { // there were entries in the database still
         $database = $database['data']; // get to the data
         $keysToDelete = array();
         // find values which are different in report, and delete the database entry
         foreach ( $database as $row => $entries ) {
            foreach ( $report as $key => $values ) {
               if ( $report[$key]['keyfield'] != $entries['keyfield'] )
                  continue;
               // if we made it here, our keyfields match, so we check
               // the contents of the other fields
               if ( $this->rowMatches( $entries, $values, array( 'keyfield', 'id' ) ) ) {
                  // they match, so remove it from the report
                  unset( $report[$key] );
               } else { // they are different in the report, so remove from database
                  $keysToDelete[] = $entries['id'];
               }
            } // inner foreach
         } // outer foreach
         if ( $keysToDelete and $queries['delete'] ) {
            $queries['delete'] = str_replace( '<ids>', implode( ',', $keysToDelete ), $queries['delete'] );
            if ( $testing )
               print "Delete Query***************************\n" . $queries['delete'] . "\n***********************************\n";
            else {
               $updates = queryDatabaseExtended( $queries['delete'] );
               $this->messages[] = $updates['affected_rows'] . " $reportLabel modified";
            }
         }
      } // if $database
      // return items to be added
      if ( $testing ) {
         print "*****************Ending\n"; 
         print_r( $report ); 
         print "*****************Ending\n";
      }
      return $report;
   } // function AddDeleteUpdate


   /*
    * routine to ensure the hardware returned as PCI hardware is in the attributes area
    */ 
   private function processPCI ( ) {
      // following are some of the synonyms we will use from the PCI
      // tables for the "name" of the device. They are listed in the
      // order of priority (ie, first one found is chosen).
      $nameSynomyms = array('name','device','sdevice','device0');

      // query which removes devices from database that are not in report
      // we assume if the report doesn't have it, it has been removed
      $queries['remove old'] = 
                    "update device
                        set removed_date = '$this->reportDateSQL'
                        where device_id in (
                           select device_id from (
                              select
                                    device_id
                                 from
                                    device join device_type using (device_type_id)
                                 where
                                    device.part_of = <device_id>
                                    and device_type.show_as_system <> 'Y'
                                    and device.removed_date is null
                                    and device.name not in ( <keys> )
                              ) as b
                              )";
      
      
      $queries['find'] = 
                    "select
                        device_id id,
                        device.name  'keyfield',
                        device.notes 'info'
                     from device join device_type using (device_type_id) 
                     where
                        device_type.name = 'PCI Card' 
                        and device.removed_date is null
                        and device.part_of = <device_id>";
                        
      $queries['delete'] =
                    "update device
                        set removed_date = '$this->reportDateSQL'
                        where device_id in ( <ids> )";


      if ( ! isset( $this->report['pci'] ) ) return;
      // make a local copy so we can modify as needed
      $pciHash = $this->report['pci'];
      # normalize the data
      foreach ( array_keys( $pciHash ) as $key ) {
         if ( ! is_array( $pciHash[$key] ) ) {
            $this->messages[] = 'Invalid PCI format, not recording';
            return false;
         }
         if ( ! isset( $pciHash[$key]['slot'] ) ) { // doesn't have a slot field
            $slotField = '';
            foreach ( array_keys( $pciHash[$key] ) as $subkey ) { // scan through all keys and see if there is something with a "slot looking" value in it
               if ( preg_match ( '/^[0-9a-f:.]+$/' , $pciHash[$key][$subkey] ) )
                  $slotField = $subkey;
            }
            if ( $slotField ) {
               $pciHash[$key]['slot'] = $pciHash[$key][$slotField];
            } else {
               $pciHash[$key]['slot'] = 'Unknown';
            }
         }
         // Each entry must have a name. Use 'device' if it doesn't exist
         // Basically, we try each option in turn, with the first test having
         // priority.
         
         if ( ! isset( $pciHash[$key]['name'] ) ) {
            // if there is no name, try to use one of the synonyms
            foreach ( $nameSynomyms as $testName ) {
               if ( isset ( $pciHash[$key][$testName] ) ) {
                  $pciHash[$key]['name'] = $pciHash[$key][$testName];
                  break;
               } // if
            } // foreach
            if ( empty( $pciHash[$key]['name'] ) ) {
               $this->messages[] = "No name given for one or more PCI devices at normalize, Computer ID: [$this->machineID], Report Date: [$this->reportDate]";
               //print_r( $pciHash ); 
               //die;
               return;
            }
         } elseif ( $pciHash[$key]['name'] == $pciHash[$key]['slot'] ) {
            $pciHash[$key]['name'] = $pciHash[$key]['device'];
         } // if..else
         // Following is what will actually be put in the device table, ie device.name
         $pciHash[$key]['keyfield'] = $pciHash[$key]['slot'] . ' - ' . $pciHash[$key]['name'];
         ksort( $pciHash[$key] );
         $ignore = array( 'keyfield' );
         $info = '';
         foreach ( $pciHash[$key] as $subkey => $value ) {
            if ( in_array( $subkey, $ignore ) )
               continue;
            $info .= "[$subkey]=$value\n";
         }
         $pciHash[$key]['info'] = $info;
      } // foreach
      // at this point, we should have a slot and a name field in all pci devices
      $toAdd = $this->AddDeleteUpdate( $pciHash, $queries, 'PCI Devices' );
      if ( $toAdd ) {
         $pciCardID = getOneDBValue( "select device_type_id from device_type where name = 'PCI Card'");
         $count = 0;
         foreach ( $toAdd as $entry => $values ) {
            $keyfield = makeSafeSQLValue( $values['keyfield'] );
            $info = makeSafeSQLValue( $values['info'] );
            $query = "insert into device 
                        select 
                           null,
                           site_id,
                           $pciCardID,
                           $keyfield,
                           $info,
                           device_id,
                           '$this->reportDateSQL',
                           null,
                           null 
                        from device 
                        where device_id = $this->machineID";
            queryDatabaseExtended( $query );
            $count++;
         } // foreach
         $this->messages[] = "$count PCI devices added/modified";
      } // if
   } // processPCI

   function getSoftwareID ( $packageName,$versionInfo,$description ) {
      $packageName = makeSafeSQLValue( $packageName );
      $versionInfo = makeSafeSQLValue( $versionInfo );
      $description = makeSafeSQLValue( $description );
      // does the package exist?
      $packageID = getOneDBValue("select software_id from software where package_name = $packageName and removed_date is null");
      if ( !$packageID ) { # NO, package doesn't exist, so add it to the database
         $packageID = queryDatabaseExtended( "insert into software (package_name,description, added_date) values ($packageName,$description, '$this->reportDateSQL')");
         if ( $packageID['insert_id'] )
            $packageID = $packageID['insert_id'];
         else {
            $this->messages[] = "ERROR: Software Packages - Could not find version $packageName and could not add to database";
            $packageID = null;
         }
      }
      // does this version number exist?
      $versionID = getOneDBValue( "select software_version_id from software_version where version = $versionInfo and removed_date is null" );
      if ( ! $versionID ) { # nope, so add it
         $versionID = queryDatabaseExtended( "insert into software_version ( version,added_date ) values ($versionInfo,'$this->reportDateSQL')");
         if ( $versionID['insert_id'] )
            $versionID = $versionID['insert_id'];
         else {
            $this->messages[] = "ERROR: Software Packages - Could not find version $versionInfo and could not add to database";
            $versionID = null;
         }
      }
      return array( 'package id' => $packageID,'version id' => $versionID);
   } // getSoftwareID


   function processSoftwarePackages (  ) {
      // query which removes devices from database that are not in report
      // we assume if the report doesn't have it, it has been removed
      $queries['remove old'] = 
                    "update installed_packages
                        set removed_date = '$this->reportDateSQL'
                        where installed_packages_id in (
                           select installed_packages_id from (
                              select
                                    installed_packages_id
                              from 
                                 installed_packages 
                                 join software using ( software_id )
                                 join software_version using (software_version_id)
                              where
                                    installed_packages.device_id = <device_id>
                                    and installed_packages.removed_date is null
                                    and concat(software.package_name,software_version.version) not in ( <keys> )
                              ) as b
                              )";
      $queries['find'] = 
           "select 
               installed_packages.installed_packages_id 'id',
               concat(software.package_name,software_version.version) 'keyfield'
            from 
               installed_packages 
               join software using ( software_id )
               join software_version using (software_version_id)
            where 
               device_id = <device_id>
               and installed_packages.removed_date is null";
      $queries['delete'] =
                    "update installed_packages
                        set removed_date = '$this->reportDateSQL'
                        where installed_packages_id in ( <ids> )";

      if ( ! isset( $this->report['software'] ) ) return;
      // make a local copy so we can modify as needed
      $softwareHash = $this->report['software'];
      foreach ( array_keys( $softwareHash ) as $key ) {
         if ( ! isset( $softwareHash[$key]['name'] ) )
            $softwareHash[$key]['name'] = $key;
         if ( ! isset( $softwareHash[$key]['description'] ) )
            $softwareHash[$key]['description'] = '';
         if ( isset( $softwareHash[$key]['release'] ) ) // some of them have a release number, so we just add that to the version
            $softwareHash[$key]['version'] = $softwareHash[$key]['version'] . '-r' . $softwareHash[$key]['release'];
         // This is needed for matching
         $softwareHash[$key]['keyfield'] = $softwareHash[$key]['name'] . $softwareHash[$key]['version'];
      } // foreach
      // at this point, we should have a slot and a name field in all pci devices
      $toAdd = $this->AddDeleteUpdate( $softwareHash, $queries, 'Software Packages' );
      if ( $toAdd ) {
         $count = 0;
         foreach ( $toAdd as $key => $value ) {
            $ids = $this->getSoftwareID ( $value['name'],$value['version'],$value['description'] );
            if ( is_null( $ids['package id'] ) or is_null( $ids['version id'] ) ) {
               $this->messages[] = "Could not create entry for package " . $value['name'] . ", version " . $value['version'];
            } else {
               $insertValues = implode( ',', array( $this->machineID, $ids['package id'], $ids['version id'], "'$this->reportDateSQL'" ) );
               $query = "insert into installed_packages
                           (device_id,software_id,software_version_id,added_date) 
                           values 
                           ( $insertValues )";
               $database = queryDatabaseExtended( $query );
               if ( $database )
                  $count++;
            } // if..else
         } // foreach
         if ( $count )
            $this->messages[] = "$count software packages added/updated";
      }
   } // processSoftwarePackages
   
   
   function processXenVirtual ( $name, $values ) {
      // Determine if the virtual is on a new machine or not
      $virtualID = getMachineID( $name ); // see if we can find the virtual

      if ( $virtualID ) {
         if ( $this->machineID == getOneDBValue( "select part_of from device where device.device_id = $virtualID" ) ) {
            return;
         }
      } else { // we could not find the device
         $this->messages[] = "Could not locate id for virtual $name which is running on $this->machineName";
         return;
      }
      // we made it this far, which means we found the virtual's ID, but it does not have
      // this machine as its parent, so we need to update it.
      queryDatabaseExtended( "update device set part_of = $this->machineID where device_id = $virtualID" );
      $this->messages[] = "Virtual $name ($virtualID) now running on $this->machineName";
   } // processXenVirtual
   
   function processXen ( ) {
      // Is this a Xen DOM0?
      if ( ! isset( $this->report['xen'] ) ) return;
      $machineID;  // id in database
      $machineName;// name in report

      foreach ( $this->report['xen']['virtual'] as $virtual => $data ) {
         $this->processXenVirtual( $virtual, $data );
      }
   } // processXen

} // class sysinfo



/*
 * we don't know where the configuration file will be, so we have a list
 * below (searchPaths) to look for it. It will load the first one it 
 * finds, then exit. THUS, you could have multiple configuration files
 * but only the first one in the list will be used
 */

$confFileName = "sysinfoRead.conf.yaml";
$searchPaths = array( '/etc/camp', '/opt/camp', '/opt/camp/sysinfo', $_SERVER['SCRIPT_FILENAME'], getcwd() );
$configuration = array();
foreach ( $searchPaths as $path ) {
   if ( file_exists( "$path/$confFileName" ) ) {
      #print "Found $path/$confFileName\n";
      $configuration = yaml_parse_file( "$path/$confFileName" );
      #require "$path/$confFileName";
      break; // exit out of the loop; we don't try to load it more than once
   } // if
} // foreach

mysql_connect( $configuration['database']['databaseServer'], $configuration['database']['databaseUsername'], $configuration['database']['databasePassword'] ) or die(mysql_error());
mysql_select_db( $configuration['database']['database'] ) or die(mysql_error());


$thisReport = new sysinfo( $configuration['bodyContents'] );
$result; 

$result = $thisReport->loadFromFile( $argc > 1 ? $argv[1] : "php://stdin" );
if ( $result == 'ini' ) {
   print $thisReport->dumpMessages();
   exit(0);
}
if ( ! $result )
   exit ( 1 );
$result = $thisReport->processReport();
print $thisReport->dumpMessages();
// var_dump( $thisReport );
exit ( $result );

?>