feat: Update Proxmox extension with various improvements and bug fixes

- Change listStates method to accept nullable integer for limit parameter.
- Modify TechnitiumDns class to return structured error messages on failure.
- Enhance VNC and xterm popup routes with user authorization checks.
- Update backups view to improve loading states and add unique keys for dynamic elements.
- Refactor cloud-init view to ensure proper data binding and improve user experience.
- Add wire:key attributes to various elements in views for better Livewire performance.
- Improve error handling and user feedback in the firewall view.
- Update server overview and statistics views to handle potential null values gracefully.
- Add new migration files to extend database schema with user-set icons and notes for Proxmox nodes.
This commit is contained in:
2026-02-23 17:23:01 -05:00
parent 9aff64be93
commit f394483daa
48 changed files with 442 additions and 268 deletions

View File

@@ -143,7 +143,7 @@ class Proxmox extends ClassServer
$resourcePool = $this->request('/pools');
$poolList = [];
foreach ($resourcePool->json()['data'] as $pool) {
foreach ($resourcePool->json()['data'] ?? [] as $pool) {
$poolList[] = [
'label' => $pool['poolid'],
'value' => $pool['poolid'],
@@ -326,7 +326,7 @@ class Proxmox extends ClassServer
],
[
'label' => 'Write Back',
'value' => 'write back',
'value' => 'writeback',
],
[
'label' => 'Write Back (unsafe)',
@@ -411,19 +411,29 @@ class Proxmox extends ClassServer
$response = $this->request($url, $method, $data);
// Check if the task is running
$task = $response->json()['data'];
$task = $response->json()['data'] ?? null;
sleep(1); // Give Proxmox some time to start the task
if (!$task) {
return 'error';
}
return $this->waitForTask($task, $node);
}
public function waitForTask($task, $node)
{
// Check if the task is running
$status = $this->request('/nodes/' . $node . '/tasks/' . $task . '/status')->json()['data']['status'];
while ($status == 'running') {
// Check if the task is running (with max 300 iterations / 5 minute timeout)
$status = $this->request('/nodes/' . $node . '/tasks/' . $task . '/status')->json()['data']['status'] ?? null;
$iterations = 0;
while ($status == 'running' && $iterations < 300) {
sleep(1);
$status = $this->request('/nodes/' . $node . '/tasks/' . $task . '/status')->json()['data']['status'];
$status = $this->request('/nodes/' . $node . '/tasks/' . $task . '/status')->json()['data']['status'] ?? null;
$iterations++;
}
if ($iterations >= 300) {
\Log::warning('Proxmox task timed out after 300 seconds', ['task' => $task, 'node' => $node]);
}
return $status;
@@ -456,7 +466,7 @@ class Proxmox extends ClassServer
$server->save();
// Create the VM
$this->requestAndWait('/nodes/' . $server->node->name . '/qemu/' . $server->os->vm_id . '/clone', 'post', [
$cloneResult = $this->requestAndWait('/nodes/' . $server->node->name . '/qemu/' . $server->os->vm_id . '/clone', 'post', [
'newid' => $server->vm_id,
'name' => $server->hostname,
'target' => $server->node->name,
@@ -465,6 +475,18 @@ class Proxmox extends ClassServer
'pool' => $settings['pool'],
], $server->node->name);
if ($cloneResult !== 'stopped' && $cloneResult !== 'OK') {
\Log::error('Proxmox VM clone failed for server ' . $server->id, [
'result' => $cloneResult,
'vm_id' => $server->vm_id,
'node' => $server->node->name,
]);
$server->status = 'error';
$server->save();
return;
}
$this->syncServerSettings($server, $settings, $password);
// Start the server
@@ -492,7 +514,7 @@ class Proxmox extends ClassServer
$this->request('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/config', 'put', $data);
// Get current disk type
$disk = $this->request('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/config')->json()['data'];
$disk = $this->request('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/config')->json()['data'] ?? [];
// As the disk type can be different, we need to check if it is scsi or virtio or sata or ide
$diskType = 'scsi0';
if (isset($disk['virtio0'])) {
@@ -518,7 +540,9 @@ class Proxmox extends ClassServer
$network .= ',rate=' . $settings['ratelimit'];
}
$config['net0'] = $network;
$config['ipconfig0'] = 'ip=' . $ipv4->ip . '/24' . ',gw=' . $ipv4->gateway . ',ip6=' . $ipv6->ip . '/64' . ',gw6=' . $ipv6->gateway;
$ipv4Cidr = $ipv4->netmask ?: '24';
$ipv6Cidr = $ipv6->netmask ?: '64';
$config['ipconfig0'] = 'ip=' . $ipv4->ip . '/' . $ipv4Cidr . ',gw=' . $ipv4->gateway . ',ip6=' . $ipv6->ip . '/' . $ipv6Cidr . ',gw6=' . $ipv6->gateway;
} else {
if ($ipv4) {
$network = $settings['model'] . '=' . ($ipv4->mac_address ? $ipv4->mac_address : $this->generateMacAddress()) . ',bridge=' . $ipv4->ipPool->network_interface;
@@ -527,7 +551,8 @@ class Proxmox extends ClassServer
$network .= ',rate=' . $settings['ratelimit'];
}
$config['net0'] = $network;
$config['ipconfig0'] = 'ip=' . $ipv4->ip . '/24' . ',gw=' . $ipv4->gateway;
$ipv4Cidr = $ipv4->netmask ?: '24';
$config['ipconfig0'] = 'ip=' . $ipv4->ip . '/' . $ipv4Cidr . ',gw=' . $ipv4->gateway;
}
if ($ipv6) {
$network = $settings['model'] . '=' . ($ipv6->mac_address ? $ipv6->mac_address : $this->generateMacAddress()) . ',bridge=' . $ipv6->ipPool->network_interface;
@@ -536,7 +561,8 @@ class Proxmox extends ClassServer
$network .= ',rate=' . $settings['ratelimit'];
}
$config['net1'] = $network;
$config['ipconfig1'] = 'ip6=' . $ipv6->ip . '/64' . ',gw6=' . $ipv6->gateway;
$ipv6Cidr = $ipv6->netmask ?: '64';
$config['ipconfig1'] = 'ip6=' . $ipv6->ip . '/' . $ipv6Cidr . ',gw6=' . $ipv6->gateway;
}
}
@@ -724,7 +750,11 @@ class Proxmox extends ClassServer
])->post(rtrim($this->config('host'), '/') . '/api2/json/access/ticket', [
'username' => 'PAYMENTER_VNC_' . $server->id . '@pve',
'password' => $password,
])->throw()->json()['data'];
])->throw()->json()['data'] ?? [];
if (empty($response['ticket']) || empty($response['CSRFPreventionToken'])) {
throw new \RuntimeException('Failed to obtain Proxmox authentication ticket.');
}
$domain = rtrim($this->config('host'), '/');
// Remove port and http
@@ -739,7 +769,7 @@ class Proxmox extends ClassServer
'verify' => $this->config('verify_ssl') ? true : false,
])->post(rtrim($this->config('host'), '/') . '/api2/json/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/vncproxy', [
'websocket' => 1,
])->json()['data'];
])->json()['data'] ?? [];
// Current
$paymenterDomain = rtrim(config('settings.app_url'), '/');
@@ -750,7 +780,7 @@ class Proxmox extends ClassServer
if (count($parts) >= 2) {
$paymenterDomain = $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1];
}
setcookie('PVEAuthCookie', $response['ticket'], 0, '/', $paymenterDomain, false, true);
setcookie('PVEAuthCookie', $response['ticket'], 0, '/', $paymenterDomain, true, true);
return rtrim($this->config('host'), '/') .
'/?console=kvm' .
@@ -770,7 +800,11 @@ class Proxmox extends ClassServer
])->post(rtrim($this->config('host'), '/') . '/api2/json/access/ticket', [
'username' => 'PAYMENTER_VNC_' . $server->id . '@pve',
'password' => $password,
])->throw()->json()['data'];
])->throw()->json()['data'] ?? [];
if (empty($response['ticket'])) {
throw new \RuntimeException('Failed to obtain Proxmox authentication ticket.');
}
$domain = rtrim($this->config('host'), '/');
// Remove port and http
@@ -786,7 +820,7 @@ class Proxmox extends ClassServer
if (count($parts) >= 2) {
$paymenterDomain = $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1];
}
setcookie('PVEAuthCookie', $response['ticket'], 0, '/', $paymenterDomain, false, true);
setcookie('PVEAuthCookie', $response['ticket'], 0, '/', $paymenterDomain, true, true);
return rtrim($this->config('host'), '/') .
'/?console=kvm' .
@@ -888,6 +922,10 @@ class Proxmox extends ClassServer
// Update server status
$server = Server::where('service_id', $service->id)->first();
if (!$server) {
return false;
}
// Stop server
$this->request('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/status/stop', 'post', [
'vmid' => $server->vm_id,
@@ -895,6 +933,8 @@ class Proxmox extends ClassServer
$server->status = 'suspended';
$server->save();
return true;
}
/**
@@ -909,8 +949,14 @@ class Proxmox extends ClassServer
// Update server status
$server = Server::where('service_id', $service->id)->first();
if (!$server) {
return false;
}
$server->status = 'active';
$server->save();
return true;
}
/**
@@ -927,11 +973,10 @@ class Proxmox extends ClassServer
}
$server = Server::where('service_id', $service->id)->first();
// Kill the server
$this->request('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/status/stop', 'post', [
// Stop the VM and wait for completion before deleting
$this->requestAndWait('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/status/stop', 'post', [
'vmid' => $server->vm_id,
]);
sleep(3);
], $server->node->name);
// Delete proxmox server
$this->request('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id, 'delete');
// Delete server