Files
proxmox-selfservice/support-provisioning-portal/includes/class-spp-http-proxmox-client.php
Sven Steinert 2c1949bf1e new file: CHANGELOG.md
modified:   README.md
	modified:   support-provisioning-portal/assets/portal.css
	modified:   support-provisioning-portal/assets/portal.js
	modified:   support-provisioning-portal/includes/class-spp-activator.php
	modified:   support-provisioning-portal/includes/class-spp-admin-page.php
	modified:   support-provisioning-portal/includes/class-spp-http-proxmox-client.php
	modified:   support-provisioning-portal/includes/class-spp-mock-proxmox-client.php
	new file:   support-provisioning-portal/includes/class-spp-permissions.php
	modified:   support-provisioning-portal/includes/class-spp-plugin.php
	modified:   support-provisioning-portal/includes/class-spp-repository.php
	modified:   support-provisioning-portal/includes/class-spp-rest-controller.php
	modified:   support-provisioning-portal/includes/class-spp-shortcode.php
	modified:   support-provisioning-portal/includes/interface-spp-proxmox-client.php
	modified:   support-provisioning-portal/support-provisioning-portal.php
2026-04-24 15:13:42 +02:00

172 lines
5.4 KiB
PHP

<?php
if (!defined('ABSPATH')) {
exit;
}
final class SPP_Http_Proxmox_Client implements SPP_Proxmox_Client
{
/** @var array{base_url:string, token_id:string, token_secret:string, node:string} */
private array $options;
/**
* @param array{base_url:string, token_id:string, token_secret:string, node:string} $options
*/
public function __construct(array $options)
{
$this->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<string, int|string> $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;
}
}