From f6743076b671098c0aa9cbace43efa504738bc42 Mon Sep 17 00:00:00 2001 From: dv Date: Fri, 10 Apr 2026 14:57:26 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=81=D1=81=D1=85?= =?UTF-8?q?=20=D0=B4=D0=B1=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/device_discovery.py | 46 +++++++++++++++------ frontend/src/components/DeviceEditModal.tsx | 13 +++--- 2 files changed, 41 insertions(+), 18 deletions(-) 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 /> - Подключение к БД - - - Подключаться к БД через SSH-туннель (через main_server объекта) - + {device?.device_meta?.is_caps_db && ( + <> + Подключение к БД + + Подключаться к БД через SSH-туннель (через main_server объекта) + + + )} )