fetch(PDO::FETCH_ASSOC)) { RebootControl::wakeViaJumpHost($row, '255.255.255.255', [['macaddr' => '00:11:22:33:44:55']]); } } /* * Client reachability test -- can be disabled */ if (mt_rand(1, 2) !== 1 || Property::get(RebootControl::KEY_AUTOSCAN_DISABLED)) return; class Stuff { public static $subnets; } function destSawPw($destTask, $destMachine, $passwd) { return strpos($destTask['data']['result'][$destMachine['machineuuid']]['stdout'], "passwd=$passwd") !== false; } function spawnDestinationListener($dstid, &$destMachine, &$destTask, &$destDeadline) { $destMachines = Stuff::$subnets[$dstid]; cron_log(count($destMachines) . ' potential destination machines for subnet ' . $dstid); shuffle($destMachines); $destMachines = array_slice($destMachines, 0, 3); $destTask = $destMachine = false; $destDeadline = 0; foreach ($destMachines as $machine) { cron_log("Trying to use {$machine['clientip']} as listener for " . long2ip($machine['bcast'])); $destTask = RebootControl::runScript([$machine], "echo 'Running-MARK'\nbusybox timeout -t 8 jawol -v -l -l", 10); Taskmanager::release($destTask); $destDeadline = time() + 10; if (!Taskmanager::isRunning($destTask)) continue; sleep(2); // Wait a bit and re-check job is running; only then proceed with this host $destTask = Taskmanager::status($destTask); cron_log("....is {$destTask['statusCode']} {$machine['machineuuid']}"); if (Taskmanager::isRunning($destTask) && strpos($destTask['data']['result'][$machine['machineuuid']]['stdout'], 'Running-MARK') !== false) { $destMachine = $machine; break; // GOOD TO GO } cron_log(print_r($destTask, true)); cron_log("Dest isn't running or didn't have MARK in output, trying another one..."); } } function testClientToClient($srcid, $dstid) { $sourceMachines = Stuff::$subnets[$srcid]; // Start listener on destination spawnDestinationListener($dstid, $destMachine, $destTask, $destDeadline); if ($destMachine === false || !Taskmanager::isRunning($destTask)) return false; // No suitable dest-host found // Find a source host $passwd = sprintf('%02x:%02x:%02x:%02x:%02x:%02x', mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255)); shuffle($sourceMachines); $sourceMachines = array_slice($sourceMachines, 0, 3); cron_log("Running sending task on " . implode(', ', array_map(function($item) { return $item['clientip']; }, $sourceMachines))); $sourceTask = RebootControl::wakeViaClient($sourceMachines, $destMachine['macaddr'], $destMachine['bcast'], $passwd); Taskmanager::release($sourceTask); if (!Taskmanager::isRunning($sourceTask)) { cron_log('Failed to launch task for source hosts...'); return false; } cron_log('Waiting for testing tasks to finish...'); // Loop as long as destination task and source task is running and we didn't see the pw at destination yet while (Taskmanager::isRunning($destTask) && Taskmanager::isRunning($sourceTask) && !destSawPw($destTask, $destMachine, $passwd) && $destDeadline > time()) { $sourceTask = Taskmanager::status($sourceTask); usleep(250000); $destTask = Taskmanager::status($destTask); } // Wait for destination listener task to finish; we might want to reuse that client, // and trying to spawn a new listener while the old one is running will fail for ($to = 0; $to < 30 && Taskmanager::isRunning($destTask); ++$to) { usleep(250000); } cron_log($destTask['data']['result'][$destMachine['machineuuid']]['stdout']); // Final moment: did dest see the packets from src? Determine this by looking for the generated password if (destSawPw($destTask, $destMachine, $passwd)) return 1; // Found pw return 0; // Nothing :-( } function testServerToClient($dstid) { spawnDestinationListener($dstid, $destMachine, $destTask, $destDeadline); if ($destMachine === false || !Taskmanager::isRunning($destTask)) return false; // No suitable dest-host found $passwd = sprintf('%02x:%02x:%02x:%02x:%02x:%02x', mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255)); cron_log('Sending WOL packets from Sat Server...'); $task = RebootControl::wakeDirectly($destMachine['macaddr'], $destMachine['bcast'], $passwd); usleep(200000); $destTask = Taskmanager::status($destTask); if (!destSawPw($destTask, $destMachine, $passwd) && !Taskmanager::isTask($task)) return false; cron_log('Waiting for receive on destination...'); $task = Taskmanager::status($task); if (!destSawPw($destTask, $destMachine, $passwd)) { $task = Taskmanager::waitComplete($task, 2000); $destTask = Taskmanager::status($destTask); } // Wait for destination listener task to finish; we might want to reuse that client, // and trying to spawn a new listener while the old one is running will fail for ($to = 0; $to < 30 && Taskmanager::isRunning($destTask); ++$to) { usleep(250000); } cron_log($destTask['data']['result'][$destMachine['machineuuid']]['stdout']); if (destSawPw($destTask, $destMachine, $passwd)) return 1; return 0; } /** * Take test result, turn into "next check" timestamp */ function resultToTime($result) { if ($result === false) { // Temporary failure -- couldn't run at least one destination and one source task $next = 7200; // 2 hours } elseif ($result === 0) { // Test finished, subnet not reachable $next = 86400 * 7; // a week } else { // Test finished, reachable $next = 86400 * 30; // a month } return time() + round($next * mt_rand(90, 133) / 100); } /* * */ // First, cleanup: delete orphaned subnets that don't exist anymore, or don't have any clients using our server $cutoff = strtotime('-180 days'); Database::exec('DELETE FROM reboot_subnet WHERE fixed = 0 AND lastseen < :cutoff', ['cutoff' => $cutoff]); // Get machines running, group by subnet $cutoff = time() - 301; // Really only the ones that didn't miss the most recent update $res = Database::simpleQuery("SELECT s.subnetid, s.end AS bcast, m.machineuuid, m.clientip, m.macaddr FROM reboot_subnet s INNER JOIN machine m ON ( (m.state = 'IDLE' OR m.state = 'OCCUPIED') AND (m.lastseen >= $cutoff) AND (INET_ATON(m.clientip) BETWEEN s.start AND s.end) )"); //cron_log('Machine: ' . $res->rowCount()); if ($res->rowCount() === 0) return; Stuff::$subnets = []; while ($row = $res->fetch(PDO::FETCH_ASSOC)) { if (!isset(Stuff::$subnets[$row['subnetid']])) { Stuff::$subnets[$row['subnetid']] = []; } Stuff::$subnets[$row['subnetid']][] = $row; } $task = Taskmanager::submit('DummyTask', []); $task = Taskmanager::waitComplete($task, 4000); if (!Taskmanager::isFinished($task)) { cron_log('Task manager down. Doing nothing.'); return; // No :-( } unset($task); /* * Try server to client */ $res = Database::simpleQuery("SELECT subnetid FROM reboot_subnet WHERE subnetid IN (:active) AND nextdirectcheck < UNIX_TIMESTAMP() AND fixed = 0 ORDER BY nextdirectcheck ASC LIMIT 10", ['active' => array_keys(Stuff::$subnets)]); cron_log('Direct checks: ' . $res->rowCount() . ' (' . implode(', ', array_keys(Stuff::$subnets)) . ')'); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { $dst = (int)$row['subnetid']; cron_log('Direct check for subnetid ' . $dst); $result = testServerToClient($dst); $next = resultToTime($result); if ($result === false) { Database::exec('UPDATE reboot_subnet SET nextdirectcheck = :nextcheck WHERE subnetid = :dst', ['nextcheck' => $next, 'dst' => $dst]); } else { Database::exec('UPDATE reboot_subnet SET nextdirectcheck = :nextcheck, isdirect = :isdirect WHERE subnetid = :dst', ['nextcheck' => $next, 'isdirect' => $result, 'dst' => $dst]); } } /* * Try client to client */ // Query all possible combos $combos = []; foreach (Stuff::$subnets as $src => $_) { $src = (int)$src; foreach (Stuff::$subnets as $dst => $_) { $dst = (int)$dst; if ($src !== $dst) { $combos[] = [$src, $dst]; } } } // Check subnet to subnet if (count($combos) > 0) { $res = Database::simpleQuery("SELECT ss.subnetid AS srcid, sd.subnetid AS dstid FROM reboot_subnet ss INNER JOIN reboot_subnet sd ON ((ss.subnetid, sd.subnetid) IN (:combos) AND sd.fixed = 0) LEFT JOIN reboot_subnet_x_subnet sxs ON (ss.subnetid = sxs.srcid AND sd.subnetid = sxs.dstid) WHERE sxs.nextcheck < UNIX_TIMESTAMP() OR sxs.nextcheck IS NULL ORDER BY sxs.nextcheck ASC LIMIT 10", ['combos' => $combos]); cron_log('C2C checks: ' . $res->rowCount()); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { $src = (int)$row['srcid']; $dst = (int)$row['dstid']; $result = testClientToClient($src, $dst); $next = resultToTime($result); Database::exec('INSERT INTO reboot_subnet_x_subnet (srcid, dstid, reachable, nextcheck) VALUES (:srcid, :dstid, :reachable, :nextcheck) ON DUPLICATE KEY UPDATE ' . ($result === false ? '' : 'reachable = VALUES(reachable),') . ' nextcheck = VALUES(nextcheck)', ['srcid' => $src, 'dstid' => $dst, 'reachable' => (int)$result, 'nextcheck' => $next]); } }