haiku вайб

This commit is contained in:
dv 2026-04-08 11:02:12 +03:00
parent 8d41095ac3
commit cdee38d433
2 changed files with 157 additions and 1 deletions

View file

@ -26,7 +26,7 @@ DEVICE_ROLES = ("plate", "face", "overview", "other")
DEVICE_LOCATIONS = ("server_room", "street") DEVICE_LOCATIONS = ("server_room", "street")
DEVICE_STATUSES = ("online", "offline", "unknown") DEVICE_STATUSES = ("online", "offline", "unknown")
DEVICE_SOURCES = ("auto_sync", "manual", "csv_import", "sheets_sync") DEVICE_SOURCES = ("auto_sync", "manual", "csv_import", "sheets_sync", "auto_ssh")
class Device(Base, TimestampMixin): class Device(Base, TimestampMixin):

View file

@ -155,6 +155,73 @@ async def _read_remote_config(
return result.stdout return result.stdout
async def _discover_mikrotik_via_ssh_client(
host: str,
port: int,
username: str,
password: str,
) -> str | None:
"""
Discover MikroTik router IP via SSH_CLIENT environment variable.
When connected via L2TP MikroTik main_server, the SSH_CLIENT env var
shows the client IP connecting to the server (MikroTik's L2TP interface IP).
"""
try:
async with asyncssh.connect(
host,
port=port,
username=username,
password=password,
known_hosts=None,
connect_timeout=10,
) as conn:
result = await conn.run("echo $SSH_CLIENT", check=False)
ssh_client_output = result.stdout.strip()
if ssh_client_output:
# SSH_CLIENT format: "client_ip client_port server_port"
parts = ssh_client_output.split()
if parts:
return parts[0]
except Exception as e:
print(f"[ssh_discovery] Failed to discover MikroTik via SSH_CLIENT: {e}")
return None
async def _discover_proxmox_via_port_scan(
host: str,
port: int,
username: str,
password: str,
) -> list[str]:
"""
Discover Proxmox servers on the local subnet by scanning port 8006.
Runs a subnet scanning script that checks port 8006 on all IPs in the subnet.
"""
try:
async with asyncssh.connect(
host,
port=port,
username=username,
password=password,
known_hosts=None,
connect_timeout=10,
) as conn:
# Scan script: determine subnet and scan all IPs for port 8006
scan_script = (
"subnet=$(ip -o -f inet addr show | awk '!/ lo |docker|br-|wg/ {print $4; exit}' | "
"cut -d/ -f1 | awk -F. '{print $1\".\"$2\".\"$3}') && "
"(set +m; for ip in $subnet.{1..254}; do "
"timeout 0.3 bash -c \"echo > /dev/tcp/$ip/8006\" 2>/dev/null && echo \"$ip\" & "
"done; wait)"
)
result = await conn.run(scan_script, check=False)
ips = [line.strip() for line in result.stdout.strip().split('\n') if line.strip()]
return ips
except Exception as e:
print(f"[ssh_discovery] Failed to discover Proxmox servers via port scan: {e}")
return []
def _parse_db_uri(uri: str) -> dict | None: def _parse_db_uri(uri: str) -> dict | None:
"""Parse postgresql+psycopg2://user:pass@host:port/dbname → dict.""" """Parse postgresql+psycopg2://user:pass@host:port/dbname → dict."""
try: try:
@ -400,7 +467,9 @@ async def discover_via_ssh(
result.errors.append(f"SSH to {rack_host} failed: {exc}") result.errors.append(f"SSH to {rack_host} failed: {exc}")
continue continue
print(f"[ssh_discovery] Processing rack {rack_host} ({rack_name})")
devices_found = _parse_config(config_text, rack_host) devices_found = _parse_config(config_text, rack_host)
print(f"[ssh_discovery] Found {len(devices_found)} devices from plugins in {rack_host}")
for found in devices_found: for found in devices_found:
# Hostname: "rack{rack_name}-{plugin_name}", e.g. "rack01-camera_grz" # Hostname: "rack{rack_name}-{plugin_name}", e.g. "rack01-camera_grz"
@ -427,6 +496,9 @@ async def discover_via_ssh(
if existing.hostname != hostname: if existing.hostname != hostname:
existing.hostname = hostname existing.hostname = hostname
changed = True changed = True
action = "UPDATED" if changed else "SKIPPED"
print(f"[ssh_discovery] Device {found.ip} ({hostname}) - {action}")
result.updated += 1 if changed else 0 result.updated += 1 if changed else 0
result.skipped += 0 if changed else 1 result.skipped += 0 if changed else 1
else: else:
@ -439,8 +511,10 @@ async def discover_via_ssh(
source="auto_ssh", source="auto_ssh",
device_meta={"plugin": found.plugin_name, "cls": found.cls}, device_meta={"plugin": found.plugin_name, "cls": found.cls},
)) ))
print(f"[ssh_discovery] Device {found.ip} ({hostname}) - ADDED")
result.created += 1 result.created += 1
except Exception as exc: except Exception as exc:
print(f"[ssh_discovery] Error upserting {found.ip}: {exc}")
result.errors.append(f"Error upserting {found.ip}: {exc}") result.errors.append(f"Error upserting {found.ip}: {exc}")
# Flush after each rack so that subsequent SELECTs (for other racks # Flush after each rack so that subsequent SELECTs (for other racks
@ -452,4 +526,86 @@ async def discover_via_ssh(
except Exception as exc: except Exception as exc:
result.errors.append(f"Flush error after rack {rack_host}: {exc}") result.errors.append(f"Flush error after rack {rack_host}: {exc}")
# Step 3: Discover MikroTik via SSH_CLIENT
print(f"[ssh_discovery] Attempting to discover MikroTik via SSH_CLIENT on {obj.server_ip or 'main_server'}")
mikrotik_ip = await _discover_mikrotik_via_ssh_client(
host=obj.server_ip or rack_hosts[0][1],
port=obj.ssh_port or 22,
username=obj.ssh_user,
password=ssh_password,
)
if mikrotik_ip:
print(f"[ssh_discovery] Discovered MikroTik at {mikrotik_ip}")
try:
existing = (await db.execute(
select(Device).where(
Device.object_id == obj.id,
Device.ip == mikrotik_ip,
)
)).scalar_one_or_none()
if existing is not None:
print(f"[ssh_discovery] Device {mikrotik_ip} (mikrotik) - SKIPPED")
result.skipped += 1
else:
db.add(Device(
object_id=obj.id,
ip=mikrotik_ip,
hostname="mikrotik",
category="router",
role="other",
source="auto_ssh",
))
print(f"[ssh_discovery] Device {mikrotik_ip} (mikrotik) - ADDED")
result.created += 1
except Exception as exc:
print(f"[ssh_discovery] Error upserting {mikrotik_ip}: {exc}")
result.errors.append(f"Error upserting {mikrotik_ip}: {exc}")
await db.flush()
else:
print(f"[ssh_discovery] No MikroTik found via SSH_CLIENT")
# Step 4: Discover Proxmox via port 8006 scan
print(f"[ssh_discovery] Attempting to discover Proxmox servers via port 8006 scan on {obj.server_ip or 'main_server'}")
proxmox_ips = await _discover_proxmox_via_port_scan(
host=obj.server_ip or rack_hosts[0][1],
port=obj.ssh_port or 22,
username=obj.ssh_user,
password=ssh_password,
)
if proxmox_ips:
print(f"[ssh_discovery] Discovered {len(proxmox_ips)} Proxmox server(s): {proxmox_ips}")
for idx, proxmox_ip in enumerate(proxmox_ips, 1):
hostname = f"proxmox_{idx}"
try:
existing = (await db.execute(
select(Device).where(
Device.object_id == obj.id,
Device.ip == proxmox_ip,
)
)).scalar_one_or_none()
if existing is not None:
print(f"[ssh_discovery] Device {proxmox_ip} ({hostname}) - SKIPPED")
result.skipped += 1
else:
db.add(Device(
object_id=obj.id,
ip=proxmox_ip,
hostname=hostname,
category="vm",
role="other",
source="auto_ssh",
))
print(f"[ssh_discovery] Device {proxmox_ip} ({hostname}) - ADDED")
result.created += 1
except Exception as exc:
print(f"[ssh_discovery] Error upserting {proxmox_ip}: {exc}")
result.errors.append(f"Error upserting {proxmox_ip}: {exc}")
await db.flush()
else:
print(f"[ssh_discovery] No Proxmox servers found via port 8006 scan")
return result return result