ソースを参照

refactor(dhcp): unify network options with centralized DHCP options tab

Proxmox Router 1 週間 前
親
コミット
3b3d055bda
3 ファイル変更、38 行追加、72 行削除
  1. 17 8
      internal/dhcp/server_manager.go
  2. 7 23
      internal/web/static/app.js
  3. 14 41
      internal/web/static/index.html

+ 17 - 8
internal/dhcp/server_manager.go

@@ -102,6 +102,7 @@ func (sm *ServerManager) initDefaultOptions() {
 		{ID: "opt-ntp", Name: "ntp-servers", Code: 42, Value: "192.168.88.1", Comment: "NTP Time Servers (Option 42)"},
 		{ID: "opt-tftp-name", Name: "tftp-server-name", Code: 66, Value: "192.168.88.50", Comment: "TFTP Server Name / IP (Option 66)"},
 		{ID: "opt-bootfile", Name: "bootfile-name", Code: 67, Value: "pxelinux.0", Comment: "PXE Boot File (Option 67)"},
+		{ID: "opt-wins", Name: "wins-servers", Code: 44, Value: "192.168.88.50", Comment: "WINS NetBIOS Servers (Option 44)"},
 		{ID: "opt-unifi", Name: "unifi-controller", Code: 43, Value: "01:04:c0:a8:58:0a", Comment: "UniFi Inform Controller (Option 43 Hex IP)"},
 		{ID: "opt-voip", Name: "cisco-voip-tftp", Code: 150, Value: "192.168.88.50", Comment: "Cisco VoIP TFTP (Option 150)"},
 		{ID: "opt-capwap", Name: "capwap-ac", Code: 138, Value: "192.168.88.10", Comment: "CAPWAP Access Controller (Option 138)"},
@@ -293,18 +294,26 @@ func (sm *ServerManager) applyDnsmasqConfigLocked() error {
 			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:domain-name,%s\n", targetTag, netw.Domain))
 		}
 
-		// Option 42: NTP Servers (MikroTik /ip dhcp-server network ntp-server)
-		if len(netw.NTPServers) > 0 {
+		// Check which codes are provided via linked custom options
+		hasLinkedOption := make(map[int]bool)
+		for _, optID := range netw.DHCPOptions {
+			if opt, exists := sm.options[optID]; exists {
+				hasLinkedOption[opt.Code] = true
+			}
+		}
+
+		// Option 42: NTP Servers (if not linked via custom option)
+		if len(netw.NTPServers) > 0 && !hasLinkedOption[42] {
 			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:ntp-server,%s\n", targetTag, strings.Join(netw.NTPServers, ",")))
 		}
 
-		// Option 66: TFTP Server Name / IP (next-server)
-		if netw.NextServer != "" {
+		// Option 66: TFTP Server Name / IP (if not linked via custom option)
+		if netw.NextServer != "" && !hasLinkedOption[66] {
 			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:tftp-server,%s\n", targetTag, netw.NextServer))
 		}
 
-		// Option 67: Boot File Name (PXE)
-		if netw.BootFileName != "" {
+		// Option 67: Boot File Name (if not linked via custom option)
+		if netw.BootFileName != "" && !hasLinkedOption[67] {
 			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:bootfile-name,%s\n", targetTag, netw.BootFileName))
 			if netw.NextServer != "" {
 				sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s,,%s\n", targetTag, netw.BootFileName, netw.NextServer))
@@ -313,8 +322,8 @@ func (sm *ServerManager) applyDnsmasqConfigLocked() error {
 			}
 		}
 
-		// Option 44: WINS Servers
-		if len(netw.WINSServers) > 0 {
+		// Option 44: WINS Servers (if not linked via custom option)
+		if len(netw.WINSServers) > 0 && !hasLinkedOption[44] {
 			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:netbios-ns,%s\n", targetTag, strings.Join(netw.WINSServers, ",")))
 		}
 

+ 7 - 23
internal/web/static/app.js

@@ -13580,17 +13580,19 @@ function renderDHCPNetworks() {
     tr.className = 'hover:bg-slate-800/40 transition text-xs';
 
     const dnsStr = (n.dns_servers || []).join(', ') || '-';
-    const ntpStr = (n.ntp_servers || []).join(', ') || '-';
+    const optionsChips = (n.dhcp_options || []).map(optId => {
+      const opt = (state.dhcpOptions || []).find(o => o.id === optId);
+      if (!opt) return '';
+      return `<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-cyan-300 border border-slate-700 inline-block mr-1 mb-0.5" title="${escapeHtml(opt.name)}: ${escapeHtml(opt.value)}">#${opt.code} ${escapeHtml(opt.name)}</span>`;
+    }).filter(Boolean).join('');
 
     tr.innerHTML = `
       <td class="py-2.5 px-3 font-bold text-emerald-400 font-mono">${escapeHtml(n.address)}</td>
       <td class="py-2.5 px-3 font-mono text-cyan-300 font-semibold">${escapeHtml(n.gateway || '-')}</td>
       <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(n.netmask || '-')}</td>
-      <td class="py-2.5 px-3 font-mono text-slate-300 truncate max-w-[120px]" title="${escapeHtml(dnsStr)}">${escapeHtml(dnsStr)}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-300 truncate max-w-[140px]" title="${escapeHtml(dnsStr)}">${escapeHtml(dnsStr)}</td>
       <td class="py-2.5 px-3 font-mono text-slate-400">${escapeHtml(n.domain || '-')}</td>
-      <td class="py-2.5 px-3 font-mono text-purple-300 truncate max-w-[120px]" title="${escapeHtml(ntpStr)}">${escapeHtml(ntpStr)}</td>
-      <td class="py-2.5 px-3 font-mono text-amber-300">${escapeHtml(n.next_server || '-')}</td>
-      <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(n.boot_file_name || '-')}</td>
+      <td class="py-2.5 px-3 font-mono">${optionsChips || '<span class="text-slate-600 font-sans">-</span>'}</td>
       <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${escapeHtml(n.comment || '')}">${escapeHtml(n.comment || '-')}</td>
       <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
         <button onclick="openEditDHCPNetworkModal('${n.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
@@ -13838,10 +13840,6 @@ function openAddDHCPNetworkModal() {
   document.getElementById('dhcp-network-modal-gateway').value = '192.168.88.1';
   document.getElementById('dhcp-network-modal-dns').value = '192.168.88.1, 1.1.1.1, 8.8.8.8';
   document.getElementById('dhcp-network-modal-domain').value = 'lan';
-  document.getElementById('dhcp-network-modal-ntp').value = '192.168.88.1, time.cloudflare.com';
-  document.getElementById('dhcp-network-modal-tftp').value = '';
-  document.getElementById('dhcp-network-modal-bootfile').value = '';
-  document.getElementById('dhcp-network-modal-wins').value = '';
   document.getElementById('dhcp-network-modal-netmask').value = '';
   document.getElementById('dhcp-network-modal-comment').value = '';
 
@@ -13859,10 +13857,6 @@ function openEditDHCPNetworkModal(id) {
   document.getElementById('dhcp-network-modal-gateway').value = n.gateway || '';
   document.getElementById('dhcp-network-modal-dns').value = (n.dns_servers || []).join(', ');
   document.getElementById('dhcp-network-modal-domain').value = n.domain || '';
-  document.getElementById('dhcp-network-modal-ntp').value = (n.ntp_servers || []).join(', ');
-  document.getElementById('dhcp-network-modal-tftp').value = n.next_server || '';
-  document.getElementById('dhcp-network-modal-bootfile').value = n.boot_file_name || '';
-  document.getElementById('dhcp-network-modal-wins').value = (n.wins_servers || []).join(', ');
   document.getElementById('dhcp-network-modal-netmask').value = n.netmask || '';
   document.getElementById('dhcp-network-modal-comment').value = n.comment || '';
 
@@ -13904,10 +13898,6 @@ async function submitSaveDHCPNetwork(e) {
   const gateway = document.getElementById('dhcp-network-modal-gateway').value.trim();
   const dnsStr = document.getElementById('dhcp-network-modal-dns').value.trim();
   const domain = document.getElementById('dhcp-network-modal-domain').value.trim();
-  const ntpStr = document.getElementById('dhcp-network-modal-ntp').value.trim();
-  const tftp = document.getElementById('dhcp-network-modal-tftp').value.trim();
-  const bootfile = document.getElementById('dhcp-network-modal-bootfile').value.trim();
-  const winsStr = document.getElementById('dhcp-network-modal-wins').value.trim();
   const netmask = document.getElementById('dhcp-network-modal-netmask').value.trim();
   const comment = document.getElementById('dhcp-network-modal-comment').value.trim();
 
@@ -13915,8 +13905,6 @@ async function submitSaveDHCPNetwork(e) {
   document.querySelectorAll('input[name="dhcp-net-opt"]:checked').forEach(cb => selectedOpts.push(cb.value));
 
   const dnsServers = dnsStr ? dnsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
-  const ntpServers = ntpStr ? ntpStr.split(',').map(s => s.trim()).filter(Boolean) : [];
-  const winsServers = winsStr ? winsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
 
   try {
     const res = await fetch(`${API_BASE}/dhcp-server/networks`, {
@@ -13929,10 +13917,6 @@ async function submitSaveDHCPNetwork(e) {
         netmask,
         dns_servers: dnsServers,
         domain,
-        ntp_servers: ntpServers,
-        next_server: tftp,
-        boot_file_name: bootfile,
-        wins_servers: winsServers,
         dhcp_options: selectedOpts,
         comment
       })

+ 14 - 41
internal/web/static/index.html

@@ -912,9 +912,7 @@
                       <th class="py-2.5 px-3">Netmask</th>
                       <th class="py-2.5 px-3">DNS Servers</th>
                       <th class="py-2.5 px-3">Domain</th>
-                      <th class="py-2.5 px-3">NTP Servers (Opt 42)</th>
-                      <th class="py-2.5 px-3">Next Server (TFTP)</th>
-                      <th class="py-2.5 px-3">Boot File (PXE)</th>
+                      <th class="py-2.5 px-3">DHCP Options</th>
                       <th class="py-2.5 px-3">Comment</th>
                       <th class="py-2.5 px-3 text-right font-sans">Actions</th>
                     </tr>
@@ -10587,55 +10585,30 @@
           </div>
         </div>
 
-        <div class="p-3 bg-slate-950/60 border border-slate-800 rounded-xl space-y-3">
-          <div class="text-[11px] font-bold text-emerald-400 uppercase tracking-wider flex items-center gap-1.5">
-            <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
-            <span>Time & Network Boot Options (NTP & TFTP/PXE)</span>
-          </div>
-
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1">NTP Servers (Option 42)</label>
-            <input type="text" id="dhcp-network-modal-ntp" placeholder="192.168.88.1, time.cloudflare.com, pool.ntp.org" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
-            <span class="text-[10px] text-slate-400">Comma-separated NTP time servers distributed to DHCP clients</span>
-          </div>
-
-          <div class="grid grid-cols-2 gap-3">
-            <div>
-              <label class="block font-semibold text-slate-300 mb-1">Next Server / TFTP Server (Option 66)</label>
-              <input type="text" id="dhcp-network-modal-tftp" placeholder="192.168.88.50" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
-              <span class="text-[10px] text-slate-400">TFTP server IP/host (siaddr / Next-Server)</span>
-            </div>
-            <div>
-              <label class="block font-semibold text-slate-300 mb-1">Boot File Name / PXE (Option 67)</label>
-              <input type="text" id="dhcp-network-modal-bootfile" placeholder="pxelinux.0 or ipxe.efi" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
-              <span class="text-[10px] text-slate-400">Network bootstrap file (file / Bootfile)</span>
-            </div>
-          </div>
-        </div>
-
         <div class="grid grid-cols-2 gap-4">
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">WINS Servers (Option 44)</label>
-            <input type="text" id="dhcp-network-modal-wins" placeholder="192.168.88.50" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
-          </div>
           <div>
             <label class="block font-semibold text-slate-300 mb-1.5">Netmask (Optional Override)</label>
             <input type="text" id="dhcp-network-modal-netmask" placeholder="255.255.255.0" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Leave blank to use CIDR mask</span>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+            <input type="text" id="dhcp-network-modal-comment" placeholder="e.g. Office LAN Subnet" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Informational label</span>
           </div>
         </div>
 
-        <div>
-          <label class="block font-semibold text-slate-300 mb-1.5">Linked Custom DHCP Options</label>
-          <div id="dhcp-network-modal-options-list" class="grid grid-cols-2 gap-2 p-2 bg-slate-950/60 border border-slate-800 rounded-lg max-h-32 overflow-y-auto font-mono text-[11px]">
+        <div class="p-3.5 bg-slate-950/60 border border-slate-800 rounded-xl space-y-2">
+          <div class="flex items-center justify-between">
+            <label class="block font-semibold text-slate-300">Linked Custom DHCP Options</label>
+            <button type="button" onclick="closeDHCPNetworkModal(); switchDHCPServerTab('options')" class="text-[10px] text-cyan-400 hover:underline">Manage Options</button>
+          </div>
+          <p class="text-[11px] text-slate-400">Select any DHCP options (NTP #42, TFTP #66, PXE Boot #67, WINS #44, UniFi #43, CAPWAP #138, VoIP #150, WPAD #252) to distribute to clients on this subnet:</p>
+          <div id="dhcp-network-modal-options-list" class="grid grid-cols-2 gap-2 p-2 bg-slate-900 border border-slate-800 rounded-lg max-h-40 overflow-y-auto font-mono text-[11px]">
             <!-- Dynamically populated options checkboxes -->
           </div>
         </div>
 
-        <div>
-          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
-          <input type="text" id="dhcp-network-modal-comment" placeholder="Office LAN Subnet" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-emerald-500">
-        </div>
-
         <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
           <button type="button" onclick="closeDHCPNetworkModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
           <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">Save Network</button>