From 22406e362872d4d53b5a64c4f40be334f4dcc1cb Mon Sep 17 00:00:00 2001 From: Florian Paul Azim Hoberg Date: Thu, 11 Dec 2025 14:27:20 +0100 Subject: [PATCH] fix: Fixed pool and ha-rules based node pinning of guests. * Fixed pool based node pinning (@gyptazy). [#395] * Add support for Proxmox's native HA (node-affinity) rules for pinning guests to nodes (@gyptazy). [#391] Fixes: #395 Fixes: #391 --- ...91_add_native_proxmox_ha_rules_support.yml | 1 + .../395_fix_pool_based_node_pinning.yml | 2 + proxlb/models/guests.py | 9 +++-- proxlb/models/ha_rules.py | 16 +++++++- proxlb/models/tags.py | 39 ++++++++++++++----- 5 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 .changelogs/1.1.11/395_fix_pool_based_node_pinning.yml diff --git a/.changelogs/1.1.11/391_add_native_proxmox_ha_rules_support.yml b/.changelogs/1.1.11/391_add_native_proxmox_ha_rules_support.yml index d274d83..d9a07dc 100644 --- a/.changelogs/1.1.11/391_add_native_proxmox_ha_rules_support.yml +++ b/.changelogs/1.1.11/391_add_native_proxmox_ha_rules_support.yml @@ -1,2 +1,3 @@ feature: - Add support for Proxmox's native HA (affinity/anti-affinity) rules (@gyptazy). [#391] + - Add support for Proxmox's native HA (node-affinity) rules for pinning guests to nodes (@gyptazy). [#391] diff --git a/.changelogs/1.1.11/395_fix_pool_based_node_pinning.yml b/.changelogs/1.1.11/395_fix_pool_based_node_pinning.yml new file mode 100644 index 0000000..fbd881c --- /dev/null +++ b/.changelogs/1.1.11/395_fix_pool_based_node_pinning.yml @@ -0,0 +1,2 @@ +fixed: + - Fixed pool based node pinning (@gyptazy). [#395] diff --git a/proxlb/models/guests.py b/proxlb/models/guests.py index ce01c12..84f7ce9 100644 --- a/proxlb/models/guests.py +++ b/proxlb/models/guests.py @@ -102,7 +102,7 @@ class Guests: 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']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags'], nodes, guests['guests'][guest['name']]['pools'], guests['guests'][guest['name']]['ha_rules'], proxlb_config) guests['guests'][guest['name']]['type'] = 'vm' logger.debug(f"Resources of Guest {guest['name']} (type VM) added: {guests['guests'][guest['name']]}") @@ -144,10 +144,11 @@ class Guests: guests['guests'][guest['name']]['pressure_hot'] = False guests['guests'][guest['name']]['tags'] = Tags.get_tags_from_guests(proxmox_api, node, guest['vmid'], 'ct') 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']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags'], nodes, guests['guests'][guest['name']]['pools'], guests['guests'][guest['name']]['ha_rules'], proxlb_config) guests['guests'][guest['name']]['type'] = 'ct' logger.debug(f"Resources of Guest {guest['name']} (type CT) added: {guests['guests'][guest['name']]}") diff --git a/proxlb/models/ha_rules.py b/proxlb/models/ha_rules.py index 5309350..a0c342b 100644 --- a/proxlb/models/ha_rules.py +++ b/proxlb/models/ha_rules.py @@ -56,9 +56,15 @@ class HaRules: ha_rules = {"ha_rules": {}} for rule in proxmox_api.cluster.ha.rules.get(): + + # Skip disabled rules (disable key exists AND is truthy) + if rule.get("disable", 0): + logger.debug(f"Skipping ha-rule: {rule['rule']} of type {rule['type']} affecting guests: {rule['resources']}. Rule is disabled.") + continue + # 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()] + resources_list_guests = [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": @@ -66,11 +72,17 @@ class HaRules: else: affinity_type = "affinity" + # Create affected nodes list + resources_list_nodes = [] + if rule.get("nodes", None): + resources_list_nodes = [n for n in rule["nodes"].split(",") if n] + # 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 + ha_rules['ha_rules'][rule['rule']]['nodes'] = resources_list_nodes + ha_rules['ha_rules'][rule['rule']]['members'] = resources_list_guests logger.debug(f"Got ha-rule: {rule['rule']} as type {affinity_type} affecting guests: {rule['resources']}") diff --git a/proxlb/models/tags.py b/proxlb/models/tags.py index ed73acc..a207f0e 100644 --- a/proxlb/models/tags.py +++ b/proxlb/models/tags.py @@ -91,6 +91,7 @@ class Tags: Args: tags (List): A list holding all defined tags for a given guest. pools (List): A list holding all defined pools for a given guest. + ha_rules (List): A list holding all defined ha_rules for a given guest. proxlb_config (Dict): A dict holding the ProxLB configuration. Returns: @@ -140,6 +141,7 @@ class Tags: Args: tags (List): A list holding all defined tags for a given guest. pools (List): A list holding all defined pools for a given guest. + ha_rules (List): A list holding all defined ha_rules for a given guest. proxlb_config (Dict): A dict holding the ProxLB configuration. Returns: @@ -203,7 +205,7 @@ class Tags: return ignore_tag @staticmethod - def get_node_relationships(tags: List[str], nodes: Dict[str, Any], pools: List[str], proxlb_config: Dict[str, Any]) -> str: + def get_node_relationships(tags: List[str], nodes: Dict[str, Any], pools: List[str], ha_rules: List[str], proxlb_config: Dict[str, Any]) -> str: """ Get a node relationship tag for a guest from the Proxmox cluster by the API to pin a guest to a node or by defined pools from ProxLB configuration. @@ -215,6 +217,7 @@ class Tags: tags (List): A list holding all defined tags for a given guest. nodes (Dict): A dictionary holding all available nodes in the cluster. pools (List): A list holding all defined pools for a given guest. + ha_rules (List): A list holding all defined ha_rules for a given guest. proxlb_config (Dict): A dict holding the ProxLB configuration. Returns: @@ -223,6 +226,7 @@ class Tags: logger.debug("Starting: get_node_relationships.") node_relationship_tags = [] + # Tag based node relationship if len(tags) > 0: logger.debug("Validating node pinning by tags.") for tag in tags: @@ -237,21 +241,38 @@ class Tags: else: logger.warning(f"Tag {node_relationship_tag} is invalid! Defined node does not exist in the cluster. Not applying pinning.") + # Pool based node relationship if len(pools) > 0: logger.debug("Validating node pinning by pools.") for pool in pools: if pool in (proxlb_config['balancing'].get('pools') or {}): - node = proxlb_config['balancing']['pools'][pool].get('pin', None) - # Validate if the node to pin is present in the cluster - if Helper.validate_node_presence(node, nodes): - logger.debug(f"Pool pinning tag {node} is valid! Defined node exists in the cluster.") - logger.debug(f"Setting node relationship because of pool {pool} to {node}.") - node_relationship_tags.append(node) - else: - logger.warning(f"Pool pinning tag {node} is invalid! Defined node does not exist in the cluster. Not applying pinning.") + pool_nodes = proxlb_config['balancing']['pools'][pool].get('pin', None) + for node in pool_nodes: + + # Validate if the node to pin is present in the cluster + if Helper.validate_node_presence(node, nodes): + logger.debug(f"Pool pinning tag {node} is valid! Defined node exists in the cluster.") + logger.debug(f"Setting node relationship because of pool {pool} to {node}.") + node_relationship_tags.append(node) + else: + logger.warning(f"Pool pinning tag {node} is invalid! Defined node does not exist in the cluster. Not applying pinning.") + else: logger.debug(f"Skipping pinning for pool {pool}. Pool is not defined in ProxLB configuration.") + # HA rule based node relationship + if len(ha_rules) > 0: + logger.debug("Validating node pinning by ha-rules.") + for ha_rule in ha_rules: + if len(ha_rule.get("nodes", 0)) > 0: + if ha_rule.get("type", None) == "affinity": + logger.debug(f"ha-rule {ha_rule['rule']} is of type affinity.") + for node in ha_rule["nodes"]: + logger.debug(f"Adding {node} as node relationship because of ha-rule {ha_rule['rule']}.") + node_relationship_tags.append(node) + else: + logger.debug(f"ha-rule {ha_rule['rule']} is of type anti-affinity. Skipping node relationship addition.") + logger.debug("Finished: get_node_relationships.") return node_relationship_tags