Adds unifi_cron_runs table (one row per scheduled-task execution) and
UnifiCronRun::record() wrapper that captures start/finish/status and
exceptions. The three scheduled commands now write through it:
- reboot-all-aps → rebooted/failed AP names per run
- rotate-passwords → rotated SSIDs + PPSKs, failures (when actually
rotating; the "is it due" early-return is silent
so we don't flood the log with no-op rows every
minute)
- sync-ppsk-schedules → enabled/disabled PPSKs (silent when there's
no work)
UnifiCronLogsController returns the most-recent 200 runs as JSON,
filterable by command + status. Behind permission:unifi.settings; no
super-admin required — read-only history is fine for any operator
who can see settings.
v1.5.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
122 lines
4.5 KiB
PHP
122 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace Dashboard\Unifi\Console;
|
|
|
|
use App\Models\Setting;
|
|
use Dashboard\Unifi\Models\UnifiCronRun;
|
|
use Dashboard\Unifi\Models\UnifiPpsk;
|
|
use Dashboard\Unifi\Services\UnifiApiClient;
|
|
use Illuminate\Console\Command;
|
|
|
|
class SyncPpskSchedules extends Command
|
|
{
|
|
protected $signature = 'unifi:sync-ppsk-schedules {--force : Run even if PPSK scheduling is disabled} {--triggered-by=schedule}';
|
|
protected $description = 'Enable or disable PPSKs based on their weekly half-hour schedule, kicking active clients when disabling';
|
|
|
|
public function handle(UnifiApiClient $unifi): int
|
|
{
|
|
$ppsks = UnifiPpsk::all();
|
|
if ($ppsks->isEmpty()) {
|
|
// Don't bother logging — no work, no audit value.
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$triggeredBy = $this->option('triggered-by') ?: 'schedule';
|
|
|
|
$run = UnifiCronRun::record('sync-ppsk-schedules', $triggeredBy, null, function () use ($unifi, $ppsks) {
|
|
$globalEnabled = (bool) Setting::get('unifi.ppsk_scheduling.enabled');
|
|
$tz = \App\Support\Timezone::current();
|
|
$now = now($tz);
|
|
$day = $now->dayOfWeek;
|
|
$slot = $now->hour * 2 + ($now->minute >= 30 ? 1 : 0);
|
|
|
|
$networksByVlan = [];
|
|
try {
|
|
foreach ($unifi->getNetworkConfs() as $n) {
|
|
if (isset($n['vlan'])) {
|
|
$networksByVlan[(int) $n['vlan']] = $n;
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->warn("Could not fetch network configs: {$e->getMessage()}");
|
|
}
|
|
|
|
$enabled = [];
|
|
$disabled = [];
|
|
$errors = [];
|
|
|
|
foreach ($ppsks as $ppsk) {
|
|
$shouldBeOn = true;
|
|
if ($globalEnabled && $ppsk->schedule) {
|
|
$shouldBeOn = (bool) ($ppsk->schedule[$day * 48 + $slot] ?? true);
|
|
}
|
|
|
|
try {
|
|
if ($shouldBeOn && $ppsk->state === 'held') {
|
|
$this->enablePpsk($ppsk, $unifi, $networksByVlan);
|
|
$enabled[] = $ppsk->name;
|
|
} elseif (! $shouldBeOn && $ppsk->state === 'active' && $ppsk->unifi_id) {
|
|
$this->disablePpsk($ppsk, $unifi);
|
|
$disabled[] = $ppsk->name;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$errors[] = ['ppsk' => $ppsk->name, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
$hasActions = count($enabled) + count($disabled) > 0;
|
|
$status = count($errors) > 0
|
|
? ($hasActions ? 'partial' : 'failed')
|
|
: ($hasActions ? 'succeeded' : 'skipped');
|
|
|
|
return [
|
|
'status' => $status,
|
|
'global_enabled' => $globalEnabled,
|
|
'enabled_ppsks' => $enabled,
|
|
'disabled_ppsks' => $disabled,
|
|
'errors' => $errors,
|
|
];
|
|
});
|
|
|
|
return $run->status === 'failed' ? self::FAILURE : self::SUCCESS;
|
|
}
|
|
|
|
private function enablePpsk(UnifiPpsk $ppsk, UnifiApiClient $unifi, array $networksByVlan): void
|
|
{
|
|
try {
|
|
$data = [
|
|
'name' => $ppsk->name,
|
|
'x_passphrase' => $ppsk->x_passphrase,
|
|
'wlan_id' => $ppsk->wlan_id,
|
|
];
|
|
|
|
if ($ppsk->vlan && isset($networksByVlan[$ppsk->vlan])) {
|
|
$data['networkconf_id'] = $networksByVlan[$ppsk->vlan]['_id'];
|
|
}
|
|
|
|
$result = $unifi->createPpsk($data);
|
|
$raw = $result[0] ?? $result;
|
|
$newId = $raw['_id'] ?? null;
|
|
|
|
$ppsk->update(['state' => 'active', 'unifi_id' => $newId]);
|
|
$this->info("Enabled: {$ppsk->name} (wlan {$ppsk->wlan_id})");
|
|
} catch (\Throwable $e) {
|
|
$this->error("Failed to enable PPSK \"{$ppsk->name}\": {$e->getMessage()}");
|
|
}
|
|
}
|
|
|
|
private function disablePpsk(UnifiPpsk $ppsk, UnifiApiClient $unifi): void
|
|
{
|
|
try {
|
|
$kicked = $unifi->kickClientsForPpsk($ppsk->unifi_id);
|
|
$unifi->deletePpsk($ppsk->unifi_id);
|
|
$ppsk->update(['state' => 'held', 'unifi_id' => null]);
|
|
|
|
$suffix = $kicked > 0 ? " — kicked {$kicked} client(s)" : '';
|
|
$this->info("Disabled: {$ppsk->name}{$suffix}");
|
|
} catch (\Throwable $e) {
|
|
$this->error("Failed to disable PPSK \"{$ppsk->name}\": {$e->getMessage()}");
|
|
}
|
|
}
|
|
}
|