diff --git a/backend/app/services/device_discovery.py b/backend/app/services/device_discovery.py
index 9c722db..b33db66 100644
--- a/backend/app/services/device_discovery.py
+++ b/backend/app/services/device_discovery.py
@@ -545,6 +545,32 @@ async def _check_if_slave_multi(
return False, "", ""
+async def _open_tunnel(tunnel_host: str, tunnel_port: int, ssh_options: list, target_host: str, target_port: int):
+ """Try each ssh_option in order; return open asyncssh connection on first success or raise."""
+ last_exc: Exception = Exception("No SSH options provided")
+ for opt in ssh_options:
+ try:
+ connect_kwargs: dict = dict(
+ host=tunnel_host, port=tunnel_port, username=opt.username,
+ known_hosts=None, connect_timeout=10,
+ )
+ if opt.type == "key":
+ connect_kwargs["client_keys"] = [opt.key_path]
+ connect_kwargs["passphrase"] = None
+ logger.info("[ssh_discovery] Tunnel attempt for %s — key: user=%s key_path=%s", tunnel_host, opt.username, opt.key_path)
+ else:
+ connect_kwargs["password"] = opt.password
+ masked = (opt.password[:1] + "***" + opt.password[-1:]) if len(opt.password) > 2 else "***"
+ logger.info("[ssh_discovery] Tunnel attempt for %s — password: user=%s password=%s", tunnel_host, opt.username, masked)
+ conn = await asyncssh.connect(**connect_kwargs)
+ logger.info("[ssh_discovery] Tunnel auth succeeded for %s with %s(%s)", tunnel_host, opt.type, opt.username)
+ return conn
+ except Exception as exc:
+ last_exc = exc
+ logger.info("[ssh_discovery] Tunnel auth failed for %s with %s(%s): %s", tunnel_host, opt.type, opt.username, exc)
+ raise last_exc
+
+
async def _get_rack_hosts_direct(
host: str,
port: int,
@@ -555,19 +581,13 @@ async def _get_rack_hosts_direct(
via_tunnel: bool = False,
tunnel_host: str | None = None,
tunnel_ssh_port: int = 22,
- tunnel_ssh_user: str | None = None,
- tunnel_ssh_password: str | None = None,
+ ssh_options: list | None = None,
) -> list[tuple[str, str, str, str | None]]:
"""Return list of (external_id, host_ip, rack_name, rack_type) from the racks table."""
- if via_tunnel and tunnel_host and tunnel_ssh_user and tunnel_ssh_password:
- async with asyncssh.connect(
- tunnel_host,
- port=tunnel_ssh_port,
- username=tunnel_ssh_user,
- password=tunnel_ssh_password,
- known_hosts=None,
- ) as tunnel:
- listener = await tunnel.forward_local_port("127.0.0.1", 0, host, port)
+ if via_tunnel and tunnel_host and ssh_options:
+ conn = await _open_tunnel(tunnel_host, tunnel_ssh_port, ssh_options, host, port)
+ async with conn:
+ listener = await conn.forward_local_port("127.0.0.1", 0, host, port)
local_port = listener.get_port()
dsn = (
f"host=127.0.0.1 port={local_port} dbname={dbname} "
@@ -729,6 +749,7 @@ async def discover_via_ssh(
source="auto_ssh",
location="server_room",
status="unknown",
+ device_meta={"is_caps_db": True},
)
db.add(db_device_obj)
await db.flush()
@@ -764,8 +785,7 @@ async def discover_via_ssh(
via_tunnel=use_tunnel,
tunnel_host=obj.server_ip,
tunnel_ssh_port=ssh_port,
- tunnel_ssh_user=obj.ssh_user,
- tunnel_ssh_password=ssh_password,
+ ssh_options=ssh_options,
)
# Connection succeeded — mark DB device online
if db_device_obj is not None:
diff --git a/frontend/src/components/DeviceEditModal.tsx b/frontend/src/components/DeviceEditModal.tsx
index c50e7fa..519036f 100644
--- a/frontend/src/components/DeviceEditModal.tsx
+++ b/frontend/src/components/DeviceEditModal.tsx
@@ -219,11 +219,14 @@ export function DeviceEditModal({ open, device, objId, onClose, onSaved }: Props
/>
-