mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
fix(monitor): handle a PBS on the same host and complete the PBS views
- Open a PBS installed on this host on the address in use, without probing it - Never carry or offer localhost addresses between Monitors - Read the PBS of a PVE+PBS host in Backup Server, with its hostname and version - Offer the PBS storages without a connection, with address and fingerprint - Show the disks and pools of a PBS in its Datastores tab - Hide guest and PVE storage settings on a PBS - Show a clearer status line for a PBS
This commit is contained in:
@@ -24,11 +24,30 @@ def platform():
|
||||
@require_auth
|
||||
def servers():
|
||||
try:
|
||||
return jsonify(servers=pbs.public_servers(), local_available=pbs.is_pbs_host())
|
||||
servers = pbs.public_servers()
|
||||
return jsonify(servers=servers, local_available=pbs.local_available(), detected=_detected(servers))
|
||||
except pbs.PBSError as exc:
|
||||
return jsonify(error=str(exc)), 500
|
||||
|
||||
|
||||
def _detected(servers):
|
||||
"""PBS storages of this PVE that have no monitoring connection yet."""
|
||||
if pbs.is_pbs_host():
|
||||
return []
|
||||
import monitor_links
|
||||
from urllib.parse import urlsplit
|
||||
known = {urlsplit(row.get("url", "")).hostname for row in servers}
|
||||
found = {}
|
||||
for row in monitor_links._storage_pbs():
|
||||
host = row["server"]
|
||||
if host in known or monitor_links._is_this_host(host):
|
||||
continue
|
||||
port = row.get("port") if str(row.get("port", "")).isdigit() else monitor_links.PBS_UI_PORT
|
||||
url = monitor_links._host_url(host, port, "https")
|
||||
found.setdefault(url, {"name": row["name"], "url": url, "fingerprint": row.get("fingerprint", "")})
|
||||
return list(found.values())
|
||||
|
||||
|
||||
def _protected_configuration():
|
||||
local = auth_manager.load_auth_config()
|
||||
return local.get("enabled") and not local.get("declined") and local.get("password_hash")
|
||||
|
||||
@@ -107,6 +107,32 @@ def _is_private(host):
|
||||
return False
|
||||
|
||||
|
||||
def _own_addresses():
|
||||
addresses = set()
|
||||
try:
|
||||
import psutil
|
||||
for entries in psutil.net_if_addrs().values():
|
||||
for entry in entries:
|
||||
if entry.family in (socket.AF_INET, socket.AF_INET6):
|
||||
addresses.add(entry.address.split("%")[0])
|
||||
except Exception:
|
||||
pass
|
||||
return addresses
|
||||
|
||||
|
||||
def _is_this_host(host):
|
||||
"""A PBS installed next to PVE is named localhost or one of the host's own addresses."""
|
||||
try:
|
||||
resolved = {info[4][0].split("%")[0] for info in socket.getaddrinfo(host, None)}
|
||||
except OSError:
|
||||
return False
|
||||
own = _own_addresses()
|
||||
try:
|
||||
return bool(resolved) and all(ipaddress.ip_address(a).is_loopback or a in own for a in resolved)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _cluster_nodes():
|
||||
try:
|
||||
result = subprocess.run(["pvesh", "get", "/cluster/status", "--output-format", "json"],
|
||||
@@ -135,7 +161,7 @@ def _storage_pbs():
|
||||
if current is not None:
|
||||
servers.append(current)
|
||||
continue
|
||||
option = re.match(r"^\s+(server|port)\s+(\S+)", line)
|
||||
option = re.match(r"^\s+(server|port|fingerprint)\s+(\S+)", line)
|
||||
if current is not None and option:
|
||||
current[option.group(1)] = option.group(2)
|
||||
return [row for row in servers if row.get("server")]
|
||||
@@ -154,8 +180,17 @@ def _pbs_servers():
|
||||
found.setdefault(host, {"name": row.get("name") or host, "native_url": normalize_url(row["url"])})
|
||||
except Exception:
|
||||
pass
|
||||
return [{"key": f"pbs:{host}", "name": info["name"], "product": "pbs", "group": "pbs",
|
||||
"host": host, "native_url": info["native_url"]} for host, info in found.items()]
|
||||
servers, local = [], None
|
||||
for host, info in found.items():
|
||||
if _is_this_host(host):
|
||||
# Its web interface is on the address the browser already uses.
|
||||
if local is None:
|
||||
local = {"key": "pbs:local", "name": info["name"], "product": "pbs", "group": "pbs", "local": True}
|
||||
servers.append(local)
|
||||
continue
|
||||
servers.append({"key": f"pbs:{host}", "name": info["name"], "product": "pbs", "group": "pbs",
|
||||
"host": host, "native_url": info["native_url"]})
|
||||
return servers
|
||||
|
||||
|
||||
def _probe(url):
|
||||
@@ -188,6 +223,8 @@ def _resolve(entry, override):
|
||||
return override["url"], "online" if _probe(override["url"]) else "offline"
|
||||
if entry.get("url"):
|
||||
return entry["url"], "online" if _probe(entry["url"]) else "offline"
|
||||
if entry.get("local"):
|
||||
return None, "local"
|
||||
host = entry["host"]
|
||||
if not _is_private(host):
|
||||
return None, "unknown"
|
||||
@@ -218,6 +255,7 @@ def list_links(include_hidden=False):
|
||||
links.append({"key": entry["key"], "name": entry["name"], "product": entry["product"],
|
||||
"group": entry["group"], "url": url, "status": state,
|
||||
"native_url": entry.get("native_url"), "manual": bool(entry.get("manual")),
|
||||
"local": bool(entry.get("local")),
|
||||
"hidden": bool(override.get("hidden")), "custom_url": bool(override.get("url"))})
|
||||
return {"self": {"name": platform.get("hostname") or socket.gethostname(), "product": platform.get("product")},
|
||||
"links": links}
|
||||
|
||||
@@ -11,6 +11,7 @@ from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -53,6 +54,26 @@ def is_pbs_host():
|
||||
return get_platform().get("product") == "pbs"
|
||||
|
||||
|
||||
def local_available():
|
||||
"""This host runs PBS: as its product, or installed next to PVE."""
|
||||
if is_pbs_host():
|
||||
return True
|
||||
return bool(shutil.which("proxmox-backup-manager")) and any(
|
||||
Path(folder, "proxmox-backup-proxy.service").exists()
|
||||
for folder in ("/lib/systemd/system", "/usr/lib/systemd/system"))
|
||||
|
||||
|
||||
def _local_version():
|
||||
if is_pbs_host():
|
||||
return get_platform().get("version")
|
||||
try:
|
||||
result = subprocess.run(["dpkg-query", "-W", "-f=${Version}", "proxmox-backup-server"],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
return result.stdout.strip() or None if result.returncode == 0 else None
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
|
||||
|
||||
def _servers_path():
|
||||
return auth_manager.CONFIG_DIR / "pbs_servers.json"
|
||||
|
||||
@@ -248,9 +269,9 @@ def get_status(identifier):
|
||||
return cached[1]
|
||||
errors = {}
|
||||
if identifier == "local":
|
||||
if not is_pbs_host():
|
||||
if not local_available():
|
||||
raise PBSError("The local host is not a PBS server.")
|
||||
name, version = "Local PBS", get_platform().get("version")
|
||||
name, version = get_platform().get("hostname") or socket.gethostname(), _local_version()
|
||||
services = _local_services()
|
||||
operations = {"datastores": _local_datastores,
|
||||
"tasks": lambda: _cli("task", "list", "--all", "true", "--limit", "20"),
|
||||
|
||||
@@ -62,6 +62,29 @@ class MonitorLinksTests(unittest.TestCase):
|
||||
self.assertEqual(by_key["pbs:pbs.example.com"]["native_url"], "https://pbs.example.com:8443")
|
||||
self.assertFalse(any("example.com" in url for url in probed))
|
||||
|
||||
def test_pbs_installed_on_this_host_opens_its_own_interface_and_is_never_probed(self):
|
||||
links.STORAGE_CFG.write_text("pbs: PBS\n\tdatastore PBS\n\tserver localhost\n\n"
|
||||
"pbs: Same\n\tdatastore x\n\tserver 10.0.0.55\n\n"
|
||||
"pbs: Lab\n\tdatastore store1\n\tserver 10.0.0.49\n")
|
||||
probed = []
|
||||
with mock.patch.object(links, "_own_addresses", return_value={"10.0.0.55"}), \
|
||||
mock.patch.object(links, "_probe", side_effect=lambda url: probed.append(url) or False), \
|
||||
mock.patch.object(links, "_is_private", return_value=True):
|
||||
result = links.list_links()
|
||||
pbs_rows = [row for row in result["links"] if row["group"] == "pbs"]
|
||||
self.assertEqual([row["key"] for row in pbs_rows], ["pbs:local", "pbs:10.0.0.49"])
|
||||
local = pbs_rows[0]
|
||||
self.assertEqual((local["name"], local["local"], local["url"], local["status"]), ("PBS", True, None, "local"))
|
||||
self.assertIsNone(local["native_url"])
|
||||
self.assertFalse(any("localhost" in url or "10.0.0.55" in url for url in probed))
|
||||
self.assertFalse(pbs_rows[1]["local"])
|
||||
|
||||
def test_loopback_names_are_this_host(self):
|
||||
with mock.patch.object(links, "_own_addresses", return_value=set()):
|
||||
self.assertTrue(links._is_this_host("127.0.0.1"))
|
||||
self.assertTrue(links._is_this_host("::1"))
|
||||
self.assertFalse(links._is_this_host("10.0.0.49"))
|
||||
|
||||
def test_pbs_host_lists_only_manual_entries(self):
|
||||
links.pbs_manager.get_platform.return_value = {"product": "pbs", "hostname": "pbs-lab"}
|
||||
links.add_link({"name": "amd", "url": "https://10.0.0.55:8008", "product": "pve"})
|
||||
|
||||
@@ -166,6 +166,39 @@ class PBSManagerTests(unittest.TestCase):
|
||||
self.assertEqual(result["datastores"][0]["used"], 30)
|
||||
self.assertEqual(set(result["services"]), set(pbs.PBS_SERVICES))
|
||||
|
||||
def test_pbs_installed_next_to_pve_is_read_locally_with_its_own_version(self):
|
||||
def run(command, **kwargs):
|
||||
if command[0] == "dpkg-query":
|
||||
return SimpleNamespace(stdout="4.2.8-1", returncode=0)
|
||||
return SimpleNamespace(stdout="[]", returncode=0)
|
||||
with mock.patch.object(pbs, "get_platform", return_value={"product": "pve", "version": "9.1.4", "hostname": "rimegra"}), \
|
||||
mock.patch.object(pbs.shutil, "which", return_value="/usr/sbin/proxmox-backup-manager"), \
|
||||
mock.patch.object(pbs.Path, "exists", return_value=True), \
|
||||
mock.patch.object(pbs, "_local_services", return_value={key: "active" for key in pbs.PBS_SERVICES}), \
|
||||
mock.patch.object(pbs.subprocess, "run", side_effect=run):
|
||||
self.assertTrue(pbs.local_available())
|
||||
result = pbs.get_status("local")
|
||||
self.assertEqual((result["name"], result["version"]), ("rimegra", "4.2.8-1"))
|
||||
with mock.patch.object(pbs, "get_platform", return_value={"product": "pve"}), \
|
||||
mock.patch.object(pbs.shutil, "which", return_value=None):
|
||||
self.assertFalse(pbs.local_available())
|
||||
|
||||
def test_storage_pbs_without_a_connection_is_offered_with_its_address_and_fingerprint(self):
|
||||
import monitor_links
|
||||
storage = self.root / "storage.cfg"
|
||||
storage.write_text("pbs: Local\n\tserver localhost\n\tdatastore a\n\n"
|
||||
"pbs: Lab\n\tserver 10.0.0.49\n\tdatastore b\n\tfingerprint AA:BB\n\n"
|
||||
"pbs: Known\n\tserver pbs.test\n\tdatastore c\n\tport 8007\n")
|
||||
with mock.patch.object(monitor_links, "STORAGE_CFG", storage), \
|
||||
mock.patch.object(monitor_links, "_own_addresses", return_value=set()), \
|
||||
mock.patch.object(pbs, "is_pbs_host", return_value=False), \
|
||||
mock.patch.object(pbs, "local_available", return_value=False), \
|
||||
mock.patch.object(pbs, "_remote_get", return_value=[]):
|
||||
pbs.save_server(self.server)
|
||||
response = self.client.get("/api/pbs/servers", headers=self.headers)
|
||||
self.assertEqual(response.json["detected"],
|
||||
[{"name": "Lab", "url": "https://10.0.0.49:8007", "fingerprint": "AA:BB"}])
|
||||
|
||||
def test_connection_api_requires_full_admin_and_does_not_return_secret(self):
|
||||
self.assertEqual(self.client.get("/api/pbs/servers").status_code, 401)
|
||||
with mock.patch.object(pbs, "_remote_get", return_value=[]):
|
||||
|
||||
Reference in New Issue
Block a user