$uuid)); if ($old !== false) { settype($old['logintime'], 'integer'); settype($old['lastseen'], 'integer'); settype($old['lastboot'], 'integer'); } // Handle event type if ($mode === false && $type === '~poweron') { // Poweron & hw stats $uptime = Request::post('uptime', 0, 'integer'); if ($macaddr === false) die("No/Invalid MAC address.\n"); if ($uptime < 0 || $uptime > 4000000) die("Implausible uptime.\n"); $realcores = Request::post('realcores', 0, 'integer'); if ($realcores < 0 || $realcores > 1024) $realcores = 0; $mbram = Request::post('mbram', 0, 'integer'); if ($mbram < 0 || $mbram > 4096000) $mbram = 0; $kvmstate = Request::post('kvmstate', 'UNKNOWN', 'string'); $valid = array('UNKNOWN', 'UNSUPPORTED', 'DISABLED', 'ENABLED'); if (!in_array($kvmstate, $valid)) $kvmstate = 'UNKNOWN'; $cpumodel = Util::cleanUtf8(Request::post('cpumodel', '', 'string')); $systemmodel = Util::cleanUtf8(Request::post('systemmodel', '', 'string')); $id44mb = Request::post('id44mb', 0, 'integer'); if ($id44mb < 0 || $id44mb > 10240000) $id44mb = 0; $badsectors = Request::post('badsectors', 0, 'integer'); if ($badsectors < 0 || $badsectors > 100000) $badsectors = 0; $hostname = gethostbyaddr($ip); if (!is_string($hostname) || $hostname === $ip) { $hostname = ''; } $data = Util::cleanUtf8(Request::post('data', '', 'string')); // Prepare insert/update to machine table $new = array( 'uuid' => $uuid, 'macaddr' => $macaddr, 'clientip' => $ip, 'lastseen' => $NOW, 'lastboot' => $NOW - $uptime, 'realcores' => $realcores, 'mbram' => $mbram, 'kvmstate' => $kvmstate, 'cpumodel' => $cpumodel, 'systemmodel'=> $systemmodel, 'id44mb' => $id44mb, 'badsectors' => $badsectors, 'data' => $data, 'state' => 'IDLE', ); // Create/update machine entry if ($old === false) { $new['firstseen'] = $NOW; $new['hostname'] = $hostname; $res = Database::exec('INSERT INTO machine ' . '(machineuuid, macaddr, clientip, firstseen, lastseen, logintime, position, lastboot, realcores, mbram,' . ' kvmstate, cpumodel, systemmodel, id44mb, badsectors, data, hostname, state) VALUES ' . "(:uuid, :macaddr, :clientip, :firstseen, :lastseen, 0, '', :lastboot, :realcores, :mbram," . ' :kvmstate, :cpumodel, :systemmodel, :id44mb, :badsectors, :data, :hostname, :state)', $new, true); if ($res === false) { die("Concurrent insert, ignored. (RESULT=0)\n"); } } else { // Update $moresql = ($uptime < 180 ? ' logintime = 0, currentuser = NULL, currentsession = NULL,' : ''); if (!empty($hostname)) { $new['hostname'] = $hostname; $moresql .= ' hostname = :hostname,'; } if (($runmode = Request::post('runmode', false, 'string')) !== false) { $new['currentrunmode'] = Util::cleanUtf8($runmode); $moresql .= ' currentrunmode = :currentrunmode,'; } $new['oldstate'] = $old['state']; $new['oldlastseen'] = $old['lastseen']; $res = Database::exec('UPDATE machine SET ' . ' macaddr = :macaddr,' . ' clientip = :clientip,' . ' lastseen = :lastseen,' . ' lastboot = :lastboot,' . $moresql . ' realcores = :realcores,' . ' mbram = :mbram,' . ' kvmstate = :kvmstate,' . ' cpumodel = :cpumodel,' . ' systemmodel = :systemmodel,' . ' id44mb = :id44mb,' . ' live_tmpsize = 0, live_swapsize = 0, live_memsize = 0, live_cpuload = 255, live_cputemp = 0,' . ' badsectors = :badsectors,' . ' data = :data,' . ' state = :state ' . " WHERE machineuuid = :uuid AND state = :oldstate AND lastseen = :oldlastseen", $new); if ($res === 0) { die("Concurrent update, ignored. (RESULT=0)\n"); } } // Maybe log old crashed session if ($uptime < 150 && $old !== false) { // See if we have a lingering session, create statistic entry if so if ($old['state'] === 'OCCUPIED' && $old['logintime'] !== 0) { $sessionLength = $old['lastseen'] - $old['logintime']; if ($sessionLength > 30 && $sessionLength < 86400*2) { Statistics::logMachineState($uuid, $ip, Statistics::SESSION_LENGTH, $old['logintime'], $sessionLength); } } // Write poweroff period length to statistic table if ($old['lastseen'] !== 0) { $lastSeen = $old['lastseen']; $offtime = ($NOW - $uptime) - $lastSeen; if ($offtime > 90 && $offtime < 86400 * 30) { Statistics::logMachineState($uuid, $ip, $old['state'] === 'STANDBY' ? Statistics::SUSPEND_LENGTH : Statistics::OFFLINE_LENGTH, $lastSeen, $offtime); } } } if (($old === false || $old['clientip'] !== $ip) && Module::isAvailable('locations')) { // New, or ip changed (dynamic pool?), update subnetlicationid Location::updateMapIpToLocation($uuid, $ip); } // Check for suspicious hardware changes if ($old !== false) { checkHardwareChange($old, $new); // Log potential crash if ($old['state'] === 'IDLE' || $old['state'] === 'OCCUPIED') { writeClientLog('machine-mismatch-poweron', 'Poweron event, but previous known state is ' . $old['state'] . '. Free RAM: ' . Util::readableFileSize($old['live_memfree'], -1, 2) . ', free Swap: ' . Util::readableFileSize($old['live_swapfree'], -1, 2) . ', free ID44: ' . Util::readableFileSize($old['live_tmpfree'], -1, 2)); } } // Write statistics data } else if ($type === '~runstate') { // Usage (occupied/free) $sessionLength = 0; $strUpdateBoottime = ''; if ($old === false) die("Unknown machine.\n"); if ($old['clientip'] !== $ip) { updateIp('runstate', $uuid, $old, $ip); } $used = Request::post('used', 0, 'integer'); $params = array( 'uuid' => $uuid, 'oldlastseen' => $old['lastseen'], 'oldstate' => $old['state'], ); if ($old['state'] === 'OFFLINE') { // This should never happen -- we expect a poweron event before runstate, which would set the state to IDLE // So it might be that the poweron event got lost, or that a couple of runstate events got lost, which // caused our cron.inc.php to time out the client and reset it to OFFLINE if ($NOW - $old['lastseen'] > 900) { $strUpdateBoottime = ' lastboot = UNIX_TIMESTAMP(), '; } // 1) Log last session length if we didn't see the machine for a while if ($NOW - $old['lastseen'] > 900 && $old['lastseen'] !== 0) { // Old session timed out - might be caused by hard reboot if ($old['logintime'] !== 0) { if ($old['lastseen'] > $old['logintime']) { $sessionLength = $old['lastseen'] - $old['logintime']; } } } } foreach (['memsize', 'tmpsize', 'swapsize', 'memfree', 'tmpfree', 'swapfree', 'cpuload', 'cputemp'] as $item) { $liveVal = Request::post($item, false, 'int'); if ($liveVal !== false) { $strUpdateBoottime .= ' live_' . $item . ' = :_' . $item . ', '; if ($item === 'cpuload' || $item === 'cputemp') { $liveVal = round($liveVal); } else { $liveVal = ceil($liveVal / 1024); } $params['_' . $item] = $liveVal; } } if (($runmode = Request::post('runmode', false, 'string')) !== false) { $params['currentrunmode'] = Util::cleanUtf8($runmode); $strUpdateBoottime .= ' currentrunmode = :currentrunmode, '; } // Figure out what's happening - state changes if ($used === 0 && $old['state'] !== 'IDLE') { if ($old['state'] === 'OCCUPIED' && $sessionLength === 0) { // Is not in use, was in use before $sessionLength = $NOW - $old['logintime']; } $res = Database::exec('UPDATE machine SET lastseen = UNIX_TIMESTAMP(),' . $strUpdateBoottime . " logintime = 0, currentuser = NULL, state = 'IDLE' " . " WHERE machineuuid = :uuid AND lastseen = :oldlastseen AND state = :oldstate", $params); } elseif ($used === 1 && $old['state'] !== 'OCCUPIED') { // Machine is in use, was free before if ($sessionLength !== 0 || $old['logintime'] === 0) { // This event is a start of a new session, rather than an update $params['user'] = Request::post('user', null, 'string'); if (is_string($params['user'])) { $params['user'] = Util::cleanUtf8($params['user']); } $res = Database::exec('UPDATE machine SET lastseen = UNIX_TIMESTAMP(),' . $strUpdateBoottime . " logintime = UNIX_TIMESTAMP(), currentuser = :user, currentsession = NULL, state = 'OCCUPIED' " . " WHERE machineuuid = :uuid AND lastseen = :oldlastseen AND state = :oldstate", $params); } else { $res = 0; } } else { // Nothing changed, simple lastseen update $res = Database::exec('UPDATE machine SET ' . $strUpdateBoottime . ' lastseen = UNIX_TIMESTAMP() WHERE machineuuid = :uuid AND lastseen = :oldlastseen AND state = :oldstate', $params); } // Did we update, or was there a concurrent update? if ($res === 0) { die("Concurrent update, ignored. (RESULT=0)\n"); } // 9) Log last session length if applicable if ($mode === false && $sessionLength > 0 && $sessionLength < 86400*2 && $old['logintime'] !== 0) { Statistics::logMachineState($uuid, $ip, Statistics::SESSION_LENGTH, $old['logintime'], $sessionLength); } } elseif ($type === '~poweroff') { if ($old === false) die("Unknown machine.\n"); if ($old['clientip'] !== $ip) { updateIp('poweroff', $uuid, $old, $ip); } if ($mode === false && $old['state'] === 'OCCUPIED' && $old['logintime'] !== 0) { $sessionLength = $old['lastseen'] - $old['logintime']; if ($sessionLength > 0 && $sessionLength < 86400*2) { Statistics::logMachineState($uuid, $ip, Statistics::SESSION_LENGTH, $old['logintime'], $sessionLength); } } Database::exec("UPDATE machine SET logintime = 0, lastseen = UNIX_TIMESTAMP(), state = 'OFFLINE' WHERE machineuuid = :uuid AND state = :oldstate AND lastseen = :oldlastseen", array('uuid' => $uuid, 'oldlastseen' => $old['lastseen'], 'oldstate' => $old['state'])); } elseif ($mode === false && $type === '~screens') { if ($old === false) die("Unknown machine.\n"); $screens = Request::post('screen', false, 'array'); if (is_array($screens)) { // `devicetype`, `devicename`, `subid`, `machineuuid` // Make sure all screens are in the general hardware table $hwids = array(); $keepPair = array(); foreach ($screens as $port => $screen) { if (!array_key_exists('name', $screen)) continue; // Filter bogus data $screen['name'] = Util::cleanUtf8($screen['name']); $port = Util::cleanUtf8($port); if (empty($screen['name'])) continue; if (array_key_exists($screen['name'], $hwids)) { $hwid = $hwids[$screen['name']]; } else { $hwid = (int)Database::insertIgnore('statistic_hw', 'hwid', array('hwtype' => DeviceType::SCREEN, 'hwname' => $screen['name'])); $hwids[$screen['name']] = $hwid; } // Now add new entries $keepPair[] = array($hwid, $port); $machinehwid = Database::insertIgnore('machine_x_hw', 'machinehwid', array( 'hwid' => $hwid, 'machineuuid' => $uuid, 'devpath' => $port, ), array('disconnecttime' => 0)); $validProps = array(); if (count($screen) > 1) { // Screen has additional properties (resolution, size, etc.) unset($screen['name']); foreach ($screen as $key => $value) { if (!preg_match('/^[a-zA-Z0-9][\x21-\x7e]{0,15}$/', $key)) { echo "No matsch '$key'\n"; continue; // Ignore evil key names } $validProps[] = $key; Database::exec("INSERT INTO machine_x_hw_prop (machinehwid, prop, value)" . " VALUES (:id, :key, :value) ON DUPLICATE KEY UPDATE value = VALUES(value)", array( 'id' => $machinehwid, 'key' => $key, 'value' => Util::cleanUtf8($value), )); } } // Purge properties that might have existed in the past if (empty($validProps)) { Database::exec("DELETE FROM machine_x_hw_prop WHERE machinehwid = :machinehwid AND prop NOT LIKE '@%'", array('machinehwid' => $machinehwid)); } else { Database::exec("DELETE FROM machine_x_hw_prop WHERE machinehwid = :mhwid AND prop NOT LIKE '@%' AND prop NOT IN (:props)", array( 'mhwid' => $machinehwid, 'props' => array_values($validProps), )); } } // Remove/disable stale entries if (empty($keepPair)) { // No screens connected at all, purge all screen entries for this machine Database::exec("UPDATE machine_x_hw x, statistic_hw h SET x.disconnecttime = UNIX_TIMESTAMP() WHERE x.machineuuid = :uuid AND x.hwid = h.hwid AND h.hwtype = :type AND x.disconnecttime = 0", array('uuid' => $uuid, 'type' => DeviceType::SCREEN)); } else { // Some screens connected, make sure old entries get removed Database::exec("UPDATE machine_x_hw x, statistic_hw h SET x.disconnecttime = UNIX_TIMESTAMP() WHERE (x.hwid, x.devpath) NOT IN (:pairs) AND x.disconnecttime = 0 AND h.hwtype = :type AND x.machineuuid = :uuid", array( 'pairs' => $keepPair, 'uuid' => $uuid, 'type' => DeviceType::SCREEN, )); } } } else if ($type === '~suspend') { // Client entering suspend if ($old === false) die("Unknown machine.\n"); if ($old['clientip'] !== $ip) { updateIp('suspend', $uuid, $old, $ip); } if ($NOW - $old['lastseen'] < 610 && $old['state'] !== 'OFFLINE') { Database::exec("UPDATE machine SET lastseen = UNIX_TIMESTAMP(), state = 'STANDBY', standbysem = If(standbysem < 6, standbysem + 1, 6) WHERE machineuuid = :uuid AND state = :oldstate AND lastseen = :oldlastseen", array('uuid' => $uuid, 'oldlastseen' => $old['lastseen'], 'oldstate' => $old['state'])); } else { EventLog::info("[suspend] Client $uuid reported switch to standby when it wasn't powered on first. Was: " . $old['state']); } } else if ($type === '~resume') { // Waking up from suspend if ($old === false) die("Unknown machine.\n"); if ($old['clientip'] !== $ip) { updateIp('resume', $uuid, $old, $ip); } if ($old['state'] === 'STANDBY') { $res = Database::exec("UPDATE machine SET state = 'IDLE', clientip = :ip, lastseen = UNIX_TIMESTAMP(), standbysem = If(standbysem > 1, standbysem - 2, 0) WHERE machineuuid = :uuid AND state = :oldstate AND lastseen = :oldlastseen", array('uuid' => $uuid, 'ip' => $ip, 'oldlastseen' => $old['lastseen'], 'oldstate' => $old['state'])); // Write standby period length to statistic table if ($mode === false && $res > 0 && $old['lastseen'] !== 0) { $lastSeen = $old['lastseen']; $duration = $NOW - $lastSeen; if ($duration > 500 && $duration < 86400 * 14) { Statistics::logMachineState($uuid, $ip, Statistics::SUSPEND_LENGTH, $lastSeen, $duration); } } } else { EventLog::info("[resume] Client $uuid reported wakeup from standby when it wasn't logged as being in standby. Was: " . $old['state']); } } else { die("INVALID ACTION '$type'\n"); } foreach (Hook::load('client-update') as $hook) { include_once $hook->file; } die("OK. (RESULT=0)\n"); } /* * Section 2/2 * Session information */ function writeStatisticLog($type, $username, $data) { global $ip; // Spam from IP $row = Database::queryFirst('SELECT Count(*) AS cnt FROM statistic WHERE clientip = :client AND dateline + 300 > UNIX_TIMESTAMP()', array(':client' => $ip)); if ($row !== false && $row['cnt'] > 8) { return; } Database::exec('INSERT INTO statistic (dateline, typeid, clientip, username, data) VALUES (UNIX_TIMESTAMP(), :type, :client, :username, :data)', array( 'type' => $type, 'client' => $ip, 'username' => $username, 'data' => $data, )); } function writeClientLog($type, $description) { global $ip, $uuid; Database::exec('INSERT INTO clientlog (dateline, logtypeid, clientip, machineuuid, description, extra) VALUES (UNIX_TIMESTAMP(), :type, :client, :uuid, :description, :longdesc)', array( 'type' => $type, 'client' => $ip, 'description' => $description, 'longdesc' => '', 'uuid' => $uuid, )); } // For backwards compat, we require the . prefix if ($type{0} === '.') { if ($type === '.vmchooser-session') { $user = Util::cleanUtf8(Request::post('user', 'unknown', 'string')); $loguser = Request::post('loguser', 0, 'int') !== 0; $sessionName = Util::cleanUtf8(Request::post('name', 'unknown', 'string')); $sessionUuid = Util::cleanUtf8(Request::post('uuid', '', 'string')); $session = strlen($sessionUuid) === 36 ? $sessionUuid : $sessionName; Database::exec("UPDATE machine SET currentuser = :user, currentsession = :session WHERE clientip = :ip", compact('user', 'session', 'ip')); writeStatisticLog('.vmchooser-session-name', ($loguser ? $user : 'anonymous'), $sessionName); } else { if (!isset($_POST['description'])) die('Missing options..'); $description = $_POST['description']; // and username embedded in message if (preg_match('#^\[([^\]]+)\]\s*(.*)$#m', $description, $out)) { writeStatisticLog($type, $out[1], $out[2]); } } } /** * @param array $old row from DB with client's old data * @param array $new new data to be written */ function checkHardwareChange($old, $new) { if ($new['mbram'] !== 0) { if ($new['mbram'] < 6200) { $ram1 = ceil($old['mbram'] / 512) / 2; $ram2 = ceil($new['mbram'] / 512) / 2; } else { $ram1 = ceil($old['mbram'] / 1024); $ram2 = ceil($new['mbram'] / 1024); } if ($ram1 !== $ram2) { $word = $ram1 > $ram2 ? 'decreased' : 'increased'; EventLog::warning('[poweron] Client ' . $new['uuid'] . ' (' . $new['clientip'] . "): RAM $word from {$ram1}GB to {$ram2}GB"); } if (!empty($old['cpumodel']) && !empty($new['cpumodel']) && $new['cpumodel'] !== $old['cpumodel']) { EventLog::warning('[poweron] Client ' . $new['uuid'] . ' (' . $new['clientip'] . "): CPU changed from '{$old['cpumodel']}' to '{$new['cpumodel']}'"); } } } function updateIp($type, $uuid, $old, $newIp) { EventLog::warning("[$type] IP address of client $uuid seems to have changed ({$old['clientip']} -> $newIp)"); Database::exec("UPDATE machine SET clientip = :ip WHERE machineuuid = :uuid AND state = :oldstate AND lastseen = :oldlastseen", ['uuid' => $uuid, 'oldlastseen' => $old['lastseen'], 'oldstate' => $old['state'], 'ip' => $newIp]); } echo "OK.\n";