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})` }))}
/>
+
+