v0.10.0: Docker + Update-Funktion + deploy.sh

This commit is contained in:
2026-07-02 21:14:18 +02:00
parent 85118c5bcc
commit af2aa1eaf5
20 changed files with 1169 additions and 11 deletions
@@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class AppCheckUpdateCommand extends Command
{
protected $signature = 'app:check-update {--json : Ausgabe als JSON}';
protected $description = 'Prüft ob ein Update auf Gitea verfügbar ist und speichert das Ergebnis im Cache';
public function handle(): int
{
$current = config('version.current');
$giteaUrl = rtrim(config('version.gitea_url'), '/');
$repo = config('version.gitea_repo');
if (empty($giteaUrl)) {
$this->warn('GITEA_URL ist nicht konfiguriert.');
Cache::put('app_update_available', null, now()->addHours(1));
return Command::SUCCESS;
}
try {
$response = Http::timeout(8)
->get("{$giteaUrl}/api/v1/repos/{$repo}/releases/latest");
if (!$response->successful()) {
$this->warn("Gitea nicht erreichbar (HTTP {$response->status()}).");
Cache::put('app_update_check_error', "HTTP {$response->status()}", now()->addHours(1));
return Command::SUCCESS;
}
$latest = $response->json('tag_name', '');
$available = !empty($latest) && version_compare(
ltrim($latest, 'v'),
ltrim($current, 'v'),
'>'
);
// Ergebnis 6 Stunden cachen
Cache::put('app_update_available', $available ? $latest : false, now()->addHours(6));
Cache::put('app_update_checked_at', now()->toDateTimeString(), now()->addHours(6));
Cache::forget('app_update_check_error');
if ($this->option('json')) {
$this->line(json_encode([
'current' => $current,
'latest' => $latest,
'available' => $available,
]));
} else {
if ($available) {
$this->info("✓ Update verfügbar: {$current}{$latest}");
} else {
$this->info("✓ Kein Update — aktuelle Version: {$current}");
}
}
} catch (\Exception $e) {
$this->error('Verbindungsfehler: ' . $e->getMessage());
Cache::put('app_update_check_error', $e->getMessage(), now()->addHours(1));
}
return Command::SUCCESS;
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
class AppInstallUpdateCommand extends Command
{
protected $signature = 'app:install-update {--tag= : Bestimmten Git-Tag installieren}';
protected $description = 'Installiert das neueste Update vom Gitea-Repository';
public function handle(): int
{
$projectPath = base_path();
$this->info('');
$this->info('╔══════════════════════════════════════╗');
$this->info('║ Network-MGMT Update ║');
$this->info('╚══════════════════════════════════════╝');
$this->info('');
// ── 1. Tags holen ──────────────────────────────────────────────────────
$this->info('▶ Git: Aktuelle Tags holen ...');
$this->exec("cd {$projectPath} && git fetch --tags 2>&1");
// ── 2. Ziel-Tag bestimmen ──────────────────────────────────────────────
if ($this->option('tag')) {
$targetTag = $this->option('tag');
} else {
$targetTag = trim(shell_exec(
"cd {$projectPath} && git describe --tags \$(git rev-list --tags --max-count=1) 2>/dev/null"
));
}
if (empty($targetTag)) {
$this->error('Kein Release-Tag im Repository gefunden.');
return Command::FAILURE;
}
$currentVersion = config('version.current');
$this->info(" Aktuell: {$currentVersion}");
$this->info(" Ziel: {$targetTag}");
$this->info('');
if ($targetTag === $currentVersion) {
$this->warn('Bereits auf der neuesten Version. Abbruch.');
return Command::SUCCESS;
}
// ── 3. Wartungsmodus ───────────────────────────────────────────────────
$this->info('▶ Wartungsmodus aktivieren ...');
Artisan::call('down', ['--render' => 'errors.maintenance']);
try {
// ── 4. Git Checkout ────────────────────────────────────────────────
$this->info("▶ Git: Checkout {$targetTag} ...");
$this->exec("cd {$projectPath} && git checkout {$targetTag} 2>&1");
// ── 5. Composer ────────────────────────────────────────────────────
$this->info('▶ Composer: Abhängigkeiten aktualisieren ...');
$this->exec("cd {$projectPath} && composer install --no-dev --optimize-autoloader --no-interaction 2>&1");
// ── 6. Assets ──────────────────────────────────────────────────────
if (file_exists("{$projectPath}/package.json")) {
$this->info('▶ Node: Assets neu bauen ...');
$this->exec("cd {$projectPath} && npm ci --silent 2>&1");
$this->exec("cd {$projectPath} && npm run build 2>&1");
}
// ── 7. Migrationen ─────────────────────────────────────────────────
$this->info('▶ Datenbank: Migrationen ausführen ...');
Artisan::call('migrate', ['--force' => true, '--no-interaction' => true]);
$this->line(Artisan::output());
// ── 8. Cache leeren und neu aufbauen ───────────────────────────────
$this->info('▶ Cache: Neu aufbauen ...');
Artisan::call('config:cache');
Artisan::call('route:cache');
Artisan::call('view:cache');
// ── 9. Update-Cache zurücksetzen ───────────────────────────────────
Cache::forget('app_update_available');
Cache::forget('app_update_checked_at');
$this->info('');
$this->info("✓ Update auf {$targetTag} erfolgreich abgeschlossen!");
} catch (\Throwable $e) {
$this->error('Update fehlgeschlagen: ' . $e->getMessage());
Artisan::call('up');
return Command::FAILURE;
} finally {
// ── 10. Wartungsmodus deaktivieren ─────────────────────────────────
$this->info('▶ Wartungsmodus deaktivieren ...');
Artisan::call('up');
}
return Command::SUCCESS;
}
private function exec(string $cmd): void
{
$output = shell_exec($cmd);
if ($output) {
foreach (explode("\n", trim($output)) as $line) {
if (trim($line)) {
$this->line(" {$line}");
}
}
}
}
}
@@ -0,0 +1,132 @@
<?php
namespace App\Console\Commands;
use App\Models\NetworkDevice;
use App\Models\NetworkHost;
use Illuminate\Console\Command;
class NetworkBackfillDevicesCommand extends Command
{
protected $signature = 'network:backfill-devices
{--dry-run : Nur anzeigen, was gemacht würde, ohne zu schreiben}';
protected $description = 'Erstellt NetworkDevice-Einträge für bestehende network_hosts ohne device_id';
public function handle(): int
{
$dryRun = $this->option('dry-run');
// Alle Hosts ohne device_id, neueste zuerst (für korrekte last_seen_at)
$hosts = NetworkHost::query()
->join('network_scans', 'network_scans.id', '=', 'network_hosts.scan_id')
->whereNull('network_hosts.device_id')
->select([
'network_hosts.*',
'network_scans.created_at as scan_time',
])
->orderByDesc('network_scans.created_at')
->get();
$this->info("Gefunden: {$hosts->count()} Hosts ohne device_id");
if ($hosts->isEmpty()) {
$this->info('Nichts zu tun.');
return Command::SUCCESS;
}
$created = 0;
$linked = 0;
foreach ($hosts as $host) {
$device = null;
// 1. Per MAC suchen/erstellen
if (!empty($host->mac_address)) {
$device = NetworkDevice::where('mac_address', $host->mac_address)->first();
if (!$device) {
if (!$dryRun) {
$device = NetworkDevice::create([
'mac_address' => $host->mac_address,
'current_ip' => $host->ip_address,
'hostname' => $host->hostname,
'mac_vendor' => $host->mac_vendor,
'status' => $host->status,
'first_seen_at' => $host->scan_time,
'last_seen_at' => $host->scan_time,
]);
$created++;
}
$this->line(" + Gerät (MAC) {$host->mac_address} / {$host->ip_address}");
}
}
// 2. Per Hostname suchen/erstellen (kein MAC)
elseif (!empty($host->hostname)) {
$device = NetworkDevice::whereNull('mac_address')
->where('hostname', $host->hostname)
->first();
if (!$device) {
if (!$dryRun) {
$device = NetworkDevice::create([
'mac_address' => null,
'current_ip' => $host->ip_address,
'hostname' => $host->hostname,
'mac_vendor' => null,
'status' => $host->status,
'first_seen_at' => $host->scan_time,
'last_seen_at' => $host->scan_time,
]);
$created++;
}
$this->line(" + Gerät (Hostname) {$host->hostname} / {$host->ip_address}");
}
}
// 3. Per IP suchen/erstellen (weder MAC noch Hostname)
else {
$device = NetworkDevice::whereNull('mac_address')
->where('current_ip', $host->ip_address)
->first();
if (!$device) {
if (!$dryRun) {
$device = NetworkDevice::create([
'mac_address' => null,
'current_ip' => $host->ip_address,
'hostname' => null,
'mac_vendor' => null,
'status' => $host->status,
'first_seen_at' => $host->scan_time,
'last_seen_at' => $host->scan_time,
]);
$created++;
}
$this->line(" + Gerät (IP) {$host->ip_address}");
}
}
// Host mit Gerät verknüpfen
if ($device && !$dryRun) {
$host->update(['device_id' => $device->id]);
// Älteste first_seen_at beibehalten
if ($host->scan_time < $device->first_seen_at) {
$device->update(['first_seen_at' => $host->scan_time]);
}
$linked++;
}
}
if ($dryRun) {
$this->warn('Dry-Run keine Änderungen geschrieben.');
} else {
$this->info("✓ Fertig: {$created} Geräte neu erstellt, {$linked} Hosts verknüpft.");
}
return Command::SUCCESS;
}
}