This commit is contained in:
dv 2026-04-08 00:00:58 +03:00
parent c724a1c175
commit 48196baed5
2 changed files with 146 additions and 29 deletions

View file

@ -155,32 +155,93 @@ async def _read_remote_config(
return result.stdout return result.stdout
async def _get_rack_hosts(obj: Object, db_password: str) -> list[tuple[str, str]]: def _parse_db_uri(uri: str) -> dict | None:
"""Return list of (external_id, host_ip) from the object's racks table.""" """Parse postgresql+psycopg2://user:pass@host:port/dbname → dict."""
if obj.db_via_tunnel and obj.server_ip and obj.ssh_user and obj.ssh_pass_enc: try:
ssh_password = decrypt(obj.ssh_pass_enc) parsed = urlparse(uri)
async with asyncssh.connect( if not parsed.hostname:
obj.server_ip, return None
port=obj.ssh_port or 22, return {
username=obj.ssh_user, "host": parsed.hostname,
"port": parsed.port or 5432,
"user": parsed.username or "",
"password": parsed.password or "",
"dbname": (parsed.path or "").lstrip("/"),
}
except Exception:
return None
async def _autodetect_db_from_server(
server_ip: str,
ssh_port: int,
ssh_user: str,
ssh_password: str,
server_config_path: str = "/etc/caps/config.ini",
) -> dict | None:
"""SSH into server, read its caps.conf, extract DB connection params."""
try:
config_text = await _read_remote_config(
host=server_ip,
port=ssh_port,
username=ssh_user,
password=ssh_password, password=ssh_password,
config_path=server_config_path,
)
except Exception:
return None
parser = RawConfigParser()
parser.read_string(config_text)
db_uri = parser.get("server", "db_uri", fallback=None)
if db_uri:
return _parse_db_uri(db_uri)
# Fallback: try [database] section with individual fields
if parser.has_section("database"):
return {
"host": parser.get("database", "host", fallback=None),
"port": int(parser.get("database", "port", fallback="5432")),
"user": parser.get("database", "user", fallback=None),
"password": parser.get("database", "password", fallback=None),
"dbname": parser.get("database", "name", fallback=None),
}
return None
async def _get_rack_hosts_direct(
host: str,
port: int,
user: str,
password: str,
dbname: str,
table: str,
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,
) -> list[tuple[str, str]]:
"""Return list of (external_id, host_ip) 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, known_hosts=None,
) as tunnel: ) as tunnel:
listener = await tunnel.forward_local_port( listener = await tunnel.forward_local_port("127.0.0.1", 0, host, port)
"127.0.0.1", 0, obj.db_host, obj.db_port or 5432
)
local_port = listener.get_port() local_port = listener.get_port()
dsn = ( dsn = (
f"host=127.0.0.1 port={local_port} dbname={obj.db_name} " f"host=127.0.0.1 port={local_port} dbname={dbname} "
f"user={obj.db_user} password={db_password} connect_timeout=10" f"user={user} password={password} connect_timeout=10"
) )
return await _fetch_rack_hosts(dsn, obj.db_table) return await _fetch_rack_hosts(dsn, table)
else: else:
dsn = ( dsn = f"host={host} port={port} dbname={dbname} user={user} password={password} connect_timeout=10"
f"host={obj.db_host} port={obj.db_port or 5432} dbname={obj.db_name} " return await _fetch_rack_hosts(dsn, table)
f"user={obj.db_user} password={db_password} connect_timeout=10"
)
return await _fetch_rack_hosts(dsn, obj.db_table)
async def _fetch_rack_hosts(dsn: str, table: str) -> list[tuple[str, str]]: async def _fetch_rack_hosts(dsn: str, table: str) -> list[tuple[str, str]]:
@ -205,27 +266,82 @@ class DiscoveryResult:
async def discover_via_ssh( async def discover_via_ssh(
db: AsyncSession, db: AsyncSession,
obj: Object, obj: Object,
config_path: str = "/etc/caps/caps.conf", config_path: str = "/etc/caps/config.ini",
) -> DiscoveryResult: ) -> DiscoveryResult:
result = DiscoveryResult() result = DiscoveryResult()
if not all([obj.db_host, obj.db_name, obj.db_user, obj.db_pass_enc, obj.db_table]):
result.errors.append("Object DB connection is not fully configured")
return result
if not obj.ssh_user or not obj.ssh_pass_enc: if not obj.ssh_user or not obj.ssh_pass_enc:
result.errors.append("SSH credentials not configured on this object") result.errors.append("SSH credentials not configured on this object (ssh_user / ssh_password)")
return result return result
try: try:
db_password = decrypt(obj.db_pass_enc)
ssh_password = decrypt(obj.ssh_pass_enc) ssh_password = decrypt(obj.ssh_pass_enc)
except Exception: except Exception:
result.errors.append("Failed to decrypt stored credentials") result.errors.append("Failed to decrypt SSH password")
return result
ssh_port = obj.ssh_port or 22
# ── Resolve DB connection ─────────────────────────────────────────────────
# Priority 1: fully configured DB on the object
# Priority 2: autodetect from server config via SSH (only server_ip needed)
db_host: str | None = obj.db_host
db_port: int = obj.db_port or 5432
db_name: str | None = obj.db_name
db_user: str | None = obj.db_user
db_password: str | None = None
db_table: str | None = obj.db_table
if db_host and db_name and db_user and obj.db_pass_enc and db_table:
try:
db_password = decrypt(obj.db_pass_enc)
except Exception:
result.errors.append("Failed to decrypt DB password")
return result
elif obj.server_ip:
# Autodetect: SSH into server, read its caps.conf to get DB URI
detected = await _autodetect_db_from_server(
server_ip=obj.server_ip,
ssh_port=ssh_port,
ssh_user=obj.ssh_user,
ssh_password=ssh_password,
)
if not detected or not all([detected.get("host"), detected.get("user"), detected.get("dbname")]):
result.errors.append(
"DB connection not configured and could not be auto-detected from server config. "
"Fill in the DB connection fields on the object or ensure /etc/caps/config.ini "
"on the server contains a [server] db_uri field."
)
return result
db_host = detected["host"]
db_port = detected["port"]
db_user = detected["user"]
db_password = detected["password"]
db_name = detected["dbname"]
db_table = db_table or obj.db_table or "racks"
else:
result.errors.append(
"DB connection not configured. Either fill in the DB fields on the object "
"or set server_ip so the DB can be auto-detected via SSH."
)
return result return result
# Step 1: get rack hosts from object's DB # Step 1: get rack hosts from object's DB
try: try:
rack_hosts = await _get_rack_hosts(obj, db_password) rack_hosts = await _get_rack_hosts_direct(
host=db_host,
port=db_port,
user=db_user,
password=db_password or "",
dbname=db_name,
table=db_table,
via_tunnel=obj.db_via_tunnel,
tunnel_host=obj.server_ip,
tunnel_ssh_port=ssh_port,
tunnel_ssh_user=obj.ssh_user,
tunnel_ssh_password=ssh_password,
)
except Exception as exc: except Exception as exc:
result.errors.append(f"Cannot fetch rack hosts from object DB: {exc}") result.errors.append(f"Cannot fetch rack hosts from object DB: {exc}")
return result return result

View file

@ -64,9 +64,10 @@ export function useMonitorSSE(handlers: MonitorHandlers) {
// ignore parse errors // ignore parse errors
} }
}, },
onerror() { onerror(err) {
// fetchEventSource retries automatically; abort only on explicit error // throw — stops fetchEventSource from auto-retrying
ctrl.abort() ctrl.abort()
throw err
}, },
}) })