diff --git a/defaults.inc b/defaults.inc index e39ee32..48c82fb 100755 --- a/defaults.inc +++ b/defaults.inc @@ -3,7 +3,7 @@ action="${1:-}" FOLDER='/opt/idssys/ta-proxmenu' -VERS='2026.7.25-63' +VERS='2026.7.25-64' noupdate=' ' diff --git a/inc/ha-status.inc b/inc/ha-status.inc new file mode 100644 index 0000000..db0dc6f --- /dev/null +++ b/inc/ha-status.inc @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +TAPM_HA_NODE_IN_MAINTENANCE() { + local node="${1:-}" + local status_file="${2:-/etc/pve/ha/manager_status}" + + [[ "$node" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ ]] || return 2 + [[ -r "$status_file" ]] || return 2 + TAPM_HA_NODE="$node" python3 - "$status_file" <<'PY' +import json +import os +import sys + +try: + with open(sys.argv[1], encoding="utf-8") as status_handle: + status = json.load(status_handle) +except (OSError, TypeError, ValueError): + raise SystemExit(2) + +node = os.environ["TAPM_HA_NODE"].strip().lower() + +def contains_maintenance(value): + if isinstance(value, str): + return "maintenance" in value.lower() + if isinstance(value, dict): + return any(contains_maintenance(item) for item in value.values()) + if isinstance(value, list): + return any(contains_maintenance(item) for item in value) + return False + +def node_is_in_maintenance(value): + if isinstance(value, dict): + for key, item in value.items(): + if str(key).strip().lower() == node and contains_maintenance(item): + return True + identity = next( + ( + value.get(key) + for key in ("node", "name", "id") + if isinstance(value.get(key), str) + ), + "", + ) + if identity.strip().lower() == node and contains_maintenance(value): + return True + return any(node_is_in_maintenance(item) for item in value.values()) + if isinstance(value, list): + return any(node_is_in_maintenance(item) for item in value) + return False + +raise SystemExit(0 if node_is_in_maintenance(status) else 1) +PY +} diff --git a/proxmenu-scripts.sh b/proxmenu-scripts.sh index f2ba36a..55e1530 100755 --- a/proxmenu-scripts.sh +++ b/proxmenu-scripts.sh @@ -6,6 +6,7 @@ source /opt/idssys/defaults/default.inc source /opt/idssys/ta-proxmenu/defaults.inc source /opt/idssys/ta-proxmenu/inc/git-update.inc +source /opt/idssys/ta-proxmenu/inc/ha-status.inc source /opt/idssys/ta-proxmenu/inc/deploy-iso-nfs-lxc.sh source /opt/idssys/ta-proxmenu/inc/deploy-pulse-lxc.sh source /opt/idssys/ta-proxmenu/inc/virtio-helpers.inc @@ -1948,9 +1949,20 @@ MONITORING_MENU() { CLUSTER_MENU() { local -a labels local -a values=("maintenance" "services" "keepalived" "iso_nfs") + local maintenance_status + local node while true; do - if ha-manager status | grep -F "$(hostname -s)" | grep -q "maintenance mode"; then + node="$(hostname -s)" + TAPM_HA_NODE_IN_MAINTENANCE "$node" + maintenance_status=$? + if (( maintenance_status == 2 )); then + ha-manager status | + grep -F "$node" | + grep -q "maintenance mode" + maintenance_status=$? + fi + if (( maintenance_status == 0 )); then labels=("Take this host out of maintenance mode") else labels=("Put this host into maintenance mode and evacuate guests") diff --git a/tests/test-ha-status.sh b/tests/test-ha-status.sh new file mode 100644 index 0000000..bec801c --- /dev/null +++ b/tests/test-ha-status.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -u -o pipefail + +TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${TEST_ROOT}/tests/testlib.sh" +source "${TEST_ROOT}/inc/ha-status.inc" + +STATUS_FILE="$(mktemp /tmp/tapm-ha-status.XXXXXX)" +cleanup_ha_status_test() { + [[ "$STATUS_FILE" == /tmp/tapm-ha-status.* ]] && rm -f -- "$STATUS_FILE" +} +trap cleanup_ha_status_test EXIT + +printf '%s\n' \ + '{"node_status":{"pve1":"online","pve2":"maintenance mode"}}' \ + >"$STATUS_FILE" +assert_success "maintenance node detected in HA manager state" \ + TAPM_HA_NODE_IN_MAINTENANCE pve2 "$STATUS_FILE" +assert_failure "active node is not reported as maintenance" \ + TAPM_HA_NODE_IN_MAINTENANCE pve1 "$STATUS_FILE" + +printf '%s\n' \ + '{"nodes":[{"node":"pve3","state":"maintenance"}]}' \ + >"$STATUS_FILE" +assert_success "structured HA node state detected" \ + TAPM_HA_NODE_IN_MAINTENANCE pve3 "$STATUS_FILE" + +printf '%s\n' 'not-json' >"$STATUS_FILE" +assert_failure "malformed HA state is rejected" \ + TAPM_HA_NODE_IN_MAINTENANCE pve1 "$STATUS_FILE" + +finish_tests