From 3e45ec44d22f03ca6642e08f695c6d7274cecfaf Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Sat, 7 Dec 2019 13:52:54 +0100 Subject: [statistics/rebootcontrol] Add WOL button to statistics module * Overhauled task display in rebootcontrol module * Can only add subnets by CIDR now instead of start and end --- inc/arrayutil.inc.php | 24 ++ modules-available/rebootcontrol/clientscript.js | 27 ++ modules-available/rebootcontrol/hooks/cron.inc.php | 223 +++++++++++ .../rebootcontrol/inc/rebootcontrol.inc.php | 411 +++++++++++++++++++-- .../rebootcontrol/inc/rebootqueries.inc.php | 53 +-- modules-available/rebootcontrol/install.inc.php | 6 +- .../rebootcontrol/lang/de/messages.json | 9 +- .../rebootcontrol/lang/de/permissions.json | 3 +- .../rebootcontrol/lang/de/template-tags.json | 20 + modules-available/rebootcontrol/page.inc.php | 42 ++- .../rebootcontrol/pages/jumphost.inc.php | 8 +- .../rebootcontrol/pages/subnet.inc.php | 42 ++- modules-available/rebootcontrol/pages/task.inc.php | 119 ++++-- .../rebootcontrol/permissions/permissions.json | 6 + modules-available/rebootcontrol/style.css | 30 +- .../rebootcontrol/templates/header.html | 26 +- .../rebootcontrol/templates/status-reboot.html | 56 +-- .../rebootcontrol/templates/status-wol.html | 52 +++ .../rebootcontrol/templates/subnet-edit.html | 10 +- .../rebootcontrol/templates/subnet-list.html | 33 +- .../rebootcontrol/templates/task-header.html | 4 + .../rebootcontrol/templates/task-list.html | 22 +- .../statistics/lang/de/template-tags.json | 1 + .../statistics/lang/en/template-tags.json | 1 + modules-available/statistics/page.inc.php | 60 ++- modules-available/statistics/pages/list.inc.php | 4 +- .../statistics/templates/clientlist.html | 6 + 27 files changed, 1050 insertions(+), 248 deletions(-) create mode 100644 inc/arrayutil.inc.php create mode 100644 modules-available/rebootcontrol/clientscript.js create mode 100644 modules-available/rebootcontrol/hooks/cron.inc.php create mode 100644 modules-available/rebootcontrol/templates/status-wol.html create mode 100644 modules-available/rebootcontrol/templates/task-header.html diff --git a/inc/arrayutil.inc.php b/inc/arrayutil.inc.php new file mode 100644 index 00000000..ec6e2a5f --- /dev/null +++ b/inc/arrayutil.inc.php @@ -0,0 +1,24 @@ + time()) { + $sourceTask = Taskmanager::status($sourceTask); + usleep(250000); + $destTask = Taskmanager::status($destTask); + } + 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); + } + 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]); + } +} diff --git a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php index 8a85e3ff..489b0252 100644 --- a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php +++ b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php @@ -5,9 +5,14 @@ class RebootControl const KEY_TASKLIST = 'rebootcontrol.tasklist'; + const KEY_AUTOSCAN_DISABLED = 'rebootcontrol.disable.scan'; + const REBOOT = 'REBOOT'; const KEXEC_REBOOT = 'KEXEC_REBOOT'; const SHUTDOWN = 'SHUTDOWN'; + const TASK_REBOOTCTL = 'TASK_REBOOTCTL'; + const TASK_WOL = 'WAKE_ON_LAN'; + const TASK_EXEC = 'REMOTE_EXEC'; /** * @param string[] $uuids List of machineuuids to reboot @@ -19,66 +24,136 @@ class RebootControl $list = RebootQueries::getMachinesByUuid($uuids); if (empty($list)) return false; - return self::execute($list, $kexec ? RebootControl::KEXEC_REBOOT : RebootControl::REBOOT, 0, 0); + return self::execute($list, $kexec ? RebootControl::KEXEC_REBOOT : RebootControl::REBOOT, 0); } /** - * @param array $list list of clients containing each keys 'machineuuid' and 'clientip' + * @param array $list list of clients containing each keys 'machineuuid', 'clientip' and 'locationid' * @param string $mode reboot mode: RebootControl::REBOOT ::KEXEC_REBOOT or ::SHUTDOWN * @param int $minutes delay in minutes for action * @param int $locationId meta data only: locationId of clients * @return array|false the task, or false if it could not be started */ - public static function execute($list, $mode, $minutes, $locationId) + public static function execute($list, $mode, $minutes) { $task = Taskmanager::submit("RemoteReboot", array( "clients" => $list, "mode" => $mode, "minutes" => $minutes, - "locationId" => $locationId, "sshkey" => SSHKey::getPrivateKey(), "port" => 9922, // Hard-coded, must match mgmt-sshd module )); if (!Taskmanager::isFailed($task)) { - Property::addToList(RebootControl::KEY_TASKLIST, $locationId . '/' . $task["id"], 60 * 24); + self::addTask($task['id'], self::TASK_REBOOTCTL, $list, $task['id'], ['action' => $mode]); } return $task; } + private static function extractLocationIds($from, &$to) + { + if (is_numeric($from)) { + $to[$from] = true; + return; + } + if (!is_array($from)) + return; + $allnum = true; + foreach ($from as $k => $v) { + if (is_numeric($k) && is_numeric($v)) + continue; + $allnum = false; + if (is_numeric($k) && is_array($v)) { + self::extractLocationIds($v, $to); + } else { + $k = strtolower($k); + if ($k === 'locationid' || $k === 'locationids' || $k === 'location' || $k === 'locations' || $k === 'lid' || $k === 'lids') { + self::extractLocationIds($v, $to); + } elseif ($k === 'client' || $k === 'clients' || $k === 'machine' || $k === 'machines') { + if (is_array($v)) { + self::extractLocationIds($v, $to); + } + } + } + } + if ($allnum) { + foreach ($from as $v) { + $to[$v] = true; + } + } + } + + private static function addTask($id, $type, $clients, $taskIds, $other = false) + { + $lids = ArrayUtil::flattenByKey($clients, 'locationid'); + $lids = array_unique($lids); + $newClients = []; + foreach ($clients as $c) { + $d = ['clientip' => $c['clientip']]; + if (isset($c['machineuuid'])) { + $d['machineuuid'] = $c['machineuuid']; + } + $newClients[] = $d; + } + if (!is_array($taskIds)) { + $taskIds = [$taskIds]; + } + $data = [ + 'id' => $id, + 'type' => $type, + 'locations' => $lids, + 'clients' => $newClients, + 'tasks' => $taskIds, + ]; + if (is_array($other)) { + $data += $other; + } + Property::addToList(RebootControl::KEY_TASKLIST, json_encode($data), 20); + } + /** * @param int[]|null $locations filter by these locations - * @return array list of active tasks for reboots/shutdowns. + * @return array|false list of active tasks for reboots/shutdowns. */ - public static function getActiveTasks($locations = null) + public static function getActiveTasks($locations = null, $id = null) { - if (is_array($locations) && in_array(0,$locations)) { + if (is_array($locations) && in_array(0, $locations)) { $locations = null; } $list = Property::getList(RebootControl::KEY_TASKLIST); $return = []; foreach ($list as $entry) { - $p = explode('/', $entry, 2); - if (count($p) !== 2) { + $p = json_decode($entry, true); + if (!is_array($p) || !isset($p['id'])) { Property::removeFromList(RebootControl::KEY_TASKLIST, $entry); continue; } - if (is_array($locations) && !in_array($p[0], $locations)) // Ignore + if (is_array($locations) && is_array($p['locations']) && array_diff($p['locations'], $locations) !== []) + continue; // Not allowed + if ($id !== null) { + if ($p['id'] === $id) + return $p; continue; - $id = $p[1]; - $task = Taskmanager::status($id); - if (!Taskmanager::isTask($task)) { + } + $valid = empty($p['tasks']); + if (!$valid) { + // Validate at least one task is still valid + foreach ($p['tasks'] as $task) { + $task = Taskmanager::status($task); + if (Taskmanager::isTask($task)) { + $p['status'] = $task['statusCode']; + $valid = true; + break; + } + } + } + if (!$valid) { Property::removeFromList(RebootControl::KEY_TASKLIST, $entry); continue; } - $return[] = [ - 'taskId' => $task['id'], - 'locationId' => $task['data']['locationId'], - 'time' => $task['data']['time'], - 'mode' => $task['data']['mode'], - 'clientCount' => count($task['data']['clients']), - 'status' => $task['statusCode'], - ]; + $return[] = $p; } + if ($id !== null) + return false; return $return; } @@ -95,6 +170,15 @@ class RebootControl * @return array|false */ public static function runScript($clients, $command, $timeout = 5, $privkey = false) + { + $task = self::runScriptInternal($clients, $command, $timeout, $privkey); + if (!Taskmanager::isFailed($task)) { + self::addTask($task['id'], self::TASK_EXEC, $clients, $task['id']); + } + return $task; + } + + private static function runScriptInternal(&$clients, $command, $timeout = 5, $privkey = false) { $valid = []; $invalid = []; @@ -110,7 +194,7 @@ class RebootControl } } if (!empty($invalid)) { - $res = Database::simpleQuery('SELECT machineuuid, clientip FROM machine WHERE machineuuid IN (:uuids)', + $res = Database::simpleQuery('SELECT machineuuid, clientip, locationid FROM machine WHERE machineuuid IN (:uuids)', ['uuids' => array_keys($invalid)]); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { if (isset($invalid[$row['machineuuid']])) { @@ -120,20 +204,21 @@ class RebootControl } } } + $clients = $valid; + if (empty($clients)) { + error_log('RebootControl::runScript called without any clients'); + return false; + } if ($privkey === false) { $privkey = SSHKey::getPrivateKey(); } - $task = Taskmanager::submit('RemoteExec', [ - 'clients' => $valid, + return Taskmanager::submit('RemoteExec', [ + 'clients' => $clients, 'command' => $command, 'timeoutSeconds' => $timeout, 'sshkey' => $privkey, 'port' => 9922, // Fallback if no port given in client struct ]); - if (!Taskmanager::isFailed($task)) { - Property::addToList(RebootControl::KEY_TASKLIST, '0/' . $task["id"], 60 * 24); - } - return $task; } public static function connectionCheckCallback($task, $hostId) @@ -150,4 +235,270 @@ class RebootControl ['id' => $hostId, 'reachable' => $reachable]); } -} \ No newline at end of file + /** + * @param array $sourceMachines list of source machines. array of [clientip, machineuuid] entries + * @param string $bcast directed broadcast address to send to + * @param string|string[] $macaddr destination mac address(es) + * @param string $passwd optional WOL password, mac address or ipv4 notation + * @return array|false task struct, false on error + */ + public static function wakeViaClient($sourceMachines, $macaddr, $bcast = false, $passwd = false) + { + $command = 'jawol'; + if (!empty($bcast)) { + $command .= " -d '$bcast'"; + } + if (!empty($passwd)) { + $command .= " -p '$passwd'"; + } + if (is_array($macaddr)) { + $macaddr = implode("' '", $macaddr); + } + $command .= " '$macaddr'"; + // Yes there is one zero missing from the usleep -- that's the whole point: we prefer 100ms sleeps + return self::runScriptInternal($sourceMachines, + "for i in 1 1 0; do $command; usleep \${i}00000 2> /dev/null || sleep \$i; done"); + } + + /** + * @param string|string[] $macaddr destination mac address(es) + * @param string $bcast directed broadcast address to send to + * @param string $passwd optional WOL password, mac address or ipv4 notation + * @return array|false task struct, false on error + */ + public static function wakeDirectly($macaddr, $bcast = false, $passwd = false) + { + if (!is_array($macaddr)) { + $macaddr = [$macaddr]; + } + return Taskmanager::submit('WakeOnLan', [ + 'ip' => $bcast, + 'password' => $passwd, + 'macs' => $macaddr, + ]); + } + + public static function wakeViaJumpHost($jumphost, $bcast, $clients) + { + $hostid = $jumphost['hostid']; + $macs = ArrayUtil::flattenByKey($clients, 'macaddr'); + if (empty($macs)) { + error_log('Called wakeViaJumpHost without clients'); + return false; + } + $macs = "'" . implode("' '", $macs) . "'"; + $macs = str_replace('-', ':', $macs); + $script = str_replace(['%IP%', '%MACS%'], [$bcast, $macs], $jumphost['script']); + $task = RebootControl::runScriptInternal($_ = [[ + 'clientip' => $jumphost['host'], + 'port' => $jumphost['port'], + 'username' => $jumphost['username'], + ]], $script, 6, $jumphost['sshkey']); + if ($task !== false && isset($task['id'])) { + TaskmanagerCallback::addCallback($task, 'rbcConnCheck', $hostid); + } + return $task; + } + + /** + * @param array $list list of clients containing each keys 'macaddr' and 'clientip' + * @return string id of this job + */ + public static function wakeMachines($list) + { + /* TODO: Refactor mom's spaghetti + * Now that I figured out what I want, do something like this: + * 1) Group clients by subnet + * 2) Only after step 1, start to collect possible ways to wake up clients for each subnet that's not empty + * 3) Habe some priority list for the methods, extend Taskmanager to have "negative dependency" + * i.e. submit task B with task A as parent task, but only launch task B if task A failed. + * If task A succeeded, mark task B as FINISHED immediately without actually running it. + * (or introduce new statusCode for this?) + */ + $errors = ''; + $tasks = []; + $bad = $unknown = []; + // Need all subnets... + $subnets = []; + $res = Database::simpleQuery('SELECT subnetid, start, end, isdirect FROM reboot_subnet'); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $row += [ + 'jumphosts' => [], + 'direct' => [], + 'indirect' => [], + ]; + $subnets[$row['subnetid']] = $row; + } + // Get all jump hosts + $jumphosts = []; + $res = Database::simpleQuery('SELECT jh.hostid, host, port, username, sshkey, script, jh.reachable, + Group_Concat(jxs.subnetid) AS subnets1, Group_Concat(sxs.dstid) AS subnets2 + FROM reboot_jumphost jh + LEFT JOIN reboot_jumphost_x_subnet jxs ON (jh.hostid = jxs.hostid) + LEFT JOIN reboot_subnet s ON (INET_ATON(jh.host) BETWEEN s.start AND s.end) + LEFT JOIN reboot_subnet_x_subnet sxs ON (sxs.srcid = s.subnetid AND sxs.reachable <> 0) + GROUP BY jh.hostid'); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if ($row['subnets1'] === null && $row['subnets2'] === null) + continue; + $nets = explode(',', $row['subnets1'] . ',' . $row['subnets2']); + foreach ($nets as $net) { + if (empty($net) || !isset($subnets[$net])) + continue; + $subnets[$net]['jumphosts'][$row['hostid']] = $row['hostid']; + } + $row['jobs'] = []; + $jumphosts[] = $row; + } + // Group by subnet + foreach ($list as $client) { + $ip = sprintf('%u', ip2long($client['clientip'])); + //$client['numip'] = $ip; + unset($subnet); + $subnet = false; + foreach ($subnets as &$sn) { + if ($sn['start'] <= $ip && $sn['end'] >= $ip) { + $subnet =& $sn; + break; + } + } + $ok = false; + if (!$ok && $subnet === false) { + $unknown[] = $client; + $ok = true; + } + if (!$ok && $subnet['isdirect']) { + // Directly reachable + $subnet['direct'][] = $client; + $ok = true; + } + if (!$ok && !empty($subnet['jumphosts'])) { + foreach ($subnet['jumphosts'] as $hostid) { + if ($jumphosts[$hostid]['reachable'] != 0) { + $jumphosts[$hostid]['jobs'][$subnet['end']][] = $client; + $ok = true; + break; + } + } + } + if (!$ok) { + // find clients in same subnet, or reachable ones + self::findMachinesForSubnet($subnet); + if (empty($subnet['dclients']) && empty($subnet['iclients'])) { + // Nothing found -- cannot wake this host + $bad[] = $client; + } else { + // Found suitable indirect host + $subnet['indirect'][] = $client; + } + } + } + unset($subnet); + // Batch process + // First, via jump host + foreach ($jumphosts as $jh) { + foreach ($jh['jobs'] as $bcast => $clients) { + $errors .= 'Via jumphost ' . $jh['host'] . ': ' . implode(', ', ArrayUtil::flattenByKey($clients, 'clientip')) . "\n"; + $task = self::wakeViaJumpHost($jh, $bcast, $clients); + if (Taskmanager::isFailed($task)) { + // TODO: Figure out $subnet from $bcast and queue as indirect + // (rather, overhaul this whole spaghetti code) + $errors .= ".... FAILED TO LAUNCH TASK ON JUMPHOST!\n"; + } + } + } + // Server or client + foreach ($subnets as $subnet) { + if (!empty($subnet['direct'])) { + // Can wake directly + if (!self::wakeGroup('From server', $tasks, $errors, null, $subnet['direct'], $subnet['end'])) { + if (!empty($subnet['dclients']) || !empty($subnet['iclients'])) { + $errors .= "Re-queueing clients for indirect wakeup\n"; + $subnet['indirect'] = array_merge($subnet['indirect'], $subnet['direct']); + } + } + } + if (!empty($subnet['indirect'])) { + // Can wake indirectly + $ok = false; + if (!empty($subnet['dclients'])) { + $ok = true; + if (!self::wakeGroup('in same subnet', $tasks, $errors, $subnet['dclients'], $subnet['indirect'])) { + if (!empty($subnet['iclients'])) { + $errors .= "Re-re-queueing clients for indirect wakeup\n"; + $ok = false; + } + } + } + if (!$ok && !empty($subnet['iclients'])) { + $ok = self::wakeGroup('across subnets', $tasks, $errors, $subnet['dclients'], $subnet['indirect'], $subnet['end']); + } + if (!$ok) { + $errors .= "I'm all out of ideas.\n"; + } + } + } + if (!empty($bad)) { + $ips = ArrayUtil::flattenByKey($bad, 'clientip'); + $errors .= "**** WARNING ****\nNo way to send WOL packets to the following machines:\n" . implode("\n", $ips) . "\n"; + } + if (!empty($unknown)) { + $ips = ArrayUtil::flattenByKey($unknown, 'clientip'); + $errors .= "**** WARNING ****\nThe following clients do not belong to a known subnet (bug?)\n" . implode("\n", $ips) . "\n"; + } + $id = Util::randomUuid(); + self::addTask($id, self::TASK_WOL, $list, $tasks, ['log' => $errors]); + return $id; + } + + private static function wakeGroup($type, &$tasks, &$errors, $via, $clients, $bcast = false) + { + $macs = ArrayUtil::flattenByKey($clients, 'macaddr'); + $ips = ArrayUtil::flattenByKey($clients, 'clientip'); + if ($via !== null) { + $srcips = ArrayUtil::flattenByKey($via, 'clientip'); + $errors .= 'Via ' . implode(', ', $srcips) . ' '; + } + $errors .= $type . ': ' . implode(', ', $ips); + if ($bcast !== false) { + $errors .= ' (UDP to ' . long2ip($bcast) . ')'; + } + $errors .= "\n"; + if ($via === null) { + $task = self::wakeDirectly($macs, $bcast); + } else { + $task = self::wakeViaClient($via, $macs, $bcast); + } + if ($task !== false && isset($task['id'])) { + $tasks[] = $task['id']; + } + if (Taskmanager::isFailed($task)) { + $errors .= ".... FAILED TO START ACCORDING TASK!\n"; + return false; + } + return true; + } + + private static function findMachinesForSubnet(&$subnet) + { + if (isset($subnet['dclients'])) + return; + $cutoff = time() - 302; + // Get clients from same subnet first + $subnet['dclients'] = Database::queryAll("SELECT machineuuid, clientip FROM machine + WHERE state IN ('IDLE', 'OCCUPIED') AND INET_ATON(clientip) BETWEEN :start AND :end AND lastseen > :cutoff + LIMIT 3", + ['start' => $subnet['start'], 'end' => $subnet['end'], 'cutoff' => $cutoff]); + $subnet['iclients'] = []; + if (!empty($subnet['dclients'])) + return; + // If none, get clients from other subnets known to be able to reach this one + $subnet['iclients'] = Database::queryAll("SELECT m.machineuuid, m.clientip FROM reboot_subnet_x_subnet sxs + INNER JOIN reboot_subnet s ON (s.subnetid = sxs.srcid AND sxs.dstid = :subnetid) + INNER JOIN machine m ON (INET_ATON(m.clientip) BETWEEN s.start AND s.end AND state IN ('IDLE', 'OCCUPIED') AND m.lastseen > :cutoff) + LIMIT 20", ['subnetid' => $subnet['subnetid'], 'cutoff' => $cutoff]); + shuffle($subnet['iclients']); + $subnet['iclients'] = array_slice($subnet['iclients'], 0, 3); + } + +} diff --git a/modules-available/rebootcontrol/inc/rebootqueries.inc.php b/modules-available/rebootcontrol/inc/rebootqueries.inc.php index 063b36e4..c0c479bd 100644 --- a/modules-available/rebootcontrol/inc/rebootqueries.inc.php +++ b/modules-available/rebootcontrol/inc/rebootqueries.inc.php @@ -3,56 +3,27 @@ class RebootQueries { - // Get Client+IP+CurrentVM+CurrentUser+Location to fill the table - public static function getMachineTable($locationId) { - $queryArgs = array('cutoff' => strtotime('-30 days')); - if ($locationId === 0) { - $where = 'machine.locationid IS NULL'; - } else { - $where = 'machine.locationid = :locationid'; - $queryArgs['locationid'] = $locationId; - } - $leftJoin = ''; - $sessionField = 'machine.currentsession'; - if (Module::get('dozmod') !== false) { - // SELECT lectureid, displayname FROM sat.lecture WHERE lectureid = :lectureid - $leftJoin = 'LEFT JOIN sat.lecture ON (lecture.lectureid = machine.currentsession)'; - $sessionField = 'IFNULL(lecture.displayname, machine.currentsession) AS currentsession'; - } - $res = Database::simpleQuery(" - SELECT machine.machineuuid, machine.hostname, machine.clientip, - machine.lastboot, machine.lastseen, machine.logintime, machine.state, - $sessionField, machine.currentuser, machine.locationid - FROM machine - $leftJoin - WHERE $where AND machine.lastseen > :cutoff", $queryArgs); - $ret = $res->fetchAll(PDO::FETCH_ASSOC); - foreach ($ret as &$row) { - if ($row['state'] === 'IDLE' || $row['state'] === 'OCCUPIED') { - $row['status'] = 1; - } else { - $row['status'] = 0; - } - if ($row['state'] !== 'OCCUPIED') { - $row['currentuser'] = ''; - $row['currentsession'] = ''; - } - } - return $ret; - } - /** * Get machines by list of UUIDs * @param string[] $list list of system UUIDs * @return array list of machines with machineuuid, hostname, clientip, state and locationid */ - public static function getMachinesByUuid($list) + public static function getMachinesByUuid($list, $assoc = false, $columns = ['machineuuid', 'hostname', 'clientip', 'state', 'locationid']) { if (empty($list)) return array(); - $res = Database::simpleQuery("SELECT machineuuid, hostname, clientip, state, locationid FROM machine + if (is_array($columns)) { + $columns = implode(',', $columns); + } + $res = Database::simpleQuery("SELECT $columns FROM machine WHERE machineuuid IN (:list)", compact('list')); - return $res->fetchAll(PDO::FETCH_ASSOC); + if (!$assoc) + return $res->fetchAll(PDO::FETCH_ASSOC); + $ret = []; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $ret[$row['machineuuid']] = $row; + } + return $ret; } } \ No newline at end of file diff --git a/modules-available/rebootcontrol/install.inc.php b/modules-available/rebootcontrol/install.inc.php index eb484d3e..0aedfa20 100644 --- a/modules-available/rebootcontrol/install.inc.php +++ b/modules-available/rebootcontrol/install.inc.php @@ -8,7 +8,7 @@ $output[] = tableCreate('reboot_subnet', " `end` INT(10) UNSIGNED NOT NULL, `fixed` BOOL NOT NULL, `isdirect` BOOL NOT NULL, - `lastdirectcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `nextdirectcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0', `lastseen` INT(10) UNSIGNED NOT NULL DEFAULT '0', `seencount` INT(10) UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (`subnetid`), @@ -33,9 +33,9 @@ $output[] = tableCreate('reboot_subnet_x_subnet', " `srcid` INT(10) UNSIGNED NOT NULL, `dstid` INT(10) UNSIGNED NOT NULL, `reachable` BOOL NOT NULL, - `lastcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `nextcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (`srcid`, `dstid`), - KEY `lastcheck` (`lastcheck`)"); + KEY `nextcheck` (`nextcheck`)"); $output[] = tableAddConstraint('reboot_jumphost_x_subnet', 'hostid', 'reboot_jumphost', 'hostid', 'ON UPDATE CASCADE ON DELETE CASCADE'); diff --git a/modules-available/rebootcontrol/lang/de/messages.json b/modules-available/rebootcontrol/lang/de/messages.json index 788e57a2..81ddcb67 100644 --- a/modules-available/rebootcontrol/lang/de/messages.json +++ b/modules-available/rebootcontrol/lang/de/messages.json @@ -1,10 +1,17 @@ { + "invalid-ip-address": "Ung\u00fcltige IP-Adresse: {{0}}", "invalid-port": "Ung\u00fcltiger Port: {{0}}", + "invalid-subnet": "Ung\u00fcltiges Subnet: {{0}}", "jumphost-saved": "Sprung-Host {{0}} gespeichert", "no-clients-selected": "Keine Clients ausgew\u00e4hlt", "no-current-tasks": "Keine aktuellen oder k\u00fcrzlich abgeschlossenen Aufgaben", "no-such-jumphost": "Sprung-Host {{0}} existiert nicht", "no-such-task": "Task {{0}} existiert nicht", "some-machine-not-found": "Einige Clients aus dem POST request wurden nicht gefunden", - "unknown-task-type": "Unbekannter Task-Typ" + "subnet-already-exists": "Subnet existiert bereits", + "subnet-created": "Subnet angelegt", + "subnet-updated": "Subnet aktualisiert", + "unknown-task-type": "Unbekannter Task-Typ", + "woldiscover-disabled": "Automatische WOL-Ermittlung deaktiviert", + "woldiscover-enabled": "Automatische WOL-Ermittlung aktiviert" } \ No newline at end of file diff --git a/modules-available/rebootcontrol/lang/de/permissions.json b/modules-available/rebootcontrol/lang/de/permissions.json index fdc0b35f..405fd9f5 100644 --- a/modules-available/rebootcontrol/lang/de/permissions.json +++ b/modules-available/rebootcontrol/lang/de/permissions.json @@ -8,5 +8,6 @@ "newkeypair": "Neues Schl\u00fcsselpaar generieren.", "subnet.edit": "Subnets hinzuf\u00fcgen\/entfernen.", "subnet.flag": "Eigenschaften eines Subnets bearbeiten.", - "subnet.view": "Liste der Subnets sehen." + "subnet.view": "Liste der Subnets sehen.", + "woldiscover": "Automatische Ermittlung von subnet\u00fcbergreifender WOL-F\u00e4higkeit." } \ No newline at end of file diff --git a/modules-available/rebootcontrol/lang/de/template-tags.json b/modules-available/rebootcontrol/lang/de/template-tags.json index 365a739b..37b01e35 100644 --- a/modules-available/rebootcontrol/lang/de/template-tags.json +++ b/modules-available/rebootcontrol/lang/de/template-tags.json @@ -1,5 +1,8 @@ { "lang_activeTasks": "Laufende Jobs", + "lang_add": "Hinzuf\u00fcgen", + "lang_addNewSubnet": "Ein Subnet manuell hinzuf\u00fcgen", + "lang_assignedJumpHosts": "Zugewiesene Sprung-Hosts", "lang_assignedSubnets": "Subnets", "lang_authFail": "Authentifizierung fehlgeschlagen", "lang_check": "Testen", @@ -9,7 +12,10 @@ "lang_clientCount": "# Clients", "lang_connecting": "Verbinde...", "lang_editJumpHost": "Sprung-Host bearbeiten", + "lang_editSubnet": "Subnet bearbeiten", "lang_error": "Nicht erreichbar", + "lang_fixSubnetDesc": "Wenn aktiviert, wird die Erreichbarkeit f\u00fcr ideses Subnet nicht mehr automatisch ermittelt. Sie k\u00f6nnen in diesem Fall selber festlegen, ob das Subnet WOL-Pakete vom Satelliten-Server empfangen kann. Au\u00dferdem wird das Subnet bei Setzen dieser Option nicht mehr automatisch aus der Datenbank gel\u00f6scht, wenn 6 Monate lang kein Client in diesem Subnet gesehen wurde.", + "lang_fixSubnetSettings": "Subnet-Einstellungen manuell festlegen", "lang_genNew": "Neues Schl\u00fcsselpaar generieren", "lang_host": "Host", "lang_hostDeleteConfirm": "Diesen Sprung-Host l\u00f6schen?", @@ -17,8 +23,11 @@ "lang_hostNotReachable": "Host nicht erreichbar", "lang_hostReachable": "Host erreichbar", "lang_ip": "IP", + "lang_isDirect": "Vom Satellit erreichbar", + "lang_isFixed": "Manuell konfiguriert", "lang_jumpHosts": "Sprung-Hosts", "lang_keypairConfirmCheck": "Ich bin sicher", + "lang_lastseen": "Zuletzt gesehen", "lang_location": "Standort", "lang_mode": "Modus", "lang_moduleHeading": "WakeOnLAN", @@ -29,14 +38,25 @@ "lang_privkey": "Geheimer Schl\u00fcssel", "lang_pubKey": "SSH Public Key:", "lang_reachable": "Erreichbar", + "lang_reachableFrom": "Erreichbar von", + "lang_reachableFromServer": "Erreichbar vom Satelliten-Server", + "lang_reachableFromServerDesc": "Wenn dieser Haken gesetzt ist wird angenommen, dass WOL-Pakete, die vom Server aus gesendet werden, dieses Subnet erreichen k\u00f6nnen. Dazu muss der Router des Ziel-Netzes sog. \"Directed Broadcasts\" unterst\u00fctzen bzw. nicht filtern.", "lang_rebootAt": "Neustart um:", "lang_rebooting": "Neustart...", + "lang_saveWolAutoDiscover": "Auto-Erkennung ein\/ausschalten", "lang_settings": "Einstellungen", "lang_shutdown": "Herunterfahren", "lang_shutdownAt": "Herunterfahren um: ", "lang_status": "Status", + "lang_subnet": "Subnet", + "lang_subnets": "Subnets", + "lang_subnetsDescription": "Dies sind dem Satelliten-Server bekannte Subnetze. Damit WOL \u00fcber Subnet-Grenzen hinaus funktioniert, muss bekannt sein, in welche Netze \"Directed Broadcasts\" gesendet werden k\u00f6nnen, bzw. f\u00fcr welche Netze ein \"Sprung-Host\" existiert. Diese Liste wird sich automatisch f\u00fcllen, wenn Clients gestartet werden. Au\u00dferdem wird automatisch ermittelt, welche Netze mittels \"Directed Broadcasts\" erreichbar sind, sofern diese Funktion nicht oben unter \"Einstellungen\" deaktiviert wird.", + "lang_taskListIntro": "Hier sehen Sie eine Liste k\u00fcrzlich gestarteter Aufgaben, wie z.B. WOL-Aktionen, das Neustarten oder Herunterfahren von Clients, etc.", "lang_time": "Zeit", "lang_wakeScriptHelp": "Dieses Script wird auf dem Sprung-Host ausgef\u00fchrt, um den\/die gew\u00fcnschten Maschinen aufzuwecken. Es wird unter der Standard-Shell des oben angegebenen Benutzers ausgef\u00fchrt. Das Script kann zwei spezielle Platzhalter enthalten, die vor dem Ausf\u00fchren des Scripts vom Satellitenserver ersetzt werden: %MACS% ist eine durch Leerzeichen getrennte Liste von MAC-Adressen, die aufzuwecken sind. Das Tool \"wakeonlan\" unterst\u00fctzt direkt mehrere MAC-Adressen, sodass der Platzhalter %MACS% direkt als Kommandozeilenargument verwendet werden kann. Das Tool \"etherwake\" hingegen kann pro Aufruf immer nur einen Host aufwecken, weshalb eine for-Schleife notwendig ist. Au\u00dferdem wird der Platzhalter %IP% ersetzt, welcher je nach Ziel entweder \"255.255.255.255\" ist, oder bei einem netz\u00fcbergreifenden WOL-Paket die \"directed broadcast address\" des Zielnetzes. Netz\u00fcbergreifende WOL-Pakete werden vom \"etherwake\" nicht unterst\u00fctzt.", "lang_wakeupScript": "Aufweck-Script", + "lang_wolAutoDiscoverCheck": "WOL-Erreichbarkeit von Subnets automatisch ermitteln", + "lang_wolDiscoverDescription": "Ist diese Option aktiv, ermitteln Server und Clients automatisch, welche Netze von wo mittels WOL erreichbar sind.", + "lang_wolDiscoverHeading": "Automatische WOL-Ermittlung", "lang_wolReachability": "WOL-Erreichbarkeit" } \ No newline at end of file diff --git a/modules-available/rebootcontrol/page.inc.php b/modules-available/rebootcontrol/page.inc.php index eaa3c2e6..764a3d7b 100644 --- a/modules-available/rebootcontrol/page.inc.php +++ b/modules-available/rebootcontrol/page.inc.php @@ -43,11 +43,23 @@ class Page_RebootControl extends Page if ($action === 'reboot' || $action === 'shutdown') { $this->execRebootShutdown($action); + } elseif ($action === 'toggle-wol') { + User::assertPermission('woldiscover'); + $enabled = Request::post('enabled', false); + Property::set(RebootControl::KEY_AUTOSCAN_DISABLED, !$enabled); + if ($enabled) { + Message::addInfo('woldiscover-enabled'); + } else { + Message::addInfo('woldiscover-disabled'); + } + $section = 'subnet'; // For redirect below } } if (Request::isPost()) { Util::redirect('?do=rebootcontrol' . ($section ? '&show=' . $section : '')); + } elseif ($section === false) { + Util::redirect('?do=rebootcontrol&show=task'); } } @@ -70,8 +82,6 @@ class Page_RebootControl extends Page if (!User::hasPermission('action.' . $action, $actualClients[$idx]['locationid'])) { Message::addWarning('locations.no-permission-location', $actualClients[$idx]['locationid']); unset($actualClients[$idx]); - } else { - $locationId = $actualClients[$idx]['locationid']; } } // See if anything is left @@ -96,7 +106,7 @@ class Page_RebootControl extends Page $mode = 'REBOOT'; $minutes = Request::post('r-minutes', 0, 'int'); } - $task = RebootControl::execute($actualClients, $mode, $minutes, $locationId); + $task = RebootControl::execute($actualClients, $mode, $minutes); if (Taskmanager::isTask($task)) { Util::redirect("?do=rebootcontrol&show=task&what=task&taskid=" . $task["id"]); } @@ -110,8 +120,11 @@ class Page_RebootControl extends Page protected function doRender() { // Always show public key (it's public, isn't it?) - $data = ['pubkey' => SSHKey::getPublicKey()]; - Permission::addGlobalTags($data['perms'], null, ['newkeypair']); + $data = [ + 'pubkey' => SSHKey::getPublicKey(), + 'wol_auto_checked' => Property::get(RebootControl::KEY_AUTOSCAN_DISABLED) ? '' : 'checked', + ]; + Permission::addGlobalTags($data['perms'], null, ['newkeypair', 'woldiscover']); Render::addTemplate('header', $data); if ($this->haveSubpage) { @@ -127,6 +140,25 @@ class Page_RebootControl extends Page User::assertPermission("newkeypair"); Property::set("rebootcontrol-private-key", false); echo SSHKey::getPublicKey(); + } elseif ($action === 'clientstatus') { + $clients = Request::post('clients'); + if (is_array($clients)) { + // XXX No permission check here, should we consider this as leaking sensitive information? + $machines = RebootQueries::getMachinesByUuid(array_values($clients), false, ['machineuuid', 'state']); + $ret = []; + foreach ($machines as $machine) { + switch ($machine['state']) { + case 'OFFLINE': $val = 'glyphicon-off'; break; + case 'IDLE': $val = 'glyphicon-ok green'; break; + case 'OCCUPIED': $val = 'glyphicon-user red'; break; + case 'STANDBY': $val = 'glyphicon-off green'; break; + default: $val = 'glyphicon-question-sign'; break; + } + $ret[$machine['machineuuid']] = $val; + } + Header('Content-Type: application/json; charset=utf-8'); + echo json_encode($ret); + } } else { echo 'Invalid action.'; } diff --git a/modules-available/rebootcontrol/pages/jumphost.inc.php b/modules-available/rebootcontrol/pages/jumphost.inc.php index 111560ef..7dcdd52c 100644 --- a/modules-available/rebootcontrol/pages/jumphost.inc.php +++ b/modules-available/rebootcontrol/pages/jumphost.inc.php @@ -30,15 +30,9 @@ class SubPage private static function execCheckConnection($hostid) { $host = self::getJumpHost($hostid); - $script = str_replace(['%IP%', '%MACS%'], ['255.255.255.255', '00:11:22:33:44:55'], $host['script']); - $task = RebootControl::runScript([[ - 'clientip' => $host['host'], - 'port' => $host['port'], - 'username' => $host['username'], - ]], $script, 5, $host['sshkey']); + $task = RebootControl::wakeViaJumpHost($host, '255.255.255.255', [['macaddr' => '00:11:22:33:44:55']]); if (!Taskmanager::isTask($task)) return; - TaskmanagerCallback::addCallback($task, 'rbcConnCheck', $hostid); Util::redirect('?do=rebootcontrol&show=task&type=checkhost&what=task&taskid=' . $task['id']); } diff --git a/modules-available/rebootcontrol/pages/subnet.inc.php b/modules-available/rebootcontrol/pages/subnet.inc.php index 946d2d64..c38c7595 100644 --- a/modules-available/rebootcontrol/pages/subnet.inc.php +++ b/modules-available/rebootcontrol/pages/subnet.inc.php @@ -20,23 +20,16 @@ class SubPage private static function addSubnet() { User::assertPermission('subnet.edit'); - $range = []; - foreach (['start', 'end'] as $key) { - $range[$key] = Request::post($key, Request::REQUIRED, 'string'); - $range[$key . '_l'] = ip2long($range[$key]); - if ($range[$key . '_l'] === false) { - Message::addError('invalid-ip-address', $range[$key]); - return; - } - } - if ($range['start_l'] > $range['end_l']) { - Message::addError('invalid-range', $range['start'], $range['end']); + $cidr = Request::post('cidr', Request::REQUIRED, 'string'); + $range = IpUtil::parseCidr($cidr); + if ($range === false) { + Message::addError('invalid-cidr', $cidr); return; } $ret = Database::exec('INSERT INTO reboot_subnet (start, end, fixed, isdirect) VALUES (:start, :end, 1, 0)', [ - 'start' => sprintf('%u', $range['start_l']), - 'end' => sprintf('%u', $range['end_l']), + 'start' => $range['start'], + 'end' => $range['end'], ], true); if ($ret === false) { Message::addError('subnet-already-exists'); @@ -97,15 +90,15 @@ class SubPage User::assertPermission('subnet.*'); $nets = []; $res = Database::simpleQuery('SELECT subnetid, start, end, fixed, isdirect, - lastdirectcheck, lastseen, seencount, Count(hxs.hostid) AS jumphostcount - FROM reboot_subnet + nextdirectcheck, lastseen, seencount, Count(hxs.hostid) AS jumphostcount, Count(sxs.srcid) AS sourcecount + FROM reboot_subnet s LEFT JOIN reboot_jumphost_x_subnet hxs USING (subnetid) + LEFT JOIN reboot_subnet_x_subnet sxs ON (s.subnetid = sxs.dstid AND sxs.reachable <> 0) GROUP BY subnetid, start, end ORDER BY start ASC, end DESC'); $deadline = strtotime('-60 days'); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $row['start_s'] = long2ip($row['start']); - $row['end_s'] = long2ip($row['end']); + $row['cidr'] = IpUtil::rangeToCidr($row['start'], $row['end']); $row['lastseen_s'] = Util::prettyTime($row['lastseen']); if ($row['lastseen'] && $row['lastseen'] < $deadline) { $row['lastseen_class'] = 'text-danger'; @@ -114,7 +107,6 @@ class SubPage } $data = ['subnets' => $nets]; Render::addTemplate('subnet-list', $data); - Module::isAvailable('js_ip'); } private static function showSubnet() @@ -127,17 +119,29 @@ class SubPage Message::addError('invalid-subnet', $id); return; } + $subnet['cidr'] = IpUtil::rangeToCidr($subnet['start'], $subnet['end']); $subnet['start_s'] = long2ip($subnet['start']); $subnet['end_s'] = long2ip($subnet['end']); + // Get list of jump hosts $res = Database::simpleQuery('SELECT h.hostid, h.host, h.port, hxs.subnetid FROM reboot_jumphost h LEFT JOIN reboot_jumphost_x_subnet hxs ON (h.hostid = hxs.hostid AND hxs.subnetid = :id) ORDER BY h.host ASC', ['id' => $id]); + // Mark those assigned to the current subnet $jh = []; while ($row = $res->fetch(PDO::FETCH_ASSOC)) { $row['checked'] = $row['subnetid'] === null ? '' : 'checked'; $jh[] = $row; } $subnet['jumpHosts'] = $jh; + // Get list of all subnets that can broadcast into this one + $res = Database::simpleQuery('SELECT s.start, s.end FROM reboot_subnet s + INNER JOIN reboot_subnet_x_subnet sxs ON (s.subnetid = sxs.srcid AND sxs.dstid = :id AND sxs.reachable = 1) + ORDER BY s.start ASC', ['id' => $id]); + $sn = []; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $sn[] = ['cidr' => IpUtil::rangeToCidr($row['start'], $row['end'])]; + } + $subnet['sourceNets'] = $sn; Permission::addGlobalTags($subnet['perms'], null, ['subnet.flag', 'jumphost.view', 'jumphost.assign-subnet']); Render::addTemplate('subnet-edit', $subnet); } @@ -147,4 +151,4 @@ class SubPage } -} \ No newline at end of file +} diff --git a/modules-available/rebootcontrol/pages/task.inc.php b/modules-available/rebootcontrol/pages/task.inc.php index 15449aaf..e52eb981 100644 --- a/modules-available/rebootcontrol/pages/task.inc.php +++ b/modules-available/rebootcontrol/pages/task.inc.php @@ -10,6 +10,12 @@ class SubPage public static function doRender() { + $xxx = Request::get('tasks'); + if (is_array($xxx)) { + $data = array_map(function($item) { return ['id' => $item]; }, $xxx); + Render::addTemplate('status-wol', ['tasks' => $data]); + return; + } $show = Request::get('what', 'tasklist', 'string'); if ($show === 'tasklist') { self::showTaskList(); @@ -21,67 +27,110 @@ class SubPage private static function showTask() { $taskid = Request::get("taskid", Request::REQUIRED, 'string'); - $task = Taskmanager::status($taskid); - - if (!Taskmanager::isTask($task) || !isset($task['data'])) { - Message::addError('no-such-task', $taskid); - return; - } - - $td =& $task['data']; $type = Request::get('type', false, 'string'); - if ($type === false) { - // Try to guess - if (isset($td['locationId']) || isset($td['clients'])) { - $type = 'reboot'; - } elseif (isset($td['result'])) { - $type = 'exec'; + if ($type === 'checkhost') { + // Override + $task = Taskmanager::status($taskid); + if (!Taskmanager::isTask($task) || !isset($task['data'])) { + Message::addError('no-such-task', $taskid); + return; } - } - if ($type === 'reboot') { - $data = [ - 'taskId' => $task['id'], - 'locationId' => $td['locationId'], - 'locationName' => Location::getName($td['locationId']), - ]; - $uuids = array_map(function ($entry) { - return $entry['machineuuid']; - }, $td['clients']); - $data['clients'] = RebootQueries::getMachinesByUuid($uuids); - Render::addTemplate('status-reboot', $data); - } elseif ($type === 'exec') { - $data = [ - 'taskId' => $task['id'], - ]; - Render::addTemplate('status-exec', $data); - } elseif ($type === 'checkhost') { + $td =& $task['data']; $ip = array_key_first($td['result']); $data = [ 'taskId' => $task['id'], 'host' => $ip, ]; Render::addTemplate('status-checkconnection', $data); - } else { + return; + } + if ($type !== false) { Message::addError('unknown-task-type'); } + + $job = RebootControl::getActiveTasks(null, $taskid); + if ($job === false) { + Message::addError('no-such-task', $taskid); + return; + } + if (isset($job['type'])) { + $type = $job['type']; + } + if ($type === RebootControl::TASK_EXEC) { + $template = $perm = 'exec'; + } elseif ($type === RebootControl::TASK_REBOOTCTL) { + $template = 'reboot'; + if ($job['action'] === RebootControl::SHUTDOWN) { + $perm = 'shutdown'; + } else { + $perm = 'reboot'; + } + } elseif ($type == RebootControl::TASK_WOL) { + $template = $perm = 'wol'; + } else { + Message::addError('unknown-task-type', $type); + return; + } + if (!empty($job['locations'])) { + $allowedLocs = User::getAllowedLocations("action.$perm"); + if (!in_array(0, $allowedLocs) && array_diff($job['locations'], $allowedLocs) !== []) { + Message::addError('main.no-permission'); + return; + } + self::expandLocationIds($job['locations']); + } + + // Output + if ($type === RebootControl::TASK_REBOOTCTL) { + $job['clients'] = RebootQueries::getMachinesByUuid(ArrayUtil::flattenByKey($job['clients'], 'machineuuid')); + } elseif ($type === RebootControl::TASK_EXEC) { + $details = RebootQueries::getMachinesByUuid(ArrayUtil::flattenByKey($job['clients'], 'machineuuid'), true); + foreach ($job['clients'] as &$client) { + if (isset($client['machineuuid']) && isset($details[$client['machineuuid']])) { + $client += $details[$client['machineuuid']]; + } + } + } elseif ($type === RebootControl::TASK_WOL) { + // Nothing (yet) + } else { + Util::traceError('oopsie'); + } + Render::addTemplate('status-' . $template, $job); } private static function showTaskList() { + Render::addTemplate('task-header'); // Append list of active reboot/shutdown tasks - $allowedLocs = User::getAllowedLocations("*"); + $allowedLocs = User::getAllowedLocations("action.*"); $active = RebootControl::getActiveTasks($allowedLocs); if (empty($active)) { Message::addInfo('no-current-tasks'); } else { foreach ($active as &$entry) { - $entry['locationName'] = Location::getName($entry['locationId']); + self::expandLocationIds($entry['locations']); + if (isset($entry['clients'])) { + $entry['clients'] = count($entry['clients']); + } } unset($entry); Render::addTemplate('task-list', ['list' => $active]); } } + private static function expandLocationIds(&$lids) + { + foreach ($lids as &$locid) { + if ($locid === 0) { + $name = '-'; + } else { + $name = Location::getName($locid); + } + $locid = ['id' => $locid, 'name' => $name]; + } + $lids = array_values($lids); + } + public static function doAjax() { diff --git a/modules-available/rebootcontrol/permissions/permissions.json b/modules-available/rebootcontrol/permissions/permissions.json index 5983447e..5416a482 100644 --- a/modules-available/rebootcontrol/permissions/permissions.json +++ b/modules-available/rebootcontrol/permissions/permissions.json @@ -2,6 +2,9 @@ "newkeypair": { "location-aware": false }, + "woldiscover": { + "location-aware": false + }, "subnet.view": { "location-aware": false }, @@ -28,5 +31,8 @@ }, "action.wol": { "location-aware": true + }, + "action.exec": { + "location-aware": true } } \ No newline at end of file diff --git a/modules-available/rebootcontrol/style.css b/modules-available/rebootcontrol/style.css index e35bce29..0f96c09f 100644 --- a/modules-available/rebootcontrol/style.css +++ b/modules-available/rebootcontrol/style.css @@ -1,25 +1,3 @@ -.rebootTimerForm { - margin-top: 20px; -} - -.statusColumn { - text-align: center; -} - -.table > tbody > tr > td { - vertical-align: middle; - height: 50px; -} - -.checkbox { - margin-top: 0; - margin-bottom: 0; -} - -.select-button { - min-width: 150px; -} - #dataTable { margin-top: 20px; } @@ -33,6 +11,10 @@ pre { white-space: pre-wrap; } -th[data-sort] { - cursor: pointer; +div.loc { + margin: 1px 2px; + border-radius: 2px; + padding: 1px 3px; + background: #eee; + float: left; } \ No newline at end of file diff --git a/modules-available/rebootcontrol/templates/header.html b/modules-available/rebootcontrol/templates/header.html index 4f240b81..6d38b939 100644 --- a/modules-available/rebootcontrol/templates/header.html +++ b/modules-available/rebootcontrol/templates/header.html @@ -15,20 +15,40 @@ - + + diff --git a/modules-available/rebootcontrol/templates/status-reboot.html b/modules-available/rebootcontrol/templates/status-reboot.html index 4be95e81..240c4387 100644 --- a/modules-available/rebootcontrol/templates/status-reboot.html +++ b/modules-available/rebootcontrol/templates/status-reboot.html @@ -1,30 +1,35 @@ -

{{lang_location}}: {{locationName}}

+

{{action}}

+{{#locations}} +
{{name}}
+{{/locations}} +
-
- - - - - - - - +
{{lang_client}}{{lang_ip}} - {{lang_status}} -
+ + + + + + + - - {{#clients}} - - - - - - {{/clients}} - -
{{lang_client}}{{lang_ip}} + {{lang_status}} +
{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}{{clientip}}
-
+ + {{#clients}} + + {{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}} + {{clientip}} + + + + + + {{/clients}} + + -
+
\ No newline at end of file diff --git a/modules-available/rebootcontrol/templates/subnet-edit.html b/modules-available/rebootcontrol/templates/subnet-edit.html index 4c3702ba..d8173863 100644 --- a/modules-available/rebootcontrol/templates/subnet-edit.html +++ b/modules-available/rebootcontrol/templates/subnet-edit.html @@ -6,7 +6,7 @@
- {{lang_editSubnet}}: {{start_s}} - {{end_s}} + {{lang_editSubnet}}: {{cidr}} ({{start_s}} - {{end_s}})
@@ -39,6 +39,12 @@
{{/jumpHosts}}
+
+ + {{#sourceNets}} +
{{cidr}}
+ {{/sourceNets}} +