Files
proxmox_manager/templates/ipam.html
2026-02-17 12:43:27 +01:00

691 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Gestione IPAM - Proxmox Manager{% endblock %}
{% block extra_styles %}
<style>
.ipam-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-lg);
flex-wrap: wrap;
gap: var(--space-md);
}
.ipam-actions {
display: flex;
gap: var(--space-sm);
}
.ipam-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.ipam-stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-lg);
}
.ipam-stat-card h3 {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: var(--space-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ipam-stat-card .stat-number {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--space-xs);
}
.ipam-stat-card .stat-bar {
width: 100%;
height: 8px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
overflow: hidden;
margin-top: var(--space-sm);
}
.ipam-stat-card .stat-bar-fill {
height: 100%;
background: var(--accent-blue);
transition: width var(--transition-fast);
}
.ipam-filters {
display: flex;
gap: var(--space-md);
margin-bottom: var(--space-lg);
flex-wrap: wrap;
}
.filter-input {
padding: var(--space-sm) var(--space-md);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 0.9rem;
min-width: 200px;
}
.ipam-table-container {
overflow-x: auto;
background: var(--bg-secondary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
}
.ipam-table {
width: 100%;
border-collapse: collapse;
}
.ipam-table thead {
background: var(--bg-tertiary);
border-bottom: 2px solid var(--border-default);
}
.ipam-table th {
padding: var(--space-md);
text-align: left;
font-weight: 600;
color: var(--text-secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
user-select: none;
}
.ipam-table th:hover {
color: var(--accent-blue);
}
.ipam-table tbody tr {
border-bottom: 1px solid var(--border-default);
transition: background var(--transition-fast);
}
.ipam-table tbody tr:hover {
background: var(--bg-tertiary);
}
.ipam-table td {
padding: var(--space-md);
color: var(--text-primary);
font-size: 0.9rem;
}
.ip-badge {
display: inline-flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
background: rgba(88, 166, 255, 0.15);
color: var(--accent-blue);
border-radius: var(--radius-sm);
font-family: 'Courier New', monospace;
font-weight: 600;
font-size: 0.9rem;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge.running {
background: rgba(63, 185, 80, 0.15);
color: var(--accent-green);
}
.status-badge.stopped {
background: rgba(255, 78, 80, 0.15);
color: var(--accent-red);
}
.status-badge.unknown {
background: rgba(139, 148, 158, 0.15);
color: var(--text-tertiary);
}
.type-badge {
display: inline-flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
font-size: 0.8rem;
font-weight: 600;
}
.type-badge.qemu {
background: rgba(163, 113, 247, 0.15);
color: var(--accent-purple);
}
.type-badge.lxc {
background: rgba(210, 153, 34, 0.15);
color: var(--accent-yellow);
}
.action-btns {
display: flex;
gap: var(--space-xs);
}
.action-btn {
padding: var(--space-xs) var(--space-sm);
background: var(--bg-tertiary);
border: 1px solid var(--border-default);
border-radius: var(--radius-sm);
color: var(--text-primary);
cursor: pointer;
transition: all var(--transition-fast);
font-size: 0.85rem;
}
.action-btn:hover {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.visible {
display: flex;
}
.modal-content {
background: var(--bg-secondary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-xl);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-lg);
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.modal-close {
cursor: pointer;
color: var(--text-secondary);
font-size: 1.5rem;
line-height: 1;
}
.modal-close:hover {
color: var(--accent-red);
}
.form-group {
margin-bottom: var(--space-md);
}
.form-label {
display: block;
margin-bottom: var(--space-xs);
color: var(--text-secondary);
font-weight: 500;
font-size: 0.9rem;
}
.form-input {
width: 100%;
padding: var(--space-sm) var(--space-md);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-tertiary);
color: var(--text-primary);
font-size: 0.9rem;
}
.form-input:focus {
outline: none;
border-color: var(--accent-blue);
}
.modal-actions {
display: flex;
gap: var(--space-md);
margin-top: var(--space-lg);
}
.conflict-badge {
background: rgba(255, 78, 80, 0.15);
color: var(--accent-red);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
margin-left: var(--space-xs);
}
</style>
{% endblock %}
{% block content %}
<div class="ipam-header">
<div>
<h1>Gestione IPAM</h1>
<p class="subtitle">IP Address Management per VM e LXC</p>
</div>
<div class="ipam-actions">
<button class="btn btn-primary" onclick="scanNetwork()">🔍 Scansiona Rete</button>
<button class="btn btn-primary" onclick="showAssignModal()"> Assegna IP Manuale</button>
<button class="btn btn-secondary" onclick="exportIPAM()">💾 Esporta CSV</button>
</div>
</div>
<div class="ipam-grid">
<div class="ipam-stat-card">
<h3>IP Totali</h3>
<div class="stat-number" id="statTotalIPs">-</div>
<div class="stat-bar">
<div class="stat-bar-fill" id="statBarTotal" style="width: 100%"></div>
</div>
</div>
<div class="ipam-stat-card">
<h3>IP Assegnati</h3>
<div class="stat-number" id="statAssignedIPs">-</div>
<div class="stat-bar">
<div class="stat-bar-fill" id="statBarAssigned" style="width: 0%; background: var(--accent-green);"></div>
</div>
</div>
<div class="ipam-stat-card">
<h3>Conflitti</h3>
<div class="stat-number" id="statConflicts">-</div>
<div class="stat-bar">
<div class="stat-bar-fill" id="statBarConflicts" style="width: 0%; background: var(--accent-red);"></div>
</div>
</div>
</div>
<div class="card">
<div class="ipam-filters">
<input type="text" class="filter-input" id="filterIP" placeholder="🔍 Filtra per IP..." oninput="filterIPAM()">
<input type="text" class="filter-input" id="filterName" placeholder="🔍 Filtra per nome VM..." oninput="filterIPAM()">
<select class="filter-input" id="filterType" onchange="filterIPAM()">
<option value="">Tutti i tipi</option>
<option value="qemu">QEMU</option>
<option value="lxc">LXC</option>
</select>
<select class="filter-input" id="filterStatus" onchange="filterIPAM()">
<option value="">Tutti gli stati</option>
<option value="running">Running</option>
<option value="stopped">Stopped</option>
</select>
</div>
</div>
<div class="card">
<div class="ipam-table-container">
<table class="ipam-table">
<thead>
<tr>
<th onclick="sortTable('vmid')">VM ID</th>
<th onclick="sortTable('name')">Nome</th>
<th onclick="sortTable('type')">Tipo</th>
<th onclick="sortTable('ip')">Indirizzo IP</th>
<th onclick="sortTable('node')">Nodo</th>
<th onclick="sortTable('status')">Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody id="ipamTableBody">
<tr>
<td colspan="7" style="text-align: center; padding: var(--space-xl);">
<div class="loading">
<div class="spinner"></div>
<p>Caricamento dati IPAM...</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Modal Assegnazione IP -->
<div class="modal" id="assignModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">Assegna Indirizzo IP</div>
<span class="modal-close" onclick="closeModal('assignModal')">×</span>
</div>
<form id="assignForm" onsubmit="submitAssignIP(event)">
<div class="form-group">
<label class="form-label">VM ID</label>
<input type="number" class="form-input" id="assignVmId" required min="100">
</div>
<div class="form-group">
<label class="form-label">Indirizzo IP</label>
<input type="text" class="form-input" id="assignIP" required placeholder="192.168.1.100" pattern="^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$">
</div>
<div class="form-group">
<label class="form-label">Note (opzionale)</label>
<textarea class="form-input" id="assignNotes" rows="3" placeholder="Note aggiuntive..."></textarea>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Assegna</button>
<button type="button" class="btn btn-secondary" onclick="closeModal('assignModal')">Annulla</button>
</div>
</form>
</div>
</div>
<!-- Modal Dettagli IP -->
<div class="modal" id="detailsModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">Dettagli IP</div>
<span class="modal-close" onclick="closeModal('detailsModal')">×</span>
</div>
<div id="detailsContent"></div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
let ipamData = [];
let filteredData = [];
let currentSort = { column: 'vmid', ascending: true };
document.addEventListener('DOMContentLoaded', function() {
loadIPAM();
});
async function loadIPAM() {
const result = await apiCall('/api/admin/ipam');
if (result.status === 'success') {
ipamData = result.data;
updateStats();
filterIPAM();
} else {
document.getElementById('ipamTableBody').innerHTML = `
<tr><td colspan="7" style="text-align: center;">
<div class="alert alert-error">${result.message}</div>
</td></tr>
`;
}
}
function updateStats() {
const totalIPs = ipamData.length;
const assignedIPs = ipamData.filter(item => item.ip && item.ip !== 'N/A').length;
const conflicts = ipamData.filter(item => item.conflict).length;
document.getElementById('statTotalIPs').textContent = totalIPs;
document.getElementById('statAssignedIPs').textContent = assignedIPs;
document.getElementById('statConflicts').textContent = conflicts;
const assignedPercent = totalIPs > 0 ? (assignedIPs / totalIPs) * 100 : 0;
const conflictPercent = totalIPs > 0 ? (conflicts / totalIPs) * 100 : 0;
document.getElementById('statBarAssigned').style.width = assignedPercent + '%';
document.getElementById('statBarConflicts').style.width = conflictPercent + '%';
}
function filterIPAM() {
const filterIP = document.getElementById('filterIP').value.toLowerCase();
const filterName = document.getElementById('filterName').value.toLowerCase();
const filterType = document.getElementById('filterType').value;
const filterStatus = document.getElementById('filterStatus').value;
filteredData = ipamData.filter(item => {
const matchIP = !filterIP || (item.ip && item.ip.toLowerCase().includes(filterIP));
const matchName = !filterName || (item.name && item.name.toLowerCase().includes(filterName));
const matchType = !filterType || item.type === filterType;
const matchStatus = !filterStatus || item.status === filterStatus;
return matchIP && matchName && matchType && matchStatus;
});
renderTable();
}
function renderTable() {
const tbody = document.getElementById('ipamTableBody');
if (filteredData.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: var(--space-xl);">Nessun risultato trovato</td></tr>';
return;
}
tbody.innerHTML = filteredData.map(item => `
<tr>
<td><strong>${item.vmid}</strong></td>
<td>${item.name || 'N/A'}</td>
<td><span class="type-badge ${item.type}">${item.type.toUpperCase()}</span></td>
<td>
<span class="ip-badge">${item.ip || 'N/A'}</span>
${item.conflict ? '<span class="conflict-badge">CONFLITTO</span>' : ''}
</td>
<td>${item.node || 'N/A'}</td>
<td><span class="status-badge ${item.status || 'unknown'}">${item.status || 'unknown'}</span></td>
<td>
<div class="action-btns">
<button class="action-btn" onclick="showDetails(${item.vmid})" title="Dettagli">👁️</button>
<button class="action-btn" onclick="pingIP('${item.ip}')" title="Ping" ${!item.ip || item.ip === 'N/A' ? 'disabled' : ''}>📡</button>
<button class="action-btn" onclick="editIP(${item.vmid})" title="Modifica">✏️</button>
</div>
</td>
</tr>
`).join('');
}
function sortTable(column) {
if (currentSort.column === column) {
currentSort.ascending = !currentSort.ascending;
} else {
currentSort.column = column;
currentSort.ascending = true;
}
filteredData.sort((a, b) => {
let valA = a[column];
let valB = b[column];
if (typeof valA === 'string') valA = valA.toLowerCase();
if (typeof valB === 'string') valB = valB.toLowerCase();
if (valA < valB) return currentSort.ascending ? -1 : 1;
if (valA > valB) return currentSort.ascending ? 1 : -1;
return 0;
});
renderTable();
}
async function scanNetwork() {
showAlert('Scansione della rete in corso...', 'info');
const result = await apiCall('/api/admin/ipam/scan', 'POST');
if (result.status === 'success') {
showAlert('Scansione completata con successo', 'success');
loadIPAM();
} else {
showAlert('Errore durante la scansione: ' + result.message, 'error');
}
}
function showAssignModal() {
document.getElementById('assignModal').classList.add('visible');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('visible');
}
async function submitAssignIP(event) {
event.preventDefault();
const vmid = document.getElementById('assignVmId').value;
const ip = document.getElementById('assignIP').value;
const notes = document.getElementById('assignNotes').value;
const result = await apiCall('/api/admin/ipam/assign', 'POST', {
vmid: parseInt(vmid),
ip: ip,
notes: notes
});
if (result.status === 'success') {
showAlert('IP assegnato con successo', 'success');
closeModal('assignModal');
loadIPAM();
} else {
showAlert('Errore: ' + result.message, 'error');
}
}
async function showDetails(vmid) {
const item = ipamData.find(i => i.vmid === vmid);
if (!item) return;
const content = document.getElementById('detailsContent');
content.innerHTML = `
<div class="form-group">
<label class="form-label">VM ID</label>
<div class="form-input" style="background: var(--bg-secondary);">${item.vmid}</div>
</div>
<div class="form-group">
<label class="form-label">Nome</label>
<div class="form-input" style="background: var(--bg-secondary);">${item.name || 'N/A'}</div>
</div>
<div class="form-group">
<label class="form-label">Tipo</label>
<div class="form-input" style="background: var(--bg-secondary);">${item.type}</div>
</div>
<div class="form-group">
<label class="form-label">IP</label>
<div class="form-input" style="background: var(--bg-secondary);">${item.ip || 'N/A'}</div>
</div>
<div class="form-group">
<label class="form-label">Nodo</label>
<div class="form-input" style="background: var(--bg-secondary);">${item.node || 'N/A'}</div>
</div>
<div class="form-group">
<label class="form-label">Stato</label>
<div class="form-input" style="background: var(--bg-secondary);">${item.status || 'unknown'}</div>
</div>
${item.mac ? `
<div class="form-group">
<label class="form-label">MAC Address</label>
<div class="form-input" style="background: var(--bg-secondary);">${item.mac}</div>
</div>
` : ''}
${item.notes ? `
<div class="form-group">
<label class="form-label">Note</label>
<div class="form-input" style="background: var(--bg-secondary);">${item.notes}</div>
</div>
` : ''}
`;
document.getElementById('detailsModal').classList.add('visible');
}
async function pingIP(ip) {
if (!ip || ip === 'N/A') return;
showAlert(`Ping di ${ip} in corso...`, 'info');
const result = await apiCall('/api/admin/ipam/ping', 'POST', { ip: ip });
if (result.status === 'success') {
showAlert(`Ping riuscito: ${result.message}`, 'success');
} else {
showAlert(`Ping fallito: ${result.message}`, 'error');
}
}
function editIP(vmid) {
const item = ipamData.find(i => i.vmid === vmid);
if (!item) return;
document.getElementById('assignVmId').value = item.vmid;
document.getElementById('assignVmId').readOnly = true;
document.getElementById('assignIP').value = item.ip || '';
document.getElementById('assignNotes').value = item.notes || '';
showAssignModal();
}
function exportIPAM() {
if (filteredData.length === 0) {
showAlert('Nessun dato da esportare', 'warning');
return;
}
const headers = ['VM ID', 'Nome', 'Tipo', 'IP', 'Nodo', 'Stato', 'MAC', 'Note'];
const rows = filteredData.map(item => [
item.vmid,
item.name || '',
item.type,
item.ip || '',
item.node || '',
item.status || '',
item.mac || '',
item.notes || ''
]);
let csvContent = headers.join(',') + '\n';
csvContent += rows.map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `ipam_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showAlert('Dati IPAM esportati con successo', 'success');
}
</script>
{% endblock %}