Skip to content

Commit

Permalink
Merge pull request #13 from gyptazy/feature/6-add-dry-run-support
Browse files Browse the repository at this point in the history
feature: Add dry-run support to see what kind of rebalancing would be done
  • Loading branch information
gyptazy authored Jul 12, 2024
2 parents 5b40b2f + bf5ba5f commit d26efea
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .changelogs/1.0.0/6_add_dry_run_support.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
added:
- Add dry-run support to see what kind of rebalancing would be done. [#6]
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) |
Expand All @@ -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]
Expand All @@ -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
Expand Down
73 changes: 55 additions & 18 deletions proxlb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()


Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
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.')

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}')

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():
Expand All @@ -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.
Expand All @@ -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()
Expand All @@ -596,4 +633,4 @@ def main():


if __name__ == '__main__':
main()
main()

0 comments on commit d26efea

Please sign in to comment.