From 03c4b6508fa58631a254f8877d2d1664858cfbe5 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 25 Jul 2026 12:50:57 +0200 Subject: [PATCH] fix(engine-b): Proxmox agent exec repeated command fields; Traefik write throws on failure Co-Authored-By: Claude Opus 4.8 --- app/Services/Proxmox/HttpProxmoxClient.php | 15 ++++++++++----- app/Services/Traefik/SshTraefikWriter.php | 5 ++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/Services/Proxmox/HttpProxmoxClient.php b/app/Services/Proxmox/HttpProxmoxClient.php index a89541b..789f637 100644 --- a/app/Services/Proxmox/HttpProxmoxClient.php +++ b/app/Services/Proxmox/HttpProxmoxClient.php @@ -96,11 +96,16 @@ class HttpProxmoxClient implements ProxmoxClient public function guestExec(string $node, int $vmid, string $command): array { - // The guest agent runs a program directly, not a shell — wrap compound - // commands (cd/&&/pipes/redirects/env) in an explicit shell. Proxmox is - // form-encoded and takes `command` as an argv list. - $pid = $this->http()->asForm() - ->post("/nodes/{$node}/qemu/{$vmid}/agent/exec", ['command' => ['/bin/sh', '-c', $command]]) + // Wrap compound commands (cd/&&/pipes/redirects/env) in an explicit shell + // — the agent execs a program directly. Proxmox wants the argv as REPEATED + // `command=` fields (not indexed command[0]=…), so build the body by hand. + $body = collect(['/bin/sh', '-c', $command]) + ->map(fn ($arg) => 'command='.rawurlencode($arg)) + ->implode('&'); + + $pid = $this->http() + ->withBody($body, 'application/x-www-form-urlencoded') + ->post("/nodes/{$node}/qemu/{$vmid}/agent/exec") ->throw()->json('data.pid'); // Poll exec-status until the command has exited. diff --git a/app/Services/Traefik/SshTraefikWriter.php b/app/Services/Traefik/SshTraefikWriter.php index 6facbd5..d30b099 100644 --- a/app/Services/Traefik/SshTraefikWriter.php +++ b/app/Services/Traefik/SshTraefikWriter.php @@ -3,6 +3,7 @@ namespace App\Services\Traefik; use Illuminate\Support\Facades\Http; +use RuntimeException; /** * Writes Traefik file-provider YAML into the configured dynamic directory (where @@ -19,7 +20,9 @@ class SshTraefikWriter implements TraefikWriter $yaml = $this->render($subdomain, $fqdn, $targetHost); $path = rtrim((string) config('provisioning.traefik.dynamic_path'), '/')."/{$subdomain}.yml"; - file_put_contents($path, $yaml); + if (file_put_contents($path, $yaml) === false) { + throw new RuntimeException("Failed to write Traefik config: {$path}"); + } } public function remove(string $subdomain): void