loadBootConfiguration(false); } public function refreshBootConfiguration() { $this->loadBootConfiguration(); $this->notify('Boot devices refreshed successfully.', 'success'); } public function resetOrder() { $this->order = $this->currentOrder; } public function addDevice(string $device): void { if (!isset($this->availableDevices[$device])) { $this->notify('Device is no longer available.', 'error'); return; } if (in_array($device, $this->order)) { return; } $this->order[] = $device; } public function removeDevice(string $device): void { $this->order = array_values(array_filter( $this->order, fn ($item) => $item !== $device )); } public function moveDevice(string $device, string $direction): void { $index = array_search($device, $this->order); if ($index === false) { return; } if ($direction === 'up' && $index > 0) { [$this->order[$index - 1], $this->order[$index]] = [$this->order[$index], $this->order[$index - 1]]; } elseif ($direction === 'down' && $index < count($this->order) - 1) { [$this->order[$index + 1], $this->order[$index]] = [$this->order[$index], $this->order[$index + 1]]; } } public function saveOrder() { try { $proxmox = new Proxmox; // If order is empty, we need to delete the boot parameter or set it to empty if (empty($this->order)) { // Delete the boot order parameter to reset to default $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'delete' => 'boot', ] ); $this->notify('Boot order reset to default. The new sequence will be used on the next power event.', 'success'); } else { $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'boot' => 'order=' . implode(';', $this->order), ] ); $this->notify('Boot order updated. The new sequence will be used on the next power event.', 'success'); } $this->currentOrder = $this->order; } catch (\Exception $e) { $this->notify('Failed to update boot order: ' . $e->getMessage(), 'error'); } } public function createCdDrive() { if (count($this->cdDrives) >= 2) { $this->notify('Maximum of 2 CD/DVD-ROM drives allowed.', 'info'); return; } $proxmox = new Proxmox; $slot = null; $lastError = null; // Try IDE slots first (ide0-ide4, up to 5 attempts) for ($i = 0; $i < 5; $i++) { $ideSlot = 'ide' . $i; // Skip if this slot already exists as a CD-ROM drive if (in_array($ideSlot, $this->cdDrives, true)) { continue; } // Skip if slot exists and is already configured (not empty) if (isset($this->rawConfig[$ideSlot]) && $this->rawConfig[$ideSlot] !== '') { // Check if it's a CD-ROM drive if (is_string($this->rawConfig[$ideSlot]) && str_contains($this->rawConfig[$ideSlot], 'media=cdrom')) { // Already a CD-ROM, skip continue; } // Slot exists but is occupied by another device type, skip continue; } // Try to create the drive on this slot try { $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ $ideSlot => 'none,media=cdrom', ] ); $slot = $ideSlot; break; // Success, exit loop } catch (\Exception $e) { // Slot creation failed, try next slot $lastError = $e->getMessage(); continue; } } // If no IDE slot worked, try SATA slots (sata0-sata5) if (!$slot) { for ($i = 0; $i < 6; $i++) { $sataSlot = 'sata' . $i; // Skip if this slot already exists as a CD-ROM drive if (in_array($sataSlot, $this->cdDrives, true)) { continue; } // Skip if slot exists and is already configured if (isset($this->rawConfig[$sataSlot]) && $this->rawConfig[$sataSlot] !== '') { if (is_string($this->rawConfig[$sataSlot]) && str_contains($this->rawConfig[$sataSlot], 'media=cdrom')) { continue; } continue; } // Try to create the drive on this slot try { $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ $sataSlot => 'none,media=cdrom', ] ); $slot = $sataSlot; break; // Success, exit loop } catch (\Exception $e) { // Slot creation failed, try next slot $lastError = $e->getMessage(); continue; } } } if ($slot) { $this->notify('CD/DVD-ROM drive created on ' . strtoupper($slot) . '.', 'success'); $this->loadBootConfiguration(false); } else { $errorMessage = $lastError ? 'Failed to create CD/DVD-ROM drive: ' . $lastError : 'No available IDE or SATA slots remain for a CD/DVD-ROM drive.'; $this->notify($errorMessage, 'error'); } } public function removeCdDrive(string $drive) { if (!in_array($drive, $this->cdDrives, true)) { $this->notify('Drive not found.', 'error'); return; } try { $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'delete' => $drive, ] ); // Remove the drive from boot order if it exists if (in_array($drive, $this->order, true)) { $this->order = array_values(array_filter( $this->order, fn ($item) => $item !== $drive )); // Also update currentOrder to reflect the change $this->currentOrder = array_values(array_filter( $this->currentOrder, fn ($item) => $item !== $drive )); } $this->notify('CD/DVD-ROM drive removed from ' . strtoupper($drive) . '.', 'success'); $this->loadBootConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to remove CD/DVD-ROM drive: ' . $e->getMessage(), 'error'); } } public function applyIso(?string $drive = null) { $targetDrive = $drive ?? $this->cdDrive; if (!$targetDrive || !in_array($targetDrive, $this->cdDrives, true)) { $this->notify('Create a CD/DVD-ROM drive first.', 'error'); return; } $selectedIso = $drive ? ($this->selectedIsos[$drive] ?? 'none') : ($this->selectedIso ?: 'none'); $iso = $selectedIso ?: 'none'; $value = $iso === 'none' ? 'none,media=cdrom' : $iso . ',media=cdrom'; try { $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ $targetDrive => $value, ] ); $message = $iso === 'none' ? 'ISO detached from CD/DVD-ROM drive on ' . strtoupper($targetDrive) . '.' : ('ISO attached to ' . strtoupper($targetDrive) . ': ' . $this->formatIsoLabel($iso)); $this->notify($message, 'success'); if ($drive) { $this->currentIsos[$drive] = $iso; } else { $this->currentIso = $iso; } $this->loadBootConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to update ISO selection: ' . $e->getMessage(), 'error'); } } public function ejectIso(?string $drive = null): void { $targetDrive = $drive ?? $this->cdDrive; if (!$targetDrive || !in_array($targetDrive, $this->cdDrives, true)) { $this->notify('Create a CD/DVD-ROM drive first.', 'error'); return; } if ($drive) { $this->selectedIsos[$drive] = 'none'; } else { $this->selectedIso = 'none'; } $this->applyIso($targetDrive); } public function getHasChangesProperty(): bool { return $this->order !== $this->currentOrder; } protected function loadBootConfiguration(bool $force = true): void { try { $proxmox = new Proxmox; $config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data']; $this->rawConfig = $config; $this->availableDevices = $this->extractBootableDevices($config); $order = $this->parseBootOrder($config); $order = array_values(array_filter( $order, fn ($device) => !$this->shouldHideDevice($device, $config) )); if ($force || empty($this->order)) { $this->order = $order; } $this->currentOrder = $order; $this->cdDrives = $this->detectCdromDevices($config); // For backward compatibility, set first drive as cdDrive $this->cdDrive = !empty($this->cdDrives) ? $this->cdDrives[0] : null; // Load ISO options if we have any drives if (!empty($this->cdDrives)) { $this->loadIsoOptionsList(); // Initialize selected and current ISOs for each drive $this->selectedIsos = []; $this->currentIsos = []; foreach ($this->cdDrives as $drive) { $iso = $this->extractIsoFromConfig($config[$drive] ?? null); $this->selectedIsos[$drive] = $iso; $this->currentIsos[$drive] = $iso; } // For backward compatibility if ($this->cdDrive) { $this->selectedIso = $this->selectedIsos[$this->cdDrive] ?? 'none'; $this->currentIso = $this->currentIsos[$this->cdDrive] ?? 'none'; } } else { $this->selectedIso = null; $this->currentIso = null; $this->selectedIsos = []; $this->currentIsos = []; $this->isoOptions = []; } } catch (\Exception $e) { $this->availableDevices = []; $this->order = []; $this->currentOrder = []; $this->cdDrive = null; $this->cdDrives = []; $this->isoOptions = []; $this->selectedIso = null; $this->currentIso = null; $this->selectedIsos = []; $this->currentIsos = []; $this->notify('Unable to load boot information: ' . $e->getMessage(), 'error'); } } /** * Extract a normalized list of bootable devices from the VM config. */ protected function extractBootableDevices(array $config): array { $devices = []; $pattern = '/^(ide|sata|scsi|virtio|net|efidisk|usb|nvme|tpmstate|sd)\d+$/'; foreach ($config as $key => $value) { if (!is_string($key)) { continue; } if (preg_match($pattern, $key)) { if ($this->shouldHideDevice($key, $config, $value)) { continue; } $devices[$key] = [ 'label' => $this->formatDeviceLabel($key, $value), 'details' => $this->formatDeviceDetails($value), 'raw' => $value, ]; } } // Ensure currently configured boot devices remain visible even if missing. foreach ($this->parseBootOrder($config) as $device) { if ($this->shouldHideDevice($device, $config)) { continue; } if (!isset($devices[$device])) { $devices[$device] = [ 'label' => strtoupper($device), 'details' => 'Device referenced in boot order but not present in config.', 'raw' => null, ]; } } ksort($devices); return $devices; } protected function formatDeviceLabel(string $device, mixed $value): string { $type = preg_replace('/\d+$/', '', $device); $map = [ 'virtio' => 'VirtIO Disk', 'scsi' => 'SCSI Disk', 'sata' => 'SATA Disk', 'ide' => 'IDE Device', 'net' => 'Network Adapter', 'efidisk' => 'EFI Disk', 'usb' => 'USB Device', 'nvme' => 'NVMe Disk', 'tpmstate' => 'TPM State', 'sd' => 'Secure Digital', ]; $label = ($map[$type] ?? Str::upper($type)) . ' (' . Str::upper($device) . ')'; if (is_string($value) && str_contains($value, 'media=cdrom')) { $label .= ' • CD/DVD'; } return $label; } protected function formatDeviceDetails(mixed $value): ?string { if (!is_string($value) || trim($value) === '') { return null; } $segments = array_map('trim', explode(',', $value)); $path = array_shift($segments); $details = []; foreach ($segments as $segment) { if (str_contains($segment, '=')) { [$key, $val] = array_map('trim', explode('=', $segment, 2)); $details[] = ucfirst($key) . ': ' . $val; } elseif ($segment !== '') { $details[] = $segment; } } $detailString = implode(', ', $details); if ($path && $detailString) { return $path . ' • ' . $detailString; } return $path ?: ($detailString ?: null); } protected function parseBootOrder(array $config): array { $boot = $config['boot'] ?? ''; $order = []; if (is_string($boot) && str_contains($boot, 'order=')) { [, $list] = explode('order=', $boot, 2); $order = array_filter(array_map('trim', explode(';', $list))); } elseif (is_string($boot) && preg_match('/^[cdn]{1,3}$/i', $boot)) { $order = $this->mapLegacyOrder($boot, $config); } if (empty($order) && isset($config['bootdisk'])) { $order[] = $config['bootdisk']; } return array_values(array_filter($order)); } protected function mapLegacyOrder(string $legacy, array $config): array { $legacy = strtolower($legacy); $order = []; $disk = $config['bootdisk'] ?? $this->detectDiskDevice($config); $cdrom = $this->detectCdromDevice($config); $network = $this->detectNetworkDevice($config); foreach (str_split($legacy) as $flag) { if ($flag === 'c' && $disk) { $order[] = $disk; } elseif ($flag === 'd' && $cdrom) { $order[] = $cdrom; } elseif ($flag === 'n' && $network) { $order[] = $network; } } return array_values(array_unique(array_filter($order))); } protected function shouldHideDevice(string $device, array $config, mixed $value = null): bool { $value ??= $config[$device] ?? null; $deviceLower = strtolower($device); if (is_array($value)) { $valueLower = strtolower(json_encode($value)); } elseif (is_string($value)) { $valueLower = strtolower($value); } else { $valueLower = strtolower((string) $value); } if ( str_contains($deviceLower, 'cloudinit') || str_contains($valueLower, 'cloudinit') ) { return true; } if ( str_starts_with($deviceLower, 'tpmstate') || str_contains($valueLower, 'tpm') ) { return true; } if (str_starts_with($deviceLower, 'efidisk')) { return true; } return false; } protected function detectDiskDevice(array $config): ?string { foreach ($config as $key => $value) { if (preg_match('/^(virtio|scsi|sata|ide|nvme|sd)\d+$/', $key)) { if (is_string($value) && str_contains($value, 'media=cdrom')) { continue; } return $key; } } return null; } protected function detectCdromDevice(array $config): ?string { $drives = $this->detectCdromDevices($config); return !empty($drives) ? $drives[0] : null; } protected function detectCdromDevices(array $config): array { $drives = []; foreach ($config as $key => $value) { if (!preg_match('/^(ide|sata)\d+$/', $key) || !is_string($value)) { continue; } if (!str_contains($value, 'media=cdrom')) { continue; } if (str_contains(strtolower($value), 'cloudinit')) { continue; } $drives[] = $key; } // Sort drives to maintain consistent order (ide0, ide1, sata0, sata1, etc.) usort($drives, function($a, $b) { // Extract type and number for comparison preg_match('/^(ide|sata)(\d+)$/', $a, $matchA); preg_match('/^(ide|sata)(\d+)$/', $b, $matchB); // Sort by type first (ide before sata), then by number if ($matchA[1] !== $matchB[1]) { return strcmp($matchA[1], $matchB[1]); } return (int)$matchA[2] <=> (int)$matchB[2]; }); return $drives; } protected function detectNetworkDevice(array $config): ?string { foreach ($config as $key => $value) { if (preg_match('/^net\d+$/', $key)) { return $key; } } return null; } protected function findNextIdeSlot(array $config): ?string { for ($i = 0; $i < 4; $i++) { $slot = 'ide' . $i; if (!array_key_exists($slot, $config)) { return $slot; } } return null; } protected function findNextCdromSlot(array $config): ?string { // First, try to find an available IDE slot (ide0-ide3) for ($i = 0; $i < 4; $i++) { $slot = 'ide' . $i; if (!array_key_exists($slot, $config)) { return $slot; } } // If all IDE slots are taken, try SATA slots (sata0-sata5) for ($i = 0; $i < 6; $i++) { $slot = 'sata' . $i; if (!array_key_exists($slot, $config)) { return $slot; } } return null; } protected function loadIsoOptionsList(): void { try { $proxmox = new Proxmox; $response = $proxmox->request( '/nodes/' . $this->server->node->name . '/storage/cephfs/content', data: [ 'content' => 'iso', ] ); $items = $response->json()['data'] ?? []; $options = array_map(function ($item) { $volid = $item['volid'] ?? ''; return [ 'value' => $volid, 'label' => $this->formatIsoLabel($volid), ]; }, $items); usort($options, fn ($a, $b) => strcmp($a['label'], $b['label'])); array_unshift($options, [ 'value' => 'none', 'label' => 'None', ]); $this->isoOptions = $options; } catch (\Exception $e) { $this->isoOptions = [ [ 'value' => 'none', 'label' => 'None', ], ]; $this->notify('Unable to fetch ISO images from cephfs: ' . $e->getMessage(), 'error'); } } protected function formatIsoLabel(?string $volid): string { if (!$volid || $volid === 'none') { return 'None'; } if (str_contains($volid, ':')) { [, $path] = explode(':', $volid, 2); $path = ltrim($path, '/'); } else { $path = $volid; } return Str::of($path)->after('iso/')->value(); } protected function extractIsoFromConfig(?string $value): string { if (!$value) { return 'none'; } $segments = array_map('trim', explode(',', $value)); $path = array_shift($segments); if (!$path || strtolower($path) === 'none') { return 'none'; } return $path; } public function render() { return view('proxmox::boot-order')->layoutData([ 'sidebar' => true, ]); } }