diff --git a/.changelogs/1.1.0/40_add_option_to_run_only_on_cluster_master_node.yml b/.changelogs/1.1.0/40_add_option_to_run_only_on_cluster_master_node.yml new file mode 100644 index 0000000..4320350 --- /dev/null +++ b/.changelogs/1.1.0/40_add_option_to_run_only_on_cluster_master_node.yml @@ -0,0 +1,2 @@ +added: + - Add option to run ProxLB only on the Proxmox's master node in the cluster. [40] diff --git a/README.md b/README.md index 14f142b..0dbf341 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ The following options can be set in the `proxlb.conf` file: | parallel_migrations | 1 | Defines if migrations should be done parallely or sequentially. (default: 1) | | ignore_nodes | dummynode01,dummynode02,test* | Defines a comma separated list of nodes to exclude. | | ignore_vms | testvm01,testvm02 | Defines a comma separated list of VMs to exclude. (`*` as suffix wildcard or tags are also supported) | +| master_only | 0 | Defines is this should only be performed (1) on the cluster master node or not (0). (default: 0) | | daemon | 1 | Run as a daemon (1) or one-shot (0). (default: 1) | | schedule | 24 | Hours to rebalance in hours. (default: 24) | | log_verbosity | INFO | Defines the log level (default: CRITICAL) where you can use `INFO`, `WARN` or `CRITICAL` | @@ -140,6 +141,10 @@ parallel_migrations: 1 ignore_nodes: dummynode01,dummynode02 ignore_vms: testvm01,testvm02 [service] +# The master_only option might be usuful if running ProxLB on all nodes in a cluster +# but only a single one should do the balancing. The master node is obtained from the Proxmox +# HA status. +master_only: 0 daemon: 1 ``` diff --git a/proxlb b/proxlb index a18c9db..b28da2a 100755 --- a/proxlb +++ b/proxlb @@ -33,6 +33,7 @@ except ImportError: import random import re import requests +import socket import sys import time import urllib3 @@ -40,7 +41,7 @@ import urllib3 # Constants __appname__ = "ProxLB" -__version__ = "1.0.0" +__version__ = "1.1.0b" __author__ = "Florian Paul Azim Hoberg @gyptazy" __errors__ = False @@ -187,6 +188,7 @@ def initialize_config_options(config_path): ignore_nodes = config['balancing'].get('ignore_nodes', None) ignore_vms = config['balancing'].get('ignore_vms', None) # Service + master_only = config['service'].get('master_only', 0) daemon = config['service'].get('daemon', 1) schedule = config['service'].get('schedule', 24) log_verbosity = config['service'].get('log_verbosity', 'CRITICAL') @@ -201,8 +203,8 @@ def initialize_config_options(config_path): sys.exit(2) logging.info(f'{info_prefix} Configuration file loaded.') - return proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_api_ssl_v, balancing_method, balancing_mode, \ - balancing_mode_option, balancing_type, balanciness, parallel_migrations, ignore_nodes, ignore_vms, daemon, schedule, log_verbosity + return proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_api_ssl_v, balancing_method, balancing_mode, balancing_mode_option, \ + balancing_type, balanciness, parallel_migrations, ignore_nodes, ignore_vms, master_only, daemon, schedule, log_verbosity def api_connect(proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_api_ssl_v): @@ -232,6 +234,34 @@ def api_connect(proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_ap return api_object +def get_cluster_master(api_object): + """ Get the current master of the Proxmox cluster. """ + info_prefix = 'Info: [cluster-master-getter]:' + + logging.info(f'{info_prefix} Getting master node from cluster.') + try: + ha_status_object = api_object.cluster().ha().status().manager_status().get() + logging.info(f'{info_prefix} Master node: {ha_status_object["manager_status"]["master_node"]}') + except: + logging.info(f'{info_prefix} Could not get master node.') + + return ha_status_object['manager_status']['master_node'] + + +def validate_cluster_master(cluster_master): + """ Validate if the current execution node is the cluster master. """ + info_prefix = 'Info: [cluster-master-validator]:' + + node_executor_hostname = socket.gethostname() + logging.info(f'{info_prefix} Node executor hostname is: {node_executor_hostname}') + + if node_executor_hostname != cluster_master: + logging.info(f'{info_prefix} {node_executor_hostname} is not the cluster master ({cluster_master}).') + return False + else: + return True + + def get_node_statistics(api_object, ignore_nodes): """ Get statistics of cpu, memory and disk for each node in the cluster. """ info_prefix = 'Info: [node-statistics]:' @@ -834,7 +864,7 @@ def main(): # Parse global config. proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_api_ssl_v, balancing_method, balancing_mode, balancing_mode_option, balancing_type, \ - balanciness, parallel_migrations, ignore_nodes, ignore_vms, daemon, schedule, log_verbosity = initialize_config_options(config_path) + balanciness, parallel_migrations, ignore_nodes, ignore_vms, master_only, daemon, schedule, log_verbosity = initialize_config_options(config_path) # Overwrite logging handler with user defined log verbosity. initialize_logger(log_verbosity, update_log_verbosity=True) @@ -843,6 +873,16 @@ def main(): # API Authentication. api_object = api_connect(proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_api_ssl_v) + # Get master node of cluster and ensure that ProxLB is only performed on the + # cluster master node to avoid ongoing rebalancing. + if bool(int(master_only)): + cluster_master_node = get_cluster_master(api_object) + cluster_master = validate_cluster_master(cluster_master_node) + # Validate daemon service and skip following tasks when not being the cluster master. + if not cluster_master: + validate_daemon(daemon, schedule) + continue + # Get metric & statistics for vms and nodes. node_statistics = get_node_statistics(api_object, ignore_nodes) vm_statistics = get_vm_statistics(api_object, ignore_vms, balancing_type)