perf: async Proxmox API, optimized IPAM queries, log cleanup scheduling, background template import

- proxmox_client.py: add ThreadPoolExecutor(max_workers=20) + async wrapper
  methods (async_get_nodes, async_get_vms, async_get_containers,
  async_get_node_status, async_get_vm_status, async_get_container_status,
  async_get_storages, async_get_all_resources) to avoid blocking event loop
- ipam_service.py: replace full-network allocation scan + Python IP loop with
  per-pool targeted SQL query — only loads IPs for the specific pool being searched
- api/ipam.py: fix N+1 full-table scans on GET /api/allocations — use IN-filtered
  queries on only the network/pool IDs present in the result set
- monitoring_worker.py: wire LoggingService.cleanup_old_logs(days=30) into the
  existing run_cleanup_expired() job so audit logs don't grow unboundedly
- api/templates.py: convert auto_import_templates to async endpoint using
  BackgroundTasks — returns immediately, heavy Proxmox I/O runs in background

https://claude.ai/code/session_019rK2wvaWg5zXFwmAPPEB7R
This commit is contained in:
Claude
2026-03-07 11:48:53 +00:00
parent f4b6b36f3b
commit e05650cc9b
5 changed files with 165 additions and 117 deletions

View File

@@ -8,7 +8,7 @@ import logging
from typing import List, Optional, Dict, Any, Tuple
from datetime import datetime, timezone
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from sqlalchemy import and_, or_, text
from .models import IPAMNetwork, IPAMPool, IPAMAllocation, IPAMHistory
@@ -183,31 +183,32 @@ class IPAMService:
logger.warning(f"No active auto-assign pools found for network {network_id}")
return None
# Get all allocated IPs in this network
allocated_ips = set(
a.ip_address for a in
self.db.query(IPAMAllocation).filter(
IPAMAllocation.network_id == network_id,
IPAMAllocation.status.in_(['allocated', 'reserved'])
).all()
)
# Also exclude gateway
if network.gateway:
allocated_ips.add(network.gateway)
# Search through pools for available IP
gateway_ip = network.gateway if network.gateway else None
# Search through pools for available IP — query only per-pool allocated IPs
for pool in pools:
try:
start_ip = ipaddress.ip_address(pool.range_start)
end_ip = ipaddress.ip_address(pool.range_end)
# Load only allocated IPs for this specific pool (much smaller set)
pool_allocated = set(
a.ip_address for a in
self.db.query(IPAMAllocation.ip_address).filter(
IPAMAllocation.pool_id == pool.id,
IPAMAllocation.status.in_(['allocated', 'reserved'])
).all()
)
if gateway_ip:
pool_allocated.add(gateway_ip)
for ip_int in range(int(start_ip), int(end_ip) + 1):
ip = str(ipaddress.ip_address(ip_int))
if ip not in allocated_ips:
if ip not in pool_allocated:
logger.info(f"Found available IP: {ip} in pool {pool.name}")
return ip
except ValueError as e:
logger.error(f"Invalid IP range in pool {pool.id}: {e}")
continue