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
This commit is contained in:
2026-03-02 21:23:10 -05:00
parent 153bcfcea5
commit ef1499cde4
5 changed files with 83 additions and 13 deletions

View File

@@ -75,9 +75,13 @@ class LocationResource extends Resource
return $rule->where('location_id', $get('../../id')); return $rule->where('location_id', $get('../../id'));
}) })
->placeholder('Enter the VM 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() ->cloneable()
->columns(2) ->columns(3)
->columnSpanFull(), ->columnSpanFull(),
]), ]),
]), ]),

View File

@@ -83,9 +83,12 @@ class Reinstall extends Component
// assuming Server has relation `node()` -> Node model // assuming Server has relation `node()` -> Node model
$locationId = $this->server->node->location_id; $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) $osList = \Paymenter\Extensions\Servers\Proxmox\Models\Location::findOrFail($locationId)
->os() ->os()
->where('is_lxc', $isLxc)
->get(); ->get();
return view('proxmox::reinstall', [ return view('proxmox::reinstall', [

View File

@@ -13,5 +13,18 @@ class OS extends Model
'name', 'name',
'vm_id', 'vm_id',
'location_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';
}
} }

View File

@@ -470,10 +470,22 @@ class Proxmox extends ClassServer
$server->save(); $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 // Warn if there's a type mismatch (shouldn't happen if OS selection is filtered correctly)
$cloneResult = $this->requestAndWait('/nodes/' . $server->node->name . '/' . $vmType . '/' . $server->os->vm_id . '/clone', 'post', [ 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, 'newid' => $server->vm_id,
'hostname' => $server->hostname, 'hostname' => $server->hostname,
'target' => $server->node->name, 'target' => $server->node->name,
@@ -698,7 +710,7 @@ class Proxmox extends ClassServer
ReinstallJob::dispatch($server); 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? // What type of selection do we want to use?
/* 'random' => 'Random Node', /* 'random' => 'Random Node',
@@ -708,12 +720,18 @@ class Proxmox extends ClassServer
'least_loaded_disk' => 'Least Loaded Node (based on disk)', 'least_loaded_disk' => 'Least Loaded Node (based on disk)',
'least_loaded_cpu' => 'Least Loaded Node (based on CPU)', 'least_loaded_cpu' => 'Least Loaded Node (based on CPU)',
*/ */
$cpuNeeded = $settings['cores'] * $settings['sockets']; $cpuNeeded = $settings['cores'] * ($settings['sockets'] ?? 1);
$memoryNeeded = (int) $settings['memory']; $memoryNeeded = (int) $settings['memory'];
$diskNeeded = (int) $settings['disk']; $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') { if ($location->server_placement == 'random') {
$nodes = $location->nodes()->inRandomOrder()->get(); $nodes = (clone $nodeQuery)->inRandomOrder()->get();
$node = null; $node = null;
foreach ($nodes as $n) { foreach ($nodes as $n) {
if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) {
@@ -723,7 +741,7 @@ class Proxmox extends ClassServer
} }
} elseif ($location->server_placement == 'fill') { } elseif ($location->server_placement == 'fill') {
// Get the nodes that have enough resources // Get the nodes that have enough resources
$nodes = $location->nodes()->get(); $nodes = (clone $nodeQuery)->get();
$node = null; $node = null;
foreach ($nodes as $n) { foreach ($nodes as $n) {
if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) {
@@ -733,7 +751,7 @@ class Proxmox extends ClassServer
} }
} elseif ($location->server_placement == 'least_loaded_server') { } elseif ($location->server_placement == 'least_loaded_server') {
// Get the node with the least // 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; $node = null;
foreach ($nodes as $n) { foreach ($nodes as $n) {
if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) {
@@ -742,7 +760,7 @@ class Proxmox extends ClassServer
} }
} }
} elseif ($location->server_placement == 'least_loaded_ram') { } 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 = []; $nodeAndRam = [];
foreach ($nodes as $n) { foreach ($nodes as $n) {
if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) {
@@ -756,7 +774,7 @@ class Proxmox extends ClassServer
break; break;
} }
} elseif ($location->server_placement == 'least_loaded_disk') { } 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 = []; $nodeAndDisk = [];
foreach ($nodes as $n) { foreach ($nodes as $n) {
if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) {
@@ -770,7 +788,7 @@ class Proxmox extends ClassServer
break; break;
} }
} elseif ($location->server_placement == 'least_loaded_cpu') { } 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 = []; $nodeAndCpu = [];
foreach ($nodes as $n) { foreach ($nodes as $n) {
if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) { if ($n->canStoreServer($memoryNeeded, $diskNeeded, $cpuNeeded)) {
@@ -967,8 +985,18 @@ class Proxmox extends ClassServer
$settings = array_merge($settings, $properties); $settings = array_merge($settings, $properties);
$location = Location::findOrFail($settings['location_id']); $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']); $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 // Get ippools, then check if a pool has enough IPs
$ipv4 = (object) [ $ipv4 = (object) [
'ips' => collect(), 'ips' => collect(),

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('ext_proxmox_oses', function (Blueprint $table) {
$table->boolean('is_lxc')->default(false)->after('location_id');
});
}
public function down(): void
{
Schema::table('ext_proxmox_oses', function (Blueprint $table) {
$table->dropColumn('is_lxc');
});
}
};