First Commit

This commit is contained in:
dedhersel
2026-02-17 12:43:27 +01:00
commit 38f85dc498
26 changed files with 9939 additions and 0 deletions

690
templates/ipam.html Normal file
View File

@@ -0,0 +1,690 @@
{% 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 %}