First Commit
This commit is contained in:
690
templates/ipam.html
Normal file
690
templates/ipam.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user