batman (working version kinda)

This commit is contained in:
pika 2025-03-30 19:20:13 +02:00
commit 6dd38036e7
65 changed files with 3950 additions and 0 deletions

View file

@ -0,0 +1,60 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<h1>IPAM Dashboard</h1>
<div class="card">
<div class="card-header">
<h3 class="card-title">Subnetz-Übersicht</h3>
<div class="card-actions">
<button class="btn btn-primary" hx-get="/ipam/subnet/new" hx-target="#modal-container">
Neues Subnetz
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>CIDR</th>
<th>Standort</th>
<th>IP-Belegung</th>
<th>Auto-Scan</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for subnet in subnets %}
<tr>
<td>{{ subnet.cidr }}</td>
<td>{{ subnet.location }}</td>
<td>
<div class="progress progress-sm">
{% set percentage = (subnet.used_ips / subnet.total_ips * 100) | int %}
<div class="progress-bar bg-blue" style="width: {{ percentage }}%" role="progressbar"
aria-valuenow="{{ percentage }}" aria-valuemin="0" aria-valuemax="100">
{{ percentage }}%
</div>
</div>
</td>
<td>{{ "Ja" if subnet.auto_scan else "Nein" }}</td>
<td>
<a href="/ipam/subnet/{{ subnet.id }}" class="btn btn-sm btn-outline-primary">Details</a>
<button hx-post="/ipam/subnet/{{ subnet.id }}/scan" hx-swap="none"
class="btn btn-sm btn-outline-secondary" {% if not subnet.auto_scan %}disabled{% endif %}>
Scan starten
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div id="modal-container"></div>
{% endblock %}

View file

@ -0,0 +1,81 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-xl">
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
IP Address Management
</h2>
</div>
<div class="col-auto ms-auto">
<a href="{{ url_for('ipam.subnet_new') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> Add Subnet
</a>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
{% if subnets %}
<div class="table-responsive">
<table class="table table-vcenter table-hover">
<thead>
<tr>
<th>Subnet</th>
<th>Location</th>
<th>Usage</th>
<th>Auto Scan</th>
<th class="w-1"></th>
</tr>
</thead>
<tbody>
{% for subnet in subnets %}
<tr>
<td>{{ subnet.cidr }}</td>
<td>{{ subnet.location }}</td>
<td>
<div class="d-flex align-items-center">
<div class="me-2">{{ subnet.used_ips }}/254</div>
<div class="progress flex-grow-1" style="height: 5px;">
<div class="progress-bar bg-primary" style="width: {{ subnet.usage_percent }}%"></div>
</div>
</div>
</td>
<td>
{% if subnet.auto_scan %}
<span class="badge bg-success">Enabled</span>
{% else %}
<span class="badge bg-secondary">Disabled</span>
{% endif %}
</td>
<td>
<div class="btn-group">
<a href="{{ url_for('ipam.subnet_view', subnet_id=subnet.id) }}" class="btn btn-sm btn-primary">
View
</a>
<a href="{{ url_for('ipam.subnet_scan', subnet_id=subnet.id) }}"
class="btn btn-sm btn-outline-primary">
Scan
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<div class="mb-3">No subnets added yet</div>
<a href="{{ url_for('ipam.subnet_new') }}" class="btn btn-primary">
Add Your First Subnet
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,149 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="d-flex mb-3">
<div>
<h1>Subnetz: {{ subnet.cidr }}</h1>
<p>Standort: {{ subnet.location }}</p>
</div>
<div class="ms-auto">
<button class="btn btn-outline-primary" hx-get="/ipam/subnet/{{ subnet.id }}/export" hx-swap="none">
CSV-Export
</button>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h3 class="card-title">Subnetz-Details</h3>
<div class="mb-3">
<label class="form-label">Netzwerk-Adresse</label>
<div>{{ subnet.cidr.split('/')[0] }}</div>
</div>
<div class="mb-3">
<label class="form-label">Präfixlänge</label>
<div>{{ subnet.cidr.split('/')[1] }}</div>
</div>
<div class="mb-3">
<label class="form-label">Erste nutzbare IP</label>
<div>{{ first_ip }}</div>
</div>
<div class="mb-3">
<label class="form-label">Letzte nutzbare IP</label>
<div>{{ last_ip }}</div>
</div>
<div class="mb-3">
<label class="form-label">Gesamte IPs</label>
<div>{{ total_ips }}</div>
</div>
<div class="mb-3">
<label class="form-label">Belegte IPs</label>
<div>{{ used_ips }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3 class="card-title">IP-Belegung</h3>
</div>
<div class="card-body">
<div id="subnet-visualization" class="subnet-map">
<!-- Wird via JavaScript befüllt -->
<div class="text-center p-3">
<div class="spinner-border text-blue" role="status"></div>
<div>Lade Daten...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Load the subnet visualization
fetch('/ipam/subnet/{{ subnet.id }}/visualization')
.then(response => response.json())
.then(data => {
const container = document.getElementById('subnet-visualization');
container.innerHTML = '';
// Create a grid of IP boxes
const grid = document.createElement('div');
grid.className = 'ip-grid';
data.forEach(ip => {
const box = document.createElement('div');
box.className = `ip-box ${ip.status}`;
box.title = ip.hostname ? `${ip.ip} - ${ip.hostname}` : ip.ip;
const addressSpan = document.createElement('span');
addressSpan.className = 'ip-address';
addressSpan.textContent = ip.ip.split('.')[3]; // Only show last octet
box.appendChild(addressSpan);
if (ip.hostname) {
const hostnameSpan = document.createElement('span');
hostnameSpan.className = 'ip-hostname';
hostnameSpan.textContent = ip.hostname;
box.appendChild(hostnameSpan);
}
grid.appendChild(box);
});
container.appendChild(grid);
})
.catch(error => {
console.error('Error loading subnet visualization:', error);
document.getElementById('subnet-visualization').innerHTML =
'<div class="alert alert-danger">Fehler beim Laden der Visualisierung</div>';
});
});
</script>
<style>
.ip-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 5px;
}
.ip-box {
border: 1px solid #ccc;
padding: 5px;
text-align: center;
font-size: 12px;
border-radius: 3px;
}
.ip-box.used {
background-color: #e6f2ff;
border-color: #99c2ff;
}
.ip-box.available {
background-color: #f2f2f2;
}
.ip-address {
display: block;
font-weight: bold;
}
.ip-hostname {
display: block;
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
{% endblock %}

View file

@ -0,0 +1,51 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-xl">
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
Add New Subnet
</h2>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('ipam.subnet_new') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label required">CIDR Notation</label>
<input type="text" class="form-control" name="cidr" placeholder="192.168.1.0/24" required>
<small class="form-hint">Example: 192.168.1.0/24</small>
</div>
<div class="mb-3">
<label class="form-label required">Location</label>
<input type="text" class="form-control" name="location" placeholder="Office, Datacenter, etc." required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="auto_scan" name="auto_scan">
<label class="form-check-label" for="auto_scan">Enable automatic scanning</label>
</div>
<div class="d-flex justify-content-end">
<a href="{{ url_for('ipam.ipam_home') }}" class="btn btn-link me-2">Cancel</a>
<button type="submit" class="btn btn-primary">Save Subnet</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,132 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-xl">
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
{{ subnet.cidr }}
</h2>
<div class="text-muted mt-1">{{ subnet.location }}</div>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<a href="{{ url_for('ipam.subnet_scan', subnet_id=subnet.id) }}" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i> Scan Subnet
</a>
<a href="{{ url_for('ipam.subnet_visualize', subnet_id=subnet.id) }}" class="btn btn-outline-primary">
<i class="fas fa-chart-network me-2"></i> Visualize
</a>
<a href="{{ url_for('ipam.ipam_home') }}" class="btn btn-link">
Back to IPAM
</a>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3 class="card-title">Registered Hosts</h3>
</div>
<div class="card-body">
{% if servers %}
<div class="table-responsive">
<table class="table table-vcenter">
<thead>
<tr>
<th>Hostname</th>
<th>IP Address</th>
<th>Created</th>
<th class="w-1"></th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr>
<td>{{ server.hostname }}</td>
<td>{{ server.ip_address }}</td>
<td>{{ server.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}"
class="btn btn-sm btn-primary">
View
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-3">
<div class="mb-3">No hosts registered in this subnet</div>
<a href="{{ url_for('dashboard.server_new') }}" class="btn btn-outline-primary">
Add New Server
</a>
<a href="{{ url_for('ipam.subnet_scan', subnet_id=subnet.id) }}" class="btn btn-outline-primary ms-2">
Scan Subnet
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3 class="card-title">Subnet Information</h3>
</div>
<div class="card-body">
<div class="mb-2">
<strong>Network:</strong> {{ subnet.cidr }}
</div>
<div class="mb-2">
<strong>Location:</strong> {{ subnet.location }}
</div>
<div class="mb-2">
<strong>Total IPs:</strong> {{ total_ips }}
</div>
<div class="mb-2">
<strong>Used IPs:</strong> {{ used_ips }} ({{ '%.1f'|format(usage_percent) }}%)
</div>
<div class="mb-2">
<strong>Available IPs:</strong> {{ total_ips - used_ips }}
</div>
<div class="mb-2">
<strong>Auto Scan:</strong>
{% if subnet.auto_scan %}
<span class="badge bg-success">Enabled</span>
{% else %}
<span class="badge bg-secondary">Disabled</span>
{% endif %}
</div>
<div class="mb-2">
<strong>Created:</strong> {{ subnet.created_at.strftime('%Y-%m-%d') }}
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title">Actions</h3>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('ipam.subnet_scan', subnet_id=subnet.id) }}" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i> Scan Now
</a>
<a href="{{ url_for('ipam.subnet_visualize', subnet_id=subnet.id) }}" class="btn btn-outline-primary">
<i class="fas fa-chart-network me-2"></i> IP Visualization
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,206 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-xl">
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
IP Visualization - {{ subnet.cidr }}
</h2>
<div class="text-muted mt-1">{{ subnet.location }}</div>
</div>
<div class="col-auto ms-auto d-print-none">
<a href="{{ url_for('ipam.subnet_view', subnet_id=subnet.id) }}" class="btn btn-link">
Back to Subnet
</a>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h3 class="card-title">IP Address Map</h3>
<div>
<span class="badge bg-success me-2">Available: {{ total_ips - used_ip_count }}</span>
<span class="badge bg-danger">Used: {{ used_ip_count }}</span>
</div>
</div>
</div>
<div class="card-body">
<div class="ip-grid">
{% set network_parts = subnet.cidr.split('/')[0].split('.') %}
{% set network_prefix = network_parts[0] + '.' + network_parts[1] + '.' + network_parts[2] + '.' %}
{% for i in range(1, 255) %}
{% set ip = network_prefix + i|string %}
{% if ip in used_ips %}
<div class="ip-cell used" title="{{ used_ips[ip] }}">
{{ ip }}
</div>
{% else %}
<div class="ip-cell available" title="Available">
{{ ip }}
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function () {
loadIpMap();
// Handle scan button response
document.body.addEventListener('htmx:afterRequest', function (event) {
if (event.detail.target.matches('button[hx-post^="/ipam/subnet/"][hx-post$="/scan"]')) {
if (event.detail.successful) {
const toast = document.createElement('div');
toast.className = 'toast align-items-center show position-fixed bottom-0 end-0 m-3';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
Subnet scan started. Results will be available shortly.
</div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
document.body.appendChild(toast);
// Auto-remove toast after 5 seconds
setTimeout(() => {
toast.remove();
}, 5000);
// Reload IP map after a delay to allow scan to complete
setTimeout(() => {
loadIpMap();
}, 3000);
}
}
});
});
function loadIpMap() {
const ipMap = document.getElementById('ip-map');
const loadingIndicator = document.getElementById('loading-indicator');
fetch('/ipam/subnet/{{ subnet.id }}/visualization')
.then(response => response.json())
.then(data => {
// Hide loading indicator
loadingIndicator.style.display = 'none';
// Create IP grid
const grid = document.createElement('div');
grid.className = 'ip-grid';
// Add each IP address to the grid
data.forEach(ip => {
const cell = document.createElement('div');
cell.className = `ip-cell ${ip.status}`;
// Add IP address
const ipAddress = document.createElement('span');
ipAddress.className = 'ip-address';
ipAddress.textContent = ip.ip;
cell.appendChild(ipAddress);
// Add hostname if exists
if (ip.hostname) {
const hostname = document.createElement('span');
hostname.className = 'ip-hostname';
hostname.textContent = ip.hostname;
cell.appendChild(hostname);
}
// Add click handler for details
cell.addEventListener('click', () => {
if (ip.status === 'used') {
window.location.href = `/dashboard/server/${ip.server_id}`;
}
});
grid.appendChild(cell);
});
ipMap.appendChild(grid);
})
.catch(error => {
console.error('Error loading subnet visualization:', error);
document.getElementById('ip-map').innerHTML =
'<div class="alert alert-danger">Error loading subnet visualization. Please try again later.</div>';
document.getElementById('loading-indicator').style.display = 'none';
});
}
</script>
<style>
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.ip-grid-container {
min-height: 400px;
position: relative;
}
.ip-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 4px;
}
.ip-cell {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 4px;
text-align: center;
cursor: pointer;
height: 50px;
display: flex;
flex-direction: column;
justify-content: center;
font-size: 12px;
transition: all 0.2s ease;
}
.ip-cell:hover {
transform: scale(1.05);
z-index: 10;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.ip-cell.used {
background-color: rgba(32, 107, 196, 0.1);
border-color: rgba(32, 107, 196, 0.5);
}
.ip-cell.available {
background-color: rgba(5, 150, 105, 0.1);
border-color: rgba(5, 150, 105, 0.5);
}
.ip-address {
font-weight: bold;
}
.ip-hostname {
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>