Files
Virtnet.bond/extensions/Servers/Proxmox/views/cloud-init.blade.php

891 lines
69 KiB
PHP

<x-proxmox::layout :server="$server" view="cloud-init" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
<style>
.formRight select {
background-color: hsl(var(--color-background));
border: 1px solid hsl(var(--color-neutral));
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
outline: none;
}
.formRight select:focus {
outline: none;
box-shadow: 0 0 0 2px hsl(var(--color-secondary));
}
.formRight select.w-full {
width: 100%;
}
</style>
<div class="space-y-6">
<div class="bg-info/10 border border-info/30 rounded-lg p-4"
x-data="{
isCollapsed: localStorage.getItem('cloud-init-notice-collapsed') === 'true',
toggle() {
this.isCollapsed = !this.isCollapsed;
localStorage.setItem('cloud-init-notice-collapsed', this.isCollapsed);
}
}">
<div class="flex items-start gap-3">
<x-ri-information-line class="size-5 text-info flex-shrink-0 mt-0.5" />
<div class="flex-1 space-y-2">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-base">Cloud-init Configuration Notice</h3>
<button @click="toggle()" type="button" class="text-base/60 hover:text-base transition-colors">
<x-ri-arrow-down-s-line x-bind:class="isCollapsed ? 'size-4 transition-transform rotate-180' : 'size-4 transition-transform'" />
</button>
</div>
<div x-show="!isCollapsed" x-transition>
<p class="text-xs text-base/70 leading-relaxed">
Cloud-init controls the metadata injected into your VM on first boot and whenever you regenerate the cloud-init drive.
Use <strong>Apply</strong> to save and regenerate the ISO, <strong>Refresh</strong> to pull fresh values, and <strong>Reset</strong> to discard local edits.
Manual installs (ISO workflows, Windows, or images without cloud-init) still require in-guest configuration.
</p>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2 min-w-0">
<h2 class="text-xl font-semibold break-words">Cloud-init Settings</h2>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<div x-show="open" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="open = false"></div>
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6"
x-on:keydown.escape.window="open = false">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-cloud-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
Cloud-init Settings
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Configure cloud-init metadata that is injected into your virtual machine. These settings control user accounts, SSH keys, hostname, networking, and other first-boot configuration.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Apply:</span> Saves your changes and regenerates the cloud-init ISO drive. Changes take effect on the next boot.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Refresh:</span> Reloads the current cloud-init configuration from the server, discarding any unsaved local changes.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Note:</span> These values map directly to QEMU configuration parameters (`ciuser`, `nameserver`, `ipconfigX`, etc.). Cloud-init only works with supported operating systems and images.
</p>
</div>
</div>
</div>
</div>
</div>
<p class="text-sm text-base/60">
These values map directly to the QEMU configuration (`ciuser`, `nameserver`, `ipconfigX`, etc.). Changes take effect on the next cloud-init run or boot.
</p>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<div class="bg-background-secondary border border-neutral rounded-lg p-4 space-y-4"
x-data="{ showNetworkModal: @entangle('showNetworkModal') }">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2 min-w-0">
<h3 class="text-lg font-semibold break-words">Access &amp; Credentials</h3>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<div x-show="open" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="open = false"></div>
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6"
x-on:keydown.escape.window="open = false">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-user-settings-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
Access & Credentials
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Configure the default user account and SSH access for your virtual machine. These settings are used by cloud-init to create accounts and configure SSH keys on first boot.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Username:</span> The default user account created by cloud-init. This user typically has sudo privileges.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Password:</span> The password for the default user. Leave empty to disable password authentication (SSH keys only).
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">SSH Keys:</span> Public SSH keys that will be authorized for the default user. One key per line.
</p>
</div>
</div>
</div>
</div>
</div>
<p class="text-xs text-base/60 mt-1">Defaults used by cloud-init to create accounts and authorize SSH.</p>
</div>
@if($this->accessCredentialsSynced)
<span class="text-xs font-semibold text-success">Synced</span>
@else
<span class="text-xs font-semibold text-warning">Unsynced</span>
@endif
</div>
<div class="space-y-3">
<div class="space-y-2" x-data="{ showCiUserModal: false }">
<label class="text-sm font-semibold text-base/80">Cloud-init User</label>
<div class="space-y-2">
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80">
<span class="truncate flex-1">
{{ $ciUser !== '' ? $ciUser : 'Template default (uses image-defined user)' }}
</span>
<button type="button"
class="text-base/70 hover:text-base/60 ml-2"
@click="showCiUserModal = true"
aria-label="Edit Cloud-init user">
<x-ri-edit-fill class="inline size-4" />
</button>
</div>
</div>
<p class="text-xs text-base/60">Use distro-friendly names. Leave blank to keep the template default.</p>
<div x-cloak x-show="showCiUserModal" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="showCiUserModal = false"></div>
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4"
x-on:keydown.escape.window="showCiUserModal = false">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Edit Cloud-init User</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showCiUserModal = false"
aria-label="Close Cloud-init user modal">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="space-y-2">
<label class="text-xs font-semibold text-base/70">Username</label>
<input type="text"
wire:model.live="ciUser"
class="form-input w-full"
placeholder="e.g. ubuntu" />
<p class="text-xs text-base/60">Leave blank to retain the template default user.</p>
</div>
<div class="flex justify-end gap-2">
<x-button.secondary type="button" @click="showCiUserModal = false">
Cancel
</x-button.secondary>
<x-button.primary type="button" @click="showCiUserModal = false">
Done
</x-button.primary>
</div>
</div>
</div>
</div>
<div class="space-y-2" x-data="{ showPasswordViewModal: false, showPasswordEditModal: @entangle('isEditingPassword') }">
<label class="text-sm font-semibold text-base/80">Root Password</label>
<div class="space-y-2">
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80">
<span class="truncate flex-1">
@if(isset($server->password) && $server->password)
••••••••••••
@else
Password is sent via email on install.
@endif
</span>
@if(isset($server->password) && $server->password)
<button type="button"
class="text-base/70 hover:text-base/60 ml-2"
@click="showPasswordViewModal = true"
aria-label="View Root Password">
<x-ri-eye-line class="inline size-4" />
</button>
@endif
<button type="button"
class="text-base/70 hover:text-base/60 ml-2"
wire:click="startEditingPassword"
aria-label="Edit Root Password">
<x-ri-edit-fill class="inline size-4" />
</button>
</div>
</div>
<p class="text-xs text-base/60">Matches the Overview tab control. Use Save for database only, Save &amp; Apply to push via cloud-init.</p>
@if(isset($server->password) && $server->password)
<div x-cloak x-show="showPasswordViewModal" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="showPasswordViewModal = false"></div>
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4"
x-on:keydown.escape.window="showPasswordViewModal = false"
x-data="{
copied: false,
copyText: {{ json_encode($server->password) }},
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 2000);
}
}">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Root Password</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showPasswordViewModal = false"
aria-label="Close password view modal">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="bg-background border border-neutral rounded-md p-3 text-sm text-base/80 overflow-auto">
<code class="whitespace-pre-wrap break-all block">{{ $server->password }}</code>
</div>
<div class="flex justify-end">
<button type="button"
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-primary text-white text-xs font-semibold hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
@click="copyToClipboard()">
<span x-show="!copied">Copy</span>
<span x-show="copied">Copied!</span>
</button>
</div>
</div>
</div>
@endif
<div x-cloak x-show="showPasswordEditModal" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="showPasswordEditModal = false; $wire.cancelEditingPassword()"></div>
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4"
x-on:keydown.escape.window="showPasswordEditModal = false; $wire.cancelEditingPassword()">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Edit Root Password</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showPasswordEditModal = false; $wire.cancelEditingPassword()"
aria-label="Close password edit modal">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-base/70">New Password</label>
<input type="password"
wire:model.defer="password"
class="form-input w-full mt-1"
placeholder="Enter new password" />
</div>
<div>
<label class="text-xs font-semibold text-base/70">Confirm Password</label>
<input type="password"
wire:model.defer="password_confirmation"
class="form-input w-full mt-1"
placeholder="Confirm password" />
</div>
@if($passwordError)
<div class="text-sm text-error">{{ $passwordError }}</div>
@endif
<p class="text-xs text-base/60">
Save writes the password to the database. Save &amp; Apply also pushes it to the hypervisor; reboot for cloud-init to configure the guest OS.
</p>
</div>
<div class="flex justify-end gap-2">
<x-button.secondary type="button" @click="showPasswordEditModal = false; $wire.cancelEditingPassword()">
Cancel
</x-button.secondary>
<x-button.primary type="button" wire:click="updatePassword">
Save
</x-button.primary>
<x-button.primary type="button" wire:click="applyPassword">
Save &amp; Apply
</x-button.primary>
</div>
</div>
</div>
</div>
<div class="space-y-2" x-data="{ showAddKeyModal: false }">
<label class="text-sm font-semibold text-base/80">SSH Public Keys</label>
<div class="space-y-2">
@forelse($sshKeys as $keyIndex => $key)
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-xs text-base/70 relative"
x-data="{
showKeyModal: false,
copied: false,
keyValue: @js($key),
copyKey() {
navigator.clipboard.writeText(this.keyValue);
this.copied = true;
setTimeout(() => this.copied = false, 2000);
}
}">
<span class="truncate flex-1" title="{{ $key }}">{{ $key }}</span>
<button type="button"
class="text-base/70 hover:text-base/60 ml-2"
@click="showKeyModal = true"
aria-label="View SSH key">
<x-ri-eye-line class="inline size-4" />
</button>
<button type="button" wire:click="removeSshKey({{ $keyIndex }})"
class="text-error hover:text-error/80 ml-2"
aria-label="Delete SSH key">
<x-ri-close-line class="inline size-4" />
</button>
<div x-cloak x-show="showKeyModal" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="showKeyModal = false"></div>
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4"
x-on:keydown.escape.window="showKeyModal = false">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">SSH Public Key</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showKeyModal = false"
aria-label="Close SSH key modal">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="bg-background border border-neutral rounded-md p-3 text-xs text-base/80 overflow-auto max-h-60">
<code class="whitespace-pre-wrap break-all block">{{ $key }}</code>
</div>
<div class="flex justify-end">
<button type="button"
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-primary text-white text-xs font-semibold hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
@click="copyKey()">
<span x-show="!copied">Copy</span>
<span x-show="copied">Copied!</span>
</button>
</div>
</div>
</div>
</div>
@empty
<p class="text-xs text-base/50">No user-provided keys. The default management key remains in place.</p>
@endforelse
</div>
<div class="flex flex-col gap-2">
<x-button.secondary type="button" @click="showAddKeyModal = true">
Add Key
</x-button.secondary>
</div>
<p class="text-xs text-base/60">Add as many keys as needed. The platform management key is always kept but never shown.</p>
<div x-cloak x-show="showAddKeyModal" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="showAddKeyModal = false"></div>
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4"
x-on:keydown.escape.window="showAddKeyModal = false">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Add SSH Public Key</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showAddKeyModal = false"
aria-label="Close add SSH key modal">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="space-y-2">
<label class="text-xs font-semibold text-base/70">SSH Key</label>
<textarea wire:model.defer="newSshKey"
class="form-input w-full h-28 resize-y"
placeholder="ssh-ed25519 AAAA... user@host"></textarea>
</div>
<div class="flex justify-end gap-2">
<x-button.secondary type="button" @click="showAddKeyModal = false">
Cancel
</x-button.secondary>
<x-button.primary type="button"
wire:click="addSshKey"
@click="showAddKeyModal = false">
Add Key
</x-button.primary>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<input id="ci-upgrade" type="checkbox" wire:model.live="upgradePackages" class="form-checkbox" />
<label for="ci-upgrade" class="text-sm font-semibold text-base/80">Upgrade packages on first boot</label>
</div>
<p class="text-xs text-base/60">
Runs <code>apt/yum update &amp;&amp; upgrade</code> when cloud-init executes. Enable only if your template supports unattended upgrades.
</p>
</div>
</div>
<div class="bg-background-secondary border border-neutral rounded-lg p-4 space-y-4"
x-data="{ showNetworkModal: @entangle('showNetworkModal') }">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2 min-w-0">
<h3 class="text-lg font-semibold break-words">Network Configuration</h3>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<div x-show="open" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="open = false"></div>
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6"
x-on:keydown.escape.window="open = false">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-router-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
Network Configuration
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Configure network interfaces for your virtual machine. Each network card corresponds to an <code>ipconfigX</code> parameter in the QEMU configuration, allowing you to set IPv4 and IPv6 addresses independently.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">IP Configuration:</span> Each network interface can have separate IPv4 and IPv6 configurations. Use CIDR notation (e.g., 192.168.1.100/24) for IP addresses.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Gateway:</span> The default gateway for each IP version. Leave empty to use the default gateway from the network configuration.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Note:</span> Changes take effect on the next cloud-init run or VM boot. Multiple network interfaces can be configured, each with its own IP configuration.
</p>
</div>
</div>
</div>
</div>
</div>
<p class="text-xs text-base/60 mt-1">Each card corresponds to an <code>ipconfigX</code>. Configure IPv4 and IPv6 independently.</p>
</div>
<span class="text-xs text-base/60">{{ count($networks) }} interfaces</span>
</div>
<div class="grid grid-cols-1 gap-4">
@foreach($networks as $index => $network)
<div class="flex flex-col gap-3 border border-neutral rounded-lg bg-background px-3 py-3">
<div class="flex items-center justify-between">
<div>
<span class="text-sm font-semibold text-base/80 uppercase">{{ $network['key'] }}</span>
<p class="text-xs text-base/60">
Network Device: NET{{ str_replace('ipconfig', '', $network['key']) }}
</p>
</div>
<button wire:click="startEditingNetwork({{ $index }})" type="button"
class="text-base/70 hover:text-base/60 ml-2"
@click="showNetworkModal = true">
<x-ri-edit-fill class="inline size-4" />
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="text-xs font-semibold text-base/70">IPv4 Address (CIDR)</label>
<div class="text-sm text-base/80 mt-1">
{{ !empty($network['ip']) ? $network['ip'] : '—' }}
</div>
</div>
<div>
<label class="text-xs font-semibold text-base/70">IPv4 Gateway</label>
<div class="text-sm text-base/80 mt-1">
{{ !empty($network['gw']) ? $network['gw'] : '—' }}
</div>
</div>
<div>
<label class="text-xs font-semibold text-base/70">IPv6 Address (CIDR)</label>
<div class="text-sm text-base/80 mt-1">
{{ !empty($network['ip6']) ? $network['ip6'] : '—' }}
</div>
</div>
<div>
<label class="text-xs font-semibold text-base/70">IPv6 Gateway</label>
<div class="text-sm text-base/80 mt-1">
{{ !empty($network['gw6']) ? $network['gw6'] : '—' }}
</div>
</div>
</div>
@if(($network['dhcp'] ?? false))
<div class="text-xs text-base/60">
<span class="font-semibold">DHCP:</span> Enabled
</div>
@endif
<p class="text-xs text-base/60">
Leave fields blank to remove static addressing. If both IPv4 fields are empty, cloud-init omits the IPv4 stanza.
</p>
</div>
@endforeach
</div>
<div x-cloak x-show="showNetworkModal" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="showNetworkModal = false; $wire.cancelEditingNetwork()"></div>
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-2xl mx-4 p-5 space-y-4"
x-on:keydown.escape.window="showNetworkModal = false; $wire.cancelEditingNetwork()">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Edit Network Configuration</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
wire:click="cancelEditingNetwork"
@click="showNetworkModal = false"
aria-label="Close network edit modal">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="text-xs font-semibold text-base/70">IPv4 Address (CIDR)</label>
<input type="text" wire:model="editingNetwork.ip" class="form-input w-full mt-1"
placeholder="45.154.99.28/24" />
</div>
<div>
<label class="text-xs font-semibold text-base/70">IPv4 Gateway</label>
<input type="text" wire:model="editingNetwork.gw" class="form-input w-full mt-1"
placeholder="45.154.99.17" />
</div>
<div>
<label class="text-xs font-semibold text-base/70">IPv6 Address (CIDR)</label>
<input type="text" wire:model="editingNetwork.ip6" class="form-input w-full mt-1"
placeholder="2a0b:4e07::2:10/64" />
</div>
<div>
<label class="text-xs font-semibold text-base/70">IPv6 Gateway</label>
<input type="text" wire:model="editingNetwork.gw6" class="form-input w-full mt-1"
placeholder="2a0b:4e07::1" />
</div>
</div>
<p class="text-xs text-base/60">
Leave fields blank to remove static addressing. If both IPv4 fields are empty, cloud-init omits the IPv4 stanza.
</p>
<div class="flex justify-end gap-2">
<x-button.secondary wire:click="cancelEditingNetwork" @click="showNetworkModal = false">
Cancel
</x-button.secondary>
<x-button.primary wire:click="saveNetwork" @click="showNetworkModal = false">
Save
</x-button.primary>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<div class="bg-background-secondary border border-neutral rounded-lg p-4 space-y-4">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2 min-w-0">
<h3 class="text-lg font-semibold break-words">DNS Settings</h3>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<div x-show="open" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="open = false"></div>
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6"
x-on:keydown.escape.window="open = false">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-global-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
DNS Settings
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Configure DNS (Domain Name System) settings for your virtual machine. These settings control how the guest OS resolves domain names to IP addresses.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Search Domain:</span> The default domain suffix appended to hostnames that don't contain a domain. For example, with "example.com" as search domain, "server" resolves to "server.example.com".
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Name Servers:</span> DNS servers used for name resolution. Multiple servers can be specified, and they will be tried in order. Common options include Google DNS (8.8.8.8, 8.8.4.4) or Cloudflare DNS (1.1.1.1, 1.0.0.1).
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Note:</span> These values map to the <code>searchdomain</code> and <code>nameserver</code> parameters in the QEMU cloud-init configuration. Changes take effect on the next cloud-init run.
</p>
</div>
</div>
</div>
</div>
</div>
<p class="text-xs text-base/60 mt-1">Configures <code>searchdomain</code> and <code>nameserver</code> values.</p>
</div>
@if($this->dnsSettingsSynced)
<span class="text-xs font-semibold text-success">Synced</span>
@else
<span class="text-xs font-semibold text-warning">Unsynced</span>
@endif
</div>
<div class="space-y-3">
<div class="space-y-2" x-data="{ showDnsDomainModal: false }">
<label class="text-sm font-semibold text-base/80">DNS Domain (searchdomain)</label>
<div class="space-y-2">
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80">
<span class="truncate flex-1">
{{ $dnsDomain !== '' ? $dnsDomain : 'Not configured (system default will be used)' }}
</span>
<button type="button"
class="text-base/70 hover:text-base/60 ml-2"
@click="showDnsDomainModal = true"
aria-label="Edit DNS Domain">
<x-ri-edit-fill class="inline size-4" />
</button>
</div>
</div>
<p class="text-xs text-base/60">Appended to short hostnames for resolution.</p>
<div x-cloak x-show="showDnsDomainModal" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="showDnsDomainModal = false"></div>
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4"
x-on:keydown.escape.window="showDnsDomainModal = false">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Edit DNS Domain</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showDnsDomainModal = false"
aria-label="Close DNS Domain modal">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="space-y-2">
<label class="text-xs font-semibold text-base/70">DNS Domain (searchdomain)</label>
<input type="text"
wire:model.live="dnsDomain"
class="form-input w-full"
placeholder="example.internal" />
<p class="text-xs text-base/60">Leave blank to use system default. Appended to short hostnames for resolution.</p>
</div>
<div class="flex justify-end gap-2">
<x-button.secondary type="button" @click="showDnsDomainModal = false">
Cancel
</x-button.secondary>
<x-button.primary type="button" @click="showDnsDomainModal = false">
Done
</x-button.primary>
</div>
</div>
</div>
</div>
<div class="space-y-2" x-data="{ showAddDnsModal: false }">
<label class="text-sm font-semibold text-base/80">DNS Nameservers</label>
<div class="space-y-2">
@forelse($dnsServers as $dnsIndex => $resolver)
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-xs text-base/70">
<span class="truncate flex-1" title="{{ $resolver }}">{{ $resolver }}</span>
<button type="button" wire:click="removeDnsServer({{ $dnsIndex }})"
class="text-error hover:text-error/80 ml-2">
<x-ri-close-line class="inline size-4" />
</button>
</div>
@empty
<p class="text-xs text-base/50">No resolvers configured. System defaults will apply.</p>
@endforelse
</div>
<div class="flex flex-col gap-2">
<x-button.secondary type="button" @click="showAddDnsModal = true">
Add Nameserver
</x-button.secondary>
</div>
<p class="text-xs text-base/60 mt-1">Entries are applied in order. Add one IPv4/IPv6 resolver per line.</p>
<div x-cloak x-show="showAddDnsModal" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="showAddDnsModal = false"></div>
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4"
x-on:keydown.escape.window="showAddDnsModal = false">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Add DNS Nameserver</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showAddDnsModal = false"
aria-label="Close add DNS nameserver modal">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="space-y-3">
<div class="space-y-2">
<label class="text-xs font-semibold text-base/70">Quick Add</label>
<div class="grid grid-cols-2 gap-2">
<button type="button"
wire:click="quickAddDnsServers('cloudflare')"
@click="showAddDnsModal = false"
class="flex items-center gap-2 px-3 py-2 bg-background border border-neutral rounded-md hover:bg-background/80 text-sm text-base/70 transition-colors">
<svg viewBox="0 -70 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" fill="#000000" class="size-4">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g>
<g transform="translate(0.000000, -1.000000)">
<path d="M202.3569,50.394 L197.0459,48.27 C172.0849,104.434 72.7859,70.289 66.8109,86.997 C65.8149,98.283 121.0379,89.143 160.5169,91.056 C172.5559,91.639 178.5929,100.727 173.4809,115.54 L183.5499,115.571 C195.1649,79.362 232.2329,97.841 233.7819,85.891 C231.2369,78.034 191.1809,85.891 202.3569,50.394 Z" fill="#FFFFFF"></path>
<path d="M176.332,109.3483 C177.925,104.0373 177.394,98.7263 174.739,95.5393 C172.083,92.3523 168.365,90.2283 163.585,89.6973 L71.17,88.6343 C70.639,88.6343 70.108,88.1033 69.577,88.1033 C69.046,87.5723 69.046,87.0413 69.577,86.5103 C70.108,85.4483 70.639,84.9163 71.701,84.9163 L164.647,83.8543 C175.801,83.3233 187.486,74.2943 191.734,63.6723 L197.046,49.8633 C197.046,49.3313 197.577,48.8003 197.046,48.2693 C191.203,21.1823 166.772,0.9993 138.091,0.9993 C111.535,0.9993 88.697,17.9953 80.73,41.8963 C75.419,38.1783 69.046,36.0533 61.61,36.5853 C48.863,37.6473 38.772,48.2693 37.178,61.0163 C36.647,64.2033 37.178,67.3903 37.71,70.5763 C16.996,71.1073 0,88.1033 0,109.3483 C0,111.4723 0,113.0663 0.531,115.1903 C0.531,116.2533 1.593,116.7843 2.125,116.7843 L172.614,116.7843 C173.676,116.7843 174.739,116.2533 174.739,115.1903 L176.332,109.3483 Z" fill="#F4811F"></path>
<path d="M205.5436,49.8628 L202.8876,49.8628 C202.3566,49.8628 201.8256,50.3938 201.2946,50.9248 L197.5766,63.6718 C195.9836,68.9828 196.5146,74.2948 199.1706,77.4808 C201.8256,80.6678 205.5436,82.7918 210.3236,83.3238 L229.9756,84.3858 C230.5066,84.3858 231.0376,84.9168 231.5686,84.9168 C232.0996,85.4478 232.0996,85.9788 231.5686,86.5098 C231.0376,87.5728 230.5066,88.1038 229.4436,88.1038 L209.2616,89.1658 C198.1076,89.6968 186.4236,98.7258 182.1746,109.3478 L181.1116,114.1288 C180.5806,114.6598 181.1116,115.7218 182.1746,115.7218 L252.2826,115.7218 C253.3446,115.7218 253.8756,115.1908 253.8756,114.1288 C254.9376,109.8798 255.9996,105.0998 255.9996,100.3188 C255.9996,72.7008 233.1616,49.8628 205.5436,49.8628" fill="#FAAD3F"></path>
</g>
</g>
</g>
</svg>
<span>Cloudflare</span>
</button>
<button type="button"
wire:click="quickAddDnsServers('google')"
@click="showAddDnsModal = false"
class="flex items-center gap-2 px-3 py-2 bg-background border border-neutral rounded-md hover:bg-background/80 text-sm text-base/70 transition-colors">
<svg viewBox="-3 0 262 262" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" fill="currentColor" class="size-4">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" fill="#4285F4"></path>
<path d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" fill="#34A853"></path>
<path d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" fill="#FBBC05"></path>
<path d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" fill="#EA4335"></path>
</g>
</svg>
<span>Google</span>
</button>
<button type="button"
wire:click="quickAddDnsServers('quad9')"
@click="showAddDnsModal = false"
class="flex items-center gap-2 px-3 py-2 bg-background border border-neutral rounded-md hover:bg-background/80 text-sm text-base/70 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" class="size-4">
<path d="M18.235 63.971h14.953l16.888-25.26c3.897-5.83 6.091-11.2 6.091-17.06C56.167 9.238 45.746 0 32.034 0S7.9 9.295 7.9 21.882c0 12.586 9.209 21.333 22.228 21.333.953 0 1.934-.058 2.887-.173L18.205 64Zm13.799-31.639c-6.467 0-11.23-4.36-11.23-10.537s4.763-10.537 11.23-10.537c6.466 0 11.23 4.302 11.23 10.537s-4.822 10.537-11.23 10.537" style="fill:#dc205e;stroke-width:2.88678"/>
<path d="M0 0h150v150H0Z" style="fill:none"/>
</svg>
<span>Quad9</span>
</button>
<button type="button"
wire:click="quickAddDnsServers('nat64')"
@click="showAddDnsModal = false"
class="flex items-center gap-2 px-3 py-2 bg-background border border-neutral rounded-md hover:bg-background/80 text-sm text-base/70 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" height="512" width="512" viewBox="0 0 512 512" class="size-4">
<circle style="fill:#FFDA44;" cx="256" cy="256" r="256"></circle>
<path style="fill:#338AF3;" d="M0,256C0,114.616,114.616,0,256,0s256,114.616,256,256"></path>
</svg>
<span>NAT64</span>
</button>
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-semibold text-base/70">Nameserver</label>
<input type="text"
wire:model.defer="newDnsServer"
class="form-input w-full"
placeholder="1.1.1.1 or 2606:4700:4700::1111" />
</div>
</div>
<div class="flex justify-end gap-2">
<x-button.secondary type="button" @click="showAddDnsModal = false">
Cancel
</x-button.secondary>
<x-button.primary type="button"
wire:click="addDnsServer"
@click="showAddDnsModal = false">
Add Nameserver
</x-button.primary>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-background-secondary border border-neutral rounded-lg p-4 space-y-4">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2 min-w-0">
<h3 class="text-lg font-semibold break-words">OS Type</h3>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<div x-show="open" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="open = false"></div>
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6"
x-on:keydown.escape.window="open = false">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-computer-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
OS Type
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Specifies the operating system type for cloud-init metadata generation. This setting helps cloud-init apply the correct configuration format and optimizations for your guest OS.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Linux:</span> Standard Linux distributions (Ubuntu, Debian, CentOS, etc.). Uses standard cloud-init configuration format.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Windows:</span> Windows operating systems. Note that Windows does not natively support cloud-init, so this setting may have limited effect.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Note:</span> This value maps to the <code>ostype</code> parameter in the QEMU configuration. It primarily affects how cloud-init metadata is formatted and which optimizations are applied.
</p>
</div>
</div>
</div>
</div>
</div>
<p class="text-xs text-base/60 mt-1">Controls the <code>ostype</code> option used when generating cloud-init media.</p>
</div>
@if($this->osTypeSynced)
<span class="text-xs font-semibold text-success">Synced</span>
@else
<span class="text-xs font-semibold text-warning">Unsynced</span>
@endif
</div>
<div>
<label class="text-sm font-semibold text-base/80">Operating System Type</label>
<div class="formRight mt-1">
<select wire:model.live="osType" class="w-full">
<option value="">Auto-detect (default)</option>
@foreach($osTypeOptions as $option)
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
</div>
<p class="text-xs text-base/60 mt-1">
Determines whether the guest receives nocloud or config-drive metadata. Choose the entry that matches your template.
</p>
<div class="bg-warning/10 border border-warning/30 rounded-lg p-3 text-xs text-base/70 leading-relaxed mt-2">
<strong>Tip:</strong> Pick a Windows option only for cloud-ready Windows images (cloudbase-init). Using a Windows entry on Linux templates prevents the nocloud ISO from mounting.
</div>
</div>
</div>
</div>
<div class="flex items-center gap-2 overflow-x-auto">
<x-button.secondary wire:click="refreshCloudInit" wire:loading.attr="disabled">
<x-loading target="refreshCloudInit" />
Refresh
</x-button.secondary>
<x-button.secondary wire:click="resetCloudInit" :disabled="!$this->hasChanges">
Reset
</x-button.secondary>
<x-button.primary wire:click="applyCloudInit" wire:loading.attr="disabled" :disabled="!$this->hasChanges">
<x-loading target="applyCloudInit" />
Apply &amp; Regenerate
</x-button.primary>
</div>
</div>
</x-proxmox::layout>