summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2022-04-29 17:38:00 +0200
committerSimon Rettberg2022-04-29 17:38:00 +0200
commit140062e0b606495f90fd77b8f290987844c79cab (patch)
treead32ce72580fa898f447c66316587ee7404e9854
parent[baseconfig_bwlp] Add more HDMI outputs for sound card (diff)
downloadslx-admin-140062e0b606495f90fd77b8f290987844c79cab.tar.gz
slx-admin-140062e0b606495f90fd77b8f290987844c79cab.tar.xz
slx-admin-140062e0b606495f90fd77b8f290987844c79cab.zip
[locations/remoteaccess] Add option to veto remoteaccess mode
Remoteaccess mode can now be forced to be disabled for individual locations in locations module, either unconditionally, or whenever the openingtimes schedule says the room is open. A reboot will be triggered whenever the room opens/closes to force clients into the proper runmode.
-rw-r--r--inc/util.inc.php14
-rw-r--r--modules-available/locations/inc/openingtimes.inc.php33
-rw-r--r--modules-available/locations/pages/details.inc.php21
-rw-r--r--modules-available/locations/templates/ajax-opening-location.html29
-rw-r--r--modules-available/locations/templates/location-subnets.html2
-rw-r--r--modules-available/rebootcontrol/inc/rebootcontrol.inc.php1
-rw-r--r--modules-available/rebootcontrol/inc/scheduler.inc.php131
-rw-r--r--modules-available/remoteaccess/baseconfig/getconfig.inc.php9
8 files changed, 179 insertions, 61 deletions
diff --git a/inc/util.inc.php b/inc/util.inc.php
index 81c7d807..c3e70f89 100644
--- a/inc/util.inc.php
+++ b/inc/util.inc.php
@@ -590,11 +590,23 @@ SADFACE;
{
$regex = '/
(
- (?: [\x20-\xFF] ){1,100} # ignore lower non-printable range
+ [\x20-\xFF]{1,100} # ignore lower non-printable range
)
| . # anything else
/x';
return iconv('MS-ANSI', 'UTF-8', preg_replace($regex, '$1', $string));
}
+ /**
+ * Clamp given value into [min, max] range.
+ */
+ public static function clamp(int &$value, int $min, int $max)
+ {
+ if ($value < $min) {
+ $value = $min;
+ } elseif ($value > $max) {
+ $value = $max;
+ }
+ }
+
}
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');