fix(cluster): корректное извлечение fingerprint из nodelist при join
All checks were successful
Backend Tests / pytest (Python 3.12) (push) Successful in 56s
Deploy to Production / Trigger production update (push) Successful in 8s

GET /cluster/config/join не возвращает fingerprint на верхнем уровне —
он лежит в nodelist[].pve_fp. Добавлен хелпер _extract_fingerprint,
который выбирает pve_fp целевой ноды (по IP), с фолбэком на
preferred_node и первую ноду. Устраняет ошибку 502
"Не удалось получить fingerprint кластера" при присоединении нод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
markmorado
2026-07-09 03:07:53 +05:00
parent 0d056a89a5
commit 3b10e903a4

View File

@@ -31,6 +31,51 @@ def _get_server_or_404(db: Session, server_id: int) -> ProxmoxServer:
return server
def _extract_fingerprint(join_data: dict, target_ip: str = None) -> Optional[str]:
"""
Достать SHA-256 fingerprint из ответа GET /cluster/config/join.
В Proxmox API нет поля `fingerprint` на верхнем уровне — оно лежит в
nodelist[].pve_fp у каждой ноды. Выбираем запись целевой кластерной ноды
по pve_addr/ring0_addr == target_ip, с фолбэком на preferred_node и на
первую ноду с непустым pve_fp.
"""
if not isinstance(join_data, dict):
return None
# Совместимость: если провайдер всё же вернул fingerprint сверху — берём его.
top = join_data.get("fingerprint")
if top:
return top
nodelist = join_data.get("nodelist") or []
def _fp(entry: dict) -> Optional[str]:
return entry.get("pve_fp") if isinstance(entry, dict) else None
# 1) По IP целевой ноды
if target_ip:
for entry in nodelist:
if not isinstance(entry, dict):
continue
if target_ip in (entry.get("pve_addr"), entry.get("ring0_addr")) and _fp(entry):
return entry["pve_fp"]
# 2) По preferred_node
preferred = join_data.get("preferred_node")
if preferred:
for entry in nodelist:
if isinstance(entry, dict) and entry.get("name") == preferred and _fp(entry):
return entry["pve_fp"]
# 3) Первая нода с непустым pve_fp
for entry in nodelist:
if _fp(entry):
return entry["pve_fp"]
return None
def _get_password_client(server: ProxmoxServer, override_password: str = None) -> ProxmoxClient:
"""
Build a ProxmoxClient that uses password auth (required for cluster ops).
@@ -200,7 +245,7 @@ def get_cluster_join_info(
"server_id": server_id,
"server_name": server.name,
"cluster_name": totem.get("cluster_name") or server.cluster_name,
"fingerprint": data.get("fingerprint"),
"fingerprint": _extract_fingerprint(data, target_ip=server.ip_address),
"nodelist": data.get("nodelist", []),
"config_digest": data.get("config_digest"),
"preferred_node_ip": server.ip_address,
@@ -525,7 +570,7 @@ async def join_cluster(
)
join_data = join_info_result.get("data", {})
fingerprint = join_data.get("fingerprint")
fingerprint = _extract_fingerprint(join_data, target_ip=cluster_server.ip_address)
cluster_name = join_data.get("totem", {}).get("cluster_name") or cluster_server.cluster_name
if not fingerprint: