<?php
 namespace App\Modules\Vm\Proxmox; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; class ProxmoxClient { private const TIMEOUT = 30; protected string $apiUrl; protected string $authMode; protected ?string $username; protected ?string $password; protected ?string $tokenId; protected ?string $tokenSecret; protected bool $verifyTls; protected ?string $ticket = null; protected ?string $csrfToken = null; public function __construct(array $config) { $this->apiUrl = rtrim((string) ($config['api_url'] ?? ''), '/'); $this->authMode = (string) ($config['auth_method'] ?? 'api_token'); $this->username = $config['username'] ?? null; $this->password = $config['password'] ?? null; $this->tokenId = $config['api_token_id'] ?? null; $this->tokenSecret = $config['api_token_secret'] ?? null; $this->verifyTls = (bool) ($config['verify_tls'] ?? true); } public function hasCredentials(): bool { if ($this->apiUrl === '') { return false; } if ($this->authMode === 'api_token') { return ! empty($this->tokenId) && ! empty($this->tokenSecret); } return ! empty($this->username) && ! empty($this->password); } public function getVersion(): array { return $this->call('GET', '/version'); } public function listNodes(): array { return $this->call('GET', '/nodes'); } public function cloneVm(string $node, int $sourceVmId, array $payload): array { return $this->call('POST', "/nodes/{$node}/qemu/{$sourceVmId}/clone", $payload); } public function setVmConfig(string $node, int $vmid, array $config): array { return $this->call('PUT', "/nodes/{$node}/qemu/{$vmid}/config", $config); } public function startVm(string $node, int $vmid): array { return $this->call('POST', "/nodes/{$node}/qemu/{$vmid}/status/start"); } public function stopVm(string $node, int $vmid): array { return $this->call('POST', "/nodes/{$node}/qemu/{$vmid}/status/stop"); } public function rebootVm(string $node, int $vmid): array { return $this->call('POST', "/nodes/{$node}/qemu/{$vmid}/status/reboot"); } public function deleteVm(string $node, int $vmid): array { return $this->call('DELETE', "/nodes/{$node}/qemu/{$vmid}"); } public function getVmStatus(string $node, int $vmid): array { return $this->call('GET', "/nodes/{$node}/qemu/{$vmid}/status/current"); } public function vncProxy(string $node, int $vmid): array { return $this->call('POST', "/nodes/{$node}/qemu/{$vmid}/vncproxy"); } public function nextVmId(): array { return $this->call('GET', '/cluster/nextid'); } protected function call(string $method, string $path, array $body = []): array { if (! $this->hasCredentials()) { return ['ok' => false, 'error' => 'Proxmox credentials missing', 'status' => 0]; } try { $req = $this->buildRequest($method, $path); $url = $this->apiUrl.$path; $resp = match ($method) { 'GET' => $req->get($url, $body), 'POST' => $req->asForm()->post($url, $body), 'PUT' => $req->asForm()->put($url, $body), 'DELETE' => $req->delete($url), }; if (! $resp->successful()) { $err = $resp->json('errors') ?? $resp->json('message') ?? "HTTP {$resp->status()}"; return [ 'ok' => false, 'error' => is_string($err) ? $err : json_encode($err), 'status' => $resp->status(), ]; } $json = $resp->json(); $data = is_array($json) && array_key_exists('data', $json) ? $json['data'] : $json; return ['ok' => true, 'data' => $data]; } catch (\Throwable $e) { return ['ok' => false, 'error' => $e->getMessage(), 'status' => 0]; } } protected function buildRequest(string $method, string $path): PendingRequest { $req = Http::withOptions(['verify' => $this->verifyTls]) ->acceptJson() ->timeout(self::TIMEOUT); if ($this->authMode === 'api_token') { return $req->withHeaders([ 'Authorization' => "PVEAPIToken={$this->tokenId}={$this->tokenSecret}", ]); } if ($this->ticket === null) { $this->fetchTicket(); } $headers = ['Cookie' => "PVEAuthCookie={$this->ticket}"]; if ($method !== 'GET' && $this->csrfToken) { $headers['CSRFPreventionToken'] = $this->csrfToken; } return $req->withHeaders($headers); } protected function fetchTicket(): void { $resp = Http::withOptions(['verify' => $this->verifyTls]) ->acceptJson() ->timeout(self::TIMEOUT) ->asForm() ->post($this->apiUrl.'/access/ticket', [ 'username' => $this->username, 'password' => $this->password, ]); if (! $resp->successful()) { throw new \RuntimeException('Proxmox ticket auth failed: HTTP '.$resp->status()); } $data = $resp->json('data') ?? []; $this->ticket = $data['ticket'] ?? null; $this->csrfToken = $data['CSRFPreventionToken'] ?? null; if (! $this->ticket) { throw new \RuntimeException('Proxmox ticket auth: missing ticket in response'); } } } 