summaryrefslogtreecommitdiffstats
path: root/modules-available/rebootcontrol
diff options
context:
space:
mode:
authorSimon Rettberg2021-03-19 13:54:22 +0100
committerSimon Rettberg2021-03-19 13:54:22 +0100
commit4c0862efe57fbdaa51a69c8dc39f6fc4ae45fa20 (patch)
treecf5d472d2bea7a4ba43339bf892db6dad710e51c /modules-available/rebootcontrol
parent[locations] Add permission for openingtimes (diff)
downloadslx-admin-4c0862efe57fbdaa51a69c8dc39f6fc4ae45fa20.tar.gz
slx-admin-4c0862efe57fbdaa51a69c8dc39f6fc4ae45fa20.tar.xz
slx-admin-4c0862efe57fbdaa51a69c8dc39f6fc4ae45fa20.zip
[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
Diffstat (limited to 'modules-available/rebootcontrol')
-rw-r--r--modules-available/rebootcontrol/hooks/cron.inc.php27
-rw-r--r--modules-available/rebootcontrol/inc/scheduler.inc.php232
2 files changed, 203 insertions, 56 deletions
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