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;
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));
}

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 DISCONNECTED = 'disconnected';
const UPDATING = 'updating';
}

View File

@ -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();
}
}

View File

@ -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();
}
}

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
{
$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
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

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

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>
@if (isset($title))
<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="description">
{{ __("More details about your server") }}
@ -14,6 +14,53 @@
<div class="border-t border-gray-200 dark:border-gray-700"></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>{{ __("Provider") }}</div>
<div class="capitalize">{{ $server->provider }}</div>

View File

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

View File

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

View File

@ -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);
}
}