options = $options; } public function list_templates(): array { $data = $this->request("/nodes/{$this->options['node']}/qemu", 'GET'); $vms = is_array($data) ? $data : []; $templates = []; foreach ($vms as $vm) { if (!is_array($vm) || empty($vm['template']) || empty($vm['vmid'])) { continue; } $templates[] = [ 'vmId' => (int) $vm['vmid'], 'name' => isset($vm['name']) && $vm['name'] !== '' ? (string) $vm['name'] : 'template-' . (int) $vm['vmid'], 'cpuCores' => max(1, (int) ($vm['cpus'] ?? 1)), 'memoryMb' => $this->bytes_to_mb((int) ($vm['maxmem'] ?? 0)), 'diskGb' => $this->bytes_to_gb((int) ($vm['maxdisk'] ?? 0)), 'status' => isset($vm['status']) ? (string) $vm['status'] : 'unknown', ]; } usort( $templates, static fn(array $left, array $right): int => strcasecmp((string) $left['name'], (string) $right['name']) ); return $templates; } public function clone_vm(array $input): array { $vm_id = (int) $this->request('/cluster/nextid', 'GET'); $template_id = (int) $input['template_vm_id']; $this->request("/nodes/{$this->options['node']}/qemu/{$template_id}/clone", 'POST', [ 'newid' => $vm_id, 'name' => (string) $input['name'], 'full' => 1, ]); $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/config", 'PUT', [ 'cores' => (int) $input['cpu_cores'], 'memory' => (int) $input['memory_mb'], ]); return ['vm_id' => $vm_id]; } public function start_vm(int $vm_id): void { $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/status/start", 'POST'); } public function stop_vm(int $vm_id): void { $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/status/stop", 'POST'); } public function delete_vm(int $vm_id): void { $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}", 'DELETE'); } public function get_status(int $vm_id): string { $data = $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/status/current", 'GET'); return is_array($data) && isset($data['status']) ? (string) $data['status'] : 'unknown'; } public function get_ip_addresses(int $vm_id): array { $data = $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/agent/network-get-interfaces", 'GET'); $interfaces = is_array($data) && isset($data['result']) && is_array($data['result']) ? $data['result'] : []; $ips = []; foreach ($interfaces as $interface) { if (!is_array($interface) || empty($interface['ip-addresses']) || !is_array($interface['ip-addresses'])) { continue; } foreach ($interface['ip-addresses'] as $address) { if (!is_array($address) || empty($address['ip-address'])) { continue; } $ip = (string) $address['ip-address']; if ($ip === '127.0.0.1' || $ip === '::1' || str_starts_with($ip, 'fe80:')) { continue; } $ips[] = $ip; } } return array_values(array_unique($ips)); } private function bytes_to_mb(int $bytes): int { if ($bytes < 1) { return 1024; } return max(128, (int) ceil($bytes / 1048576)); } private function bytes_to_gb(int $bytes): int { if ($bytes < 1) { return 8; } return max(1, (int) ceil($bytes / 1073741824)); } /** * @param array $body * @return mixed */ private function request(string $path, string $method, array $body = []) { foreach (['base_url', 'token_id', 'token_secret', 'node'] as $key) { if ($this->options[$key] === '') { throw new RuntimeException('Missing Proxmox HTTP configuration.'); } } $url = trailingslashit($this->options['base_url']) . 'api2/json' . $path; $response = wp_remote_request($url, [ 'method' => $method, 'timeout' => 30, 'headers' => [ 'Authorization' => 'PVEAPIToken=' . $this->options['token_id'] . '=' . $this->options['token_secret'], ], 'body' => $body, ]); if (is_wp_error($response)) { throw new RuntimeException($response->get_error_message()); } $status = (int) wp_remote_retrieve_response_code($response); if ($status < 200 || $status >= 300) { throw new RuntimeException('Proxmox request failed with HTTP ' . $status); } $payload = json_decode(wp_remote_retrieve_body($response), true); return is_array($payload) && array_key_exists('data', $payload) ? $payload['data'] : null; } }