140 lines
4.3 KiB
Python
140 lines
4.3 KiB
Python
|
|
"""Resolve public/private IPs for server alert Telegram messages."""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import ipaddress
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
logger = logging.getLogger("nexus.server_alert_ips")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def is_public_ip(value: str | None) -> bool:
|
|||
|
|
text = (value or "").strip()
|
|||
|
|
if not text:
|
|||
|
|
return False
|
|||
|
|
try:
|
|||
|
|
addr = ipaddress.ip_address(text)
|
|||
|
|
except ValueError:
|
|||
|
|
return False
|
|||
|
|
return not (
|
|||
|
|
addr.is_private
|
|||
|
|
or addr.is_loopback
|
|||
|
|
or addr.is_link_local
|
|||
|
|
or addr.is_reserved
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def is_private_ip(value: str | None) -> bool:
|
|||
|
|
text = (value or "").strip()
|
|||
|
|
if not text:
|
|||
|
|
return False
|
|||
|
|
try:
|
|||
|
|
addr = ipaddress.ip_address(text)
|
|||
|
|
except ValueError:
|
|||
|
|
return False
|
|||
|
|
return addr.is_private or addr.is_loopback or addr.is_link_local
|
|||
|
|
|
|||
|
|
|
|||
|
|
def classify_domain_ip(domain: str | None) -> tuple[str | None, str | None]:
|
|||
|
|
"""Return (public_ip, private_ip) when domain is a literal IP."""
|
|||
|
|
text = (domain or "").strip()
|
|||
|
|
if not text:
|
|||
|
|
return None, None
|
|||
|
|
try:
|
|||
|
|
ipaddress.ip_address(text)
|
|||
|
|
except ValueError:
|
|||
|
|
return None, None
|
|||
|
|
if is_public_ip(text):
|
|||
|
|
return text, None
|
|||
|
|
if is_private_ip(text):
|
|||
|
|
return None, text
|
|||
|
|
return None, None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def merge_alert_ips(
|
|||
|
|
*,
|
|||
|
|
domain: str | None = None,
|
|||
|
|
extra_attrs: dict[str, Any] | None = None,
|
|||
|
|
system_info: dict[str, Any] | None = None,
|
|||
|
|
) -> tuple[str | None, str | None]:
|
|||
|
|
"""Pick public/private IPs for Telegram display.
|
|||
|
|
|
|||
|
|
外网 SSOT:servers.domain 为公网 IP 时优先(运维登记连接地址,如 120.79.11.13)。
|
|||
|
|
内网:Agent 上报 private_ip 优先,否则 domain 为私网 IP 时使用。
|
|||
|
|
Agent ipify 仅作 domain/extra 均无公网时的兜底。
|
|||
|
|
"""
|
|||
|
|
public_ip: str | None = None
|
|||
|
|
private_ip: str | None = None
|
|||
|
|
|
|||
|
|
info = system_info if isinstance(system_info, dict) else {}
|
|||
|
|
attrs = extra_attrs if isinstance(extra_attrs, dict) else {}
|
|||
|
|
dom_pub, dom_priv = classify_domain_ip(domain)
|
|||
|
|
|
|||
|
|
if dom_pub:
|
|||
|
|
public_ip = dom_pub
|
|||
|
|
|
|||
|
|
if not public_ip:
|
|||
|
|
for key in ("public_ip", "wan_ip", "external_ip"):
|
|||
|
|
raw = attrs.get(key)
|
|||
|
|
if raw and is_public_ip(str(raw)):
|
|||
|
|
public_ip = str(raw).strip()
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not public_ip:
|
|||
|
|
for key in ("public_ip", "wan_ip", "external_ip"):
|
|||
|
|
raw = info.get(key)
|
|||
|
|
if raw and is_public_ip(str(raw)):
|
|||
|
|
public_ip = str(raw).strip()
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
for key in ("private_ip", "lan_ip", "internal_ip"):
|
|||
|
|
raw = info.get(key)
|
|||
|
|
if raw and is_private_ip(str(raw)):
|
|||
|
|
private_ip = str(raw).strip()
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not private_ip and dom_priv:
|
|||
|
|
private_ip = dom_priv
|
|||
|
|
|
|||
|
|
return public_ip, private_ip
|
|||
|
|
|
|||
|
|
|
|||
|
|
def format_alert_ip_line(public_ip: str | None, private_ip: str | None) -> str:
|
|||
|
|
pub = (public_ip or "").strip() or "—"
|
|||
|
|
priv = (private_ip or "").strip() or "—"
|
|||
|
|
return f"IP地址:{pub}(外) {priv}(内)"
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def resolve_alert_ips_for_server(server_id: int) -> tuple[str | None, str | None]:
|
|||
|
|
"""Load server row + Redis heartbeat system_info for Telegram IP lines."""
|
|||
|
|
from server.infrastructure.database.server_repo import ServerRepositoryImpl
|
|||
|
|
from server.infrastructure.database.session import AsyncSessionLocal
|
|||
|
|
from server.infrastructure.redis.client import get_redis
|
|||
|
|
|
|||
|
|
domain: str | None = None
|
|||
|
|
extra_attrs: dict[str, Any] | None = None
|
|||
|
|
system_info: dict[str, Any] | None = None
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
async with AsyncSessionLocal() as session:
|
|||
|
|
server = await ServerRepositoryImpl(session).get_by_id(server_id)
|
|||
|
|
if server:
|
|||
|
|
domain = server.domain
|
|||
|
|
extra_attrs = server.extra_attrs if isinstance(server.extra_attrs, dict) else None
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("resolve_alert_ips DB read failed server_id=%s: %s", server_id, e)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
redis = get_redis()
|
|||
|
|
raw = await redis.hget(f"heartbeat:{server_id}", "system_info")
|
|||
|
|
if raw:
|
|||
|
|
parsed = json.loads(raw)
|
|||
|
|
if isinstance(parsed, dict):
|
|||
|
|
system_info = parsed
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.debug("resolve_alert_ips Redis read failed server_id=%s: %s", server_id, e)
|
|||
|
|
|
|||
|
|
return merge_alert_ips(domain=domain, extra_attrs=extra_attrs, system_info=system_info)
|