From 4c0862efe57fbdaa51a69c8dc39f6fc4ae45fa20 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Fri, 19 Mar 2021 13:54:22 +0100 Subject: [locations/rebootcontrol] Inherit openingtimes for WOL/shutdown The opening times schedule is now inherited to child locations, so it's easy to toggle WOL or shutdown for individual rooms in a building, where you only have to set the opening times once for the entire building. As of now, WOL and shutdown settings are *not* inherited to child locations, as I'm not sure if you always want to inherit those by default. Closes #3710 --- modules-available/rebootcontrol/hooks/cron.inc.php | 27 +-- .../rebootcontrol/inc/scheduler.inc.php | 232 ++++++++++++++++++--- 2 files changed, 203 insertions(+), 56 deletions(-) (limited to 'modules-available/rebootcontrol') diff --git a/modules-available/rebootcontrol/hooks/cron.inc.php b/modules-available/rebootcontrol/hooks/cron.inc.php index c1136c98..8f5c73a0 100644 --- a/modules-available/rebootcontrol/hooks/cron.inc.php +++ b/modules-available/rebootcontrol/hooks/cron.inc.php @@ -11,32 +11,7 @@ if (in_array((int)date('G'), [6, 7, 9, 12, 15]) && in_array(date('i'), ['00', '0 } // CRON for Scheduler -$now = time(); -$res = Database::simpleQuery("SELECT s.locationid, s.action, s.nextexecution, s.options, l.openingtime - FROM reboot_scheduler s - INNER JOIN location l USING (locationid) - WHERE s.nextexecution <= :now", ['now' => $now]); -while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $options = json_decode($row['options'], true); - - // Calculate next_execution for the event. - Scheduler::updateSchedule($row['locationid'], $options, $row['openingtime']); - - if ($row['nextexecution'] + 1200 < $now) - continue; - - $machines = Database::queryAll("SELECT machineuuid, clientip, macaddr, locationid FROM machine - WHERE locationid = :locid", ['locid' => $row['locationid']]); - if ($row['action'] === Scheduler::SHUTDOWN) { - RebootControl::execute($machines, RebootControl::SHUTDOWN, 0); - } elseif ($row['action'] === Scheduler::WOL) { - RebootControl::wakeMachines($machines); - } elseif ($row['action'] === Scheduler::REBOOT) { - RebootControl::execute($machines, RebootControl::REBOOT, 0); - } else { - EventLog::warning("Invalid action '{$row['action']}' in schedule for location " . $row['locationid']); - } -} +Scheduler::cron(); /* * Client reachability test -- can be disabled diff --git a/modules-available/rebootcontrol/inc/scheduler.inc.php b/modules-available/rebootcontrol/inc/scheduler.inc.php index 45aedcc1..292529fa 100644 --- a/modules-available/rebootcontrol/inc/scheduler.inc.php +++ b/modules-available/rebootcontrol/inc/scheduler.inc.php @@ -7,38 +7,23 @@ class Scheduler const REBOOT = 'REBOOT'; const WOL = 'WOL'; - public static function updateSchedule($locationid, $options, $openingTimes) - { - if (empty($openingTimes)) { - self::deleteSchedule($locationid); - return false; - } - $nextexec = self::calculateNext($options, $openingTimes); - if ($nextexec !== false) { - $json_options = json_encode($options); - Database::exec("INSERT INTO `reboot_scheduler` (locationid, action, nextexecution, options) - VALUES (:lid, :act, :next, :opt) - ON DUPLICATE KEY UPDATE - action = VALUES(action), nextexecution = VALUES(nextexecution), options = VALUES(options)", [ - 'lid' => $locationid, - 'act' => $nextexec['action'], - 'next' => $nextexec['time'], - 'opt' => $json_options, - ]); - } else { - // All times are getting ignored because they are within 5 minutes of each other, delete possible db entries. - self::deleteSchedule($locationid); - } - return true; - } - - public static function deleteSchedule($locationid) + /** + * @param int $locationid ID of location to delete WOL/shutdown settings for + */ + public static function deleteSchedule(int $locationid) { Database::exec("DELETE FROM `reboot_scheduler` WHERE locationid = :lid", ['lid' => $locationid]); } - private static function calculateTimestamp($now, $day, $time) + /** + * Calculate next time the given time description is reached + * @param int $now unix timestamp representing now + * @param string $day Name of weekday + * @param string $time Time, fi. 13:45 + * @return false|int unix timestamp in the future when we reach the given time + */ + private static function calculateTimestamp(int $now, string $day, string $time) { $ts = strtotime("$day $time"); if ($ts < $now) { @@ -51,11 +36,14 @@ class Scheduler return $ts; } - private static function calculateNext($options, $openingTimes) + /** + * Take WOL/SD options and opening times schedule, return next event. + * @return array|false array with keys 'time' and 'action' false if no next event + */ + private static function calculateNext(array $options, array $openingTimes) { - if (!$options['wol'] && !$options['sd']) + if ((!$options['wol'] && !$options['sd']) || empty($openingTimes)) return false; - $openingTimes = json_decode($openingTimes, true); $now = time(); $events = []; foreach ($openingTimes as $row) { @@ -120,4 +108,188 @@ class Scheduler return false; } + /** + * Check if any actions have to be taken. To be called periodically by cron. + */ + public static function cron() + { + $now = time(); + $res = Database::simpleQuery("SELECT s.locationid, s.action, s.nextexecution, s.options + FROM reboot_scheduler s + WHERE s.nextexecution < :now AND s.nextexecution > 0", ['now' => $now]); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + // Calculate next_execution for the event and update DB. + $options = json_decode($row['options'], true); + // Determine proper opening times by waling up tree + $openingTimes = OpeningTimes::forLocation($row['locationid']); + if ($openingTimes !== null) { + self::updateScheduleSingle($row['locationid'], $options, $openingTimes); + } + // Weird clock drift? Server offline for a while? Do nothing. + if ($row['nextexecution'] + 900 < $now) + continue; + self::executeCronForLocation($row['locationid'], $row['action']); + } + } + + /** + * Execute the given action for the given location. + */ + private static function executeCronForLocation(int $locationId, string $action) + { + $machines = Database::queryAll("SELECT machineuuid, clientip, macaddr, locationid FROM machine + WHERE locationid = :locid", ['locid' => $locationId]); + if (empty($machines)) + return; + if ($action === Scheduler::SHUTDOWN) { + RebootControl::execute($machines, RebootControl::SHUTDOWN, 0); + } elseif ($action === Scheduler::WOL) { + RebootControl::wakeMachines($machines); + } elseif ($action === Scheduler::REBOOT) { + RebootControl::execute($machines, RebootControl::REBOOT, 0); + } else { + EventLog::warning("Invalid action '$action' in schedule for location " . $locationId); + } + } + + /** + * Get current settings for given location, or false if none. + * @param int $id + * @return false|array + */ + public static function getLocationOptions(int $id) + { + $res = Database::queryFirst("SELECT options FROM `reboot_scheduler` + WHERE locationid = :id", ['id' => $id]); + if ($res !== false) { + return json_decode($res['options'], true); + } + return false; + } + + /** + * Write new WOL/Shutdown options for given location. + * @param int $locationId + * @param bool $wol whether WOL is enabled + * @param bool $sd whether Shutdown is enabled + * @param int $wolOffset how many minutes prior to opening time the WOL should be triggered + * @param int $sdOffset how many minutes after closing time a shutdown should be triggered + */ + public static function setLocationOptions(int $locationId, bool $wol, bool $sd, int $wolOffset, int $sdOffset) + { + $openingTimes = OpeningTimes::forLocation($locationId); + if (!$wol && !$sd) { + self::deleteSchedule($locationId); + } else { + // Sanity checks + if ($wolOffset > 60) { + $wolOffset = 60; + } elseif ($wolOffset < 0) { + $wolOffset = 0; + } + if ($sdOffset > 60) { + $sdOffset = 60; + } elseif ($sdOffset < 0) { + $sdOffset = 0; + } + $options = [ + 'wol' => $wol, + 'sd' => $sd, + 'wol-offset' => $wolOffset, + 'sd-offset' => $sdOffset, + ]; + $json_options = json_encode($options); + // Write settings, reset schedule + Database::exec("INSERT INTO `reboot_scheduler` (locationid, action, nextexecution, options) + VALUES (:lid, :act, :next, :opt) + ON DUPLICATE KEY UPDATE + action = VALUES(action), nextexecution = VALUES(nextexecution), options = VALUES(options)", [ + 'lid' => $locationId, + 'act' => 'WOL', + 'next' => 0, + 'opt' => $json_options, + ]); + // Write new timestamps for this location + if ($openingTimes !== null) { + self::updateScheduleSingle($locationId, $options, $openingTimes); + } + } + // In either case, refresh data for children as well + if ($openingTimes !== null) { + self::updateScheduleRecursive($locationId, $openingTimes); + } + } + + /** + * Write next WOL/shutdown action to DB, using given options and opening times. + * @param int $locationid Location to store settings for + * @param array $options Options for calculation (WOL/Shutdown enabled, offsets) + * @param array $openingTimes Opening times to use + */ + private static function updateScheduleSingle(int $locationid, array $options, array $openingTimes) + { + if (!$options['wol'] && !$options['sd']) { + self::deleteSchedule($locationid); + return; + } + $nextexec = self::calculateNext($options, $openingTimes); + if ($nextexec === false) { + // Empty opening times, or all intervals seem to be < 5 minutes, disable. + $nextexec = [ + 'action' => 'WOL', + 'time' => 0, + ]; + } + Database::exec("UPDATE reboot_scheduler + SET action = :act, nextexecution = :next + WHERE locationid = :lid", [ + 'lid' => $locationid, + 'act' => $nextexec['action'], + 'next' => $nextexec['time'], + ]); + } + + /** + * Recurse into all child locations of the given location-id and re-calculate the next + * WOL or shutdown event, based on the given opening times. Recursion stops at locations + * that come with their own opening times. + * @param int $parentId parent location to start recursion from. Not actually processed. + * @param array $openingTimes Opening times to use for calculations + */ + private static function updateScheduleRecursive(int $parentId, array $openingTimes) + { + $list = Location::getLocationsAssoc(); + if (!isset($list[$parentId])) + return; + $childIdList = $list[$parentId]['directchildren']; + if (empty($childIdList)) + return; + $res = Database::simpleQuery("SELECT l.locationid, l.openingtime, rs.options + FROM location l + LEFT JOIN reboot_scheduler rs USING (locationid) + WHERE l.locationid IN (:list)", ['list' => $childIdList]); + $locationData = []; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $locationData[$row['locationid']] = $row; + } + // Handle all child locations + foreach ($childIdList as $locationId) { + if (!isset($locationData[$locationId]) || $locationData[$locationId]['openingtime'] !== null) { + continue; // Ignore entire sub-tree where new opening times are assigned + } + // This location doesn't have a new openingtimes schedule + // If any options are set for this location, update its schedule + if ($locationData[$locationId]['options'] !== null) { + $options = json_decode($locationData[$locationId]['options'], true); + if (!is_array($options)) { + trigger_error("Invalid options for lid:$locationId", E_USER_WARNING); + } else { + self::updateScheduleSingle($locationId, $options, $openingTimes); + } + } + // Either way, further walk down the tree + self::updateScheduleRecursive($locationId, $openingTimes); + } + } + } \ No newline at end of file -- cgit v1.2.3-55-g7522