feat: ✨ subnet utilisation stats
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user