feat: subnet utilisation stats

This commit is contained in:
2025-12-04 20:01:52 +00:00
parent 61e3200207
commit f98e92da06
3 changed files with 125 additions and 12 deletions
+97 -6
View File
@@ -172,7 +172,8 @@ def register_routes(app):
@permission_required('view_index') @permission_required('view_index')
def index(): def index():
from flask import current_app from flask import current_app
with get_db_connection(current_app) as conn: conn = get_db_connection(current_app)
try:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT id, name, cidr, site FROM Subnet') cursor.execute('SELECT id, name, cidr, site FROM Subnet')
subnets = cursor.fetchall() subnets = cursor.fetchall()
@@ -181,8 +182,36 @@ def register_routes(app):
site = subnet[3] or 'Unassigned' site = subnet[3] or 'Unassigned'
if site not in sites_subnets: if site not in sites_subnets:
sites_subnets[site] = [] sites_subnets[site] = []
sites_subnets[site].append({'id': subnet[0], 'name': subnet[1], 'cidr': subnet[2]})
# Calculate utilization for each subnet
subnet_id = subnet[0]
cursor.execute('SELECT COUNT(*) FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
total_ips = cursor.fetchone()[0]
cursor.execute('''
SELECT COUNT(*) FROM IPAddress ip
WHERE ip.subnet_id = %s AND ip.id IN (SELECT ip_id FROM DeviceIPAddress)
''', (subnet_id,))
assigned_ips = cursor.fetchone()[0]
cursor.execute('''
SELECT COUNT(*) FROM IPAddress ip
WHERE ip.subnet_id = %s AND ip.hostname = 'DHCP' AND ip.id NOT IN (SELECT ip_id FROM DeviceIPAddress)
''', (subnet_id,))
dhcp_ips = cursor.fetchone()[0]
used_ips = assigned_ips + dhcp_ips
utilization_percent = (used_ips / total_ips * 100) if total_ips > 0 else 0
sites_subnets[site].append({
'id': subnet[0],
'name': subnet[1],
'cidr': subnet[2],
'utilization': round(utilization_percent, 1)
})
return render_with_user('index.html', sites_subnets=sites_subnets) return render_with_user('index.html', sites_subnets=sites_subnets)
finally:
conn.close()
@app.route('/devices') @app.route('/devices')
@permission_required('view_devices') @permission_required('view_devices')
@@ -504,6 +533,35 @@ def register_routes(app):
subnet = cursor.fetchone() subnet = cursor.fetchone()
cursor.execute('SELECT * FROM IPAddress WHERE subnet_id = %s', (subnet_id,)) cursor.execute('SELECT * FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
ip_addresses = cursor.fetchall() ip_addresses = cursor.fetchall()
# Calculate utilization stats
cursor.execute('SELECT COUNT(*) FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
total_ips = cursor.fetchone()[0]
cursor.execute('''
SELECT COUNT(*) FROM IPAddress ip
WHERE ip.subnet_id = %s AND ip.id IN (SELECT ip_id FROM DeviceIPAddress)
''', (subnet_id,))
assigned_ips = cursor.fetchone()[0]
cursor.execute('''
SELECT COUNT(*) FROM IPAddress ip
WHERE ip.subnet_id = %s AND ip.hostname = 'DHCP' AND ip.id NOT IN (SELECT ip_id FROM DeviceIPAddress)
''', (subnet_id,))
dhcp_ips = cursor.fetchone()[0]
available_ips = total_ips - assigned_ips - dhcp_ips
used_ips = assigned_ips + dhcp_ips
utilization_percent = (used_ips / total_ips * 100) if total_ips > 0 else 0
utilization_stats = {
'total': total_ips,
'assigned': assigned_ips,
'dhcp': dhcp_ips,
'available': available_ips,
'percent': round(utilization_percent, 1)
}
cursor.execute('SELECT id, name, description FROM Device') cursor.execute('SELECT id, name, description FROM Device')
devices = cursor.fetchall() devices = cursor.fetchall()
device_name_map = {name.lower(): (id, description) for id, name, description in devices} device_name_map = {name.lower(): (id, description) for id, name, description in devices}
@@ -517,7 +575,7 @@ def register_routes(app):
if match: if match:
device_id, device_description = match device_id, device_description = match
ip_addresses_with_device.append((ip[0], ip[1], hostname, device_id, device_description)) ip_addresses_with_device.append((ip[0], ip[1], hostname, device_id, device_description))
return render_with_user('subnet.html', subnet={'id': subnet[0], 'name': subnet[1], 'cidr': subnet[2]}, ip_addresses=ip_addresses_with_device) return render_with_user('subnet.html', subnet={'id': subnet[0], 'name': subnet[1], 'cidr': subnet[2]}, ip_addresses=ip_addresses_with_device, utilization=utilization_stats)
@app.route('/add_subnet', methods=['POST']) @app.route('/add_subnet', methods=['POST'])
@permission_required('add_subnet') @permission_required('add_subnet')
@@ -594,7 +652,42 @@ def register_routes(app):
with get_db_connection(current_app) as conn: with get_db_connection(current_app) as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name') cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
subnets = [dict(id=row[0], name=row[1], cidr=row[2], site=row[3] or 'Unassigned') for row in cursor.fetchall()] subnet_rows = cursor.fetchall()
subnets = []
for row in subnet_rows:
subnet_id = row[0]
# Calculate utilization for each subnet
cursor.execute('SELECT COUNT(*) FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
total_ips = cursor.fetchone()[0]
cursor.execute('''
SELECT COUNT(*) FROM IPAddress ip
WHERE ip.subnet_id = %s AND ip.id IN (SELECT ip_id FROM DeviceIPAddress)
''', (subnet_id,))
assigned_ips = cursor.fetchone()[0]
cursor.execute('''
SELECT COUNT(*) FROM IPAddress ip
WHERE ip.subnet_id = %s AND ip.hostname = 'DHCP' AND ip.id NOT IN (SELECT ip_id FROM DeviceIPAddress)
''', (subnet_id,))
dhcp_ips = cursor.fetchone()[0]
available_ips = total_ips - assigned_ips - dhcp_ips
used_ips = assigned_ips + dhcp_ips
utilization_percent = (used_ips / total_ips * 100) if total_ips > 0 else 0
subnets.append({
'id': row[0],
'name': row[1],
'cidr': row[2],
'site': row[3] or 'Unassigned',
'utilization': {
'percent': round(utilization_percent, 1),
'assigned': assigned_ips,
'used': used_ips,
'total': total_ips
}
})
return render_with_user('admin.html', subnets=subnets, return render_with_user('admin.html', subnets=subnets,
can_add_subnet=has_permission('add_subnet'), can_add_subnet=has_permission('add_subnet'),
can_edit_subnet=has_permission('edit_subnet'), can_edit_subnet=has_permission('edit_subnet'),
@@ -1676,8 +1769,6 @@ def register_routes(app):
reserved_for_dhcp = True reserved_for_dhcp = True
break break
if candidate_ip == end_ip: if candidate_ip == end_ip:
if candidate_ip == ip:
reserved_for_dhcp = True
in_range = False in_range = False
if reserved_for_dhcp: if reserved_for_dhcp:
return jsonify({'error': 'This IP is reserved for DHCP and cannot be assigned to a device'}), 400 return jsonify({'error': 'This IP is reserved for DHCP and cannot be assigned to a device'}), 400
+9
View File
@@ -85,6 +85,7 @@
<th class="text-center p-3">Name</th> <th class="text-center p-3">Name</th>
<th class="text-center p-3">CIDR</th> <th class="text-center p-3">CIDR</th>
<th class="text-center p-3">Site</th> <th class="text-center p-3">Site</th>
<th class="text-center p-3">Utilisation</th>
<th class="text-center p-3">Actions</th> <th class="text-center p-3">Actions</th>
</tr> </tr>
</thead> </thead>
@@ -96,6 +97,14 @@
<td class="p-3 text-center"> <td class="p-3 text-center">
<span class="px-2 py-1 bg-gray-300 dark:bg-zinc-700 rounded text-sm">{{ subnet.site }}</span> <span class="px-2 py-1 bg-gray-300 dark:bg-zinc-700 rounded text-sm">{{ subnet.site }}</span>
</td> </td>
<td class="p-3 text-center">
{% if subnet.utilization %}
<span class="text-sm text-gray-600 dark:text-gray-400">{{ subnet.utilization.percent }}%</span>
<span class="text-xs text-gray-500 dark:text-gray-500 ml-1">({{ subnet.utilization.used }}/{{ subnet.utilization.total }})</span>
{% else %}
<span class="text-sm text-gray-500"></span>
{% endif %}
</td>
<td class="p-3 text-center"> <td class="p-3 text-center">
<div class="flex items-center justify-center space-x-2"> <div class="flex items-center justify-center space-x-2">
<a href="/subnet/{{ subnet.id }}" class="text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300" title="View Subnet"> <a href="/subnet/{{ subnet.id }}" class="text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300" title="View Subnet">
+13
View File
@@ -20,6 +20,19 @@
<i class="fas fa-file-csv fa-lg"></i> <i class="fas fa-file-csv fa-lg"></i>
</button> </button>
</div> </div>
{% if utilization %}
<div class="hidden sm:flex justify-center mb-4">
<div class="bg-gray-200 dark:bg-zinc-800 px-4 py-2 rounded-lg text-sm">
<span class="font-medium">{{ utilization.percent }}% used</span>
<span class="text-gray-600 dark:text-gray-400 mx-2"></span>
<span class="text-gray-600 dark:text-gray-400">{{ utilization.assigned }} assigned</span>
<span class="text-gray-600 dark:text-gray-400 mx-2"></span>
<span class="text-gray-600 dark:text-gray-400">{{ utilization.dhcp }} DHCP</span>
<span class="text-gray-600 dark:text-gray-400 mx-2"></span>
<span class="text-gray-600 dark:text-gray-400">{{ utilization.available }} available</span>
</div>
</div>
{% endif %}
<div class="flex justify-center mb-4"> <div class="flex justify-center mb-4">
<a href="/subnet/{{ subnet.id }}/dhcp" class="hidden sm:flex bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg shadow items-center gap-2"> <a href="/subnet/{{ subnet.id }}/dhcp" class="hidden sm:flex bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg shadow items-center gap-2">
<i class="fas fa-network-wired"></i> Define DHCP Pool <i class="fas fa-network-wired"></i> Define DHCP Pool