223 lines
7.4 KiB
PHP
223 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Actions\Server;
|
|
|
|
use App\Models\Server;
|
|
use Lorisleiva\Actions\Concerns\AsAction;
|
|
|
|
class CheckUpdates
|
|
{
|
|
use AsAction;
|
|
|
|
public string $jobQueue = 'high';
|
|
|
|
public function handle(Server $server)
|
|
{
|
|
try {
|
|
if ($server->serverStatus() === false) {
|
|
return [
|
|
'error' => 'Server is not reachable or not ready.',
|
|
];
|
|
}
|
|
|
|
// Try first method - using instant_remote_process
|
|
$output = instant_remote_process(['cat /etc/os-release'], $server);
|
|
|
|
// Parse os-release into an associative array
|
|
$osInfo = [];
|
|
foreach (explode("\n", $output) as $line) {
|
|
if (empty($line)) {
|
|
continue;
|
|
}
|
|
if (strpos($line, '=') === false) {
|
|
continue;
|
|
}
|
|
[$key, $value] = explode('=', $line, 2);
|
|
$osInfo[$key] = trim($value, '"');
|
|
}
|
|
|
|
// Get the main OS identifier
|
|
$osId = $osInfo['ID'] ?? '';
|
|
// $osIdLike = $osInfo['ID_LIKE'] ?? '';
|
|
// $versionId = $osInfo['VERSION_ID'] ?? '';
|
|
|
|
// Normalize OS types based on install.sh logic
|
|
switch ($osId) {
|
|
case 'manjaro':
|
|
case 'manjaro-arm':
|
|
case 'endeavouros':
|
|
$osType = 'arch';
|
|
break;
|
|
case 'pop':
|
|
case 'linuxmint':
|
|
case 'zorin':
|
|
$osType = 'ubuntu';
|
|
break;
|
|
case 'fedora-asahi-remix':
|
|
$osType = 'fedora';
|
|
break;
|
|
default:
|
|
$osType = $osId;
|
|
}
|
|
|
|
// Determine package manager based on OS type
|
|
$packageManager = match ($osType) {
|
|
'arch' => 'pacman',
|
|
'alpine' => 'apk',
|
|
'ubuntu', 'debian', 'raspbian' => 'apt',
|
|
'centos', 'fedora', 'rhel', 'ol', 'rocky', 'almalinux', 'amzn' => 'dnf',
|
|
'sles', 'opensuse-leap', 'opensuse-tumbleweed' => 'zypper',
|
|
default => null
|
|
};
|
|
|
|
switch ($packageManager) {
|
|
case 'zypper':
|
|
$output = instant_remote_process(['LANG=C zypper -tx list-updates'], $server);
|
|
$out = $this->parseZypperOutput($output);
|
|
$out['osId'] = $osId;
|
|
$out['package_manager'] = $packageManager;
|
|
|
|
return $out;
|
|
case 'dnf':
|
|
$output = instant_remote_process(['LANG=C dnf list -q --updates --refresh'], $server);
|
|
$out = $this->parseDnfOutput($output);
|
|
$out['osId'] = $osId;
|
|
$out['package_manager'] = $packageManager;
|
|
|
|
return $out;
|
|
case 'apt':
|
|
instant_remote_process(['apt-get update -qq'], $server);
|
|
$output = instant_remote_process(['LANG=C apt list --upgradable 2>/dev/null'], $server);
|
|
|
|
$out = $this->parseAptOutput($output);
|
|
$out['osId'] = $osId;
|
|
$out['package_manager'] = $packageManager;
|
|
|
|
return $out;
|
|
default:
|
|
return [
|
|
'osId' => $osId,
|
|
'error' => 'Unsupported package manager',
|
|
'package_manager' => $packageManager,
|
|
];
|
|
}
|
|
} catch (\Throwable $e) {
|
|
|
|
return [
|
|
'osId' => $osId,
|
|
'package_manager' => $packageManager,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
];
|
|
}
|
|
}
|
|
|
|
private function parseZypperOutput(string $output): array
|
|
{
|
|
$updates = [];
|
|
|
|
try {
|
|
$xml = simplexml_load_string($output);
|
|
if ($xml === false) {
|
|
return [
|
|
'total_updates' => 0,
|
|
'updates' => [],
|
|
'error' => 'Failed to parse XML output',
|
|
];
|
|
}
|
|
|
|
// Navigate to the update-list node
|
|
$updateList = $xml->xpath('//update-list/update');
|
|
|
|
foreach ($updateList as $update) {
|
|
$updates[] = [
|
|
'package' => (string) $update['name'],
|
|
'new_version' => (string) $update['edition'],
|
|
'current_version' => (string) $update['edition-old'],
|
|
'architecture' => (string) $update['arch'],
|
|
'repository' => (string) $update->source['alias'],
|
|
'summary' => (string) $update->summary,
|
|
'description' => (string) $update->description,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'total_updates' => count($updates),
|
|
'updates' => $updates,
|
|
];
|
|
} catch (\Throwable $e) {
|
|
return [
|
|
'total_updates' => 0,
|
|
'updates' => [],
|
|
'error' => 'Error parsing zypper output: '.$e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
private function parseDnfOutput(string $output): array
|
|
{
|
|
$updates = [];
|
|
$lines = explode("\n", $output);
|
|
|
|
foreach ($lines as $line) {
|
|
if (empty($line)) {
|
|
continue;
|
|
}
|
|
|
|
// Split by multiple spaces/tabs and filter out empty elements
|
|
$parts = array_values(array_filter(preg_split('/\s+/', $line)));
|
|
|
|
if (count($parts) >= 3) {
|
|
$package = $parts[0];
|
|
$new_version = $parts[1];
|
|
$repository = $parts[2];
|
|
|
|
// Extract architecture from package name (e.g., "cloud-init.noarch" -> "noarch")
|
|
$architecture = str_contains($package, '.') ? explode('.', $package)[1] : 'noarch';
|
|
|
|
$updates[] = [
|
|
'package' => $package,
|
|
'new_version' => $new_version,
|
|
'repository' => $repository,
|
|
'architecture' => $architecture,
|
|
'current_version' => 'unknown', // DNF doesn't show current version in check-update output
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'total_updates' => count($updates),
|
|
'updates' => $updates,
|
|
];
|
|
}
|
|
|
|
private function parseAptOutput(string $output): array
|
|
{
|
|
$updates = [];
|
|
$lines = explode("\n", $output);
|
|
|
|
foreach ($lines as $line) {
|
|
// Skip the "Listing... Done" line and empty lines
|
|
if (empty($line) || str_contains($line, 'Listing...')) {
|
|
continue;
|
|
}
|
|
|
|
// Example line: package/stable 2.0-1 amd64 [upgradable from: 1.0-1]
|
|
if (preg_match('/^(.+?)\/(\S+)\s+(\S+)\s+(\S+)\s+\[upgradable from: (.+?)\]/', $line, $matches)) {
|
|
$updates[] = [
|
|
'package' => $matches[1],
|
|
'repository' => $matches[2],
|
|
'new_version' => $matches[3],
|
|
'architecture' => $matches[4],
|
|
'current_version' => $matches[5],
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'total_updates' => count($updates),
|
|
'updates' => $updates,
|
|
];
|
|
}
|
|
}
|