From b4cf94ee1a039933411da29313cf9d170f0bafd0 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 25 Jul 2026 12:33:45 +0200 Subject: [PATCH] fix(engine-b): real backup/monitoring registration + OC_PASS scoping - CreateCustomerAdmin puts OC_PASS on the docker invocation (survives the &&). - RegisterBackup creates a real vzdump job via ProxmoxClient::createBackupJob. - RegisterMonitoring registers via a new MonitoringClient service (interface + fake + http). Both persist the external id [E] before the breadcrumb, so acceptance reflects a real registration, not a fabricated row. Co-Authored-By: Claude Opus 4.8 --- app/Providers/AppServiceProvider.php | 3 ++ .../Steps/Customer/CreateCustomerAdmin.php | 7 ++-- .../Steps/Customer/RegisterBackup.php | 13 +++++-- .../Steps/Customer/RegisterMonitoring.php | 9 +++-- .../Monitoring/FakeMonitoringClient.php | 24 +++++++++++++ .../Monitoring/HttpMonitoringClient.php | 34 +++++++++++++++++++ app/Services/Monitoring/MonitoringClient.php | 11 ++++++ app/Services/Proxmox/FakeProxmoxClient.php | 11 ++++++ app/Services/Proxmox/HttpProxmoxClient.php | 13 +++++++ app/Services/Proxmox/ProxmoxClient.php | 3 ++ config/services.php | 5 +++ tests/Pest.php | 4 ++- 12 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 app/Services/Monitoring/FakeMonitoringClient.php create mode 100644 app/Services/Monitoring/HttpMonitoringClient.php create mode 100644 app/Services/Monitoring/MonitoringClient.php diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fad8432..45dbf0b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,8 @@ namespace App\Providers; use App\Provisioning\PipelineRegistry; use App\Services\Dns\HetznerDnsClient; use App\Services\Dns\HttpHetznerDnsClient; +use App\Services\Monitoring\HttpMonitoringClient; +use App\Services\Monitoring\MonitoringClient; use App\Services\Proxmox\HttpProxmoxClient; use App\Services\Proxmox\ProxmoxClient; use App\Services\Ssh\PhpseclibRemoteShell; @@ -32,6 +34,7 @@ class AppServiceProvider extends ServiceProvider $this->app->bind(ProxmoxClient::class, HttpProxmoxClient::class); $this->app->bind(HetznerDnsClient::class, HttpHetznerDnsClient::class); $this->app->bind(TraefikWriter::class, SshTraefikWriter::class); + $this->app->bind(MonitoringClient::class, HttpMonitoringClient::class); } /** diff --git a/app/Provisioning/Steps/Customer/CreateCustomerAdmin.php b/app/Provisioning/Steps/Customer/CreateCustomerAdmin.php index 47c45e5..57ce0dd 100644 --- a/app/Provisioning/Steps/Customer/CreateCustomerAdmin.php +++ b/app/Provisioning/Steps/Customer/CreateCustomerAdmin.php @@ -46,8 +46,11 @@ class CreateCustomerAdmin extends CustomerStep ? 'user:resetpassword --password-from-env '.escapeshellarg($username) : 'user:add --password-from-env --group=admin '.escapeshellarg($username); - // Pass the password via env (OC_PASS), never on the argv/command line. - $this->guest($pve, $run, 'OC_PASS='.escapeshellarg($password).' '.$occ.' -e OC_PASS app php occ '.$action); + // Pass the password via env (OC_PASS) on the docker invocation itself — + // an assignment before `cd` would not survive the `&&`. + $this->guest($pve, $run, + 'cd /opt/nextcloud && OC_PASS='.escapeshellarg($password). + ' docker compose exec -T -e OC_PASS app php occ '.$action); // Persist only the username (encrypted ref); hand the password to step 15 // encrypted-in-context for delivery, then it is scrubbed. Never plaintext. diff --git a/app/Provisioning/Steps/Customer/RegisterBackup.php b/app/Provisioning/Steps/Customer/RegisterBackup.php index fe8836a..26435c0 100644 --- a/app/Provisioning/Steps/Customer/RegisterBackup.php +++ b/app/Provisioning/Steps/Customer/RegisterBackup.php @@ -4,9 +4,12 @@ namespace App\Provisioning\Steps\Customer; use App\Models\ProvisioningRun; use App\Provisioning\StepResult; +use App\Services\Proxmox\ProxmoxClient; class RegisterBackup extends CustomerStep { + public function __construct(private ProxmoxClient $pve) {} + public function key(): string { return 'register_backup'; @@ -24,14 +27,18 @@ class RegisterBackup extends CustomerStep } $instance = $this->instance($run); - $jobId = 'vzdump-'.$instance->vmid.'-daily'; + $schedule = 'daily 02:00'; + // Create a real scheduled vzdump job on the host [E] before recording it. + $jobId = $this->pve->forHost($instance->host) + ->createBackupJob((string) $run->context('node'), (int) $instance->vmid, $schedule); + + $this->recordResource($run, $instance->host, 'backup_job_id', $jobId); $instance->backups()->create([ 'external_job_id' => $jobId, - 'schedule' => 'daily 02:00', + 'schedule' => $schedule, 'status' => 'scheduled', ]); - $this->recordResource($run, $instance->host, 'backup_job_id', $jobId); return StepResult::advance(); } diff --git a/app/Provisioning/Steps/Customer/RegisterMonitoring.php b/app/Provisioning/Steps/Customer/RegisterMonitoring.php index ea267d2..33e704b 100644 --- a/app/Provisioning/Steps/Customer/RegisterMonitoring.php +++ b/app/Provisioning/Steps/Customer/RegisterMonitoring.php @@ -4,9 +4,12 @@ namespace App\Provisioning\Steps\Customer; use App\Models\ProvisioningRun; use App\Provisioning\StepResult; +use App\Services\Monitoring\MonitoringClient; class RegisterMonitoring extends CustomerStep { + public function __construct(private MonitoringClient $monitoring) {} + public function key(): string { return 'register_monitoring'; @@ -24,15 +27,17 @@ class RegisterMonitoring extends CustomerStep } $instance = $this->instance($run); - $targetId = 'mon-'.$instance->vmid; $url = 'https://'.$instance->subdomain.'.'.config('provisioning.dns.zone').'/status.php'; + // Register with the monitoring service [E] before recording it. + $targetId = $this->monitoring->registerTarget($instance->subdomain, $url); + + $this->recordResource($run, $instance->host, 'monitoring_target_id', $targetId); $instance->monitoringTargets()->create([ 'external_id' => $targetId, 'url' => $url, 'status' => 'up', ]); - $this->recordResource($run, $instance->host, 'monitoring_target_id', $targetId); return StepResult::advance(); } diff --git a/app/Services/Monitoring/FakeMonitoringClient.php b/app/Services/Monitoring/FakeMonitoringClient.php new file mode 100644 index 0000000..0766ca2 --- /dev/null +++ b/app/Services/Monitoring/FakeMonitoringClient.php @@ -0,0 +1,24 @@ + id => url */ + public array $targets = []; + + private int $counter = 1; + + public function registerTarget(string $name, string $url): string + { + $id = 'mon-'.$this->counter++; + $this->targets[$id] = $url; + + return $id; + } + + public function deregisterTarget(string $externalId): void + { + unset($this->targets[$externalId]); + } +} diff --git a/app/Services/Monitoring/HttpMonitoringClient.php b/app/Services/Monitoring/HttpMonitoringClient.php new file mode 100644 index 0000000..01f3721 --- /dev/null +++ b/app/Services/Monitoring/HttpMonitoringClient.php @@ -0,0 +1,34 @@ +post(rtrim($endpoint, '/').'/monitors', ['friendly_name' => $name, 'url' => $url, 'type' => 'https']) + ->throw()->json('monitor.id'); + } + + public function deregisterTarget(string $externalId): void + { + $endpoint = (string) config('services.monitoring.url'); + if (filled($endpoint)) { + Http::withToken((string) config('services.monitoring.token')) + ->delete(rtrim($endpoint, '/').'/monitors/'.$externalId)->throw(); + } + } +} diff --git a/app/Services/Monitoring/MonitoringClient.php b/app/Services/Monitoring/MonitoringClient.php new file mode 100644 index 0000000..b50ca9c --- /dev/null +++ b/app/Services/Monitoring/MonitoringClient.php @@ -0,0 +1,11 @@ +firewallCalls[] = (string) $vmid; } + + /** @var array */ + public array $backupJobs = []; + + public function createBackupJob(string $node, int $vmid, string $schedule): string + { + $id = 'backup-'.$vmid; + $this->backupJobs[] = $id; + + return $id; + } } diff --git a/app/Services/Proxmox/HttpProxmoxClient.php b/app/Services/Proxmox/HttpProxmoxClient.php index ceefe33..280441e 100644 --- a/app/Services/Proxmox/HttpProxmoxClient.php +++ b/app/Services/Proxmox/HttpProxmoxClient.php @@ -133,4 +133,17 @@ class HttpProxmoxClient implements ProxmoxClient } $this->http()->asForm()->put("/nodes/{$node}/qemu/{$vmid}/firewall/options", ['enable' => 1])->throw(); } + + public function createBackupJob(string $node, int $vmid, string $schedule): string + { + $data = $this->http()->asForm()->post('/cluster/backup', [ + 'vmid' => $vmid, + 'schedule' => $schedule, + 'storage' => 'local', + 'mode' => 'snapshot', + 'enabled' => 1, + ])->throw()->json('data'); + + return is_string($data) ? $data : (string) ($data['id'] ?? 'backup-'.$vmid); + } } diff --git a/app/Services/Proxmox/ProxmoxClient.php b/app/Services/Proxmox/ProxmoxClient.php index 3dcb6bc..981b69d 100644 --- a/app/Services/Proxmox/ProxmoxClient.php +++ b/app/Services/Proxmox/ProxmoxClient.php @@ -55,4 +55,7 @@ interface ProxmoxClient /** @param array> $rules */ public function applyFirewall(string $node, int $vmid, array $rules): void; + + /** Create a scheduled vzdump backup job for the VM; returns the job id. */ + public function createBackupJob(string $node, int $vmid, string $schedule): string; } diff --git a/config/services.php b/config/services.php index 580a3b6..60222e5 100644 --- a/config/services.php +++ b/config/services.php @@ -39,4 +39,9 @@ return [ 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), ], + 'monitoring' => [ + 'url' => env('MONITORING_API_URL'), + 'token' => env('MONITORING_API_TOKEN'), + ], + ]; diff --git a/tests/Pest.php b/tests/Pest.php index 7d8c088..5544609 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -62,12 +62,14 @@ function fakeServices(): array $pve = new \App\Services\Proxmox\FakeProxmoxClient; $dns = new \App\Services\Dns\FakeHetznerDnsClient; $traefik = new \App\Services\Traefik\FakeTraefikWriter; + $monitoring = new \App\Services\Monitoring\FakeMonitoringClient; app()->instance(\App\Services\Ssh\RemoteShell::class, $shell); app()->instance(\App\Services\Wireguard\WireguardHub::class, $hub); app()->instance(\App\Services\Proxmox\ProxmoxClient::class, $pve); app()->instance(\App\Services\Dns\HetznerDnsClient::class, $dns); app()->instance(\App\Services\Traefik\TraefikWriter::class, $traefik); + app()->instance(\App\Services\Monitoring\MonitoringClient::class, $monitoring); - return ['shell' => $shell, 'hub' => $hub, 'pve' => $pve, 'dns' => $dns, 'traefik' => $traefik]; + return ['shell' => $shell, 'hub' => $hub, 'pve' => $pve, 'dns' => $dns, 'traefik' => $traefik, 'monitoring' => $monitoring]; }