- Added a new job, ServerPatchCheckJob, to handle server patch checks and notifications. - Introduced a new notification class, ServerPatchCheck, for sending updates via email, Discord, Slack, Pushover, and Telegram. - Updated notification settings models to include server patch notification options for email, Discord, Slack, Pushover, and Telegram. - Created a migration to add server patch notification fields to the respective settings tables. - Enhanced the UI to allow users to enable/disable server patch notifications across different channels.
		
			
				
	
	
		
			224 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			224 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) {
 | 
						|
            ray('Error:', $e->getMessage());
 | 
						|
 | 
						|
            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,
 | 
						|
        ];
 | 
						|
    }
 | 
						|
}
 |