mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
fix(monitor): read the node name without waiting for the Proxmox API
This commit is contained in:
@@ -113,16 +113,17 @@ logging.basicConfig(
|
||||
# -------------------------------------------------------------------
|
||||
_PROXMOX_NODE_CACHE = {"name": None, "timestamp": 0.0}
|
||||
_PROXMOX_NODE_CACHE_TTL = 300 # seconds (5 minutes)
|
||||
_PVE_LOCAL_NODE_LINK = "/etc/pve/local"
|
||||
|
||||
|
||||
def get_proxmox_node_name() -> str:
|
||||
"""
|
||||
Retrieve the real Proxmox node name for the LOCAL node.
|
||||
The name of the LOCAL node in Proxmox.
|
||||
|
||||
- First tries reading from: `pvesh get /nodes`
|
||||
- In a cluster, matches the local hostname against the node list
|
||||
- Uses an in-memory cache to avoid repeated API calls
|
||||
- Falls back to the short hostname if the API call fails
|
||||
`/etc/pve/local` is a link to the folder of the local node, on a single
|
||||
host and on every node of a cluster, so the name is read from it without
|
||||
asking the API. The short hostname is used while `/etc/pve` is not
|
||||
mounted; that answer is not kept, so the link is read again next time.
|
||||
"""
|
||||
now = time.time()
|
||||
cached_name = _PROXMOX_NODE_CACHE.get("name")
|
||||
@@ -132,52 +133,17 @@ def get_proxmox_node_name() -> str:
|
||||
if cached_name and (now - float(cached_ts)) < _PROXMOX_NODE_CACHE_TTL:
|
||||
return str(cached_name)
|
||||
|
||||
# Get local hostname for matching
|
||||
local_hostname = socket.gethostname().split(".", 1)[0].lower()
|
||||
|
||||
# Try Proxmox API
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pvesh", "get", "/nodes", "--output-format", "json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout:
|
||||
nodes = json.loads(result.stdout)
|
||||
if isinstance(nodes, list) and nodes:
|
||||
# In a cluster, find the node that matches local hostname
|
||||
# Node names in Proxmox typically match the hostname
|
||||
for node_info in nodes:
|
||||
node_name = node_info.get("node", "")
|
||||
if node_name.lower() == local_hostname:
|
||||
_PROXMOX_NODE_CACHE["name"] = node_name
|
||||
_PROXMOX_NODE_CACHE["timestamp"] = now
|
||||
return node_name
|
||||
|
||||
# If no exact match, try partial match (hostname might be truncated)
|
||||
for node_info in nodes:
|
||||
node_name = node_info.get("node", "")
|
||||
if local_hostname.startswith(node_name.lower()) or node_name.lower().startswith(local_hostname):
|
||||
_PROXMOX_NODE_CACHE["name"] = node_name
|
||||
_PROXMOX_NODE_CACHE["timestamp"] = now
|
||||
return node_name
|
||||
|
||||
# Last resort: if single node cluster, use that node
|
||||
if len(nodes) == 1:
|
||||
node_name = nodes[0].get("node")
|
||||
if node_name:
|
||||
_PROXMOX_NODE_CACHE["name"] = node_name
|
||||
_PROXMOX_NODE_CACHE["timestamp"] = now
|
||||
return node_name
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to get Proxmox node name from API: %s", exc)
|
||||
node_name = os.path.basename(os.readlink(_PVE_LOCAL_NODE_LINK))
|
||||
except OSError:
|
||||
node_name = ""
|
||||
if re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?", node_name):
|
||||
_PROXMOX_NODE_CACHE["name"] = node_name
|
||||
_PROXMOX_NODE_CACHE["timestamp"] = now
|
||||
return node_name
|
||||
|
||||
# Fallback: short hostname (without domain)
|
||||
return local_hostname
|
||||
return socket.gethostname().split(".", 1)[0].lower()
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""The name of the local node is read from the link Proxmox keeps for it, so
|
||||
finding it out never waits for the API, however slow the host."""
|
||||
import ast
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import socket
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
SERVER = Path(__file__).resolve().parents[1] / 'flask_server.py'
|
||||
|
||||
|
||||
class NodeNameTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tree = ast.parse(SERVER.read_text(encoding='utf-8'))
|
||||
nodes = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == 'get_proxmox_node_name']
|
||||
self.assertEqual(len(nodes), 1)
|
||||
self.source = ast.get_source_segment(SERVER.read_text(encoding='utf-8'), nodes[0])
|
||||
folder = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(folder.cleanup)
|
||||
self.link = os.path.join(folder.name, 'local')
|
||||
|
||||
def no_subprocess(*args, **kwargs):
|
||||
raise AssertionError('the node name must not run a command')
|
||||
|
||||
self.scope = {'os': os, 're': re, 'socket': socket, 'time': time,
|
||||
'subprocess': type('S', (), {'run': staticmethod(no_subprocess)}),
|
||||
'_PROXMOX_NODE_CACHE': {'name': None, 'timestamp': 0.0}, '_PROXMOX_NODE_CACHE_TTL': 300,
|
||||
'_PVE_LOCAL_NODE_LINK': self.link}
|
||||
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(SERVER), 'exec'), self.scope)
|
||||
self.name = self.scope['get_proxmox_node_name']
|
||||
|
||||
def test_the_name_is_the_folder_the_link_points_to(self):
|
||||
os.symlink('nodes/Hades-01', self.link)
|
||||
self.assertEqual(self.name(), 'Hades-01')
|
||||
|
||||
def test_the_api_is_not_asked(self):
|
||||
self.assertNotIn('pvesh', self.source)
|
||||
self.assertNotIn('subprocess', self.source)
|
||||
|
||||
def test_without_the_link_the_hostname_is_used_and_not_kept(self):
|
||||
hostname = socket.gethostname().split('.', 1)[0].lower()
|
||||
self.assertEqual(self.name(), hostname)
|
||||
os.symlink('nodes/pve2', self.link)
|
||||
self.assertEqual(self.name(), 'pve2')
|
||||
|
||||
def test_the_answer_is_kept_once_read(self):
|
||||
os.symlink('nodes/pve1', self.link)
|
||||
self.assertEqual(self.name(), 'pve1')
|
||||
os.unlink(self.link)
|
||||
self.assertEqual(self.name(), 'pve1')
|
||||
|
||||
def test_a_link_that_names_no_node_is_not_taken(self):
|
||||
os.symlink('nodes/bad name', self.link)
|
||||
self.assertEqual(self.name(), socket.gethostname().split('.', 1)[0].lower())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user