From ef1499cde430e321a439a88c6337be4685918e3a Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Mon, 2 Mar 2026 21:23:10 -0500 Subject: [PATCH] fix: OS template type matching for LXC/KVM nodes - Add is_lxc column to ext_proxmox_oses for tracking template type - OS model now has vmType() helper returning 'lxc' or 'qemu' - Install uses OS template's type for clone API path (not node type) - createServer validates OS type matches node type before provisioning - calculateNode filters nodes by LXC/KVM type based on selected OS - Reinstall OS list filtered to only show matching template types - Admin location OS repeater now has LXC Template checkbox --- .../Admin/Resources/LocationResource.php | 6 ++- .../Servers/Proxmox/Livewire/Reinstall.php | 3 ++ extensions/Servers/Proxmox/Models/OS.php | 13 +++++ extensions/Servers/Proxmox/Proxmox.php | 52 ++++++++++++++----- ...2_add_is_lxc_to_ext_proxmox_oses_table.php | 22 ++++++++ 5 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 extensions/Servers/Proxmox/database/migrations/2026_03_02_000002_add_is_lxc_to_ext_proxmox_oses_table.php diff --git a/extensions/Servers/Proxmox/Admin/Resources/LocationResource.php b/extensions/Servers/Proxmox/Admin/Resources/LocationResource.php index 0d0455646..6f8afdbee 100644 --- a/extensions/Servers/Proxmox/Admin/Resources/LocationResource.php +++ b/extensions/Servers/Proxmox/Admin/Resources/LocationResource.php @@ -75,9 +75,13 @@ class LocationResource extends Resource return $rule->where('location_id', $get('../../id')); }) ->placeholder('Enter the VM ID'), + Forms\Components\Checkbox::make('is_lxc') + ->label('LXC Template') + ->default(false) + ->helperText('Check if this is an LXC container template (not KVM)'), ]) ->cloneable() - ->columns(2) + ->columns(3) ->columnSpanFull(), ]), ]), diff --git a/extensions/Servers/Proxmox/Livewire/Reinstall.php b/extensions/Servers/Proxmox/Livewire/Reinstall.php index 7e8604c38..6745b86ad 100644 --- a/extensions/Servers/Proxmox/Livewire/Reinstall.php +++ b/extensions/Servers/Proxmox/Livewire/Reinstall.php @@ -83,9 +83,12 @@ class Reinstall extends Component // assuming Server has relation `node()` -> Node model $locationId = $this->server->node->location_id; + $isLxc = $this->server->node->is_lxc ?? false; + // Filter OS templates by location AND type (LXC templates for LXC nodes, KVM for KVM) $osList = \Paymenter\Extensions\Servers\Proxmox\Models\Location::findOrFail($locationId) ->os() + ->where('is_lxc', $isLxc) ->get(); return view('proxmox::reinstall', [ diff --git a/extensions/Servers/Proxmox/Models/OS.php b/extensions/Servers/Proxmox/Models/OS.php index ffd1a3b47..f49708d7c 100644 --- a/extensions/Servers/Proxmox/Models/OS.php +++ b/extensions/Servers/Proxmox/Models/OS.php @@ -13,5 +13,18 @@ class OS extends Model 'name', 'vm_id', 'location_id', + 'is_lxc', ]; + + protected $casts = [ + 'is_lxc' => 'boolean', + ]; + + /** + * Get the VM type string for Proxmox API paths. + */ + public function vmType(): string + { + return $this->is_lxc ? 'lxc' : 'qemu'; + } } diff --git a/extensions/Servers/Proxmox/Proxmox.php b/extensions/Servers/Proxmox/Proxmox.php index 40af93d93..658ba011b 100644 --- a/extensions/Servers/Proxmox/Proxmox.php +++ b/extensions/Servers/Proxmox/Proxmox.php @@ -470,10 +470,22 @@ class Proxmox extends ClassServer $server->save(); - $vmType = $server->node->vmType(); + // Use the OS template's type for cloning (source type determines API path) + // The template type should match the node type (LXC templates for LXC nodes, KVM for KVM) + $templateVmType = $server->os->vmType(); + $nodeVmType = $server->node->vmType(); - // Create the VM/CT - $cloneResult = $this->requestAndWait('/nodes/' . $server->node->name . '/' . $vmType . '/' . $server->os->vm_id . '/clone', 'post', [ + // Warn if there's a type mismatch (shouldn't happen if OS selection is filtered correctly) + if ($templateVmType !== $nodeVmType) { + \Log::warning('OS template type does not match node type', [ + 'server_id' => $server->id, + 'os_type' => $templateVmType, + 'node_type' => $nodeVmType, + ]); + } + + // Create the VM/CT - use template's vmType for source path + $cloneResult = $this->requestAndWait('/nodes/' . $server->node->name . '/' . $templateVmType . '/' . $server->os->vm_id . '/clone', 'post', [ 'newid' => $server->vm_id, 'hostname' => $server->hostname, 'target' => $server->node->name, @@ -698,7 +710,7 @@ class Proxmox extends ClassServer ReinstallJob::dispatch($server); } - private function calculateNode(Location $location, Service $service, $settings) + private function calculateNode(Location $location, Service $service, $settings, ?bool $isLxc = null) { // What type of selection do we want to use? /* 'random' => 'Random Node', @@ -708,12 +720,18 @@ class Proxmox extends ClassServer 'least_loaded_disk' => 'Least Loaded Node (based on disk)', 'least_loaded_cpu' => 'Least Loaded Node (based on CPU)', */ - $cpuNeeded = $settings['cores'] * $settings['sockets']; + $cpuNeeded = $settings['cores'] * ($settings['sockets'] ?? 1); $memoryNeeded = (int) $settings['memory']; $diskNeeded = (int) $settings['disk']; + // Build base query for nodes, optionally filtering by type (LXC or KVM) + $nodeQuery = $location->nodes(); + if ($isLxc !== null) { + $nodeQuery->where('is_lxc', $isLxc); + } + if ($location->server_placement == 'random') { - $nodes = $location->nodes()->inRandomOrder()->get(); + $nodes = (clone $nodeQuery)->inRandomOrder()->get(); $node = null; foreach ($nodes as $n) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { @@ -723,7 +741,7 @@ class Proxmox extends ClassServer } } elseif ($location->server_placement == 'fill') { // Get the nodes that have enough resources - $nodes = $location->nodes()->get(); + $nodes = (clone $nodeQuery)->get(); $node = null; foreach ($nodes as $n) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { @@ -733,7 +751,7 @@ class Proxmox extends ClassServer } } elseif ($location->server_placement == 'least_loaded_server') { // Get the node with the least - $nodes = $location->nodes()->withCount('servers')->orderBy('servers_count', 'asc')->get(); + $nodes = (clone $nodeQuery)->withCount('servers')->orderBy('servers_count', 'asc')->get(); $node = null; foreach ($nodes as $n) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { @@ -742,7 +760,7 @@ class Proxmox extends ClassServer } } } elseif ($location->server_placement == 'least_loaded_ram') { - $nodes = $location->nodes()->withCount('servers')->orderBy('ram', 'asc')->get(); + $nodes = (clone $nodeQuery)->withCount('servers')->orderBy('ram', 'asc')->get(); $nodeAndRam = []; foreach ($nodes as $n) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { @@ -756,7 +774,7 @@ class Proxmox extends ClassServer break; } } elseif ($location->server_placement == 'least_loaded_disk') { - $nodes = $location->nodes()->withCount('servers')->orderBy('disk', 'asc')->get(); + $nodes = (clone $nodeQuery)->withCount('servers')->orderBy('disk', 'asc')->get(); $nodeAndDisk = []; foreach ($nodes as $n) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { @@ -770,7 +788,7 @@ class Proxmox extends ClassServer break; } } elseif ($location->server_placement == 'least_loaded_cpu') { - $nodes = $location->nodes()->withCount('servers')->orderBy('cpu', 'asc')->get(); + $nodes = (clone $nodeQuery)->withCount('servers')->orderBy('cpu', 'asc')->get(); $nodeAndCpu = []; foreach ($nodes as $n) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { @@ -967,8 +985,18 @@ class Proxmox extends ClassServer $settings = array_merge($settings, $properties); $location = Location::findOrFail($settings['location_id']); - $node = $this->calculateNode($location, $service, $settings); + + // Get the OS first to determine if it's LXC or KVM $os = $location->os()->findOrFail($settings['os']); + + // Calculate node - pass the OS type so it selects a matching node (LXC or KVM) + $node = $this->calculateNode($location, $service, $settings, $os->is_lxc); + + // Verify OS type matches node type + if ($os->is_lxc !== ($node->is_lxc ?? false)) { + throw new Exception('OS template type (' . ($os->is_lxc ? 'LXC' : 'KVM') . ') does not match available node type (' . ($node->is_lxc ? 'LXC' : 'KVM') . '). Please select a different OS or contact support.'); + } + // Get ippools, then check if a pool has enough IPs $ipv4 = (object) [ 'ips' => collect(), diff --git a/extensions/Servers/Proxmox/database/migrations/2026_03_02_000002_add_is_lxc_to_ext_proxmox_oses_table.php b/extensions/Servers/Proxmox/database/migrations/2026_03_02_000002_add_is_lxc_to_ext_proxmox_oses_table.php new file mode 100644 index 000000000..c9af90cfb --- /dev/null +++ b/extensions/Servers/Proxmox/database/migrations/2026_03_02_000002_add_is_lxc_to_ext_proxmox_oses_table.php @@ -0,0 +1,22 @@ +boolean('is_lxc')->default(false)->after('location_id'); + }); + } + + public function down(): void + { + Schema::table('ext_proxmox_oses', function (Blueprint $table) { + $table->dropColumn('is_lxc'); + }); + } +};