132 lines
5.6 KiB
PHP
132 lines
5.6 KiB
PHP
<?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 = "sh -lc 'if ! command -v apt-get >/dev/null 2>&1; then echo \"__ERR__NO_APT\"; exit 2; fi; if sudo apt update -qq >/dev/null 2>&1; then count=$(apt-get -s dist-upgrade | grep -c \"^Inst \"); if [ \"$count\" -gt 0 ]; then echo \"__UPDATE__=1\"; else echo \"__UPDATE__=0\"; fi; echo \"__COUNT__=$count\"; else echo \"__ERR__APT_UPDATE\"; exit 3; fi'";
|
|
$upgradeCmd = "sh -lc 'id=\"$(. /etc/os-release 2>/dev/null && echo \"${ID:-}\")\"; current=\"$(. /etc/os-release 2>/dev/null && echo \"${VERSION_CODENAME:-}\")\"; if [ \"$id\" != \"debian\" ] || [ -z \"$current\" ]; then echo \"__ERR__NO_CODENAME\"; exit 2; fi; latest=\"$( (command -v curl >/dev/null 2>&1 && curl -fsSL https://deb.debian.org/debian/dists/stable/Release) || (command -v wget >/dev/null 2>&1 && wget -qO- https://deb.debian.org/debian/dists/stable/Release) )\"; latest=\"$(printf \"%s\" \"$latest\" | awk -F\": \" \"/^Codename:/{print $2}\")\"; if [ -z \"$latest\" ]; then echo \"__ERR__NO_LATEST\"; exit 3; fi; if [ \"$current\" != \"$latest\" ]; then echo \"__UPGRADE__=1\"; else echo \"__UPGRADE__=0\"; fi'";
|
|
|
|
$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);
|
|
$updOutStr = (string)$updOut;
|
|
$updErrStr = (string)$updErr;
|
|
$updateCount = null;
|
|
$updatePreview = '';
|
|
if ($updExit === 0 && !str_contains($updOutStr, '__ERR__')) {
|
|
if (preg_match('/^__COUNT__=(\d+)$/m', $updOutStr, $m)) {
|
|
$updateCount = (int)$m[1];
|
|
}
|
|
}
|
|
|
|
[$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25);
|
|
$upgOutStr = (string)$upgOut;
|
|
$upgErrStr = (string)$upgErr;
|
|
$upgradeAvailable = null;
|
|
if ($upgExit === 0 && !str_contains($upgOutStr, '__ERR__')) {
|
|
if (preg_match('/^__UPGRADE__=(0|1)$/m', $upgOutStr, $m)) {
|
|
$upgradeAvailable = $m[1] === '1';
|
|
}
|
|
}
|
|
|
|
$updErrVal = $updExit === 0 && !str_contains($updOutStr, '__ERR__') ? null : trim($updErrStr ?: $updOutStr);
|
|
$upgErrVal = $upgExit === 0 && !str_contains($upgOutStr, '__ERR__') ? null : trim($upgErrStr ?: $upgOutStr);
|
|
|
|
$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($upgOutStr) : 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);
|
|
}
|
|
}
|