server updates (#202)

* server updates

* add last update check
This commit is contained in:
Saeed Vaziry 2024-05-11 10:09:46 +02:00 committed by GitHub
parent bbe3ca802d
commit fe331fd2b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 229 additions and 10 deletions

View File

@ -2,6 +2,7 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use App\Enums\ServerStatus;
use App\Facades\Notifier; use App\Facades\Notifier;
use App\Models\Server; use App\Models\Server;
use App\Notifications\ServerDisconnected; use App\Notifications\ServerDisconnected;
@ -15,12 +16,12 @@ public function check(Server $server): Server
try { try {
$server->ssh()->connect(); $server->ssh()->connect();
$server->refresh(); $server->refresh();
if ($status == 'disconnected') { if (in_array($status, [ServerStatus::DISCONNECTED, ServerStatus::UPDATING])) {
$server->status = 'ready'; $server->status = ServerStatus::READY;
$server->save(); $server->save();
} }
} catch (Throwable) { } catch (Throwable) {
$server->status = 'disconnected'; $server->status = ServerStatus::DISCONNECTED;
$server->save(); $server->save();
Notifier::send($server, new ServerDisconnected($server)); Notifier::send($server, new ServerDisconnected($server));
} }

View File

@ -0,0 +1,25 @@
<?php
namespace App\Actions\Server;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Models\Server;
use App\Notifications\ServerUpdateFailed;
class Update
{
public function update(Server $server): void
{
$server->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');
}
}

View File

@ -11,4 +11,6 @@ final class ServerStatus
const INSTALLATION_FAILED = 'installation_failed'; const INSTALLATION_FAILED = 'installation_failed';
const DISCONNECTED = 'disconnected'; const DISCONNECTED = 'disconnected';
const UPDATING = 'updating';
} }

View File

@ -4,6 +4,7 @@
use App\Actions\Server\EditServer; use App\Actions\Server\EditServer;
use App\Actions\Server\RebootServer; use App\Actions\Server\RebootServer;
use App\Actions\Server\Update;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Models\Server; use App\Models\Server;
@ -64,4 +65,24 @@ public function edit(Request $request, Server $server): RedirectResponse
return back(); 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();
}
} }

View File

@ -9,6 +9,7 @@
use App\SSH\Cron\Cron; use App\SSH\Cron\Cron;
use App\SSH\OS\OS; use App\SSH\OS\OS;
use App\SSH\Systemd\Systemd; use App\SSH\Systemd\Systemd;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -55,6 +56,8 @@
* @property Queue[] $daemons * @property Queue[] $daemons
* @property SshKey[] $sshKeys * @property SshKey[] $sshKeys
* @property string $hostname * @property string $hostname
* @property int $updates
* @property Carbon $last_update_check
*/ */
class Server extends AbstractModel class Server extends AbstractModel
{ {
@ -82,6 +85,8 @@ class Server extends AbstractModel
'security_updates', 'security_updates',
'progress', 'progress',
'progress_step', 'progress_step',
'updates',
'last_update_check',
]; ];
protected $casts = [ protected $casts = [
@ -95,6 +100,8 @@ class Server extends AbstractModel
'available_updates' => 'integer', 'available_updates' => 'integer',
'security_updates' => 'integer', 'security_updates' => 'integer',
'progress' => 'integer', 'progress' => 'integer',
'updates' => 'integer',
'last_update_check' => 'datetime',
]; ];
protected $hidden = [ protected $hidden = [
@ -384,4 +391,11 @@ public function cron(): Cron
{ {
return new Cron($this); return new Cron($this);
} }
public function checkForUpdates(): void
{
$this->updates = $this->os()->availableUpdates();
$this->last_update_check = now();
$this->save();
}
} }

View File

@ -0,0 +1,33 @@
<?php
namespace App\Notifications;
use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage;
class ServerUpdateFailed extends AbstractNotification
{
protected Server $server;
public function __construct(Server $server)
{
$this->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'));
}
}

View File

@ -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 public function createUser(string $user, string $password, string $key): void
{ {
$this->server->ssh()->exec( $this->server->ssh()->exec(

View File

@ -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"

View File

@ -1,3 +1,8 @@
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl 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.email "__email__"
git config --global user.name "__name__" 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

View File

@ -2,8 +2,3 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get clean
sudo DEBIAN_FRONTEND=noninteractive apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
sudo DEBIAN_FRONTEND=noninteractive apt-get autoremove -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

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -1,4 +1,4 @@
<div class="mx-auto mb-10"> <div {{ $attributes->merge(["class" => "mx-auto mb-10"]) }}>
<x-card-header> <x-card-header>
@if (isset($title)) @if (isset($title))
<x-slot name="title">{{ $title }}</x-slot> <x-slot name="title">{{ $title }}</x-slot>

View File

@ -1,4 +1,4 @@
<x-card> <x-card id="server-details">
<x-slot name="title">{{ __("Details") }}</x-slot> <x-slot name="title">{{ __("Details") }}</x-slot>
<x-slot name="description"> <x-slot name="description">
{{ __("More details about your server") }} {{ __("More details about your server") }}
@ -14,6 +14,53 @@
<div class="border-t border-gray-200 dark:border-gray-700"></div> <div class="border-t border-gray-200 dark:border-gray-700"></div>
</div> </div>
</div> </div>
<div class="flex items-center justify-between">
<div>{{ __("Last Update Checked") }}</div>
<div>
<x-datetime :value="$server->last_update_check" />
</div>
</div>
<div>
<div class="py-5">
<div class="border-t border-gray-200 dark:border-gray-700"></div>
</div>
</div>
<div id="available-updates" class="flex items-center justify-between">
<div>{{ __("Available Updates") }} ({{ $server->updates }})</div>
<div class="flex flex-col items-end md:flex-row md:items-center">
@if ($server->updates > 0)
<x-primary-button
id="btn-update-server"
hx-post="{{ route('servers.settings.update', $server) }}"
hx-swap="outerHTML"
hx-target="#server-details"
hx-select="#server-details"
hx-ext="disable-element"
hx-disable-element="#btn-update-server"
>
{{ __("Update") }}
</x-primary-button>
@endif
<x-secondary-button
id="btn-check-updates"
class="mb-2 md:mb-0 md:ml-2"
hx-post="{{ route('servers.settings.check-updates', $server) }}"
hx-swap="outerHTML"
hx-target="#server-details"
hx-select="#server-details"
hx-ext="disable-element"
hx-disable-element="#btn-check-updates"
>
{{ __("Check") }}
</x-secondary-button>
</div>
</div>
<div>
<div class="py-5">
<div class="border-t border-gray-200 dark:border-gray-700"></div>
</div>
</div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div>{{ __("Provider") }}</div> <div>{{ __("Provider") }}</div>
<div class="capitalize">{{ $server->provider }}</div> <div class="capitalize">{{ $server->provider }}</div>

View File

@ -14,4 +14,8 @@
@if ($server->status == \App\Enums\ServerStatus::INSTALLATION_FAILED) @if ($server->status == \App\Enums\ServerStatus::INSTALLATION_FAILED)
<x-status status="danger">{{ $server->status }}</x-status> <x-status status="danger">{{ $server->status }}</x-status>
@endif @endif
@if ($server->status == \App\Enums\ServerStatus::UPDATING)
<x-status status="warning">{{ $server->status }}</x-status>
@endif
</div> </div>

View File

@ -145,6 +145,8 @@
Route::post('/check-connection', [ServerSettingController::class, 'checkConnection'])->name('servers.settings.check-connection'); Route::post('/check-connection', [ServerSettingController::class, 'checkConnection'])->name('servers.settings.check-connection');
Route::post('/reboot', [ServerSettingController::class, 'reboot'])->name('servers.settings.reboot'); Route::post('/reboot', [ServerSettingController::class, 'reboot'])->name('servers.settings.reboot');
Route::post('/edit', [ServerSettingController::class, 'edit'])->name('servers.settings.edit'); 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 // logs

View File

@ -280,4 +280,32 @@ public function test_edit_server_ip_address_and_disconnect(): void
'status' => ServerStatus::DISCONNECTED, '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);
}
} }