Limit scan concurrency: serialise logins, cap at 5 sessions

Only one SSH handshake/auth runs at a time via a module-level semaphore
to avoid RADIUS lockout. Up to 5 sessions can remain open concurrently
(down from 10) while commands run in parallel after login.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 20:04:27 +00:00
parent 5dcd908f33
commit d3a761baf5
+10 -2
View File
@@ -1,6 +1,7 @@
# ssh_client.py - SSH connections to FS, HP/Aruba ProCurve, and Dell switches # ssh_client.py - SSH connections to FS, HP/Aruba ProCurve, and Dell switches
import re import re
import logging import logging
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
from config import SSH_USERNAME, SSH_PASSWORD, SSH_PORT, SSH_TIMEOUT, DEVICE_TYPE from config import SSH_USERNAME, SSH_PASSWORD, SSH_PORT, SSH_TIMEOUT, DEVICE_TYPE
@@ -10,6 +11,9 @@ from parser import (parse_lldp_neighbors, parse_mgmt_ip_from_interfaces,
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Serialise SSH logins — only one handshake/auth at a time to avoid RADIUS lockout
_login_lock = threading.Semaphore(1)
def connect_and_query(ip): def connect_and_query(ip):
device = { device = {
@@ -36,9 +40,11 @@ def connect_and_query(ip):
"ecdsa-sha2-nistp521", "ssh-ed25519", "ecdsa-sha2-nistp521", "ssh-ed25519",
) )
with ConnectHandler(**device) as conn: with _login_lock:
conn = ConnectHandler(**device)
_pt.Transport._preferred_keys = _orig_preferred_keys _pt.Transport._preferred_keys = _orig_preferred_keys
try:
hostname = conn.find_prompt().replace('#', '').replace('>', '').strip() hostname = conn.find_prompt().replace('#', '').replace('>', '').strip()
version_output = conn.send_command("show version", read_timeout=30) version_output = conn.send_command("show version", read_timeout=30)
vendor = _detect_vendor(version_output) vendor = _detect_vendor(version_output)
@@ -47,6 +53,8 @@ def connect_and_query(ip):
chassis_id, mgmt_ip, model, firmware, neighbors = _query_aruba(conn, version_output, ip) chassis_id, mgmt_ip, model, firmware, neighbors = _query_aruba(conn, version_output, ip)
else: else:
chassis_id, mgmt_ip, model, firmware, neighbors = _query_fs(conn, version_output, ip) chassis_id, mgmt_ip, model, firmware, neighbors = _query_fs(conn, version_output, ip)
finally:
conn.disconnect()
logger.info(f" {hostname} ({ip}) [{vendor}]: {len(neighbors)} neighbors") logger.info(f" {hostname} ({ip}) [{vendor}]: {len(neighbors)} neighbors")
@@ -167,7 +175,7 @@ def _aruba_firmware(version_output):
# ── Scan orchestration ──────────────────────────────────────────────────────── # ── Scan orchestration ────────────────────────────────────────────────────────
def scan_all_switches(ip_list, progress_callback=None, max_workers=10): def scan_all_switches(ip_list, progress_callback=None, max_workers=5):
results = [] results = []
total = len(ip_list) total = len(ip_list)
done = 0 done = 0