feature: Add support for Proxmox's native HA (affinity/anti-affinity) rules.

* Add support of native rules for affinity/anti-affinity types in Proxmox VE
  * Streamline affinity/anti-affinity rules by Tags, Pools and native Proxmox rules

Fixes: #391
This commit is contained in:
Florian Paul Azim Hoberg
2025-12-10 09:11:28 +01:00
parent 9ea04f904d
commit c133ef1aee
5 changed files with 139 additions and 7 deletions

View File

@@ -0,0 +1,2 @@
feature:
- Add support for Proxmox's native HA (affinity/anti-affinity) rules (@gyptazy). [#391]

View File

@@ -25,6 +25,7 @@ from models.groups import Groups
from models.calculations import Calculations
from models.balancing import Balancing
from models.pools import Pools
from models.ha_rules import HaRules
from utils.helper import Helper
@@ -74,11 +75,12 @@ def main():
meta = {"meta": proxlb_config}
nodes = Nodes.get_nodes(proxmox_api, proxlb_config)
pools = Pools.get_pools(proxmox_api)
guests = Guests.get_guests(proxmox_api, pools, nodes, meta, proxlb_config)
ha_rules = HaRules.get_ha_rules(proxmox_api)
guests = Guests.get_guests(proxmox_api, pools, ha_rules, nodes, meta, proxlb_config)
groups = Groups.get_groups(guests, nodes)
# Merge obtained objects from the Proxmox cluster for further usage
proxlb_data = {**meta, **nodes, **guests, **pools, **groups}
proxlb_data = {**meta, **nodes, **guests, **pools, **ha_rules, **groups}
Helper.log_node_metrics(proxlb_data)
# Validate usable features by PVE versions

View File

@@ -11,6 +11,7 @@ __license__ = "GPL-3.0"
from typing import Dict, Any
from utils.logger import SystemdLogger
from models.pools import Pools
from models.ha_rules import HaRules
from models.tags import Tags
import time
@@ -36,7 +37,7 @@ class Guests:
"""
@staticmethod
def get_guests(proxmox_api: any, pools: Dict[str, Any], nodes: Dict[str, Any], meta: Dict[str, Any], proxlb_config: Dict[str, Any]) -> Dict[str, Any]:
def get_guests(proxmox_api: any, pools: Dict[str, Any], ha_rules: Dict[str, Any], nodes: Dict[str, Any], meta: Dict[str, Any], proxlb_config: Dict[str, Any]) -> Dict[str, Any]:
"""
Get metrics of all guests in a Proxmox cluster.
@@ -46,6 +47,8 @@ class Guests:
Args:
proxmox_api (any): The Proxmox API client instance.
pools (Dict[str, Any]): A dictionary containing information about the pools in the Proxmox cluster.
ha_rules (Dict[str, Any]): A dictionary containing information about the HA rules in the
nodes (Dict[str, Any]): A dictionary containing information about the nodes in the Proxmox cluster.
meta (Dict[str, Any]): A dictionary containing metadata information.
proxmox_config (Dict[str, Any]): A dictionary containing the ProxLB configuration.
@@ -95,8 +98,9 @@ class Guests:
guests['guests'][guest['name']]['pressure_hot'] = False
guests['guests'][guest['name']]['tags'] = Tags.get_tags_from_guests(proxmox_api, node, guest['vmid'], 'vm')
guests['guests'][guest['name']]['pools'] = Pools.get_pools_for_guest(guest['name'], pools)
guests['guests'][guest['name']]['affinity_groups'] = Tags.get_affinity_groups(guests['guests'][guest['name']]['tags'], guests['guests'][guest['name']]['pools'], proxlb_config)
guests['guests'][guest['name']]['anti_affinity_groups'] = Tags.get_anti_affinity_groups(guests['guests'][guest['name']]['tags'], guests['guests'][guest['name']]['pools'], proxlb_config)
guests['guests'][guest['name']]['ha_rules'] = HaRules.get_ha_rules_for_guest(guest['name'], ha_rules, guest['vmid'])
guests['guests'][guest['name']]['affinity_groups'] = Tags.get_affinity_groups(guests['guests'][guest['name']]['tags'], guests['guests'][guest['name']]['pools'], guests['guests'][guest['name']]['ha_rules'], proxlb_config)
guests['guests'][guest['name']]['anti_affinity_groups'] = Tags.get_anti_affinity_groups(guests['guests'][guest['name']]['tags'], guests['guests'][guest['name']]['pools'], guests['guests'][guest['name']]['ha_rules'], proxlb_config)
guests['guests'][guest['name']]['ignore'] = Tags.get_ignore(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags'], nodes, guests['guests'][guest['name']]['pools'], proxlb_config)
guests['guests'][guest['name']]['type'] = 'vm'

106
proxlb/models/ha_rules.py Normal file
View File

@@ -0,0 +1,106 @@
"""
The HaRules class retrieves all HA rules defined on a Proxmox cluster
including their affinity settings and member resources.
"""
__author__ = "Florian Paul Azim Hoberg <gyptazy>"
__copyright__ = "Copyright (C) 2025 Florian Paul Azim Hoberg (@gyptazy)"
__license__ = "GPL-3.0"
from typing import Dict, Any
from utils.logger import SystemdLogger
logger = SystemdLogger()
class HaRules:
"""
The HaRules class retrieves all HA rules defined on a Proxmox cluster
including their (anti)a-ffinity settings and member resources and translates
them into a ProxLB usable format.
Methods:
__init__:
Initializes the HaRules class.
get_ha_rules(proxmox_api: any) -> Dict[str, Any]:
Retrieve HA rule definitions from the Proxmox cluster.
Returns a dict with a top-level "ha_rules" mapping each rule id to
{"rule": <rule_id>, "type": <affinity_type>, "members": [<resource_ids>...]}.
Converts affinity settings to descriptive format (affinity or anti-affinity).
"""
def __init__(self):
"""
Initializes the HA Rules class with the provided ProxLB data.
"""
@staticmethod
def get_ha_rules(proxmox_api: any) -> Dict[str, Any]:
"""
Retrieve all HA rules from a Proxmox cluster.
Queries the Proxmox API for HA rule definitions and returns a dictionary
containing each rule's id, affinity type, and member resources (VM/CT IDs).
This function processes rule affinity settings and converts them to a more
descriptive format (affinity or anti-affinity).
Args:
proxmox_api (any): Proxmox API client instance.
Returns:
Dict[str, Any]: Dictionary with a top-level "ha_rules" key mapping rule id
to {"rule": <rule_id>, "type": <affinity_type>, "members": [<resource_ids>...]}.
"""
logger.debug("Starting: get_ha_rules.")
ha_rules = {"ha_rules": {}}
for rule in proxmox_api.cluster.ha.rules.get():
# Create a resource list by splitting on commas and stripping whitespace containing
# the VM and CT IDs that are part of this HA rule
resources_list = [int(r.split(":")[1]) for r in rule["resources"].split(",") if r.strip()]
# Convert the affinity field to a more descriptive type
if rule.get("affinity", None) == "negative":
affinity_type = "anti-affinity"
else:
affinity_type = "affinity"
# Create the ha_rule element
ha_rules['ha_rules'][rule['rule']] = {}
ha_rules['ha_rules'][rule['rule']]['rule'] = rule['rule']
ha_rules['ha_rules'][rule['rule']]['type'] = affinity_type
ha_rules['ha_rules'][rule['rule']]['members'] = resources_list
logger.debug(f"Got ha-rule: {rule['rule']} as type {affinity_type} affecting guests: {rule['resources']}")
logger.debug("Finished: ha_rules.")
return ha_rules
@staticmethod
def get_ha_rules_for_guest(guest_name: str, ha_rules: Dict[str, Any], vm_id: int) -> Dict[str, Any]:
"""
Return the list of HA rules that include the given guest.
Args:
guest_name (str): Name of the VM or CT to look up.
ha_rules (Dict[str, Any]): HA rules structure as returned by get_ha_rules(),
expected to contain a top-level "ha_rules" mapping each rule id to
{"rule": <rule_id>, "type": <affinity_type>, "members": [<resource_ids>...]}.
vm_id (int): VM or CT ID of the guest.
Returns:
list: IDs of HA rules the guest is a member of (empty list if none).
"""
logger.debug("Starting: get_ha_rules_for_guest.")
guest_ha_rules = []
for rule in ha_rules["ha_rules"].values():
if vm_id in rule.get("members", []):
logger.debug(f"Guest: {guest_name} (VMID: {vm_id}) is member of HA Rule: {rule['rule']}.")
guest_ha_rules.append(rule)
else:
logger.debug(f"Guest: {guest_name} (VMID: {vm_id}) is NOT member of HA Rule: {rule['rule']}.")
logger.debug("Finished: get_ha_rules_for_guest.")
return guest_ha_rules

View File

@@ -80,7 +80,7 @@ class Tags:
return tags
@staticmethod
def get_affinity_groups(tags: List[str], pools: List[str], proxlb_config: Dict[str, Any]) -> List[str]:
def get_affinity_groups(tags: List[str], pools: List[str], ha_rules: List[str], proxlb_config: Dict[str, Any]) -> List[str]:
"""
Get affinity tags for a guest from the Proxmox cluster by the API.
@@ -99,6 +99,7 @@ class Tags:
logger.debug("Starting: get_affinity_groups.")
affinity_tags = []
# Tag based affinity groups
if len(tags) > 0:
for tag in tags:
if tag.startswith("plb_affinity"):
@@ -107,6 +108,7 @@ class Tags:
else:
logger.debug(f"Skipping affinity group for tag {tag}.")
# Pool based affinity groups
if len(pools) > 0:
for pool in pools:
if pool in (proxlb_config['balancing'].get('pools') or {}):
@@ -116,11 +118,18 @@ class Tags:
else:
logger.debug(f"Skipping affinity group for pool {pool}.")
# HA rule based affinity groups
if len(ha_rules) > 0:
for ha_rule in ha_rules:
if ha_rule.get('type', None) == 'affinity':
logger.debug(f"Adding affinity group for ha-rule {ha_rule}.")
affinity_tags.append(ha_rule['rule'])
logger.debug("Finished: get_affinity_groups.")
return affinity_tags
@staticmethod
def get_anti_affinity_groups(tags: List[str], pools: List[str], proxlb_config: Dict[str, Any]) -> List[str]:
def get_anti_affinity_groups(tags: List[str], pools: List[str], ha_rules: List[str], proxlb_config: Dict[str, Any]) -> List[str]:
"""
Get anti-affinity tags for a guest from the Proxmox cluster by the API.
@@ -139,6 +148,7 @@ class Tags:
logger.debug("Starting: get_anti_affinity_groups.")
anti_affinity_tags = []
# Tag based anti-affinity groups
if len(tags) > 0:
for tag in tags:
if tag.startswith("plb_anti_affinity"):
@@ -147,6 +157,7 @@ class Tags:
else:
logger.debug(f"Skipping anti-affinity group for tag {tag}.")
# Pool based anti-affinity groups
if len(pools) > 0:
for pool in pools:
if pool in (proxlb_config['balancing'].get('pools') or {}):
@@ -156,6 +167,13 @@ class Tags:
else:
logger.debug(f"Skipping anti-affinity group for pool {pool}.")
# HA rule based anti-affinity groups
if len(ha_rules) > 0:
for ha_rule in ha_rules:
if ha_rule.get('type', None) == 'anti-affinity':
logger.debug(f"Adding anti-affinity group for ha-rule {ha_rule}.")
anti_affinity_tags.append(ha_rule['rule'])
logger.debug("Finished: get_anti_affinity_groups.")
return anti_affinity_tags