fix(engine-b): Proxmox agent exec repeated command fields; Traefik write throws on failure

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 12:50:57 +02:00
parent 8825f9abfe
commit 03c4b6508f
2 changed files with 14 additions and 6 deletions

View File

@ -96,11 +96,16 @@ class HttpProxmoxClient implements ProxmoxClient
public function guestExec(string $node, int $vmid, string $command): array public function guestExec(string $node, int $vmid, string $command): array
{ {
// The guest agent runs a program directly, not a shell — wrap compound // Wrap compound commands (cd/&&/pipes/redirects/env) in an explicit shell
// commands (cd/&&/pipes/redirects/env) in an explicit shell. Proxmox is // — the agent execs a program directly. Proxmox wants the argv as REPEATED
// form-encoded and takes `command` as an argv list. // `command=` fields (not indexed command[0]=…), so build the body by hand.
$pid = $this->http()->asForm() $body = collect(['/bin/sh', '-c', $command])
->post("/nodes/{$node}/qemu/{$vmid}/agent/exec", ['command' => ['/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'); ->throw()->json('data.pid');
// Poll exec-status until the command has exited. // Poll exec-status until the command has exited.

View File

@ -3,6 +3,7 @@
namespace App\Services\Traefik; namespace App\Services\Traefik;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use RuntimeException;
/** /**
* Writes Traefik file-provider YAML into the configured dynamic directory (where * 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); $yaml = $this->render($subdomain, $fqdn, $targetHost);
$path = rtrim((string) config('provisioning.traefik.dynamic_path'), '/')."/{$subdomain}.yml"; $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 public function remove(string $subdomain): void