Files
Nexus/server/utils/server_alert_ips.py
2026-07-08 22:31:31 +08:00

140 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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.
外网 SSOTservers.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)