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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue