feature: Add dry-run support to see what kind of rebalancing would be done. [#6]

* Add new cli param: -d for dry-run: Prints movements to cli
  * Add new cli param: -j for json: Prints movement as a json to cli

Fixes: #6
This commit is contained in:
Florian Paul Azim Hoberg (@gyptazy)
2024-07-11 08:58:48 +02:00
parent a242f0ebce
commit bf5ba5f8a6
3 changed files with 74 additions and 18 deletions

View File

@@ -0,0 +1,2 @@
added:
- Add dry-run support to see what kind of rebalancing would be done. [#6]

View File

@@ -49,6 +49,13 @@ Automated rebalancing reduces the need for manual actions, allowing operators to
* Filter
* Exclude nodes
* Exclude virtual machines
* Grouping
* Include groups (VMs that are rebalanced to nodes together)
* Exclude groups (VMs that must run on different nodes)
* Ignore groups (VMs that should be untouched)
* Dry-run support
* Human readable output in cli
* JSON output for further parsing
* Migrate VM workloads away (e.g. maintenance preparation)
* Fully based on Proxmox API
* Usage
@@ -73,6 +80,7 @@ The following options can be set in the `proxlb.conf` file:
| api_pass | FooBar | Password for the API. |
| verify_ssl | 1 | Validate SSL certificates (1) or ignore (0). (default: 1) |
| method | memory | Defines the balancing method (default: memory) where you can use `memory`, `disk` or `cpu`. |
| balanciness | 10 | Value of the percentage of lowest and highest resource consumption on nodes may differ before rebalancing. (default: 10) |
| 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) |
| daemon | 1 | Run as a daemon (1) or one-shot (0). (default: 1) |
@@ -87,6 +95,13 @@ api_pass: FooBar
verify_ssl: 1
[balancing]
method: memory
# Balanciness defines how much difference may be
# between the lowest & highest resource consumption
# of nodes before rebalancing will be done.
# Examples:
# Rebalancing: node01: 41% memory consumption :: node02: 52% consumption
# No rebalancing: node01: 43% memory consumption :: node02: 50% consumption
balanciness: 10
ignore_nodes: dummynode01,dummynode02
ignore_vms: testvm01,testvm02
[service]
@@ -99,6 +114,8 @@ The following options and parameters are currently supported:
| Option | Long Option | Description | Default |
|------|:------:|------:|------:|
| -c | --config | Path to a config file. | /etc/proxlb/proxlb.conf (default) |
| -d | --dry-run | Perform a dry-run without doing any actions. | Unset |
| -j | --json | Return a JSON of the VM movement. | Unset |
### Grouping

73
proxlb
View File

@@ -22,12 +22,13 @@
import argparse
import configparser
import json
import logging
import os
try:
import proxmoxer
_imports = True
except ImportError as error:
except ImportError:
_imports = False
import random
import re
@@ -140,7 +141,9 @@ def __validate_config_file(config_path):
def initialize_args():
""" Initialize given arguments for ProxLB. """
argparser = argparse.ArgumentParser(description='ProxLB')
argparser.add_argument('-c', '--config', type=str, help='Path to config file.')
argparser.add_argument('-c', '--config', type=str, help='Path to config file.', required=True)
argparser.add_argument('-d', '--dry-run', help='Perform a dry-run without doing any actions.', action='store_true', required=False)
argparser.add_argument('-j', '--json', help='Return a JSON of the VM movement.', action='store_true', required=False)
return argparser.parse_args()
@@ -172,6 +175,7 @@ def initialize_config_options(config_path):
proxmox_api_ssl_v = config['proxmox']['verify_ssl']
# Balancing
balancing_method = config['balancing'].get('method', 'memory')
balanciness = config['balancing'].get('balanciness', 10)
ignore_nodes = config['balancing'].get('ignore_nodes', None)
ignore_vms = config['balancing'].get('ignore_vms', None)
# Service
@@ -189,7 +193,7 @@ def initialize_config_options(config_path):
logging.info(f'{info_prefix} Configuration file loaded.')
return proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_api_ssl_v, balancing_method, \
ignore_nodes, ignore_vms, daemon, schedule
balanciness, ignore_nodes, ignore_vms, daemon, schedule
def api_connect(proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_api_ssl_v):
@@ -346,10 +350,10 @@ def __get_proxlb_groups(vm_tags):
return group_include, group_exclude, vm_ignore
def balancing_calculations(balancing_method, node_statistics, vm_statistics):
def balancing_calculations(balancing_method, node_statistics, vm_statistics, balanciness):
""" Calculate re-balancing of VMs on present nodes across the cluster. """
info_prefix = 'Info: [rebalancing-calculator]:'
balanciness = 10
balanciness = int(balanciness)
rebalance = False
processed_vms = []
rebalance = True
@@ -546,20 +550,53 @@ def __get_vm_tags_exclude_groups(vm_statistics, node_statistics, balancing_metho
return node_statistics, vm_statistics
def run_vm_rebalancing(api_object, vm_statistics_rebalanced):
def run_vm_rebalancing(api_object, vm_statistics_rebalanced, app_args):
""" Run rebalancing of vms to new nodes in cluster. """
error_prefix = 'Error: [rebalancing-executor]:'
info_prefix = 'Info: [rebalancing-executor]:'
logging.info(f'{info_prefix} Starting to rebalance vms to their new nodes.')
for vm, value in vm_statistics_rebalanced.items():
if not app_args.dry_run:
logging.info(f'{info_prefix} Starting to rebalance vms to their new nodes.')
for vm, value in vm_statistics_rebalanced.items():
try:
logging.info(f'{info_prefix} Rebalancing vm {vm} from node {value["node_parent"]} to node {value["node_rebalance"]}.')
api_object.nodes(value['node_parent']).qemu(value['vmid']).migrate().post(target=value['node_rebalance'],online=1)
except proxmoxer.core.ResourceException as error_resource:
__errors__ = True
logging.critical(f'{error_prefix} {error_resource}')
try:
logging.info(f'{info_prefix} Rebalancing vm {vm} from node {value["node_parent"]} to node {value["node_rebalance"]}.')
api_object.nodes(value['node_parent']).qemu(value['vmid']).migrate().post(target=value['node_rebalance'],online=1)
except proxmoxer.core.ResourceException as error_resource:
logging.critical(f'{error_prefix} {error_resource}')
if app_args.json:
logging.info(f'{info_prefix} Printing json output of VM statistics.')
json.dumps(vm_statistics_rebalanced)
else:
logging.info(f'{info_prefix} Starting dry-run to rebalance vms to their new nodes.')
_vm_to_node_list = []
_vm_to_node_list.append(['VM', 'Current Node', 'Rebalanced Node'])
for vm_name, vm_values in vm_statistics_rebalanced.items():
_vm_to_node_list.append([vm_name, vm_values['node_parent'], vm_values['node_rebalance']])
if app_args.json:
logging.info(f'{info_prefix} Printing json output of VM statistics.')
json.dumps(vm_statistics_rebalanced)
else:
if len(vm_statistics_rebalanced) > 0:
logging.info(f'{info_prefix} Printing cli output of VM rebalancing.')
print_table_cli(_vm_to_node_list)
else:
logging.info(f'{info_prefix} No rebalancing needed according to the defined balanciness.')
print('No rebalancing needed according to the defined balanciness.')
def print_table_cli(table):
""" Pretty print a given table to the cli. """
longest_cols = [
(max([len(str(row[i])) for row in table]) + 3)
for i in range(len(table[0]))
]
row_format = "".join(["{:>" + str(longest_col) + "}" for longest_col in longest_cols])
for row in table:
print(row_format.format(*row))
def main():
@@ -572,7 +609,7 @@ def main():
# Parse global config
proxmox_api_host, proxmox_api_user, proxmox_api_pass, proxmox_api_ssl_v, balancing_method, \
ignore_nodes, ignore_vms, daemon, schedule = initialize_config_options(config_path)
balanciness, ignore_nodes, ignore_vms, daemon, schedule = initialize_config_options(config_path)
while True:
# API Authentication.
@@ -583,10 +620,10 @@ def main():
vm_statistics = get_vm_statistics(api_object, ignore_vms)
# Calculate rebalancing of vms.
node_statistics_rebalanced, vm_statistics_rebalanced = balancing_calculations(balancing_method, node_statistics, vm_statistics)
node_statistics_rebalanced, vm_statistics_rebalanced = balancing_calculations(balancing_method, node_statistics, vm_statistics, balanciness)
# Rebalance vms to new nodes within the cluster.
run_vm_rebalancing(api_object, vm_statistics_rebalanced)
run_vm_rebalancing(api_object, vm_statistics_rebalanced, app_args)
# Validate for any errors
post_validations()
@@ -596,4 +633,4 @@ def main():
if __name__ == '__main__':
main()
main()