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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
proxmox_api:
|
|
2
|
+
hosts: ['virt01.example.com', '10.10.10.10', 'fe01:bad:code::cafe']
|
|
3
|
+
user: root@pam
|
|
4
|
+
pass: crazyPassw0rd!
|
|
5
|
+
# API Token method
|
|
6
|
+
# token_id: proxlb
|
|
7
|
+
# token_secret: 430e308f-1337-1337-beef-1337beefcafe
|
|
8
|
+
ssl_verification: True
|
|
9
|
+
timeout: 10
|
|
10
|
+
# API Connection retries
|
|
11
|
+
# retries: 1
|
|
12
|
+
# wait_time: 1
|
|
13
|
+
|
|
14
|
+
proxmox_cluster:
|
|
15
|
+
maintenance_nodes: ['virt66.example.com']
|
|
16
|
+
ignore_nodes: []
|
|
17
|
+
overprovisioning: True
|
|
18
|
+
|
|
19
|
+
balancing:
|
|
20
|
+
enable: True
|
|
21
|
+
enforce_affinity: False
|
|
22
|
+
enforce_pinning: False
|
|
23
|
+
parallel: False
|
|
24
|
+
# If running parallel job, you can define
|
|
25
|
+
# the amount of prallel jobs (default: 5)
|
|
26
|
+
parallel_jobs: 1
|
|
27
|
+
live: True
|
|
28
|
+
with_local_disks: True
|
|
29
|
+
with_conntrack_state: True
|
|
30
|
+
balance_types: ['vm', 'ct'] # 'vm' | 'ct'
|
|
31
|
+
max_job_validation: 1800 # Maximum time (in seconds) a job validation may take
|
|
32
|
+
memory_threshold: 75 # Optional: Maximum threshold (in percent) to trigger balancing actions
|
|
33
|
+
cpu_threshold: 75 # Optional: Maximum threshold (in percent) to trigger balancing actions
|
|
34
|
+
balanciness: 5 # Maximum delta of resource usage between highest and lowest usage node
|
|
35
|
+
method: memory # 'memory' | 'cpu' | 'disk'
|
|
36
|
+
mode: used # 'assigned' | 'used' | 'psi'
|
|
37
|
+
balance_larger_guests_first: False # Option to prioritize balancing of larger or smaller guests first
|
|
38
|
+
node_resource_reserve: # Optional: Define resource reservations for nodes (in GB)
|
|
39
|
+
defaults: # Default reservation values applying to all nodes (unless explicitly overridden)
|
|
40
|
+
memory: 4 # Default: 4 GB memory reserved per node
|
|
41
|
+
node01: # Specific node reservation override for node 'node01'
|
|
42
|
+
memory: 6 # Specific: 6 GB memory reserved for node 'node01'
|
|
43
|
+
# # PSI thresholds only apply when using mode 'psi'
|
|
44
|
+
# psi:
|
|
45
|
+
# nodes:
|
|
46
|
+
# memory:
|
|
47
|
+
# pressure_full: 0.20
|
|
48
|
+
# pressure_some: 0.20
|
|
49
|
+
# pressure_spikes: 1.00
|
|
50
|
+
# cpu:
|
|
51
|
+
# pressure_full: 0.20
|
|
52
|
+
# pressure_some: 0.20
|
|
53
|
+
# pressure_spikes: 1.00
|
|
54
|
+
# disk:
|
|
55
|
+
# pressure_full: 0.20
|
|
56
|
+
# pressure_some: 0.20
|
|
57
|
+
# pressure_spikes: 1.00
|
|
58
|
+
# guests:
|
|
59
|
+
# memory:
|
|
60
|
+
# pressure_full: 0.20
|
|
61
|
+
# pressure_some: 0.20
|
|
62
|
+
# pressure_spikes: 1.00
|
|
63
|
+
# cpu:
|
|
64
|
+
# pressure_full: 0.20
|
|
65
|
+
# pressure_some: 0.20
|
|
66
|
+
# pressure_spikes: 1.00
|
|
67
|
+
# disk:
|
|
68
|
+
# pressure_full: 0.20
|
|
69
|
+
# pressure_some: 0.20
|
|
70
|
+
# pressure_spikes: 1.00
|
|
71
|
+
pools: # Optional: Define affinity/anti-affinity rules per pool
|
|
72
|
+
dev: # Pool name: dev
|
|
73
|
+
type: affinity # Type: affinity (keeping VMs together)
|
|
74
|
+
de-nbg01-db: # Pool name: de-nbg01-db
|
|
75
|
+
type: anti-affinity # Type: anti-affinity (spreading VMs apart)
|
|
76
|
+
pin: # Define a pinning og guests to specific node(s)
|
|
77
|
+
- virt66
|
|
78
|
+
- virt77
|
|
79
|
+
strict: False # Disable strict mode of node pinning for this pool
|
|
80
|
+
|
|
81
|
+
service:
|
|
82
|
+
daemon: True
|
|
83
|
+
schedule:
|
|
84
|
+
interval: 12
|
|
85
|
+
format: hours
|
|
86
|
+
delay:
|
|
87
|
+
enable: False
|
|
88
|
+
time: 1
|
|
89
|
+
format: hours
|
|
90
|
+
log_level: INFO
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=ProxLB - A loadbalancer for Proxmox clusters
|
|
3
|
+
After=network-online.target pveproxy.service
|
|
4
|
+
Wants=network-online.target pveproxy.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
ExecStart=python3 /usr/lib/python3/dist-packages/proxlb/main.py -c /etc/proxlb/proxlb.yaml
|
|
8
|
+
User=plb
|
|
9
|
+
ExecReload=/bin/kill -HUP $MAINPID
|
|
10
|
+
KillMode=process
|
|
11
|
+
|
|
12
|
+
[Install]
|
|
13
|
+
WantedBy=multi-user.target
|
proxlb/__init__.py
ADDED
|
File without changes
|
proxlb/main.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ProxLB is a load balancing tool for Proxmox Virtual Environment (PVE) clusters.
|
|
3
|
+
It connects to the Proxmox API, retrieves information about nodes, guests, and groups,
|
|
4
|
+
and performs calculations to determine the optimal distribution of resources across the
|
|
5
|
+
cluster. The tool supports daemon mode for continuous operation and can log metrics and
|
|
6
|
+
perform balancing actions based on the configuration provided. It also includes a CLI
|
|
7
|
+
parser for handling command-line arguments and a custom logger for systemd integration.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__author__ = "Florian Paul Azim Hoberg <gyptazy>"
|
|
11
|
+
__copyright__ = "Copyright (C) 2025 Florian Paul Azim Hoberg (@gyptazy)"
|
|
12
|
+
__license__ = "GPL-3.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import signal
|
|
17
|
+
from utils.logger import SystemdLogger
|
|
18
|
+
from utils.cli_parser import CliParser
|
|
19
|
+
from utils.config_parser import ConfigParser
|
|
20
|
+
from utils.proxmox_api import ProxmoxApi
|
|
21
|
+
from models.nodes import Nodes
|
|
22
|
+
from models.features import Features
|
|
23
|
+
from models.guests import Guests
|
|
24
|
+
from models.groups import Groups
|
|
25
|
+
from models.calculations import Calculations
|
|
26
|
+
from models.balancing import Balancing
|
|
27
|
+
from models.pools import Pools
|
|
28
|
+
from models.ha_rules import HaRules
|
|
29
|
+
from utils.helper import Helper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
"""
|
|
34
|
+
ProxLB main function
|
|
35
|
+
"""
|
|
36
|
+
# Initialize logging handler
|
|
37
|
+
logger = SystemdLogger(level=logging.INFO)
|
|
38
|
+
|
|
39
|
+
# Initialize handlers
|
|
40
|
+
signal.signal(signal.SIGHUP, Helper.handler_sighup)
|
|
41
|
+
signal.signal(signal.SIGINT, Helper.handler_sigint)
|
|
42
|
+
|
|
43
|
+
# Parses arguments passed from the CLI
|
|
44
|
+
cli_parser = CliParser()
|
|
45
|
+
cli_args = cli_parser.parse_args()
|
|
46
|
+
Helper.get_version(cli_args.version)
|
|
47
|
+
|
|
48
|
+
# Parse ProxLB config file
|
|
49
|
+
config_parser = ConfigParser(cli_args.config)
|
|
50
|
+
proxlb_config = config_parser.get_config()
|
|
51
|
+
|
|
52
|
+
# Update log level from config and fallback to INFO if not defined
|
|
53
|
+
logger.set_log_level(proxlb_config.get('service', {}).get('log_level', 'INFO'))
|
|
54
|
+
|
|
55
|
+
# Validate of an optional service delay
|
|
56
|
+
Helper.get_service_delay(proxlb_config)
|
|
57
|
+
|
|
58
|
+
# Connect to Proxmox API & create API object
|
|
59
|
+
proxmox_api = ProxmoxApi(proxlb_config)
|
|
60
|
+
|
|
61
|
+
# Overwrite password after creating the API object
|
|
62
|
+
proxlb_config["proxmox_api"]["pass"] = "********"
|
|
63
|
+
|
|
64
|
+
while True:
|
|
65
|
+
|
|
66
|
+
# Validate if reload signal was sent during runtime
|
|
67
|
+
# and reload the ProxLB configuration and adjust log level
|
|
68
|
+
if Helper.proxlb_reload:
|
|
69
|
+
logger.info("Reloading ProxLB configuration.")
|
|
70
|
+
proxlb_config = config_parser.get_config()
|
|
71
|
+
logger.set_log_level(proxlb_config.get('service', {}).get('log_level', 'INFO'))
|
|
72
|
+
Helper.proxlb_reload = False
|
|
73
|
+
|
|
74
|
+
# Get all required objects from the Proxmox cluster
|
|
75
|
+
meta = {"meta": proxlb_config}
|
|
76
|
+
nodes = Nodes.get_nodes(proxmox_api, proxlb_config)
|
|
77
|
+
meta = Features.validate_any_non_pve9_node(meta, nodes)
|
|
78
|
+
pools = Pools.get_pools(proxmox_api)
|
|
79
|
+
ha_rules = HaRules.get_ha_rules(proxmox_api, meta)
|
|
80
|
+
guests = Guests.get_guests(proxmox_api, pools, ha_rules, nodes, meta, proxlb_config)
|
|
81
|
+
groups = Groups.get_groups(guests, nodes)
|
|
82
|
+
|
|
83
|
+
# Merge obtained objects from the Proxmox cluster for further usage
|
|
84
|
+
proxlb_data = {**meta, **nodes, **guests, **pools, **ha_rules, **groups}
|
|
85
|
+
Helper.log_node_metrics(proxlb_data)
|
|
86
|
+
|
|
87
|
+
# Validate usable features by PVE versions
|
|
88
|
+
Features.validate_available_features(proxlb_data)
|
|
89
|
+
|
|
90
|
+
# Update the initial node resource assignments
|
|
91
|
+
# by the previously created groups.
|
|
92
|
+
Calculations.set_node_assignments(proxlb_data)
|
|
93
|
+
Helper.log_node_metrics(proxlb_data, init=False)
|
|
94
|
+
Calculations.set_node_hot(proxlb_data)
|
|
95
|
+
Calculations.set_guest_hot(proxlb_data)
|
|
96
|
+
target = Calculations.get_most_free_node(proxlb_data, cli_args.best_node)
|
|
97
|
+
if target is None:
|
|
98
|
+
logger.warning("No suitable target node found for balancing. Skipping this run.")
|
|
99
|
+
else:
|
|
100
|
+
Calculations.validate_affinity_map(proxlb_data)
|
|
101
|
+
Calculations.relocate_guests_on_maintenance_nodes(proxlb_data)
|
|
102
|
+
Calculations.get_balanciness(proxlb_data)
|
|
103
|
+
Calculations.relocate_guests(proxlb_data)
|
|
104
|
+
Helper.log_node_metrics(proxlb_data, init=False)
|
|
105
|
+
|
|
106
|
+
# Perform balancing actions via Proxmox API
|
|
107
|
+
if proxlb_data["meta"]["balancing"].get("enable", False):
|
|
108
|
+
if not cli_args.dry_run:
|
|
109
|
+
Balancing(proxmox_api, proxlb_data)
|
|
110
|
+
|
|
111
|
+
# Validate if the JSON output should be
|
|
112
|
+
# printed to stdout
|
|
113
|
+
Helper.print_json(proxlb_data, cli_args.json)
|
|
114
|
+
# Validate daemon mode
|
|
115
|
+
Helper.get_daemon_mode(proxlb_config)
|
|
116
|
+
logger.debug(f"Finished: __main__")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Balancing class is responsible for processing workloads on Proxmox clusters.
|
|
3
|
+
It processes the previously generated data (held in proxlb_data) and moves guests
|
|
4
|
+
and other supported types across Proxmox clusters based on the defined values by an operator.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__author__ = "Florian Paul Azim Hoberg <gyptazy>"
|
|
9
|
+
__copyright__ = "Copyright (C) 2025 Florian Paul Azim Hoberg (@gyptazy)"
|
|
10
|
+
__license__ = "GPL-3.0"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import proxmoxer
|
|
14
|
+
import time
|
|
15
|
+
from itertools import islice
|
|
16
|
+
from utils.logger import SystemdLogger
|
|
17
|
+
from typing import Dict, Any
|
|
18
|
+
|
|
19
|
+
logger = SystemdLogger()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Balancing:
|
|
23
|
+
"""
|
|
24
|
+
The balancing class is responsible for processing workloads on Proxmox clusters.
|
|
25
|
+
The previously generated data (hold in proxlb_data) will processed and guests and
|
|
26
|
+
other supported types will be moved across Proxmox clusters based on the defined
|
|
27
|
+
values by an operator.
|
|
28
|
+
|
|
29
|
+
Methods:
|
|
30
|
+
__init__(self, proxmox_api: any, proxlb_data: Dict[str, Any]):
|
|
31
|
+
Initializes the Balancing class with the provided ProxLB data and initiates the rebalancing
|
|
32
|
+
process for guests.
|
|
33
|
+
|
|
34
|
+
exec_rebalancing_vm(self, proxmox_api: any, proxlb_data: Dict[str, Any], guest_name: str) -> None:
|
|
35
|
+
Executes the rebalancing of a virtual machine (VM) to a new node within the cluster. Logs the migration
|
|
36
|
+
process and handles exceptions.
|
|
37
|
+
|
|
38
|
+
exec_rebalancing_ct(self, proxmox_api: any, proxlb_data: Dict[str, Any], guest_name: str) -> None:
|
|
39
|
+
Executes the rebalancing of a container (CT) to a new node within the cluster. Logs the migration
|
|
40
|
+
process and handles exceptions.
|
|
41
|
+
|
|
42
|
+
get_rebalancing_job_status(self, proxmox_api: any, proxlb_data: Dict[str, Any], guest_name: str, guest_current_node: str, job_id: int, retry_counter: int = 1) -> bool:
|
|
43
|
+
Monitors the status of a rebalancing job on a Proxmox node until it completes or a timeout
|
|
44
|
+
is reached. Returns True if the job completed successfully, False otherwise.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, proxmox_api: any, proxlb_data: Dict[str, Any]):
|
|
48
|
+
"""
|
|
49
|
+
Initializes the Balancing class with the provided ProxLB data.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
proxmox_api (object): The Proxmox API client instance used to interact with the Proxmox cluster.
|
|
53
|
+
proxlb_data (dict): A dictionary containing data related to the ProxLB load balancing configuration.
|
|
54
|
+
"""
|
|
55
|
+
def chunk_dict(data, size):
|
|
56
|
+
"""
|
|
57
|
+
Splits a dictionary into chunks of a specified size.
|
|
58
|
+
Args:
|
|
59
|
+
data (dict): The dictionary to be split into chunks.
|
|
60
|
+
size (int): The size of each chunk.
|
|
61
|
+
Yields:
|
|
62
|
+
dict: A chunk of the original dictionary with the specified size.
|
|
63
|
+
"""
|
|
64
|
+
logger.debug("Starting: chunk_dict.")
|
|
65
|
+
it = iter(data.items())
|
|
66
|
+
for chunk in range(0, len(data), size):
|
|
67
|
+
yield dict(islice(it, size))
|
|
68
|
+
|
|
69
|
+
# Validate if balancing should be performed in parallel or sequentially.
|
|
70
|
+
# If parallel balancing is enabled, set the number of parallel jobs.
|
|
71
|
+
parallel_jobs = proxlb_data["meta"]["balancing"].get("parallel_jobs", 5)
|
|
72
|
+
if not proxlb_data["meta"]["balancing"].get("parallel", False):
|
|
73
|
+
parallel_jobs = 1
|
|
74
|
+
logger.debug("Balancing: Parallel balancing is disabled. Running sequentially.")
|
|
75
|
+
else:
|
|
76
|
+
logger.debug(f"Balancing: Parallel balancing is enabled. Running with {parallel_jobs} parallel jobs.")
|
|
77
|
+
|
|
78
|
+
for chunk in chunk_dict(proxlb_data["guests"], parallel_jobs):
|
|
79
|
+
jobs_to_wait = []
|
|
80
|
+
|
|
81
|
+
for guest_name, guest_meta in chunk.items():
|
|
82
|
+
|
|
83
|
+
# Check if the guest's target is not the same as the current node
|
|
84
|
+
if guest_meta["node_current"] != guest_meta["node_target"]:
|
|
85
|
+
|
|
86
|
+
# Check if the guest is not ignored and perform the balancing
|
|
87
|
+
# operation based on the guest type
|
|
88
|
+
if not guest_meta["ignore"]:
|
|
89
|
+
job_id = None
|
|
90
|
+
|
|
91
|
+
# VM Balancing
|
|
92
|
+
if guest_meta["type"] == "vm":
|
|
93
|
+
if 'vm' in proxlb_data["meta"]["balancing"].get("balance_types", []):
|
|
94
|
+
logger.debug(f"Balancing: Balancing for guest {guest_name} of type VM started.")
|
|
95
|
+
job_id = self.exec_rebalancing_vm(proxmox_api, proxlb_data, guest_name)
|
|
96
|
+
else:
|
|
97
|
+
logger.debug(
|
|
98
|
+
f"Balancing: Balancing for guest {guest_name} will not be performed. "
|
|
99
|
+
"Guest is of type VM which is not included in allowed balancing types.")
|
|
100
|
+
|
|
101
|
+
# CT Balancing
|
|
102
|
+
elif guest_meta["type"] == "ct":
|
|
103
|
+
if 'ct' in proxlb_data["meta"]["balancing"].get("balance_types", []):
|
|
104
|
+
logger.debug(f"Balancing: Balancing for guest {guest_name} of type CT started.")
|
|
105
|
+
job_id = self.exec_rebalancing_ct(proxmox_api, proxlb_data, guest_name)
|
|
106
|
+
else:
|
|
107
|
+
logger.debug(
|
|
108
|
+
f"Balancing: Balancing for guest {guest_name} will not be performed. "
|
|
109
|
+
"Guest is of type CT which is not included in allowed balancing types.")
|
|
110
|
+
|
|
111
|
+
# Just in case we get a new type of guest in the future
|
|
112
|
+
else:
|
|
113
|
+
logger.critical(f"Balancing: Got unexpected guest type: {guest_meta['type']}. Cannot proceed guest: {guest_meta['name']}.")
|
|
114
|
+
|
|
115
|
+
if job_id:
|
|
116
|
+
jobs_to_wait.append((guest_name, guest_meta["node_current"], job_id))
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
logger.debug(f"Balancing: Guest {guest_name} is ignored and will not be rebalanced.")
|
|
120
|
+
else:
|
|
121
|
+
logger.debug(f"Balancing: Guest {guest_name} is already on the target node {guest_meta['node_target']} and will not be rebalanced.")
|
|
122
|
+
|
|
123
|
+
# Wait for all jobs in the current chunk to complete
|
|
124
|
+
for guest_name, node, job_id in jobs_to_wait:
|
|
125
|
+
if job_id:
|
|
126
|
+
self.get_rebalancing_job_status(proxmox_api, proxlb_data, guest_name, node, job_id)
|
|
127
|
+
|
|
128
|
+
def exec_rebalancing_vm(self, proxmox_api: any, proxlb_data: Dict[str, Any], guest_name: str) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Executes the rebalancing of a virtual machine (VM) to a new node within the cluster.
|
|
131
|
+
This function initiates the migration of a specified VM to a target node as part of the
|
|
132
|
+
load balancing process. It logs the migration process and handles any exceptions that
|
|
133
|
+
may occur during the migration.
|
|
134
|
+
Args:
|
|
135
|
+
proxmox_api (object): The Proxmox API client instance used to interact with the Proxmox cluster.
|
|
136
|
+
proxlb_data (dict): A dictionary containing data related to the ProxLB load balancing configuration.
|
|
137
|
+
guest_name (str): The name of the guest VM to be migrated.
|
|
138
|
+
Raises:
|
|
139
|
+
proxmox_api.core.ResourceException: If an error occurs during the migration process.
|
|
140
|
+
Returns:
|
|
141
|
+
None
|
|
142
|
+
"""
|
|
143
|
+
logger.debug("Starting: exec_rebalancing_vm.")
|
|
144
|
+
guest_id = proxlb_data["guests"][guest_name]["id"]
|
|
145
|
+
guest_node_current = proxlb_data["guests"][guest_name]["node_current"]
|
|
146
|
+
guest_node_target = proxlb_data["guests"][guest_name]["node_target"]
|
|
147
|
+
job_id = None
|
|
148
|
+
|
|
149
|
+
if proxlb_data["meta"]["balancing"].get("live", True):
|
|
150
|
+
online_migration = 1
|
|
151
|
+
else:
|
|
152
|
+
online_migration = 0
|
|
153
|
+
|
|
154
|
+
if proxlb_data["meta"]["balancing"].get("with_local_disks", True):
|
|
155
|
+
with_local_disks = 1
|
|
156
|
+
else:
|
|
157
|
+
with_local_disks = 0
|
|
158
|
+
|
|
159
|
+
migration_options = {
|
|
160
|
+
'target': guest_node_target,
|
|
161
|
+
'online': online_migration,
|
|
162
|
+
'with-local-disks': with_local_disks,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Conntrack state aware migrations are not supported in older
|
|
166
|
+
# PVE versions, so we should not add it by default.
|
|
167
|
+
if proxlb_data["meta"]["balancing"].get("with_conntrack_state", True):
|
|
168
|
+
migration_options['with-conntrack-state'] = 1
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
logger.info(f"Balancing: Starting to migrate VM guest {guest_name} from {guest_node_current} to {guest_node_target}.")
|
|
172
|
+
job_id = proxmox_api.nodes(guest_node_current).qemu(guest_id).migrate().post(**migration_options)
|
|
173
|
+
except proxmoxer.core.ResourceException as proxmox_api_error:
|
|
174
|
+
logger.critical(f"Balancing: Failed to migrate guest {guest_name} of type VM due to some Proxmox errors. Please check if resource is locked or similar.")
|
|
175
|
+
logger.debug(f"Balancing: Failed to migrate guest {guest_name} of type VM due to some Proxmox errors: {proxmox_api_error}")
|
|
176
|
+
|
|
177
|
+
logger.debug("Finished: exec_rebalancing_vm.")
|
|
178
|
+
return job_id
|
|
179
|
+
|
|
180
|
+
def exec_rebalancing_ct(self, proxmox_api: any, proxlb_data: Dict[str, Any], guest_name: str) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Executes the rebalancing of a container (CT) to a new node within the cluster.
|
|
183
|
+
This function initiates the migration of a specified CT to a target node as part of the
|
|
184
|
+
load balancing process. It logs the migration process and handles any exceptions that
|
|
185
|
+
may occur during the migration.
|
|
186
|
+
Args:
|
|
187
|
+
proxmox_api (object): The Proxmox API client instance used to interact with the Proxmox cluster.
|
|
188
|
+
proxlb_data (dict): A dictionary containing data related to the ProxLB load balancing configuration.
|
|
189
|
+
guest_name (str): The name of the guest CT to be migrated.
|
|
190
|
+
Raises:
|
|
191
|
+
proxmox_api.core.ResourceException: If an error occurs during the migration process.
|
|
192
|
+
Returns:
|
|
193
|
+
None
|
|
194
|
+
"""
|
|
195
|
+
logger.debug("Starting: exec_rebalancing_ct.")
|
|
196
|
+
guest_id = proxlb_data["guests"][guest_name]["id"]
|
|
197
|
+
guest_node_current = proxlb_data["guests"][guest_name]["node_current"]
|
|
198
|
+
guest_node_target = proxlb_data["guests"][guest_name]["node_target"]
|
|
199
|
+
job_id = None
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
logger.info(f"Balancing: Starting to migrate CT guest {guest_name} from {guest_node_current} to {guest_node_target}.")
|
|
203
|
+
job_id = proxmox_api.nodes(guest_node_current).lxc(guest_id).migrate().post(target=guest_node_target, restart=1)
|
|
204
|
+
except proxmoxer.core.ResourceException as proxmox_api_error:
|
|
205
|
+
logger.critical(f"Balancing: Failed to migrate guest {guest_name} of type CT due to some Proxmox errors. Please check if resource is locked or similar.")
|
|
206
|
+
logger.debug(f"Balancing: Failed to migrate guest {guest_name} of type CT due to some Proxmox errors: {proxmox_api_error}")
|
|
207
|
+
|
|
208
|
+
logger.debug("Finished: exec_rebalancing_ct.")
|
|
209
|
+
return job_id
|
|
210
|
+
|
|
211
|
+
def get_rebalancing_job_status(self, proxmox_api: any, proxlb_data: Dict[str, Any], guest_name: str, guest_current_node: str, job_id: int, retry_counter: int = 1) -> bool:
|
|
212
|
+
"""
|
|
213
|
+
Monitors the status of a rebalancing job on a Proxmox node until it completes or a timeout is reached.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
proxmox_api (object): The Proxmox API client instance.
|
|
217
|
+
proxlb_data (dict): The ProxLB configuration data.
|
|
218
|
+
guest_name (str): The name of the guest (virtual machine) being rebalanced.
|
|
219
|
+
guest_current_node (str): The current node where the guest is running.
|
|
220
|
+
job_id (str): The ID of the rebalancing job to monitor.
|
|
221
|
+
retry_counter (int, optional): The current retry count. Defaults to 1.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
bool: True if the job completed successfully, False otherwise.
|
|
225
|
+
"""
|
|
226
|
+
logger.debug("Starting: get_rebalancing_job_status.")
|
|
227
|
+
job = proxmox_api.nodes(guest_current_node).tasks(job_id).status().get()
|
|
228
|
+
|
|
229
|
+
# Fetch actual migration job status if this got spawned by a HA job
|
|
230
|
+
if job["type"] == "hamigrate":
|
|
231
|
+
logger.debug(f"Balancing: Job ID {job_id} (guest: {guest_name}) is a HA migration job. Fetching underlying migration job...")
|
|
232
|
+
time.sleep(1)
|
|
233
|
+
vm_id = int(job["id"])
|
|
234
|
+
qm_migrate_jobs = proxmox_api.nodes(guest_current_node).tasks.get(typefilter="qmigrate", vmid=vm_id, start=0, source="active", limit=1)
|
|
235
|
+
|
|
236
|
+
if len(qm_migrate_jobs) > 0:
|
|
237
|
+
job = qm_migrate_jobs[0]
|
|
238
|
+
job_id = job["upid"]
|
|
239
|
+
logger.debug(f'Overwriting job polling for: ID {job_id} (guest: {guest_name}) by {job}')
|
|
240
|
+
else:
|
|
241
|
+
logger.debug(f"Balancing: Job ID {job_id} (guest: {guest_name}) is a standard migration job. Proceeding with status check.")
|
|
242
|
+
|
|
243
|
+
# Watch job id until it finalizes
|
|
244
|
+
# Note: Unsaved jobs are delivered in uppercase from Proxmox API
|
|
245
|
+
if job.get("status", "").lower() == "running":
|
|
246
|
+
# Do not hammer the API while
|
|
247
|
+
# watching the job status
|
|
248
|
+
time.sleep(10)
|
|
249
|
+
retry_counter += 1
|
|
250
|
+
|
|
251
|
+
# Run recursion until we hit the soft-limit of maximum migration time for a guest
|
|
252
|
+
if retry_counter < proxlb_data["meta"]["balancing"].get("max_job_validation", 1800):
|
|
253
|
+
logger.debug(f"Balancing: Job ID {job_id} (guest: {guest_name}) for migration is still running... (Run: {retry_counter})")
|
|
254
|
+
self.get_rebalancing_job_status(proxmox_api, proxlb_data, guest_name, guest_current_node, job_id, retry_counter)
|
|
255
|
+
else:
|
|
256
|
+
logger.warning(f"Balancing: Job ID {job_id} (guest: {guest_name}) for migration took too long. Please check manually.")
|
|
257
|
+
logger.debug("Finished: get_rebalancing_job_status.")
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
# Validate job output for errors when finished
|
|
261
|
+
if job["status"] == "stopped":
|
|
262
|
+
|
|
263
|
+
if job["exitstatus"] == "OK":
|
|
264
|
+
logger.debug(f"Balancing: Job ID {job_id} (guest: {guest_name}) was successfully.")
|
|
265
|
+
logger.debug("Finished: get_rebalancing_job_status.")
|
|
266
|
+
return True
|
|
267
|
+
else:
|
|
268
|
+
logger.critical(f"Balancing: Job ID {job_id} (guest: {guest_name}) went into an error! Please check manually.")
|
|
269
|
+
logger.debug("Finished: get_rebalancing_job_status.")
|
|
270
|
+
return False
|