From 317b701afe5c584f903abb52fcd311bcd1d511c6 Mon Sep 17 00:00:00 2001 From: dv Date: Fri, 10 Apr 2026 13:54:28 +0300 Subject: [PATCH] =?UTF-8?q?ssh=20=D1=82=D1=83=D0=BD=D0=BD=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20=D0=B4=D0=BB=D1=8F=20=D0=91=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/device_discovery.py | 57 ++++++++++++++++++--- frontend/src/components/DeviceEditModal.tsx | 13 ++++- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/backend/app/services/device_discovery.py b/backend/app/services/device_discovery.py index f50ba76..9c722db 100644 --- a/backend/app/services/device_discovery.py +++ b/backend/app/services/device_discovery.py @@ -710,11 +710,48 @@ async def discover_via_ssh( ) return result + # Step 0.5: upsert DB server as a device now (before connection attempt), + # so the user can see it and configure tunnel even if connection fails. + now_pre = datetime.now(timezone.utc) + db_device_obj: Device | None = None + if db_host and db_host != obj.server_ip: + try: + db_device_obj = (await db.execute( + select(Device).where(Device.object_id == obj.id, Device.ip == db_host) + )).scalar_one_or_none() + if db_device_obj is None: + db_device_obj = Device( + object_id=obj.id, + ip=db_host, + hostname="db_server", + category="vm", + role="other", + source="auto_ssh", + location="server_room", + status="unknown", + ) + db.add(db_device_obj) + await db.flush() + logger.info("[ssh_discovery] %s (db_server) - PRE-ADDED before connection attempt", db_host) + result.created += 1 + result.actions.append(DeviceAction( + ip=db_host, hostname="db_server", action="ADDED", + reason="object infrastructure device (vm/db_server)", category="vm", role="other", + )) + except Exception as exc: + logger.warning("[ssh_discovery] Failed to pre-upsert db_server device %s: %s", db_host, exc) + + # Read tunnel flag from the device (user may have set it via UI), fall back to object setting + device_via_tunnel = bool( + (db_device_obj.connection_params or {}).get("db_via_tunnel") + ) if db_device_obj else False + use_tunnel = device_via_tunnel or obj.db_via_tunnel + # Step 1: get rack hosts from object's DB await _emit("disc_action", {"message": "Получение списка стоек из БД..."}) logger.info( "[ssh_discovery] Connecting to object DB: host=%s port=%s dbname=%s user=%s table=%s via_tunnel=%s", - db_host, db_port, db_name, db_user, db_table, obj.db_via_tunnel, + db_host, db_port, db_name, db_user, db_table, use_tunnel, ) try: rack_hosts = await _get_rack_hosts_direct( @@ -724,14 +761,24 @@ async def discover_via_ssh( password=db_password or "", dbname=db_name, table=db_table, - via_tunnel=obj.db_via_tunnel, + 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, ) + # Connection succeeded — mark DB device online + if db_device_obj is not None: + db_device_obj.status = "online" + db_device_obj.last_seen = now_pre except Exception as exc: - result.errors.append(f"Cannot fetch rack hosts from object DB: {exc}") + # Mark DB device offline so the user sees it's unreachable + if db_device_obj is not None: + db_device_obj.status = "offline" + err_msg = f"Cannot fetch rack hosts from object DB: {exc}" + if "timeout" in str(exc).lower(): + err_msg += ". Попробуйте настроить SSH-туннель (отредактируйте устройство БД и включите опцию)" + result.errors.append(err_msg) return result if not rack_hosts: @@ -774,14 +821,12 @@ async def discover_via_ssh( rack_id_mapping[ext_id] = new_rack.id logger.info(f"[ssh_discovery] Created rack entry '{rack_name}' rack_type={rack_type_db} (id={new_rack.id})") - # Step 1b: Add main CAPS server and DB server as devices + # Step 1b: Add main CAPS server as device (DB server was already upserted in Step 0.5) # verified=True means we successfully connected to this host during this discovery run now = datetime.now(timezone.utc) infra_devices = [] if obj.server_ip: infra_devices.append((obj.server_ip, "main_server", "caps_server", server_ip_ssh_verified)) - if db_host and db_host != obj.server_ip: - infra_devices.append((db_host, "vm", "db_server", True)) # DB query succeeded = online for infra_ip, infra_category, infra_hostname, infra_verified in infra_devices: try: diff --git a/frontend/src/components/DeviceEditModal.tsx b/frontend/src/components/DeviceEditModal.tsx index 90b9c6d..c50e7fa 100644 --- a/frontend/src/components/DeviceEditModal.tsx +++ b/frontend/src/components/DeviceEditModal.tsx @@ -78,6 +78,7 @@ export function DeviceEditModal({ open, device, objId, onClose, onSaved }: Props ssh_user_override: device.ssh_user_override ?? '', ssh_port_override: device.ssh_port_override ?? undefined, ssh_key_id_override: device.ssh_key_id_override ?? undefined, + db_via_tunnel: !!(device.connection_params?.db_via_tunnel), }) } else { form.resetFields() @@ -86,10 +87,14 @@ export function DeviceEditModal({ open, device, objId, onClose, onSaved }: Props const handleOk = async () => { const values = await form.validateFields() - const payload = stripEmpty(values) + const { db_via_tunnel, ...rest } = values + const payload = stripEmpty(rest) if ('rack_id' in values && values.rack_id === undefined) { payload.rack_id = null } + // Merge db_via_tunnel into connection_params + const existingParams = device?.connection_params ?? {} + payload.connection_params = { ...existingParams, db_via_tunnel: !!db_via_tunnel } try { let saved: DeviceItem if (device) { @@ -213,6 +218,12 @@ export function DeviceEditModal({ open, device, objId, onClose, onSaved }: Props options={(sshKeys ?? []).map((k) => ({ value: k.id, label: `${k.name} (${k.path})` }))} /> + + Подключение к БД + + + Подключаться к БД через SSH-туннель (через main_server объекта) + )