cron und module
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
const modalTitle = document.querySelector('[data-host-modal-title]');
|
const modalTitle = document.querySelector('[data-host-modal-title]');
|
||||||
const closeBtn = document.querySelector('[data-host-close]');
|
const closeBtn = document.querySelector('[data-host-close]');
|
||||||
const newBtn = document.querySelector('[data-host-new]');
|
const newBtn = document.querySelector('[data-host-new]');
|
||||||
|
const checkAllBtn = document.querySelector('[data-host-check-all]');
|
||||||
const unsavedBar = document.querySelector('[data-host-unsaved]');
|
const unsavedBar = document.querySelector('[data-host-unsaved]');
|
||||||
const discardBtn = document.querySelector('[data-host-discard]');
|
const discardBtn = document.querySelector('[data-host-discard]');
|
||||||
|
|
||||||
@@ -148,4 +149,128 @@
|
|||||||
document.querySelectorAll('.host-card').forEach((card) => {
|
document.querySelectorAll('.host-card').forEach((card) => {
|
||||||
fetchStatus(card);
|
fetchStatus(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setUpdateUi = (card, data) => {
|
||||||
|
const upd = card.querySelector('[data-update-badge]');
|
||||||
|
const upg = card.querySelector('[data-upgrade-badge]');
|
||||||
|
const time = card.querySelector('[data-update-time]');
|
||||||
|
if (upd) {
|
||||||
|
if (data.updates && typeof data.updates.count === 'number') {
|
||||||
|
upd.textContent = `Updates: ${data.updates.count}`;
|
||||||
|
upd.classList.toggle('badge-warn', data.updates.count > 0);
|
||||||
|
upd.classList.toggle('badge-ok', data.updates.count === 0);
|
||||||
|
if (data.updates.preview) {
|
||||||
|
upd.setAttribute('title', data.updates.preview);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
upd.textContent = 'Updates: Fehler';
|
||||||
|
upd.classList.add('badge-error');
|
||||||
|
if (data.updates && data.updates.error) {
|
||||||
|
upd.setAttribute('title', data.updates.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (upg) {
|
||||||
|
if (data.os && typeof data.os.available === 'boolean') {
|
||||||
|
upg.textContent = data.os.available ? 'OS: Upgrade verfügbar' : 'OS: OK';
|
||||||
|
upg.classList.toggle('badge-warn', data.os.available);
|
||||||
|
upg.classList.toggle('badge-ok', !data.os.available);
|
||||||
|
if (data.os.raw) upg.setAttribute('title', data.os.raw);
|
||||||
|
} else {
|
||||||
|
upg.textContent = 'OS: Fehler';
|
||||||
|
upg.classList.add('badge-error');
|
||||||
|
if (data.os && data.os.error) {
|
||||||
|
upg.setAttribute('title', data.os.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (time && data.checked_at) {
|
||||||
|
const dt = new Date(data.checked_at);
|
||||||
|
time.textContent = isNaN(dt.getTime()) ? data.checked_at : dt.toLocaleString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkHostUpdates = (card) => {
|
||||||
|
const id = card.dataset.hostId;
|
||||||
|
if (!id) return Promise.resolve();
|
||||||
|
const btn = card.querySelector('[data-host-check]');
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Prüfe...';
|
||||||
|
}
|
||||||
|
return fetch(`${window.location.pathname}?update_json=1&id=${encodeURIComponent(id)}`, { cache: 'no-store' })
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.ok) {
|
||||||
|
setUpdateUi(card, data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Updates prüfen';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-host-check]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const card = btn.closest('.host-card');
|
||||||
|
if (card) checkHostUpdates(card);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkAllBtn) {
|
||||||
|
checkAllBtn.addEventListener('click', async () => {
|
||||||
|
const cards = Array.from(document.querySelectorAll('.host-card'));
|
||||||
|
for (const card of cards) {
|
||||||
|
await checkHostUpdates(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyStoredUpdate = (card) => {
|
||||||
|
const checkedAt = card.dataset.updateChecked || '';
|
||||||
|
const updateCount = card.dataset.updateCount;
|
||||||
|
const updateError = card.dataset.updateError || '';
|
||||||
|
const upgradeAvailable = card.dataset.upgradeAvailable;
|
||||||
|
const upgradeRaw = card.dataset.upgradeRaw || '';
|
||||||
|
const upgradeError = card.dataset.upgradeError || '';
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
updates: {},
|
||||||
|
os: {},
|
||||||
|
checked_at: checkedAt || '',
|
||||||
|
};
|
||||||
|
if (updateError) {
|
||||||
|
payload.updates.error = updateError;
|
||||||
|
} else if (updateCount !== undefined && updateCount !== '') {
|
||||||
|
payload.updates.count = Number(updateCount);
|
||||||
|
payload.updates.preview = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upgradeError) {
|
||||||
|
payload.os.error = upgradeError;
|
||||||
|
} else if (upgradeAvailable !== undefined && upgradeAvailable !== '') {
|
||||||
|
payload.os.available = upgradeAvailable === '1' || upgradeAvailable === 'true';
|
||||||
|
payload.os.raw = upgradeRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpdateUi(card, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isStale = (checkedAt) => {
|
||||||
|
if (!checkedAt) return true;
|
||||||
|
const dt = new Date(checkedAt);
|
||||||
|
if (isNaN(dt.getTime())) return true;
|
||||||
|
const ageMs = Date.now() - dt.getTime();
|
||||||
|
return ageMs > 24 * 60 * 60 * 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('.host-card').forEach((card) => {
|
||||||
|
applyStoredUpdate(card);
|
||||||
|
if (isStale(card.dataset.updateChecked || '')) {
|
||||||
|
checkHostUpdates(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -126,6 +126,35 @@
|
|||||||
.status-auth { background: #fbbf24; }
|
.status-auth { background: #fbbf24; }
|
||||||
.status-down { background: #ef4444; }
|
.status-down { background: #ef4444; }
|
||||||
|
|
||||||
|
.host-update-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.update-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--panel-2);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge-ok {
|
||||||
|
border-color: rgba(49,196,141,0.5);
|
||||||
|
background: rgba(49,196,141,0.15);
|
||||||
|
}
|
||||||
|
.badge-warn {
|
||||||
|
border-color: rgba(251,191,36,0.6);
|
||||||
|
background: rgba(251,191,36,0.2);
|
||||||
|
}
|
||||||
|
.badge-error {
|
||||||
|
border-color: rgba(239,68,68,0.6);
|
||||||
|
background: rgba(239,68,68,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.action-menu {
|
.action-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName
|
|||||||
key_path TEXT NULL,
|
key_path TEXT NULL,
|
||||||
password TEXT NULL,
|
password TEXT NULL,
|
||||||
image_url TEXT NULL,
|
image_url TEXT NULL,
|
||||||
|
update_checked_at TIMESTAMP NULL,
|
||||||
|
update_count INTEGER NULL,
|
||||||
|
update_preview TEXT NULL,
|
||||||
|
update_error TEXT NULL,
|
||||||
|
upgrade_available BOOLEAN NULL,
|
||||||
|
upgrade_raw TEXT NULL,
|
||||||
|
upgrade_error TEXT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
)");
|
)");
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS {$cmdTable} (
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$cmdTable} (
|
||||||
@@ -113,6 +120,13 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName
|
|||||||
key_path TEXT NULL,
|
key_path TEXT NULL,
|
||||||
password TEXT NULL,
|
password TEXT NULL,
|
||||||
image_url TEXT NULL,
|
image_url TEXT NULL,
|
||||||
|
update_checked_at DATETIME NULL,
|
||||||
|
update_count INTEGER NULL,
|
||||||
|
update_preview TEXT NULL,
|
||||||
|
update_error TEXT NULL,
|
||||||
|
upgrade_available INTEGER NULL,
|
||||||
|
upgrade_raw TEXT NULL,
|
||||||
|
upgrade_error TEXT NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
)");
|
)");
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS {$cmdTable} (
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$cmdTable} (
|
||||||
@@ -155,6 +169,13 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName
|
|||||||
// Schema migrations for existing tables
|
// Schema migrations for existing tables
|
||||||
if ($driver === 'pgsql') {
|
if ($driver === 'pgsql') {
|
||||||
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS image_url TEXT NULL");
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS image_url TEXT NULL");
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS update_checked_at TIMESTAMP NULL");
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS update_count INTEGER NULL");
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS update_preview TEXT NULL");
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS update_error TEXT NULL");
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS upgrade_available BOOLEAN NULL");
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS upgrade_raw TEXT NULL");
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS upgrade_error TEXT NULL");
|
||||||
$pdo->exec("ALTER TABLE {$cmdTable} ADD COLUMN IF NOT EXISTS timeout_sec INTEGER NULL");
|
$pdo->exec("ALTER TABLE {$cmdTable} ADD COLUMN IF NOT EXISTS timeout_sec INTEGER NULL");
|
||||||
$pdo->exec("ALTER TABLE {$cmdTable} ADD COLUMN IF NOT EXISTS sort_order INTEGER NULL");
|
$pdo->exec("ALTER TABLE {$cmdTable} ADD COLUMN IF NOT EXISTS sort_order INTEGER NULL");
|
||||||
$pdo->exec("ALTER TABLE {$runTable} ADD COLUMN IF NOT EXISTS error TEXT NULL");
|
$pdo->exec("ALTER TABLE {$runTable} ADD COLUMN IF NOT EXISTS error TEXT NULL");
|
||||||
@@ -172,6 +193,27 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName
|
|||||||
if (empty($columns['image_url'])) {
|
if (empty($columns['image_url'])) {
|
||||||
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN image_url TEXT NULL");
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN image_url TEXT NULL");
|
||||||
}
|
}
|
||||||
|
if (empty($columns['update_checked_at'])) {
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN update_checked_at DATETIME NULL");
|
||||||
|
}
|
||||||
|
if (empty($columns['update_count'])) {
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN update_count INTEGER NULL");
|
||||||
|
}
|
||||||
|
if (empty($columns['update_preview'])) {
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN update_preview TEXT NULL");
|
||||||
|
}
|
||||||
|
if (empty($columns['update_error'])) {
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN update_error TEXT NULL");
|
||||||
|
}
|
||||||
|
if (empty($columns['upgrade_available'])) {
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN upgrade_available INTEGER NULL");
|
||||||
|
}
|
||||||
|
if (empty($columns['upgrade_raw'])) {
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN upgrade_raw TEXT NULL");
|
||||||
|
}
|
||||||
|
if (empty($columns['upgrade_error'])) {
|
||||||
|
$pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN upgrade_error TEXT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
$columns = [];
|
$columns = [];
|
||||||
$stmt = $pdo->query('PRAGMA table_info(' . $cmdTable . ')');
|
$stmt = $pdo->query('PRAGMA table_info(' . $cmdTable . ')');
|
||||||
|
|||||||
@@ -32,6 +32,78 @@ if (isset($_GET['status_json'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['update_json'])) {
|
||||||
|
require_admin();
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM ' . $table('hosts') . ' WHERE id = :id LIMIT 1');
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$host = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$host) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'not_found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$settings = modules()->settings('pi_control');
|
||||||
|
$strictHostKey = !empty($settings['terminal_strict_hostkey']);
|
||||||
|
|
||||||
|
$updateCmd = "apt-get -s upgrade | grep '^Inst'";
|
||||||
|
$upgradeCmd = 'current="$(. /etc/os-release && echo "$VERSION_CODENAME")"; latest="$(curl -fsSL https://deb.debian.org/debian/dists/stable/Release | awk -F\': \' \'/^Codename:/{print $2}\')"; echo "Installed: $current"; echo "Latest stable: $latest"; [ "$current" != "$latest" ] && echo "OS UPGRADE AVAILABLE" || echo "NO OS UPGRADE AVAILABLE"';
|
||||||
|
|
||||||
|
[$updExit, $updOut, $updErr] = runSshCommandCapture($host, $updateCmd, $strictHostKey, 20);
|
||||||
|
$lines = array_values(array_filter(preg_split('/\r?\n/', (string)$updOut)));
|
||||||
|
$updateCount = $updExit === 0 ? count($lines) : null;
|
||||||
|
$updatePreview = $updateCount ? implode("\n", array_slice($lines, 0, 6)) : '';
|
||||||
|
|
||||||
|
[$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25);
|
||||||
|
$upgradeAvailable = null;
|
||||||
|
if ($upgExit === 0) {
|
||||||
|
$upgradeAvailable = str_contains($upgOut, 'OS UPGRADE AVAILABLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
$driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
|
$nowExpr = $driver === 'pgsql' ? 'NOW()' : "DATETIME('now')";
|
||||||
|
$updCountVal = $updateCount;
|
||||||
|
$updErrVal = $updExit === 0 ? null : trim($updErr ?: $updOut);
|
||||||
|
$upgAvailVal = $upgradeAvailable;
|
||||||
|
$upgErrVal = $upgExit === 0 ? null : trim($upgErr ?: $upgOut);
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'UPDATE ' . $table('hosts') . ' SET update_checked_at = ' . $nowExpr . ',
|
||||||
|
update_count = :update_count,
|
||||||
|
update_preview = :update_preview,
|
||||||
|
update_error = :update_error,
|
||||||
|
upgrade_available = :upgrade_available,
|
||||||
|
upgrade_raw = :upgrade_raw,
|
||||||
|
upgrade_error = :upgrade_error
|
||||||
|
WHERE id = :id'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'update_count' => $updCountVal,
|
||||||
|
'update_preview' => $updatePreview !== '' ? $updatePreview : null,
|
||||||
|
'update_error' => $updErrVal,
|
||||||
|
'upgrade_available' => $upgAvailVal === null ? null : ($upgAvailVal ? 1 : 0),
|
||||||
|
'upgrade_raw' => $upgExit === 0 ? trim($upgOut) : null,
|
||||||
|
'upgrade_error' => $upgErrVal,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'updates' => [
|
||||||
|
'count' => $updateCount,
|
||||||
|
'preview' => $updatePreview,
|
||||||
|
'error' => $updExit === 0 ? '' : trim($updErr ?: $updOut),
|
||||||
|
],
|
||||||
|
'os' => [
|
||||||
|
'available' => $upgradeAvailable,
|
||||||
|
'raw' => $upgExit === 0 ? trim($upgOut) : '',
|
||||||
|
'error' => $upgExit === 0 ? '' : trim($upgErr ?: $upgOut),
|
||||||
|
],
|
||||||
|
'checked_at' => date('c'),
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
require_admin();
|
require_admin();
|
||||||
$deleteId = (int)($_POST['delete_id'] ?? 0);
|
$deleteId = (int)($_POST['delete_id'] ?? 0);
|
||||||
@@ -138,6 +210,64 @@ function runSshCommand(string $cmd, int $timeoutSec): int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runSshCommandCapture(array $host, string $command, bool $strictHostKey, int $timeoutSec): array
|
||||||
|
{
|
||||||
|
$hostAddr = (string)($host['host'] ?? '');
|
||||||
|
$user = (string)($host['username'] ?? '');
|
||||||
|
$port = (int)($host['port'] ?? 22);
|
||||||
|
$authType = (string)($host['auth_type'] ?? 'key');
|
||||||
|
$keyPath = (string)($host['key_path'] ?? '');
|
||||||
|
$password = (string)($host['password'] ?? '');
|
||||||
|
|
||||||
|
$opts = $strictHostKey
|
||||||
|
? '-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts'
|
||||||
|
: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
|
||||||
|
$opts .= ' -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1';
|
||||||
|
|
||||||
|
$target = escapeshellarg($user . '@' . $hostAddr);
|
||||||
|
$remote = '/bin/bash -lc ' . escapeshellarg($command);
|
||||||
|
$cmd = 'ssh ' . $opts . ' -p ' . (int)$port . ' ';
|
||||||
|
if ($authType === 'key' && $keyPath !== '') {
|
||||||
|
$cmd .= '-i ' . escapeshellarg($keyPath) . ' -o BatchMode=yes ';
|
||||||
|
} elseif ($authType === 'key') {
|
||||||
|
$cmd .= '-o BatchMode=yes ';
|
||||||
|
}
|
||||||
|
$cmd .= $target . ' -- ' . $remote;
|
||||||
|
if ($authType === 'pass' && $password !== '') {
|
||||||
|
$cmd = 'sshpass -p ' . escapeshellarg($password) . ' ' . $cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
$descriptors = [
|
||||||
|
1 => ['pipe', 'w'],
|
||||||
|
2 => ['pipe', 'w'],
|
||||||
|
];
|
||||||
|
$process = proc_open($cmd, $descriptors, $pipes);
|
||||||
|
if (!is_resource($process)) {
|
||||||
|
return [255, '', 'proc_open failed'];
|
||||||
|
}
|
||||||
|
stream_set_blocking($pipes[1], false);
|
||||||
|
stream_set_blocking($pipes[2], false);
|
||||||
|
$out = '';
|
||||||
|
$err = '';
|
||||||
|
$start = time();
|
||||||
|
while (true) {
|
||||||
|
$status = proc_get_status($process);
|
||||||
|
$out .= stream_get_contents($pipes[1]);
|
||||||
|
$err .= stream_get_contents($pipes[2]);
|
||||||
|
if (!$status['running']) {
|
||||||
|
$exit = (int)$status['exitcode'];
|
||||||
|
proc_close($process);
|
||||||
|
return [$exit, $out, $err];
|
||||||
|
}
|
||||||
|
if (time() - $start > $timeoutSec) {
|
||||||
|
proc_terminate($process, 9);
|
||||||
|
proc_close($process);
|
||||||
|
return [124, $out, $err];
|
||||||
|
}
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hostAuthOk(array $host, bool $strictHostKey): bool
|
function hostAuthOk(array $host, bool $strictHostKey): bool
|
||||||
{
|
{
|
||||||
$hostAddr = (string)($host['host'] ?? '');
|
$hostAddr = (string)($host['host'] ?? '');
|
||||||
@@ -179,7 +309,10 @@ function hostAuthOk(array $host, bool $strictHostKey): bool
|
|||||||
<div class="pill">Pi Control</div>
|
<div class="pill">Pi Control</div>
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:.75rem;">
|
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:.75rem;">
|
||||||
<h1 style="margin:0;">Hosts</h1>
|
<h1 style="margin:0;">Hosts</h1>
|
||||||
<button class="cta-button" type="button" data-host-new>+ Neuer Host</button>
|
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
||||||
|
<button class="nav-link" type="button" data-host-check-all>Alle Hosts prüfen</button>
|
||||||
|
<button class="cta-button" type="button" data-host-new>+ Neuer Host</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Verwalte die Raspberry Pis, die du steuern möchtest.</p>
|
<p class="muted">Verwalte die Raspberry Pis, die du steuern möchtest.</p>
|
||||||
|
|
||||||
@@ -209,7 +342,13 @@ function hostAuthOk(array $host, bool $strictHostKey): bool
|
|||||||
data-username="<?= e((string)($h['username'] ?? '')) ?>"
|
data-username="<?= e((string)($h['username'] ?? '')) ?>"
|
||||||
data-auth="<?= e((string)($h['auth_type'] ?? 'key')) ?>"
|
data-auth="<?= e((string)($h['auth_type'] ?? 'key')) ?>"
|
||||||
data-key-path="<?= e((string)($h['key_path'] ?? '')) ?>"
|
data-key-path="<?= e((string)($h['key_path'] ?? '')) ?>"
|
||||||
data-image-url="<?= e((string)($h['image_url'] ?? '')) ?>">
|
data-image-url="<?= e((string)($h['image_url'] ?? '')) ?>"
|
||||||
|
data-update-checked="<?= e((string)($h['update_checked_at'] ?? '')) ?>"
|
||||||
|
data-update-count="<?= e((string)($h['update_count'] ?? '')) ?>"
|
||||||
|
data-update-error="<?= e((string)($h['update_error'] ?? '')) ?>"
|
||||||
|
data-upgrade-available="<?= e((string)($h['upgrade_available'] ?? '')) ?>"
|
||||||
|
data-upgrade-raw="<?= e((string)($h['upgrade_raw'] ?? '')) ?>"
|
||||||
|
data-upgrade-error="<?= e((string)($h['upgrade_error'] ?? '')) ?>">
|
||||||
<div class="host-card-image"<?= !empty($h['image_url']) ? ' style="background-image:url(' . e((string)$h['image_url']) . ')"' : '' ?>>
|
<div class="host-card-image"<?= !empty($h['image_url']) ? ' style="background-image:url(' . e((string)$h['image_url']) . ')"' : '' ?>>
|
||||||
<div class="host-card-overlay"></div>
|
<div class="host-card-overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,6 +371,14 @@ function hostAuthOk(array $host, bool $strictHostKey): bool
|
|||||||
</div>
|
</div>
|
||||||
<div class="muted"><?= e((string)($h['host'] ?? '')) ?>:<?= e((string)($h['port'] ?? 22)) ?></div>
|
<div class="muted"><?= e((string)($h['host'] ?? '')) ?>:<?= e((string)($h['port'] ?? 22)) ?></div>
|
||||||
<div class="muted"><?= e((string)($h['username'] ?? '')) ?> · <?= e((string)($h['auth_type'] ?? '')) ?></div>
|
<div class="muted"><?= e((string)($h['username'] ?? '')) ?> · <?= e((string)($h['auth_type'] ?? '')) ?></div>
|
||||||
|
<div class="host-update-row">
|
||||||
|
<span class="update-badge" data-update-badge>Updates: –</span>
|
||||||
|
<span class="update-badge" data-upgrade-badge>OS: –</span>
|
||||||
|
<span class="muted" data-update-time>Nie geprüft</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||||
|
<button class="nav-link" type="button" data-host-check>Updates prüfen</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
121
tools/pi_control/check_updates.php
Normal file
121
tools/pi_control/check_updates.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$root = dirname(__DIR__, 2);
|
||||||
|
chdir($root);
|
||||||
|
require $root . '/config/fileload.php';
|
||||||
|
|
||||||
|
$module = 'pi_control';
|
||||||
|
$pdo = module_fn($module, 'pdo');
|
||||||
|
module_fn($module, 'ensure_schema');
|
||||||
|
$table = fn(string $name) => module_fn($module, 'table', $name);
|
||||||
|
|
||||||
|
$settings = modules()->settings($module);
|
||||||
|
$strictHostKey = !empty($settings['terminal_strict_hostkey']);
|
||||||
|
|
||||||
|
$updateCmd = "apt-get -s upgrade | grep '^Inst'";
|
||||||
|
$upgradeCmd = 'current="$(. /etc/os-release && echo "$VERSION_CODENAME")"; latest="$(curl -fsSL https://deb.debian.org/debian/dists/stable/Release | awk -F\': \' \'/^Codename:/{print $2}\')"; echo "Installed: $current"; echo "Latest stable: $latest"; [ "$current" != "$latest" ] && echo "OS UPGRADE AVAILABLE" || echo "NO OS UPGRADE AVAILABLE"';
|
||||||
|
|
||||||
|
$driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
|
$nowExpr = $driver === 'pgsql' ? 'NOW()' : "DATETIME('now')";
|
||||||
|
|
||||||
|
$hosts = $pdo->query('SELECT * FROM ' . $table('hosts'))->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($hosts as $host) {
|
||||||
|
$id = (int)($host['id'] ?? 0);
|
||||||
|
if ($id <= 0) continue;
|
||||||
|
|
||||||
|
[$updExit, $updOut, $updErr] = runSshCommandCapture($host, $updateCmd, $strictHostKey, 20);
|
||||||
|
$lines = array_values(array_filter(preg_split('/\r?\n/', (string)$updOut)));
|
||||||
|
$updateCount = $updExit === 0 ? count($lines) : null;
|
||||||
|
$updatePreview = $updateCount ? implode("\n", array_slice($lines, 0, 6)) : '';
|
||||||
|
|
||||||
|
[$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25);
|
||||||
|
$upgradeAvailable = null;
|
||||||
|
if ($upgExit === 0) {
|
||||||
|
$upgradeAvailable = str_contains($upgOut, 'OS UPGRADE AVAILABLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
$updErrVal = $updExit === 0 ? null : trim($updErr ?: $updOut);
|
||||||
|
$upgErrVal = $upgExit === 0 ? null : trim($upgErr ?: $upgOut);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'UPDATE ' . $table('hosts') . ' SET update_checked_at = ' . $nowExpr . ',
|
||||||
|
update_count = :update_count,
|
||||||
|
update_preview = :update_preview,
|
||||||
|
update_error = :update_error,
|
||||||
|
upgrade_available = :upgrade_available,
|
||||||
|
upgrade_raw = :upgrade_raw,
|
||||||
|
upgrade_error = :upgrade_error
|
||||||
|
WHERE id = :id'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'update_count' => $updateCount,
|
||||||
|
'update_preview' => $updatePreview !== '' ? $updatePreview : null,
|
||||||
|
'update_error' => $updErrVal,
|
||||||
|
'upgrade_available' => $upgradeAvailable === null ? null : ($upgradeAvailable ? 1 : 0),
|
||||||
|
'upgrade_raw' => $upgExit === 0 ? trim($upgOut) : null,
|
||||||
|
'upgrade_error' => $upgErrVal,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "OK\n";
|
||||||
|
|
||||||
|
function runSshCommandCapture(array $host, string $command, bool $strictHostKey, int $timeoutSec): array
|
||||||
|
{
|
||||||
|
$hostAddr = (string)($host['host'] ?? '');
|
||||||
|
$user = (string)($host['username'] ?? '');
|
||||||
|
$port = (int)($host['port'] ?? 22);
|
||||||
|
$authType = (string)($host['auth_type'] ?? 'key');
|
||||||
|
$keyPath = (string)($host['key_path'] ?? '');
|
||||||
|
$password = (string)($host['password'] ?? '');
|
||||||
|
|
||||||
|
$opts = $strictHostKey
|
||||||
|
? '-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts'
|
||||||
|
: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
|
||||||
|
$opts .= ' -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1';
|
||||||
|
|
||||||
|
$target = escapeshellarg($user . '@' . $hostAddr);
|
||||||
|
$remote = '/bin/bash -lc ' . escapeshellarg($command);
|
||||||
|
$cmd = 'ssh ' . $opts . ' -p ' . (int)$port . ' ';
|
||||||
|
if ($authType === 'key' && $keyPath !== '') {
|
||||||
|
$cmd .= '-i ' . escapeshellarg($keyPath) . ' -o BatchMode=yes ';
|
||||||
|
} elseif ($authType === 'key') {
|
||||||
|
$cmd .= '-o BatchMode=yes ';
|
||||||
|
}
|
||||||
|
$cmd .= $target . ' -- ' . $remote;
|
||||||
|
if ($authType === 'pass' && $password !== '') {
|
||||||
|
$cmd = 'sshpass -p ' . escapeshellarg($password) . ' ' . $cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
$descriptors = [
|
||||||
|
1 => ['pipe', 'w'],
|
||||||
|
2 => ['pipe', 'w'],
|
||||||
|
];
|
||||||
|
$process = proc_open($cmd, $descriptors, $pipes);
|
||||||
|
if (!is_resource($process)) {
|
||||||
|
return [255, '', 'proc_open failed'];
|
||||||
|
}
|
||||||
|
stream_set_blocking($pipes[1], false);
|
||||||
|
stream_set_blocking($pipes[2], false);
|
||||||
|
$out = '';
|
||||||
|
$err = '';
|
||||||
|
$start = time();
|
||||||
|
while (true) {
|
||||||
|
$status = proc_get_status($process);
|
||||||
|
$out .= stream_get_contents($pipes[1]);
|
||||||
|
$err .= stream_get_contents($pipes[2]);
|
||||||
|
if (!$status['running']) {
|
||||||
|
$exit = (int)$status['exitcode'];
|
||||||
|
proc_close($process);
|
||||||
|
return [$exit, $out, $err];
|
||||||
|
}
|
||||||
|
if (time() - $start > $timeoutSec) {
|
||||||
|
proc_terminate($process, 9);
|
||||||
|
proc_close($process);
|
||||||
|
return [124, $out, $err];
|
||||||
|
}
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user