summaryrefslogblamecommitdiffstats
path: root/modules-available/rebootcontrol/hooks/cron.inc.php
blob: 93f2de38da5960300d5cc40e8004a5567b254190 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

     













                                                                                                                 





















                                                                                                                      
                                                                                                                                 












































                                                                                                                              




                                                                                            

























                                                                                                                




                                                                                            





























































































































                                                                                                                                                           
<?php

/*
 * JumpHost availability test, 5 times a day...
 */
if (in_array((int)date('G'), [6, 7, 9, 12, 15]) && in_array(date('i'), ['00', '01', '02', '03'])) {
	$res = Database::simpleQuery('SELECT hostid, host, port, username, sshkey, script FROM reboot_jumphost');
	while ($row = $res->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]);
	}
}