Files
Nexus/server/utils/server_alert_ips.py
T

140 lines
4.3 KiB
Python
Raw Normal View History

"""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)