fix ssh
This commit is contained in:
parent
c724a1c175
commit
48196baed5
2 changed files with 146 additions and 29 deletions
|
|
@ -155,32 +155,93 @@ async def _read_remote_config(
|
|||
return result.stdout
|
||||
|
||||
|
||||
async def _get_rack_hosts(obj: Object, db_password: str) -> list[tuple[str, str]]:
|
||||
"""Return list of (external_id, host_ip) from the object's racks table."""
|
||||
if obj.db_via_tunnel and obj.server_ip and obj.ssh_user and obj.ssh_pass_enc:
|
||||
ssh_password = decrypt(obj.ssh_pass_enc)
|
||||
async with asyncssh.connect(
|
||||
obj.server_ip,
|
||||
port=obj.ssh_port or 22,
|
||||
username=obj.ssh_user,
|
||||
def _parse_db_uri(uri: str) -> dict | None:
|
||||
"""Parse postgresql+psycopg2://user:pass@host:port/dbname → dict."""
|
||||
try:
|
||||
parsed = urlparse(uri)
|
||||
if not parsed.hostname:
|
||||
return None
|
||||
return {
|
||||
"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,
|
||||
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,
|
||||
) as tunnel:
|
||||
listener = await tunnel.forward_local_port(
|
||||
"127.0.0.1", 0, obj.db_host, obj.db_port or 5432
|
||||
)
|
||||
listener = await tunnel.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={obj.db_name} "
|
||||
f"user={obj.db_user} password={db_password} connect_timeout=10"
|
||||
f"host=127.0.0.1 port={local_port} dbname={dbname} "
|
||||
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:
|
||||
dsn = (
|
||||
f"host={obj.db_host} port={obj.db_port or 5432} dbname={obj.db_name} "
|
||||
f"user={obj.db_user} password={db_password} connect_timeout=10"
|
||||
)
|
||||
return await _fetch_rack_hosts(dsn, obj.db_table)
|
||||
dsn = f"host={host} port={port} dbname={dbname} user={user} password={password} connect_timeout=10"
|
||||
return await _fetch_rack_hosts(dsn, table)
|
||||
|
||||
|
||||
async def _fetch_rack_hosts(dsn: str, table: str) -> list[tuple[str, str]]:
|
||||
|
|
@ -205,27 +266,82 @@ class DiscoveryResult:
|
|||
async def discover_via_ssh(
|
||||
db: AsyncSession,
|
||||
obj: Object,
|
||||
config_path: str = "/etc/caps/caps.conf",
|
||||
config_path: str = "/etc/caps/config.ini",
|
||||
) -> 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:
|
||||
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
|
||||
|
||||
try:
|
||||
db_password = decrypt(obj.db_pass_enc)
|
||||
ssh_password = decrypt(obj.ssh_pass_enc)
|
||||
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
|
||||
|
||||
# Step 1: get rack hosts from object's DB
|
||||
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:
|
||||
result.errors.append(f"Cannot fetch rack hosts from object DB: {exc}")
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -64,9 +64,10 @@ export function useMonitorSSE(handlers: MonitorHandlers) {
|
|||
// ignore parse errors
|
||||
}
|
||||
},
|
||||
onerror() {
|
||||
// fetchEventSource retries automatically; abort only on explicit error
|
||||
onerror(err) {
|
||||
// throw — stops fetchEventSource from auto-retrying
|
||||
ctrl.abort()
|
||||
throw err
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue