mirror of
https://github.com/gyptazy/ProxLB.git
synced 2026-04-05 20:31:57 +02:00
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:
@@ -0,0 +1,2 @@
|
||||
feature:
|
||||
- Add support for Proxmox's native HA (affinity/anti-affinity) rules (@gyptazy). [#391]
|
||||
@@ -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
|
||||
|
||||
@@ -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
106
proxlb/models/ha_rules.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user