proxlb 1.1.12.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
proxlb/models/nodes.py ADDED
@@ -0,0 +1,312 @@
1
+ """
2
+ The Nodes class retrieves all running nodes in a Proxmox cluster
3
+ and collects their resource metrics.
4
+
5
+ Methods:
6
+ __init__:
7
+ Initializes the Nodes class.
8
+
9
+ get_nodes(proxmox_api: any, proxlb_config: Dict[str, Any]) -> Dict[str, Any]:
10
+ Gets metrics of all nodes in a Proxmox cluster.
11
+
12
+ set_node_maintenance(proxlb_config: Dict[str, Any], node_name: str) -> Dict[str, Any]:
13
+ Sets Proxmox nodes to a maintenance mode if required.
14
+
15
+ set_node_ignore(proxlb_config: Dict[str, Any], node_name: str) -> Dict[str, Any]:
16
+ Sets Proxmox nodes to be ignored if requested.
17
+ """
18
+
19
+ __author__ = "Florian Paul Azim Hoberg <gyptazy>"
20
+ __copyright__ = "Copyright (C) 2025 Florian Paul Azim Hoberg (@gyptazy)"
21
+ __license__ = "GPL-3.0"
22
+
23
+
24
+ import time
25
+ from typing import Dict, Any
26
+ from utils.logger import SystemdLogger
27
+ from utils.helper import Helper
28
+
29
+ logger = SystemdLogger()
30
+
31
+
32
+ class Nodes:
33
+ """
34
+ The Nodes class retrieves all running nodes in a Proxmox cluster
35
+ and collects their resource metrics.
36
+ """
37
+ def __init__(self):
38
+ """
39
+ Initializes the Nodes class with the provided ProxLB data.
40
+ """
41
+
42
+ @staticmethod
43
+ def get_nodes(proxmox_api: any, proxlb_config: Dict[str, Any]) -> Dict[str, Any]:
44
+ """
45
+ Get metrics of all nodes in a Proxmox cluster.
46
+
47
+ This method retrieves metrics for all available nodes in the Proxmox cluster.
48
+ It iterates over each node and collects resource metrics including CPU, memory, and disk usage.
49
+
50
+ Args:
51
+ proxmox_api (any): The Proxmox API client instance.
52
+ proxmox_config (Dict[str, Any]): A dictionary containing the ProxLB configuration.
53
+ nodes (Dict[str, Any]): A dictionary containing information about the nodes in the Proxmox cluster.
54
+
55
+ Returns:
56
+ Dict[str, Any]: A dictionary containing metrics and information for all running nodes.
57
+ """
58
+ logger.debug("Starting: get_nodes.")
59
+ nodes = {"nodes": {}}
60
+
61
+ for node in proxmox_api.nodes.get():
62
+ # Ignoring a node results into ignoring all placed guests on the ignored node!
63
+ if node["status"] == "online" and not Nodes.set_node_ignore(proxlb_config, node["node"]):
64
+ nodes["nodes"][node["node"]] = {}
65
+ nodes["nodes"][node["node"]]["name"] = node["node"]
66
+ nodes["nodes"][node["node"]]["pve_version"] = Nodes.get_node_pve_version(proxmox_api, node["node"])
67
+ nodes["nodes"][node["node"]]["pressure_hot"] = False
68
+ nodes["nodes"][node["node"]]["maintenance"] = False
69
+ nodes["nodes"][node["node"]]["cpu_total"] = node["maxcpu"]
70
+ nodes["nodes"][node["node"]]["cpu_assigned"] = 0
71
+ nodes["nodes"][node["node"]]["cpu_used"] = node["cpu"] * node["maxcpu"]
72
+ nodes["nodes"][node["node"]]["cpu_free"] = (node["maxcpu"]) - (node["cpu"] * node["maxcpu"])
73
+ nodes["nodes"][node["node"]]["cpu_assigned_percent"] = nodes["nodes"][node["node"]]["cpu_assigned"] / nodes["nodes"][node["node"]]["cpu_total"] * 100
74
+ nodes["nodes"][node["node"]]["cpu_free_percent"] = nodes["nodes"][node["node"]]["cpu_free"] / node["maxcpu"] * 100
75
+ nodes["nodes"][node["node"]]["cpu_used_percent"] = nodes["nodes"][node["node"]]["cpu_used"] / node["maxcpu"] * 100
76
+ nodes["nodes"][node["node"]]["cpu_pressure_some_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "cpu", "some")
77
+ nodes["nodes"][node["node"]]["cpu_pressure_full_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "cpu", "full")
78
+ nodes["nodes"][node["node"]]["cpu_pressure_some_spikes_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "cpu", "some", spikes=True)
79
+ nodes["nodes"][node["node"]]["cpu_pressure_full_spikes_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "cpu", "full", spikes=True)
80
+ nodes["nodes"][node["node"]]["cpu_pressure_hot"] = False
81
+ nodes["nodes"][node["node"]]["memory_total"] = Nodes.set_node_resource_reservation(node["node"], node["maxmem"], proxlb_config, "memory")
82
+ nodes["nodes"][node["node"]]["memory_assigned"] = 0
83
+ nodes["nodes"][node["node"]]["memory_used"] = node["mem"]
84
+ nodes["nodes"][node["node"]]["memory_free"] = node["maxmem"] - node["mem"]
85
+ nodes["nodes"][node["node"]]["memory_assigned_percent"] = nodes["nodes"][node["node"]]["memory_assigned"] / nodes["nodes"][node["node"]]["memory_total"] * 100
86
+ nodes["nodes"][node["node"]]["memory_free_percent"] = nodes["nodes"][node["node"]]["memory_free"] / node["maxmem"] * 100
87
+ nodes["nodes"][node["node"]]["memory_used_percent"] = nodes["nodes"][node["node"]]["memory_used"] / node["maxmem"] * 100
88
+ nodes["nodes"][node["node"]]["memory_pressure_some_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "memory", "some")
89
+ nodes["nodes"][node["node"]]["memory_pressure_full_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "memory", "full")
90
+ nodes["nodes"][node["node"]]["memory_pressure_some_spikes_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "memory", "some", spikes=True)
91
+ nodes["nodes"][node["node"]]["memory_pressure_full_spikes_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "memory", "full", spikes=True)
92
+ nodes["nodes"][node["node"]]["memory_pressure_hot"] = False
93
+ nodes["nodes"][node["node"]]["disk_total"] = node["maxdisk"]
94
+ nodes["nodes"][node["node"]]["disk_assigned"] = 0
95
+ nodes["nodes"][node["node"]]["disk_used"] = node["disk"]
96
+ nodes["nodes"][node["node"]]["disk_free"] = node["maxdisk"] - node["disk"]
97
+ nodes["nodes"][node["node"]]["disk_assigned_percent"] = nodes["nodes"][node["node"]]["disk_assigned"] / nodes["nodes"][node["node"]]["disk_total"] * 100
98
+ nodes["nodes"][node["node"]]["disk_free_percent"] = nodes["nodes"][node["node"]]["disk_free"] / node["maxdisk"] * 100
99
+ nodes["nodes"][node["node"]]["disk_used_percent"] = nodes["nodes"][node["node"]]["disk_used"] / node["maxdisk"] * 100
100
+ nodes["nodes"][node["node"]]["disk_pressure_some_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "disk", "some")
101
+ nodes["nodes"][node["node"]]["disk_pressure_full_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "disk", "full")
102
+ nodes["nodes"][node["node"]]["disk_pressure_some_spikes_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "disk", "some", spikes=True)
103
+ nodes["nodes"][node["node"]]["disk_pressure_full_spikes_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "disk", "full", spikes=True)
104
+ nodes["nodes"][node["node"]]["disk_pressure_hot"] = False
105
+
106
+ # Evaluate if node should be set to maintenance mode
107
+ if Nodes.set_node_maintenance(proxmox_api, proxlb_config, node["node"]):
108
+ nodes["nodes"][node["node"]]["maintenance"] = True
109
+
110
+ logger.debug(f"Node metrics collected: {nodes}")
111
+ logger.debug("Finished: get_nodes.")
112
+ return nodes
113
+
114
+ @staticmethod
115
+ def set_node_maintenance(proxmox_api, proxlb_config: Dict[str, Any], node_name: str) -> Dict[str, Any]:
116
+ """
117
+ Set nodes to maintenance mode based on the provided configuration.
118
+
119
+ This method updates the nodes dictionary to mark certain nodes as being in maintenance mode
120
+ based on the configuration provided in proxlb_config.
121
+
122
+ Args:
123
+ proxmox_api (any): The Proxmox API client instance.
124
+ proxlb_config (Dict[str, Any]): A dictionary containing the ProxLB configuration, including maintenance nodes.
125
+ node_name: (str): The current node name within the outer iteration.
126
+
127
+ Returns:
128
+ Bool: Returns a bool if the provided node name is present in the maintenance section of the config file.
129
+ """
130
+ logger.debug("Starting: set_node_maintenance.")
131
+
132
+ # Evaluate maintenance mode by config
133
+ if proxlb_config.get("proxmox_cluster", None).get("maintenance_nodes", None) is not None:
134
+ if len(proxlb_config.get("proxmox_cluster", {}).get("maintenance_nodes", [])) > 0:
135
+ if node_name in proxlb_config.get("proxmox_cluster", {}).get("maintenance_nodes", []):
136
+ logger.info(f"Node: {node_name} has been set to maintenance mode (by ProxLB config).")
137
+ return True
138
+ else:
139
+ logger.debug(f"Node: {node_name} is not in maintenance mode by ProxLB config.")
140
+
141
+ # Evaluate maintenance mode by Proxmox HA
142
+ for ha_element in proxmox_api.cluster.ha.status.current.get():
143
+ if ha_element.get("status"):
144
+ if "maintenance mode" in ha_element.get("status"):
145
+ if ha_element.get("node") == node_name:
146
+ logger.info(f"Node: {node_name} has been set to maintenance mode (by Proxmox HA API).")
147
+ return True
148
+ else:
149
+ logger.debug(f"Node: {node_name} is not in maintenance mode by Proxmox HA API.")
150
+
151
+ logger.debug("Finished: set_node_maintenance.")
152
+
153
+ @staticmethod
154
+ def set_node_ignore(proxlb_config: Dict[str, Any], node_name: str) -> Dict[str, Any]:
155
+ """
156
+ Set nodes to be ignored based on the provided configuration.
157
+
158
+ This method updates the nodes dictionary to mark certain nodes as being ignored
159
+ based on the configuration provided in proxlb_config.
160
+
161
+ Args:
162
+ proxlb_config (Dict[str, Any]): A dictionary containing the ProxLB configuration, including maintenance nodes.
163
+ node_name: (str): The current node name within the outer iteration.
164
+
165
+ Returns:
166
+ Bool: Returns a bool if the provided node name is present in the ignore section of the config file.
167
+ """
168
+ logger.debug("Starting: set_node_ignore.")
169
+
170
+ if proxlb_config.get("proxmox_cluster", None).get("ignore_nodes", None) is not None:
171
+ if len(proxlb_config.get("proxmox_cluster", {}).get("ignore_nodes", [])) > 0:
172
+ if node_name in proxlb_config.get("proxmox_cluster", {}).get("ignore_nodes", []):
173
+ logger.info(f"Node: {node_name} has been set to be ignored. Not adding node!")
174
+ return True
175
+
176
+ logger.debug("Finished: set_node_ignore.")
177
+
178
+ @staticmethod
179
+ def get_node_rrd_data(proxmox_api, node_name: str, object_name: str, object_type: str, spikes=False) -> float:
180
+ """
181
+ Retrieves the rrd data metrics for a specific resource (CPU, memory, disk) of a node.
182
+
183
+ Args:
184
+ proxmox_api (Any): The Proxmox API client instance.
185
+ node_name (str): The name of the node hosting the guest.
186
+ object_name (str): The resource type to query (e.g., 'cpu', 'memory', 'disk').
187
+ object_type (str, optional): The pressure type ('some', 'full') or None for average usage.
188
+ spikes (bool, optional): Whether to consider spikes in the calculation. Defaults to False.
189
+
190
+ Returns:
191
+ float: The calculated average usage value for the specified resource.
192
+ """
193
+ logger.debug("Starting: get_node_rrd_data.")
194
+ time.sleep(0.1)
195
+
196
+ try:
197
+ if spikes:
198
+ logger.debug(f"Getting spike RRD data for {object_name} from node: {node_name}.")
199
+ node_data_rrd = proxmox_api.nodes(node_name).rrddata.get(timeframe="hour", cf="MAX")
200
+ else:
201
+ logger.debug(f"Getting average RRD data for {object_name} from node: {node_name}.")
202
+ node_data_rrd = proxmox_api.nodes(node_name).rrddata.get(timeframe="hour", cf="AVERAGE")
203
+
204
+ except Exception:
205
+ logger.error(f"Failed to retrieve RRD data for guest: {node_name}. Using 0.0 as value.")
206
+ logger.debug("Finished: get_node_rrd_data.")
207
+ return 0.0
208
+
209
+ lookup_key = f"pressure{object_name}{object_type}"
210
+
211
+ if spikes:
212
+ # RRD data is collected every minute, so we look at the last 6 entries
213
+ # and take the maximum value to represent the spike
214
+ rrd_data_value = [row.get(lookup_key) for row in node_data_rrd if row.get(lookup_key) is not None]
215
+ rrd_data_value = max(rrd_data_value[-6:], default=0.0)
216
+ else:
217
+ # Calculate the average value from the RRD data entries
218
+ rrd_data_value = sum(entry.get(lookup_key, 0.0) for entry in node_data_rrd) / len(node_data_rrd)
219
+
220
+ logger.debug(f"RRD data (spike: {spikes}) for {object_name} from node: {node_name}: {rrd_data_value}")
221
+ logger.debug("Finished: get_node_rrd_data.")
222
+ return rrd_data_value
223
+
224
+ @staticmethod
225
+ def get_node_pve_version(proxmox_api, node_name: str) -> float:
226
+ """
227
+ Return the Proxmox VE (PVE) version for a given node by querying the Proxmox API.
228
+
229
+ This function calls proxmox_api.nodes(node_name).version.get() and extracts the
230
+ 'version' field from the returned mapping. The value is expected to be numeric
231
+ (or convertible to float) and is returned as a float.
232
+
233
+ Args:
234
+ proxmox_api (Any): The Proxmox API client instance.
235
+ node_name (str): The name of the node hosting the guest.
236
+
237
+ Returns:
238
+ float: The PVE version for the specified node as a floating point number.
239
+
240
+ Raises:
241
+ Exception: If the proxmox_api call fails, returns an unexpected structure, or the
242
+ 'version' field is missing or cannot be converted to float. Callers should
243
+ handle or propagate exceptions as appropriate.
244
+ """
245
+ logger.debug("Starting: get_node_pve_version.")
246
+ time.sleep(0.1)
247
+
248
+ try:
249
+ logger.debug(f"Trying to get PVE version for node: {node_name}.")
250
+ version = proxmox_api.nodes(node_name).version.get()
251
+ except Exception:
252
+ logger.error(f"Failed to get PVE version for node: {node_name}.")
253
+
254
+ logger.debug(f"Got version {version['version']} for node {node_name}.")
255
+ logger.debug("Finished: get_node_pve_version.")
256
+ return version["version"]
257
+
258
+ @staticmethod
259
+ def set_node_resource_reservation(node_name, resource_value, proxlb_config, resource_type) -> int:
260
+ """
261
+ Check if there is a configured resource reservation for the current node and apply it as needed.
262
+ Checks for a node specific config first, then if there is any configured default and if neither then nothing is reserved.
263
+ Reservations are applied by directly modifying the resource value.
264
+
265
+ Args:
266
+ node_name (str): The name of the node.
267
+ resource_value (int): The total resource value in bytes.
268
+ proxlb_config (Dict[str, Any]): A dictionary containing the ProxLB configuration.
269
+ resource_type (str): The type of resource ('memory', 'disk', etc.).
270
+
271
+ Returns:
272
+ int: The resource value after applying any configured reservations.
273
+ """
274
+ logger.debug(f"Starting: apply_resource_reservation")
275
+
276
+ balancing_cfg = proxlb_config.get("balancing", {})
277
+ reserve_cfg = balancing_cfg.get("node_resource_reserve", {})
278
+ node_resource_reservation = reserve_cfg.get(node_name, {}).get(resource_type, 0)
279
+ default_resource_reservation = reserve_cfg.get("defaults", {}).get(resource_type, 0)
280
+
281
+ # Ensure reservations are numeric values
282
+ node_resource_reservation = node_resource_reservation if isinstance(node_resource_reservation, (int, float)) else 0
283
+ default_resource_reservation = default_resource_reservation if isinstance(default_resource_reservation, (int, float)) else 0
284
+
285
+ # Apply node specific reservation if set
286
+ if node_resource_reservation > 0:
287
+ if resource_value < (node_resource_reservation * 1024 ** 3):
288
+ logger.critical(f"Configured resource reservation for node {node_name} of type {resource_type} with {node_resource_reservation} GB is higher than available resource value {resource_value / (1024 ** 3):.2f} GB. Not applying...")
289
+ return resource_value
290
+ else:
291
+ logger.debug(f"Applying node specific reservation for {node_name} of type {resource_type} with {node_resource_reservation} GB.")
292
+ resource_value_new = resource_value - (node_resource_reservation * 1024 ** 3)
293
+ logger.debug(f'Switched resource value for node {node_name} of type {resource_type} from {resource_value / (1024 ** 3):.2f} GB to {resource_value_new / (1024 ** 3):.2f} GB after applying reservation.')
294
+ logger.debug(f"Before: {resource_value} | After: {resource_value_new}")
295
+ return resource_value_new
296
+
297
+ # Apply default reservation if set and no node specific reservation has been performed
298
+ elif default_resource_reservation > 0:
299
+ if resource_value < (default_resource_reservation * 1024 ** 3):
300
+ logger.critical(f"Configured default reservation for node {node_name} of type {resource_type} with {default_resource_reservation} GB is higher than available resource value {resource_value / (1024 ** 3):.2f} GB. Not applying...")
301
+ return resource_value
302
+ else:
303
+ logger.debug(f"Applying default reservation for {node_name} of type {resource_type} with {default_resource_reservation} GB.")
304
+ resource_value_new = resource_value - (default_resource_reservation * 1024 ** 3)
305
+ logger.debug(f'Switched resource value for node {node_name} of type {resource_type} from {resource_value / (1024 ** 3):.2f} GB to {resource_value_new / (1024 ** 3):.2f} GB after applying reservation.')
306
+ logger.debug(f"Before: {resource_value} | After: {resource_value_new}")
307
+ return resource_value_new
308
+
309
+ else:
310
+ logger.debug(f"No default or node specific resource reservation for node {node_name} found. Skipping...")
311
+ logger.debug(f"Finished: apply_resource_reservation")
312
+ return resource_value
proxlb/models/pools.py ADDED
@@ -0,0 +1,148 @@
1
+ """
2
+ The Pools class retrieves all present pools defined on a Proxmox cluster
3
+ including the chield objects.
4
+ """
5
+
6
+ __author__ = "Florian Paul Azim Hoberg <gyptazy>"
7
+ __copyright__ = "Copyright (C) 2025 Florian Paul Azim Hoberg (@gyptazy)"
8
+ __license__ = "GPL-3.0"
9
+
10
+
11
+ from typing import Dict, Any
12
+ from utils.logger import SystemdLogger
13
+ from models.tags import Tags
14
+ import time
15
+
16
+ logger = SystemdLogger()
17
+
18
+
19
+ class Pools:
20
+ """
21
+ The Pools class retrieves all present pools defined on a Proxmox cluster
22
+ including the chield objects.
23
+
24
+ Methods:
25
+ __init__:
26
+ Initializes the Pools class.
27
+
28
+ get_pools(proxmox_api: any) -> Dict[str, Any]:
29
+ Retrieve pool definitions and membership from the Proxmox cluster.
30
+ Returns a dict with a top-level "pools" mapping each poolid to
31
+ {"name": <poolid>, "members": [<member_names>...]}.
32
+ This method does not collect per-member metrics or perform node filtering.
33
+ """
34
+ def __init__(self):
35
+ """
36
+ Initializes the Pools class with the provided ProxLB data.
37
+ """
38
+
39
+ @staticmethod
40
+ def get_pools(proxmox_api: any) -> Dict[str, Any]:
41
+ """
42
+ Retrieve all pools and their members from a Proxmox cluster.
43
+
44
+ Queries the Proxmox API for pool definitions and returns a dictionary
45
+ containing each pool's id/name and a list of its member VM/CT names.
46
+ This function does not perform per-member metric collection or node
47
+ filtering — it only gathers pool membership information.
48
+
49
+ Args:
50
+ proxmox_api (any): Proxmox API client instance.
51
+
52
+ Returns:
53
+ Dict[str, Any]: Dictionary with a top-level "pools" key mapping poolid
54
+ to {"name": <poolid>, "members": [<member_names>...]}.
55
+ """
56
+ logger.debug("Starting: get_pools.")
57
+ pools = {"pools": {}}
58
+
59
+ # Pool objects: iterate over all pools in the cluster.
60
+ # We keep pool members even if their nodes are ignored so resource accounting
61
+ # for rebalancing remains correct and we avoid overprovisioning nodes.
62
+ for pool in proxmox_api.pools.get():
63
+ logger.debug(f"Got pool: {pool['poolid']}")
64
+ pools['pools'][pool['poolid']] = {}
65
+ pools['pools'][pool['poolid']]['name'] = pool['poolid']
66
+ pools['pools'][pool['poolid']]['members'] = []
67
+
68
+ # Fetch pool details and collect member names
69
+ try:
70
+ pool_details = proxmox_api.pools(pool['poolid']).get()
71
+ except Exception as e:
72
+ logger.error(f"Error fetching pool details for pool {pool['poolid']}: {e}")
73
+ continue
74
+
75
+ for member in pool_details.get("members", []):
76
+
77
+ # We might also have objects without the key "name", e.g. storage pools
78
+ if "name" not in member:
79
+ logger.debug(f"Skipping member without name in pool: {pool['poolid']}")
80
+ continue
81
+
82
+ logger.debug(f"Got member: {member['name']} for pool: {pool['poolid']}")
83
+ pools['pools'][pool['poolid']]['members'].append(member["name"])
84
+
85
+ logger.debug("Finished: get_pools.")
86
+ return pools
87
+
88
+ @staticmethod
89
+ def get_pools_for_guest(guest_name: str, pools: Dict[str, Any]) -> Dict[str, Any]:
90
+ """
91
+ Return the list of pool names that include the given guest.
92
+
93
+ Args:
94
+ guest_name (str): Name of the VM or CT to look up.
95
+ pools (Dict[str, Any]): Pools structure as returned by get_pools(),
96
+ expected to contain a top-level "pools" mapping each poolid to
97
+ {"name": <poolid>, "members": [<member_names>...]}.
98
+
99
+ Returns:
100
+ list[str]: Names of pools the guest is a member of (empty list if none).
101
+ """
102
+ logger.debug("Starting: get_pools_for_guests.")
103
+ guest_pools = []
104
+
105
+ for pool in pools.items():
106
+ for pool_id, pool_data in pool[1].items():
107
+
108
+ if type(pool_data) is dict:
109
+ pool_name = pool_data.get("name", "")
110
+ pool_name_members = pool_data.get("members", [])
111
+
112
+ if guest_name in pool_name_members:
113
+ logger.debug(f"Guest: {guest_name} is member of Pool: {pool_name}.")
114
+ guest_pools.append(pool_name)
115
+ else:
116
+ logger.debug(f"Guest: {guest_name} is NOT member of Pool: {pool_name}.")
117
+
118
+ else:
119
+ logger.debug(f"Pool data for pool_id {pool_id} is not a dict: {pool_data}")
120
+
121
+ logger.debug("Finished: get_pools_for_guests.")
122
+ return guest_pools
123
+
124
+ @staticmethod
125
+ def get_pool_node_affinity_strictness(proxlb_config: Dict[str, Any], guest_pools: list) -> bool:
126
+ """
127
+ Retrieve the node affinity strictness setting for a guest across its pools.
128
+
129
+ Queries the ProxLB configuration to determine the node affinity strictness
130
+ level for the specified guest based on its pool memberships. Returns the
131
+ strictness setting from the first matching pool configuration.
132
+
133
+ Args:
134
+ proxlb_config (Dict[str, Any]): ProxLB configuration dictionary.
135
+ guest_pools (list): List of pool names the guest belongs to.
136
+
137
+ Returns:
138
+ bool: Node affinity strictness setting (default True if not specified).
139
+ """
140
+ logger.debug("Starting: get_pool_node_affinity_strictness.")
141
+
142
+ node_strictness = True
143
+ for pool in guest_pools:
144
+ pool_settings = proxlb_config.get("balancing", {}).get("pools", {}).get(pool, {})
145
+ node_strictness = pool_settings.get("strict", True)
146
+
147
+ logger.debug("Finished: get_pool_node_affinity_strictness.")
148
+ return node_strictness