diff options
Diffstat (limited to 'modules-available')
7 files changed, 166 insertions, 60 deletions
diff --git a/modules-available/locations/inc/openingtimes.inc.php b/modules-available/locations/inc/openingtimes.inc.php index 72ce92f4..18e25063 100644 --- a/modules-available/locations/inc/openingtimes.inc.php +++ b/modules-available/locations/inc/openingtimes.inc.php @@ -6,11 +6,12 @@ class OpeningTimes /** * Get opening times for given location. * Format is the decoded JSON from DB column, i.e. currently a list of entries: - * { + * <pre>{ * "days": ["Monday", "Tuesday", ...], * "openingtime": "8:00", * "closingtime": "20:00" - * } + * }</pre> + * @return array|null */ public static function forLocation(int $locationId) { @@ -31,4 +32,32 @@ class OpeningTimes return null; } + /** + * Check whether given location is open according to openingtimes. + * @param int $locationId location + * @param int $openOffsetMin offset to apply to opening times when checking. this is subtracted from opening time + * @param int $closeOffsetMin offset to apply to closing times when checking. this is added to closing time + */ + public static function isRoomOpen(int $locationId, int $openOffsetMin = 0, int $closeOffsetMin = 0): bool + { + $openingTimes = self::forLocation($locationId); + if ($openingTimes === null) + return true; // No opening times should mean room is always open + $now = time(); + $today = date('l', $now); + foreach ($openingTimes as $row) { + foreach ($row['days'] as $day) { + if ($day !== $today) + continue; // Not today! + if (strtotime("today {$row['openingtime']} -$openOffsetMin minutes") > $now) + continue; + if (strtotime("today {$row['closingtime']} +$closeOffsetMin minutes") < $now) + continue; + // Bingo! + return true; + } + } + return false; + } + }
\ No newline at end of file diff --git a/modules-available/locations/pages/details.inc.php b/modules-available/locations/pages/details.inc.php index 86bfebd6..d2ec7b24 100644 --- a/modules-available/locations/pages/details.inc.php +++ b/modules-available/locations/pages/details.inc.php @@ -39,9 +39,10 @@ class SubPage $openingTimes = Request::post('openingtimes', Request::REQUIRED, 'string'); $locationid = Request::post('locationid', Request::REQUIRED, 'int'); $wol = Request::post('wol', false, 'bool'); - $woloffset = Request::post('wol-offset', 0, 'int'); + $wolOffset = Request::post('wol-offset', 0, 'int'); $sd = Request::post('sd', false, 'bool'); - $sdoffset = Request::post('sd-offset', 0, 'int'); + $sdOffset = Request::post('sd-offset', 0, 'int'); + $raMode = Request::post('ra-mode', 'ALWAYS', 'string'); User::assertPermission('location.edit.openingtimes', $locationid); @@ -93,7 +94,16 @@ class SubPage if (Module::isAvailable('rebootcontrol')) { // Set options - Scheduler::setLocationOptions($locationid, $wol, $sd, $woloffset, $sdoffset); + if (!Scheduler::isValidRaMode($raMode)) { + $raMode = Scheduler::RA_ALWAYS; + } + Scheduler::setLocationOptions($locationid, [ + 'wol' => $wol, + 'sd' => $sd, + 'wol-offset' => $wolOffset, + 'sd-offset' => $sdOffset, + 'ra-mode' => $raMode, + ]); } } @@ -396,7 +406,7 @@ class SubPage if (Module::get('rebootcontrol') !== false) { $res = Database::queryFirst("SELECT action, nextexecution FROM `reboot_scheduler` WHERE locationid = :id", ['id' => $locationId]); - if ($res !== false) { + if ($res !== false && $res['nextexecution'] > 0) { $data['next_action'] = $res['action']; $data['next_time'] = Util::prettyTime($res['nextexecution']); } @@ -431,12 +441,13 @@ class SubPage $data['rebootcontrol'] = $rebootcontrol; if ($rebootcontrol) { $data['scheduler-options'] = Scheduler::getLocationOptions($id); + $data['scheduler_' . $data['scheduler-options']['ra-mode'] . '_checked'] = 'checked'; } echo Render::parse('ajax-opening-location', $data); } - private static function isSimpleMode(&$array) + private static function isSimpleMode(&$array): bool { if (empty($array)) return true; diff --git a/modules-available/locations/templates/ajax-opening-location.html b/modules-available/locations/templates/ajax-opening-location.html index 967e111c..861bef65 100644 --- a/modules-available/locations/templates/ajax-opening-location.html +++ b/modules-available/locations/templates/ajax-opening-location.html @@ -1,5 +1,5 @@ <div> - <h3>{{lang_openingTime}}</h3> + <h4>{{lang_openingTime}}</h4> <div class="checkbox"> <input id="oi{{id}}" class="openingtimes-inherited" type="checkbox" name="openingtimes-inherited" value="1" {{openingtimes_inherited}}> @@ -129,7 +129,7 @@ </div> {{#rebootcontrol}} -<hr> +<h4>{{lang_automatedMachineActions}}</h4> <div class="row wol"> <div class="col-sm-4"> <div class="checkbox checkbox-inline"> @@ -164,9 +164,32 @@ </div> </div> </div> +<h4>{{lang_remoteAccessConstraints}}</h4> +<div class="slx-smallspace"> + <div class="radio"> + <input id="ra-ALWAYS-check-{{id}}" name="ra-mode" value="ALWAYS" type="radio" + {{scheduler_ALWAYS_checked}}> + <label for="ra-ALWAYS-check-{{id}}">{{lang_remoteAccessNoRestriction}}</label> + </div> +</div> +<div class="slx-smallspace"> + <div class="radio"> + <input id="ra-SELECTIVE-check-{{id}}" name="ra-mode" value="SELECTIVE" type="radio" + {{scheduler_SELECTIVE_checked}}> + <label for="ra-SELECTIVE-check-{{id}}">{{lang_remoteAccessOnlyWhenClosed}}</label> + </div> +</div> +<div class="slx-smallspace"> + <div class="radio"> + <input id="ra-NEVER-check-{{id}}" name="ra-mode" value="NEVER" type="radio" + {{scheduler_NEVER_checked}}> + <label for="ra-NEVER-check-{{id}}">{{lang_remoteAccessNever}}</label> + </div> +</div> +<p><i>{{lang_remoteAccessHelp}}</i></p> {{/rebootcontrol}} -<script type="application/javascript"> +<script> (function() { var $loc = $('#openingTimesModal{{id}}'); diff --git a/modules-available/locations/templates/location-subnets.html b/modules-available/locations/templates/location-subnets.html index 80e63abf..85c5a744 100644 --- a/modules-available/locations/templates/location-subnets.html +++ b/modules-available/locations/templates/location-subnets.html @@ -150,7 +150,7 @@ <input type="hidden" name="openingtimes" value=""> <input type="hidden" name="locationid" value="{{locationid}}"> - <div class="modal-header">{{locationname}}</div> + <div class="modal-header"><h3>{{locationname}}</h3></div> <div class="modal-body"></div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">{{lang_close}}</button> diff --git a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php index b5f47c1d..d68e4dea 100644 --- a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php +++ b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php @@ -37,7 +37,6 @@ class RebootControl * @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) diff --git a/modules-available/rebootcontrol/inc/scheduler.inc.php b/modules-available/rebootcontrol/inc/scheduler.inc.php index 7da4b46b..9e91df25 100644 --- a/modules-available/rebootcontrol/inc/scheduler.inc.php +++ b/modules-available/rebootcontrol/inc/scheduler.inc.php @@ -3,9 +3,13 @@ class Scheduler { - const SHUTDOWN = 'SHUTDOWN'; - const REBOOT = 'REBOOT'; - const WOL = 'WOL'; + const ACTION_SHUTDOWN = 'SHUTDOWN'; + const ACTION_REBOOT = 'REBOOT'; + const ACTION_WOL = 'WOL'; + const RA_NEVER = 'NEVER'; + const RA_SELECTIVE = 'SELECTIVE'; + const RA_ALWAYS = 'ALWAYS'; + const SCHEDULE_OPTIONS_DEFAULT = ['wol' => false, 'sd' => false, 'wol-offset' => 0, 'sd-offset' => 0, 'ra-mode' => self::RA_ALWAYS]; /** * @param int $locationid ID of location to delete WOL/shutdown settings for @@ -17,7 +21,7 @@ class Scheduler } /** - * Calculate next time the given time description is reached + * 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 @@ -42,18 +46,23 @@ class Scheduler */ private static function calculateNext(array $options, array $openingTimes) { - if ((!$options['wol'] && !$options['sd']) || empty($openingTimes)) + // If ra-mode is selective, still execute even if wol and shutdown is disabled, + // because we still want to shutdown any sessions in the wrong runmode then + $selectiveRa = ($options['ra-mode'] === self::RA_SELECTIVE); + if ((!$options['wol'] && !$options['sd'] && !$selectiveRa) || empty($openingTimes)) return false; $now = time(); $events = []; + $findWol = $options['wol'] || $options['ra-mode'] === self::RA_SELECTIVE; + $findSd = $options['sd'] || $options['ra-mode'] === self::RA_SELECTIVE; foreach ($openingTimes as $row) { foreach ($row['days'] as $day) { - if ($options['wol']) { - $events[] = ['action' => self::WOL, + if ($findWol) { + $events[] = ['action' => self::ACTION_WOL, 'time' => self::calculateTimestamp($now, $day, $row['openingtime'])]; } - if ($options['sd']) { - $events[] = ['action' => self::SHUTDOWN, + if ($findSd) { + $events[] = ['action' => self::ACTION_SHUTDOWN, 'time' => self::calculateTimestamp($now, $day, $row['closingtime'])]; } } @@ -67,9 +76,9 @@ class Scheduler $prev = PHP_INT_MAX; for ($i = count($events) - 1; $i >= 0; --$i) { $event =& $events[$i]; - if ($event['action'] === self::WOL) { + if ($event['action'] === self::ACTION_WOL) { $event['time'] -= $wolOffset; - } elseif ($event['action'] === self::SHUTDOWN) { + } elseif ($event['action'] === self::ACTION_SHUTDOWN) { $event['time'] += $sdOffset; } else { error_log('BUG Unhandled event type ' . $event['action']); @@ -94,9 +103,9 @@ class Scheduler // If difference to next event is < 5 min, ignore. continue; } - if ($diff < 900 && $event['action'] === self::SHUTDOWN && $events[$i + 1]['action'] === self::WOL) { + if ($diff < 900 && $event['action'] === self::ACTION_SHUTDOWN && $events[$i + 1]['action'] === self::ACTION_WOL) { // If difference to next WOL is < 15 min and this is a shutdown, reboot instead. - $res['action'] = self::REBOOT; + $res['action'] = self::ACTION_REBOOT; $res['time'] = $event['time']; } else { // Use first event. @@ -119,7 +128,7 @@ class Scheduler WHERE s.nextexecution < :now AND s.nextexecution > 0", ['now' => $now]); foreach ($res as $row) { // Calculate next_execution for the event and update DB. - $options = json_decode($row['options'], true); + $options = json_decode($row['options'], true) + self::SCHEDULE_OPTIONS_DEFAULT; // Determine proper opening times by waling up tree $openingTimes = OpeningTimes::forLocation($row['locationid']); if ($openingTimes !== null) { @@ -128,24 +137,61 @@ class Scheduler // Weird clock drift? Server offline for a while? Do nothing. if ($row['nextexecution'] + 900 < $now) continue; - self::executeCronForLocation($row['locationid'], $row['action']); + $selectiveRa = ($options['ra-mode'] === self::RA_SELECTIVE); + // Now, if selective remote access is active, we might modify the actual event: + if ($selectiveRa) { + // If this is WOL, and WOL is actually enabled, then reboot any running machines + // in remoteaccess mode, in addition to waking the others, so they exit remote access mode. + if ($row['action'] === Scheduler::ACTION_WOL && $options['wol']) { + self::executeCronForLocation($row['locationid'], Scheduler::ACTION_REBOOT, 'remoteaccess'); + self::executeCronForLocation($row['locationid'], Scheduler::ACTION_WOL); + } + // If this is WOL, and WOL is disabled, shut down any running machines, this is so + // anybody walking into this room will not mess with a user's session by yanking the + // power cord etc. + if ($row['action'] === Scheduler::ACTION_WOL && !$options['wol']) { + self::executeCronForLocation($row['locationid'], Scheduler::ACTION_SHUTDOWN, 'remoteaccess'); + } + // If this is SHUTDOWN, and SHUTDOWN is enabled, leave it at that. + if ($row['action'] === Scheduler::ACTION_SHUTDOWN && $options['sd']) { + self::executeCronForLocation($row['locationid'], Scheduler::ACTION_SHUTDOWN); + } + // If this is SHUTDOWN, and SHUTDOWN is disabled, do a reboot, so the machine ends up + // in the proper runmode. + if ($row['action'] === Scheduler::ACTION_SHUTDOWN && !$options['sd']) { + self::executeCronForLocation($row['locationid'], Scheduler::ACTION_REBOOT, ''); + } + } else { + // Regular case, no selective remoteaccess – just do what the cron entry says + self::executeCronForLocation($row['locationid'], $row['action']); + } } } /** * Execute the given action for the given location. + * @param int $locationId location + * @param string $action action to perform, Scheduler::* + * @param string|null $onlyRunmode if not null, only process running clients in given runmode + * @return void */ - private static function executeCronForLocation(int $locationId, string $action) + private static function executeCronForLocation(int $locationId, string $action, string $onlyRunmode = null) { - $machines = Database::queryAll("SELECT machineuuid, clientip, macaddr, locationid FROM machine + if ($onlyRunmode === null) { + $machines = Database::queryAll("SELECT machineuuid, clientip, macaddr, locationid FROM machine WHERE locationid = :locid", ['locid' => $locationId]); + } else { + $machines = Database::queryAll("SELECT machineuuid, clientip, macaddr, locationid FROM machine + WHERE locationid = :locid AND currentrunmode = :runmode AND state <> 'OFFLINE'", + ['locid' => $locationId, 'runmode' => $onlyRunmode]); + } if (empty($machines)) return; - if ($action === Scheduler::SHUTDOWN) { + if ($action === Scheduler::ACTION_SHUTDOWN) { RebootControl::execute($machines, RebootControl::SHUTDOWN, 0); - } elseif ($action === Scheduler::WOL) { + } elseif ($action === Scheduler::ACTION_WOL) { RebootControl::wakeMachines($machines); - } elseif ($action === Scheduler::REBOOT) { + } elseif ($action === Scheduler::ACTION_REBOOT) { RebootControl::execute($machines, RebootControl::REBOOT, 0); } else { EventLog::warning("Invalid action '$action' in schedule for location " . $locationId); @@ -155,49 +201,32 @@ class Scheduler /** * Get current settings for given location, or false if none. * @param int $id - * @return false|array */ - public static function getLocationOptions(int $id) + public static function getLocationOptions(int $id): array { $res = Database::queryFirst("SELECT options FROM `reboot_scheduler` WHERE locationid = :id", ['id' => $id]); if ($res !== false) { - return json_decode($res['options'], true); + return (json_decode($res['options'], true) ?? []) + self::SCHEDULE_OPTIONS_DEFAULT; } - return false; + return self::SCHEDULE_OPTIONS_DEFAULT; } /** * 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 + * @param array $options 'wol' 'sd' 'wol-offset' 'sd-offset' 'ra-mode' */ - public static function setLocationOptions(int $locationId, bool $wol, bool $sd, int $wolOffset, int $sdOffset) + public static function setLocationOptions(int $locationId, array $options) { + $options += self::SCHEDULE_OPTIONS_DEFAULT; $openingTimes = OpeningTimes::forLocation($locationId); - if (!$wol && !$sd) { + if (!$options['wol'] && !$options['sd'] && $options['ra-mode'] === self::RA_ALWAYS) { 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, - ]; + // Sanitize + Util::clamp($options['wol-offset'], 0, 60); + Util::clamp($options['sd-offset'], 0, 60); $json_options = json_encode($options); // Write settings, reset schedule Database::exec("INSERT INTO `reboot_scheduler` (locationid, action, nextexecution, options) @@ -228,7 +257,7 @@ class Scheduler */ private static function updateScheduleSingle(int $locationid, array $options, array $openingTimes) { - if (!$options['wol'] && !$options['sd']) { + if (!$options['wol'] && !$options['sd'] && $options['ra-mode'] === self::RA_ALWAYS) { self::deleteSchedule($locationid); return; } @@ -284,6 +313,7 @@ class Scheduler if (!is_array($options)) { trigger_error("Invalid options for lid:$locationId", E_USER_WARNING); } else { + $options += self::SCHEDULE_OPTIONS_DEFAULT; self::updateScheduleSingle($locationId, $options, $openingTimes); } } @@ -292,4 +322,9 @@ class Scheduler } } + public static function isValidRaMode(string $raMode): bool + { + return $raMode === self::RA_ALWAYS || $raMode === self::RA_NEVER || $raMode === self::RA_SELECTIVE; + } + }
\ No newline at end of file diff --git a/modules-available/remoteaccess/baseconfig/getconfig.inc.php b/modules-available/remoteaccess/baseconfig/getconfig.inc.php index 3d0c4470..84923dc6 100644 --- a/modules-available/remoteaccess/baseconfig/getconfig.inc.php +++ b/modules-available/remoteaccess/baseconfig/getconfig.inc.php @@ -18,6 +18,15 @@ ['lid' => $locationId], true); // TODO Remove true after next point release (2020-05-12) if ($ret === false) return; + // Special case – location admin can limit accessibility of this machine to never, or only when room is closed + $opts = Scheduler::getLocationOptions($locationId); + if ($opts['ra-mode'] === Scheduler::RA_NEVER) + return; // Completely disallowed + if ($opts['ra-mode'] === Scheduler::RA_SELECTIVE) { + // Only when room is closed + if (OpeningTimes::isRoomOpen($locationId, $opts['wol-offset'], $opts['sd-offset'])) + return; // Open, do not interfere with ongoing lectures etc., do nothing + } // TODO Properly merge if (Property::get(RemoteAccess::PROP_TRY_VIRT_HANDOVER)) { ConfigHolder::add("SLX_REMOTE_VNC", 'vmware virtualbox'); |