diff --git a/app/Actions/Server/CheckConnection.php b/app/Actions/Server/CheckConnection.php index 444d5c1..00bf916 100644 --- a/app/Actions/Server/CheckConnection.php +++ b/app/Actions/Server/CheckConnection.php @@ -2,6 +2,7 @@ namespace App\Actions\Server; +use App\Enums\ServerStatus; use App\Facades\Notifier; use App\Models\Server; use App\Notifications\ServerDisconnected; @@ -15,12 +16,12 @@ public function check(Server $server): Server try { $server->ssh()->connect(); $server->refresh(); - if ($status == 'disconnected') { - $server->status = 'ready'; + if (in_array($status, [ServerStatus::DISCONNECTED, ServerStatus::UPDATING])) { + $server->status = ServerStatus::READY; $server->save(); } } catch (Throwable) { - $server->status = 'disconnected'; + $server->status = ServerStatus::DISCONNECTED; $server->save(); Notifier::send($server, new ServerDisconnected($server)); } diff --git a/app/Actions/Server/Update.php b/app/Actions/Server/Update.php new file mode 100644 index 0000000..818c4fd --- /dev/null +++ b/app/Actions/Server/Update.php @@ -0,0 +1,25 @@ +status = ServerStatus::UPDATING; + $server->save(); + dispatch(function () use ($server) { + $server->os()->upgrade(); + $server->checkConnection(); + $server->checkForUpdates(); + })->catch(function () use ($server) { + Notifier::send($server, new ServerUpdateFailed($server)); + $server->checkConnection(); + })->onConnection('ssh'); + } +} diff --git a/app/Enums/ServerStatus.php b/app/Enums/ServerStatus.php index 9698995..193983b 100644 --- a/app/Enums/ServerStatus.php +++ b/app/Enums/ServerStatus.php @@ -11,4 +11,6 @@ final class ServerStatus const INSTALLATION_FAILED = 'installation_failed'; const DISCONNECTED = 'disconnected'; + + const UPDATING = 'updating'; } diff --git a/app/Http/Controllers/ServerSettingController.php b/app/Http/Controllers/ServerSettingController.php index a89ae60..6c5b409 100644 --- a/app/Http/Controllers/ServerSettingController.php +++ b/app/Http/Controllers/ServerSettingController.php @@ -4,6 +4,7 @@ use App\Actions\Server\EditServer; use App\Actions\Server\RebootServer; +use App\Actions\Server\Update; use App\Facades\Toast; use App\Helpers\HtmxResponse; use App\Models\Server; @@ -64,4 +65,24 @@ public function edit(Request $request, Server $server): RedirectResponse return back(); } + + public function checkUpdates(Server $server): RedirectResponse + { + $this->authorize('manage', $server); + + $server->checkForUpdates(); + + return back(); + } + + public function update(Server $server): HtmxResponse + { + $this->authorize('manage', $server); + + app(Update::class)->update($server); + + Toast::info('Updating server. This may take a few minutes.'); + + return htmx()->back(); + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index c53cc41..48b3aff 100755 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -9,6 +9,7 @@ use App\SSH\Cron\Cron; use App\SSH\OS\OS; use App\SSH\Systemd\Systemd; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -55,6 +56,8 @@ * @property Queue[] $daemons * @property SshKey[] $sshKeys * @property string $hostname + * @property int $updates + * @property Carbon $last_update_check */ class Server extends AbstractModel { @@ -82,6 +85,8 @@ class Server extends AbstractModel 'security_updates', 'progress', 'progress_step', + 'updates', + 'last_update_check', ]; protected $casts = [ @@ -95,6 +100,8 @@ class Server extends AbstractModel 'available_updates' => 'integer', 'security_updates' => 'integer', 'progress' => 'integer', + 'updates' => 'integer', + 'last_update_check' => 'datetime', ]; protected $hidden = [ @@ -384,4 +391,11 @@ public function cron(): Cron { return new Cron($this); } + + public function checkForUpdates(): void + { + $this->updates = $this->os()->availableUpdates(); + $this->last_update_check = now(); + $this->save(); + } } diff --git a/app/Notifications/ServerUpdateFailed.php b/app/Notifications/ServerUpdateFailed.php new file mode 100644 index 0000000..71a61d0 --- /dev/null +++ b/app/Notifications/ServerUpdateFailed.php @@ -0,0 +1,33 @@ +server = $server; + } + + public function rawText(): string + { + return __("Update failed for server [:server] \nCheck your server's logs \n:logs", [ + 'server' => $this->server->name, + 'logs' => url('/servers/'.$this->server->id.'/logs'), + ]); + } + + public function toEmail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject(__('Server update failed!')) + ->line('Your server ['.$this->server->name.'] update has been failed.') + ->line('Check your server logs') + ->action('View Logs', url('/servers/'.$this->server->id.'/logs')); + } +} diff --git a/app/SSH/OS/OS.php b/app/SSH/OS/OS.php index e8f4a86..a12f186 100644 --- a/app/SSH/OS/OS.php +++ b/app/SSH/OS/OS.php @@ -30,6 +30,19 @@ public function upgrade(): void ); } + public function availableUpdates(): int + { + $result = $this->server->ssh()->exec( + $this->getScript('available-updates.sh'), + 'check-available-updates' + ); + + // -1 because the first line is not a package + $availableUpdates = str($result)->after('Available updates:')->trim()->toInteger() - 1; + + return max($availableUpdates, 0); + } + public function createUser(string $user, string $password, string $key): void { $this->server->ssh()->exec( diff --git a/app/SSH/OS/scripts/available-updates.sh b/app/SSH/OS/scripts/available-updates.sh new file mode 100644 index 0000000..ef3eaa3 --- /dev/null +++ b/app/SSH/OS/scripts/available-updates.sh @@ -0,0 +1,5 @@ +sudo DEBIAN_FRONTEND=noninteractive apt-get update + +AVAILABLE_UPDATES=$(sudo DEBIAN_FRONTEND=noninteractive apt list --upgradable | wc -l) + +echo "Available updates:$AVAILABLE_UPDATES" diff --git a/app/SSH/OS/scripts/install-dependencies.sh b/app/SSH/OS/scripts/install-dependencies.sh index c22b0b5..945b5b3 100755 --- a/app/SSH/OS/scripts/install-dependencies.sh +++ b/app/SSH/OS/scripts/install-dependencies.sh @@ -1,3 +1,8 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl git config --global user.email "__email__" git config --global user.name "__name__" + +# Install Node.js +curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -; +sudo DEBIAN_FRONTEND=noninteractive apt-get update +sudo DEBIAN_FRONTEND=noninteractive apt-get install nodejs -y diff --git a/app/SSH/OS/scripts/upgrade.sh b/app/SSH/OS/scripts/upgrade.sh index 22e04ae..1b2a3fe 100755 --- a/app/SSH/OS/scripts/upgrade.sh +++ b/app/SSH/OS/scripts/upgrade.sh @@ -2,8 +2,3 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get clean sudo DEBIAN_FRONTEND=noninteractive apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y sudo DEBIAN_FRONTEND=noninteractive apt-get autoremove -y - -# Install Node.js -curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -; -sudo DEBIAN_FRONTEND=noninteractive apt-get update -sudo DEBIAN_FRONTEND=noninteractive apt-get install nodejs -y diff --git a/database/migrations/2024_05_10_212155_add_updates_field_to_servers_table.php b/database/migrations/2024_05_10_212155_add_updates_field_to_servers_table.php new file mode 100644 index 0000000..aa0c95a --- /dev/null +++ b/database/migrations/2024_05_10_212155_add_updates_field_to_servers_table.php @@ -0,0 +1,24 @@ +integer('updates')->default(0); + $table->timestamp('last_update_check')->nullable(); + }); + } + + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('updates'); + $table->dropColumn('last_update_check'); + }); + } +}; diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php index fe5f438..7ca3ee5 100644 --- a/resources/views/components/card.blade.php +++ b/resources/views/components/card.blade.php @@ -1,4 +1,4 @@ -
+
merge(["class" => "mx-auto mb-10"]) }}> @if (isset($title)) {{ $title }} diff --git a/resources/views/server-settings/partials/server-details.blade.php b/resources/views/server-settings/partials/server-details.blade.php index a0de63e..8647361 100644 --- a/resources/views/server-settings/partials/server-details.blade.php +++ b/resources/views/server-settings/partials/server-details.blade.php @@ -1,4 +1,4 @@ - + {{ __("Details") }} {{ __("More details about your server") }} @@ -14,6 +14,53 @@
+
+
{{ __("Last Update Checked") }}
+
+ +
+
+
+
+
+
+
+
+
{{ __("Available Updates") }} ({{ $server->updates }})
+
+ @if ($server->updates > 0) + + {{ __("Update") }} + + @endif + + + {{ __("Check") }} + +
+
+
+
+
+
+
{{ __("Provider") }}
{{ $server->provider }}
diff --git a/resources/views/servers/partials/server-status.blade.php b/resources/views/servers/partials/server-status.blade.php index f92ef07..6fcbd01 100644 --- a/resources/views/servers/partials/server-status.blade.php +++ b/resources/views/servers/partials/server-status.blade.php @@ -14,4 +14,8 @@ @if ($server->status == \App\Enums\ServerStatus::INSTALLATION_FAILED) {{ $server->status }} @endif + + @if ($server->status == \App\Enums\ServerStatus::UPDATING) + {{ $server->status }} + @endif
diff --git a/routes/server.php b/routes/server.php index 6d9a278..cc92719 100644 --- a/routes/server.php +++ b/routes/server.php @@ -145,6 +145,8 @@ Route::post('/check-connection', [ServerSettingController::class, 'checkConnection'])->name('servers.settings.check-connection'); Route::post('/reboot', [ServerSettingController::class, 'reboot'])->name('servers.settings.reboot'); Route::post('/edit', [ServerSettingController::class, 'edit'])->name('servers.settings.edit'); + Route::post('/check-updates', [ServerSettingController::class, 'checkUpdates'])->name('servers.settings.check-updates'); + Route::post('/update', [ServerSettingController::class, 'update'])->name('servers.settings.update'); }); // logs diff --git a/tests/Feature/ServerTest.php b/tests/Feature/ServerTest.php index c8f9d61..01d7737 100644 --- a/tests/Feature/ServerTest.php +++ b/tests/Feature/ServerTest.php @@ -280,4 +280,32 @@ public function test_edit_server_ip_address_and_disconnect(): void 'status' => ServerStatus::DISCONNECTED, ]); } + + public function test_check_updates(): void + { + SSH::fake('Available updates:10'); + + $this->actingAs($this->user); + + $this->post(route('servers.settings.check-updates', $this->server)) + ->assertSessionDoesntHaveErrors(); + + $this->server->refresh(); + $this->assertEquals(9, $this->server->updates); + } + + public function test_update_server(): void + { + SSH::fake('Available updates:0'); + + $this->actingAs($this->user); + + $this->post(route('servers.settings.update', $this->server)) + ->assertSessionDoesntHaveErrors(); + + $this->server->refresh(); + + $this->assertEquals(ServerStatus::READY, $this->server->status); + $this->assertEquals(0, $this->server->updates); + } }