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.
- etc/proxlb/proxlb_example.yaml +90 -0
- etc/systemd/system/proxlb.service +13 -0
- proxlb/__init__.py +0 -0
- proxlb/main.py +120 -0
- proxlb/models/__init__.py +0 -0
- proxlb/models/balancing.py +270 -0
- proxlb/models/calculations.py +889 -0
- proxlb/models/features.py +124 -0
- proxlb/models/groups.py +124 -0
- proxlb/models/guests.py +215 -0
- proxlb/models/ha_rules.py +126 -0
- proxlb/models/nodes.py +312 -0
- proxlb/models/pools.py +148 -0
- proxlb/models/tags.py +283 -0
- proxlb/utils/__init__.py +0 -0
- proxlb/utils/cli_parser.py +90 -0
- proxlb/utils/config_parser.py +96 -0
- proxlb/utils/helper.py +337 -0
- proxlb/utils/logger.py +146 -0
- proxlb/utils/proxmox_api.py +440 -0
- proxlb/utils/version.py +7 -0
- proxlb-1.1.12.dev4.dist-info/METADATA +26 -0
- proxlb-1.1.12.dev4.dist-info/RECORD +26 -0
- proxlb-1.1.12.dev4.dist-info/WHEEL +5 -0
- proxlb-1.1.12.dev4.dist-info/licenses/LICENSE +674 -0
- proxlb-1.1.12.dev4.dist-info/top_level.txt +1 -0
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
|