diff --git a/.changelogs/1.1.4/245_add_guest_pinning_to_groups_of_nodes.yml b/.changelogs/1.1.4/245_add_guest_pinning_to_groups_of_nodes.yml new file mode 100644 index 0000000..4271147 --- /dev/null +++ b/.changelogs/1.1.4/245_add_guest_pinning_to_groups_of_nodes.yml @@ -0,0 +1,2 @@ +added: + - Allow pinning of guests to a group of nodes (@gyptazy). [#245] diff --git a/README.md b/README.md index 4982dee..b0e05a4 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ As a result, ProxLB will not migrate this guest with the `plb_ignore_dev` tag to **Note:** Ignored guests are really ignored. Even by enforcing affinity rules this guest will be ignored. ### Pin VMs to Specific Hypervisor Nodes - Guests, such as VMs or CTs, can also be pinned to specific nodes in the cluster. This might be usefull when running applications with some special licensing requirements that are only fulfilled on certain nodes. It might also be interesting, when some physical hardware is attached to a node, that is not available in general within the cluster. + Guests, such as VMs or CTs, can also be pinned to specific (and multiple) nodes in the cluster. This might be usefull when running applications with some special licensing requirements that are only fulfilled on certain nodes. It might also be interesting, when some physical hardware is attached to a node, that is not available in general within the cluster. To pin a guest to a specific cluster node, users assign a tag with the prefix `plb_pin_$nodename` to the desired guest: @@ -388,6 +388,8 @@ plb_pin_node03 As a result, ProxLB will pin the guest `dev-vm01` to the node `virt03`. +You can also repeat this step multiple times for different node names to create a potential group of allowed hosts where a the guest may be served on. In this case, ProxLB takes the node with the lowest used resources according to the defined balancing values from this group. + **Note:** The given node names from the tag are validated. This means, ProxLB validated if the given node name is really part of the cluster. In case of a wrongly defined or unavailable node name it continous to use the regular processes to make sure the guest keeps running. ## Maintenance diff --git a/docs/03_configuration.md b/docs/03_configuration.md index f5965e2..010e73f 100644 --- a/docs/03_configuration.md +++ b/docs/03_configuration.md @@ -137,6 +137,8 @@ plb_pin_node03 As a result, ProxLB will pin the guest `dev-vm01` to the node `virt03`. +You can also repeat this step multiple times for different node names to create a potential group of allowed hosts where a the guest may be served on. In this case, ProxLB takes the node with the lowest used resources according to the defined balancing values from this group. + **Note:** The given node names from the tag are validated. This means, ProxLB validated if the given node name is really part of the cluster. In case of a wrongly defined or unavailable node name it continous to use the regular processes to make sure the guest keeps running. ### API Loadbalancing diff --git a/proxlb/models/calculations.py b/proxlb/models/calculations.py index 46d9bf7..7199ca9 100644 --- a/proxlb/models/calculations.py +++ b/proxlb/models/calculations.py @@ -129,7 +129,7 @@ class Calculations: logger.debug("Finished: get_balanciness.") @staticmethod - def get_most_free_node(proxlb_data: Dict[str, Any], return_node: bool = False) -> Dict[str, Any]: + def get_most_free_node(proxlb_data: Dict[str, Any], return_node: bool = False, guest_node_relation_list: list = []) -> Dict[str, Any]: """ Get the name of the Proxmox node in the cluster with the most free resources based on the user defined method (e.g.: memory) and mode (e.g.: used). @@ -138,6 +138,8 @@ class Calculations: proxlb_data (Dict[str, Any]): The data holding all content of all objects. return_node (bool): The indicator to simply return the best node for further assignments. + guest_node_relation_list (list): A list of nodes that have a tag on the given + guest relationship for pinning. Returns: Dict[str, Any]: Updated meta data section of the node with the most free resources that should @@ -146,8 +148,15 @@ class Calculations: logger.debug("Starting: get_most_free_node.") proxlb_data["meta"]["balancing"]["balance_next_node"] = "" - # Do not include nodes that are marked in 'maintenance' + # Filter and exclude nodes that are in maintenance mode filtered_nodes = [node for node in proxlb_data["nodes"].values() if not node["maintenance"]] + + # Filter and include nodes that given by a relationship between guest and node. This is only + # used if the guest has a relationship to a node defined by "pin" tags. + if len(guest_node_relation_list) > 0: + filtered_nodes = [node for node in proxlb_data["nodes"].values() if node["name"] in guest_node_relation_list] + + # Filter by the defined methods and modes for balancing method = proxlb_data["meta"]["balancing"].get("method", "memory") mode = proxlb_data["meta"]["balancing"].get("mode", "used") lowest_usage_node = min(filtered_nodes, key=lambda x: x[f"{method}_{mode}_percent"]) @@ -226,7 +235,7 @@ class Calculations: for guest_name in proxlb_data["groups"]["affinity"][group_name]["guests"]: proxlb_data["meta"]["balancing"]["balance_next_guest"] = guest_name Calculations.val_anti_affinity(proxlb_data, guest_name) - Calculations.val_node_relationship(proxlb_data, guest_name) + Calculations.val_node_relationships(proxlb_data, guest_name) Calculations.update_node_resources(proxlb_data) logger.debug("Finished: relocate_guests.") @@ -281,7 +290,7 @@ class Calculations: logger.debug("Finished: val_anti_affinity.") @staticmethod - def val_node_relationship(proxlb_data: Dict[str, Any], guest_name: str): + def val_node_relationships(proxlb_data: Dict[str, Any], guest_name: str): """ Validates and assigns guests to nodes based on defined relationships based on tags. @@ -292,24 +301,26 @@ class Calculations: Returns: None """ - logger.debug("Starting: val_node_relationship.") + logger.debug("Starting: val_node_relationships.") proxlb_data["guests"][guest_name]["processed"] = True - if proxlb_data["guests"][guest_name]["node_relationship"]: - logger.info(f"Guest '{guest_name}' has a specific relationship defined to node: {proxlb_data['guests'][guest_name]['node_relationship']}. Pinning to node.") + if len(proxlb_data["guests"][guest_name]["node_relationships"]) > 0: + logger.info(f"Guest '{guest_name}' has relationships defined to node(s): {','.join(proxlb_data['guests'][guest_name]['node_relationships'])}. Pinning to node.") + + # Get the node with the most free resources of the group + guest_node_relation_list = proxlb_data["guests"][guest_name]["node_relationships"] + Calculations.get_most_free_node(proxlb_data, False, guest_node_relation_list) # Validate if the specified node name is really part of the cluster - if proxlb_data['guests'][guest_name]['node_relationship'] in proxlb_data["nodes"].keys(): - logger.info(f"Guest '{guest_name}' has a specific relationship defined to node: {proxlb_data['guests'][guest_name]['node_relationship']} is a known hypervisor node in the cluster.") - # Pin the guest to the specified hypervisor node. - proxlb_data["meta"]["balancing"]["balance_next_node"] = proxlb_data['guests'][guest_name]['node_relationship'] + if proxlb_data["meta"]["balancing"]["balance_next_node"] in proxlb_data["nodes"].keys(): + logger.info(f"Guest '{guest_name}' has a specific relationship defined to node: {proxlb_data['meta']['balancing']['balance_next_node']} is a known hypervisor node in the cluster.") else: - logger.warning(f"Guest '{guest_name}' has a specific relationship defined to node: {proxlb_data['guests'][guest_name]['node_relationship']} but this node name is not known in the cluster!") + logger.warning(f"Guest '{guest_name}' has a specific relationship defined to node: {proxlb_data['meta']['balancing']['balance_next_node']} but this node name is not known in the cluster!") else: logger.info(f"Guest '{guest_name}' does not have any specific node relationships.") - logger.debug("Finished: val_node_relationship.") + logger.debug("Finished: val_node_relationships.") @staticmethod def update_node_resources(proxlb_data): diff --git a/proxlb/models/guests.py b/proxlb/models/guests.py index 0cc329e..2bf7d61 100644 --- a/proxlb/models/guests.py +++ b/proxlb/models/guests.py @@ -79,7 +79,7 @@ class Guests: guests['guests'][guest['name']]['affinity_groups'] = Tags.get_affinity_groups(guests['guests'][guest['name']]['tags']) guests['guests'][guest['name']]['anti_affinity_groups'] = Tags.get_anti_affinity_groups(guests['guests'][guest['name']]['tags']) guests['guests'][guest['name']]['ignore'] = Tags.get_ignore(guests['guests'][guest['name']]['tags']) - guests['guests'][guest['name']]['node_relationship'] = Tags.get_node_relationship(guests['guests'][guest['name']]['tags']) + guests['guests'][guest['name']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags']) guests['guests'][guest['name']]['type'] = 'vm' logger.debug(f"Resources of Guest {guest['name']} (type VM) added: {guests['guests'][guest['name']]}") @@ -107,7 +107,7 @@ class Guests: guests['guests'][guest['name']]['affinity_groups'] = Tags.get_affinity_groups(guests['guests'][guest['name']]['tags']) guests['guests'][guest['name']]['anti_affinity_groups'] = Tags.get_anti_affinity_groups(guests['guests'][guest['name']]['tags']) guests['guests'][guest['name']]['ignore'] = Tags.get_ignore(guests['guests'][guest['name']]['tags']) - guests['guests'][guest['name']]['node_relationship'] = Tags.get_node_relationship(guests['guests'][guest['name']]['tags']) + guests['guests'][guest['name']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags']) 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/tags.py b/proxlb/models/tags.py index f9e1959..1317cc7 100644 --- a/proxlb/models/tags.py +++ b/proxlb/models/tags.py @@ -153,7 +153,7 @@ class Tags: return ignore_tag @staticmethod - def get_node_relationship(tags: List[str]) -> str: + def get_node_relationships(tags: List[str]) -> str: """ Get a node relationship tag for a guest from the Proxmox cluster by the API to pin a guest to a node. @@ -167,13 +167,14 @@ class Tags: Returns: Str: The related hypervisor node name. """ - logger.debug("Starting: get_node_relationship.") - node_relationship_tag = False + logger.debug("Starting: get_node_relationships.") + node_relationship_tags = [] if len(tags) > 0: for tag in tags: if tag.startswith("plb_pin"): node_relationship_tag = tag.replace("plb_pin_", "") + node_relationship_tags.append(node_relationship_tag) - logger.debug("Finished: get_node_relationship.") - return node_relationship_tag + logger.debug("Finished: get_node_relationships.") + return node_relationship_tags