Compare commits

...

38 Commits

Author SHA1 Message Date
gyptazy
7e5b72cfc7 docs: Adjust container version to 1.1.6.1 2025-09-04 17:39:43 +02:00
gyptazy
0ba76f80f3 Merge pull request #302 from gyptazy/fix/296-vm
fix: Validate for node presence when pinning VMs to avoid crashing
2025-09-04 17:33:47 +02:00
gyptazy
b48ff9d677 fix: Validate for node presence when pinning VMs to avoid crashing
Fixes: #296
2025-09-04 17:31:24 +02:00
gyptazy
b5c11af474 Merge pull request #301 from gyptazy/gyptazy-patch-1
Set image version to 1.1.6
2025-09-04 08:34:49 +02:00
gyptazy
af2992747d Set image version to 1.1.6 2025-09-04 08:34:23 +02:00
gyptazy
fb8dc40c16 Merge pull request #300 from gyptazy/release/1.1.6
release: Create release 1.1.6
2025-09-04 07:55:38 +02:00
Florian Paul Azim Hoberg
34f1de8367 release: Create release 1.1.6
Fixes: #297
2025-09-04 07:53:29 +02:00
gyptazy
0e992e99de Merge pull request #298 from gyptazy/fix/296-validate-node-presence-when-pinning
fix: Validate for node presence when pinning guests to avoid crashing
2025-08-29 11:33:29 +02:00
gyptazy
f5d073dc02 fix: Validate for node presence when pinning guests to avoid crashing
Fixes: #296
Fixes: #271
2025-08-29 11:26:15 +02:00
gyptazy
70ba1f2dfc Merge pull request #295 from MiBUl-eu/main
fix: overprovisioning first node if anti_affinity group has only one member
2025-08-28 17:16:28 +02:00
MiBUl-eu
c9855f1991 Added changeloge and fixed typo 2025-08-26 10:34:48 +00:00
MiBUl-eu
9bd29158b9 fix: overprovisioning first node if anti_affinity group has only one member 2025-08-25 10:11:45 +02:00
gyptazy
1ff0c5d96e Merge pull request #293 from gyptazy/feature/290-validate-token-input-from-config-file
feature: Add validation for provided API user token id to avoid confusions
2025-08-25 08:11:56 +02:00
Florian Paul Azim Hoberg
3eb4038723 feature: Add validation for provided API user token id to avoid confusions
Fixes: #290
2025-08-25 08:07:44 +02:00
gyptazy
47e7dd3c56 Merge pull request #292 from gyptazy/fix/291-catch-stack-trace-when-user-account-is-not-given-or-wrong
fix(proxmox-api): Fix stacktrace output when validating permissions on non existing users in Proxmox
2025-08-25 07:58:32 +02:00
Florian Paul Azim Hoberg
bb8cf9033d fix(proxmox-api): Fix stacktrace output when validating permissions on non existing users in Proxmox
Fixes: #291
2025-08-25 07:55:02 +02:00
gyptazy
756b4efcbd Merge pull request #288 from gyptazy/feature/281-helm-chart-versioning
feature: Add Helm chart support for ProxLB
2025-08-19 06:28:31 +02:00
gyptazy
8630333e4b feature: Add Helm chart support for ProxLB
Fixes: #281
2025-08-19 06:27:38 +02:00
gyptazy
7bd9a9b038 Merge pull request #282 from MaoMaoCake/main
Feat: Add support for helm
2025-08-18 19:31:14 +02:00
maomaocake
16651351de Implemented changes from comments 2025-08-18 12:46:29 +07:00
maomaocake
63805f1f50 Added support for resource limits and requests 2025-08-17 16:11:18 +07:00
maomaocake
c0ff1b5273 Added support for new user defined labels 2025-08-17 15:59:17 +07:00
maomaocake
07f8596fc5 Helm Chart 2025-08-17 15:41:55 +07:00
Florian
affbe433f9 Merge pull request #280 from gyptazy/docs/fix-ipv6-example-syntax
docs: Fix IPv6 address syntax in given examples
2025-08-06 16:42:13 +02:00
Florian Paul Azim Hoberg
7bda22e754 docs: Fix IPv6 address syntax in given examples 2025-08-06 16:38:23 +02:00
Florian
253dcf8eb9 Merge pull request #274 from gyptazy/docs/273-proxmox-9-compatibility
docs: Add compatibility matrix of tested Proxmox versions
2025-07-20 12:37:20 +02:00
gyptazy
6212d23268 docs: Add compatibility matrix of tested Proxmox versions
Fixes: #273
2025-07-20 12:36:25 +02:00
Florian
cf8c06393f Merge pull request #270 from gyptazy/docs/258-extend-doc-of-cluster-node-names-usage
docs: Adjust docs regarding maintenance node hostnames
2025-07-17 11:53:33 +02:00
Florian Paul Azim Hoberg
5c23fd3433 docs: Adjust docs regarding maintenance node hostnames
Fixes: #258
2025-07-17 11:52:08 +02:00
Florian
0fb732fc8c Merge pull request #269 from gyptazy/fix/268-evaluate-balancing-types
fix: Fix balancing evaluation of guest types (e.g., VM or CT).
2025-07-17 11:44:29 +02:00
Florian Paul Azim Hoberg
f36d96c72a fix: Fix balancing evaluation of guest types (e.g., VM or CT).
Fixes: #268
2025-07-17 11:41:00 +02:00
Florian
9cc03717ef Merge pull request #267 from gyptazy/release/1.1.5
release: Create release 1.1.5
2025-07-14 11:13:12 +02:00
Florian Paul Azim Hoberg
4848887ccc release: Create release 1.1.5
Fixes: #266
2025-07-14 11:08:38 +02:00
Gombócz Márton
04476feeaf Docs/custom api port (#264)
* docs(README.md): added description and examples of port declaration in hosts
* docs(docs/03_configuration): updated hosts description about using ports
2025-07-10 10:33:44 +02:00
Florian
b3765bf0ae Merge pull request #261 from gyptazy/feature/260-custom-api-ports
feature: Allow custom (instead of static tcp/8006) API ports for API hosts
2025-07-10 09:19:34 +02:00
Florian Paul Azim Hoberg
806b728a14 feature: Allow custom (instead of static tcp/8006) API ports for API hosts.
Fixes: #260
2025-07-08 17:39:29 +02:00
Florian
2c34ec91b1 Merge pull request #257 from gyptazy/release/prepare-1.1.5
release: Create release 1.1.5 beta 1
2025-06-29 11:08:15 +02:00
gyptazy
08b746a53b release: Create release 1.1.5 beta 1 2025-06-27 16:50:17 +02:00
29 changed files with 425 additions and 44 deletions

View File

@@ -0,0 +1,2 @@
added:
- Allow custom API ports instead of fixed tcp/8006 (@gyptazy). [#260]

View File

@@ -0,0 +1 @@
date: 2025-07-14

View File

@@ -0,0 +1,2 @@
fixed:
- Validate for node presence when pinning VMs to avoid crashing (@gyptazy). [#296]

View File

@@ -0,0 +1 @@
date: 2025-09-04

View File

@@ -0,0 +1,2 @@
fixed:
- Fix balancing evaluation of guest types (e.g., VM or CT) (@gyptazy). [#268]

View File

@@ -0,0 +1,2 @@
added:
- Add validation for provided API user token id to avoid confusions (@gyptazy). [#291]

View File

@@ -0,0 +1,2 @@
fixed:
- Fix stacktrace output when validating permissions on non existing users in Proxmox (@gyptazy). [#291]

View File

@@ -0,0 +1,3 @@
fixed:
- Fix Overprovisioning first node if anti_affinity_group has only one member (@MiBUl-eu). [#295]

View File

@@ -0,0 +1,3 @@
fixed:
- Validate for node presence when pinning guests to avoid crashing (@gyptazy). [#296]

View File

@@ -0,0 +1 @@
date: 2025-09-04

View File

@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.6.1] - 2025-09-04
### Fixed
- Validate for node presence when pinning VMs to avoid crashing (@gyptazy). [#296]
## [1.1.6] - 2025-09-04
### Added
- Add validation for provided API user token id to avoid confusions (@gyptazy). [#291]
### Fixed
- Fix stacktrace output when validating permissions on non existing users in Proxmox (@gyptazy). [#291]
- Fix Overprovisioning first node if anti_affinity_group has only one member (@MiBUl-eu). [#295]
- Validate for node presence when pinning guests to avoid crashing (@gyptazy). [#296]
- Fix balancing evaluation of guest types (e.g., VM or CT) (@gyptazy). [#268]
## [1.1.5] - 2025-07-14
### Added
- Allow custom API ports instead of fixed tcp/8006 (@gyptazy). [#260]
## [1.1.4] - 2025-06-27
### Added

View File

@@ -1,5 +1,5 @@
# ProxLB - (Re)Balance VM Workloads in Proxmox Clusters
<img align="left" src="https://cdn.gyptazy.com/images/Prox-LB-logo.jpg"/>
<img align="left" src="https://cdn.gyptazy.com/img/ProxLB.jpg"/>
<br>
<p float="center"><img src="https://img.shields.io/github/license/gyptazy/ProxLB"/><img src="https://img.shields.io/github/contributors/gyptazy/ProxLB"/><img src="https://img.shields.io/github/last-commit/gyptazy/ProxLB/main"/><img src="https://img.shields.io/github/issues-raw/gyptazy/ProxLB"/><img src="https://img.shields.io/github/issues-pr/gyptazy/ProxLB"/></p>
@@ -77,6 +77,10 @@ Before starting any migrations, ProxLB validates that rebalancing actions are ne
## Installation
### Requirements / Dependencies
* Proxmox
* Proxmox 7.x
* Proxmox 8.x
* Proxmox 9.x
* Python3.x
* proxmoxer
* requests
@@ -130,7 +134,7 @@ wget -O /etc/apt/trusted.gpg.d/proxlb.asc https://repo.gyptazy.com/repository.gp
#### Debian Packages (.deb files)
If you do not want to use the repository you can also find the debian packages as a .deb file on gyptazy's CDN at:
* https://cdn.gyptazy.com/files/os/debian/proxlb/
* https://cdn.gyptazy.com/debian/
Afterwards, you can simply install the package by running:
```bash
@@ -161,6 +165,9 @@ docker run -it --rm -v $(pwd)/proxlb.yaml:/etc/proxlb/proxlb.yaml proxlb
| Version | Image |
|------|:------:|
| latest | cr.gyptazy.com/proxlb/proxlb:latest |
| v1.1.6.1 | cr.gyptazy.com/proxlb/proxlb:v1.1.6.1 |
| v1.1.6 | cr.gyptazy.com/proxlb/proxlb:v1.1.6 |
| v1.1.5 | cr.gyptazy.com/proxlb/proxlb:v1.1.5 |
| v1.1.4 | cr.gyptazy.com/proxlb/proxlb:v1.1.4 |
| v1.1.3 | cr.gyptazy.com/proxlb/proxlb:v1.1.3 |
| v1.1.2 | cr.gyptazy.com/proxlb/proxlb:v1.1.2 |
@@ -240,7 +247,7 @@ The following options can be set in the configuration file `proxlb.yaml`:
| Section | Option | Sub Option | Example | Type | Description |
|---------|:------:|:----------:|:-------:|:----:|:-----------:|
| `proxmox_api` | | | | | |
| | hosts | | ['virt01.example.com', '10.10.10.10', 'fe01::bad:code::cafe'] | `List` | List of Proxmox nodes. Can be IPv4, IPv6 or mixed. |
| | hosts | | ['virt01.example.com', '10.10.10.10', 'fe01:bad:code::cafe', 'virt01.example.com:443', '[fc00::1]', '[fc00::1]:443', 'fc00::1:8006'] | `List` | List of Proxmox nodes. Can be IPv4, IPv6 or mixed. You can specify custom ports. In case of IPv6 without brackets the port is considered after the last colon |
| | user | | root@pam | `Str` | Username for the API. |
| | pass | | FooBar | `Str` | Password for the API. (Recommended: Use API token authorization!) |
| | token_id | | proxlb | `Str` | Token ID of the user for the API. |
@@ -250,7 +257,7 @@ The following options can be set in the configuration file `proxlb.yaml`:
| | retries | | 1 | `Int` | How often a connection attempt to the defined API host should be performed. |
| | wait_time | | 1 | `Int` | How many seconds should be waited before performing another connection attempt to the API host. |
| `proxmox_cluster` | | | | | |
| | maintenance_nodes | | ['virt66.example.com'] | `List` | A list of Proxmox nodes that are defined to be in a maintenance. |
| | maintenance_nodes | | ['virt66.example.com'] | `List` | A list of Proxmox nodes that are defined to be in a maintenance. (must be the same node names as used within the cluster) |
| | ignore_nodes | | [] | `List` | A list of Proxmox nodes that are defined to be ignored. |
| | overprovisioning | | False | `Bool` | Avoids balancing when nodes would become overprovisioned. |
| `balancing` | | | | | |
@@ -280,7 +287,7 @@ The following options can be set in the configuration file `proxlb.yaml`:
An example of the configuration file looks like:
```
proxmox_api:
hosts: ['virt01.example.com', '10.10.10.10', 'fe01::bad:code::cafe']
hosts: ['virt01.example.com', '10.10.10.10', 'fe01:bad:code::cafe']
user: root@pam
pass: crazyPassw0rd!
# API Token method

View File

@@ -1,5 +1,5 @@
proxmox_api:
hosts: ['virt01.example.com', '10.10.10.10', 'fe01::bad:code::cafe']
hosts: ['virt01.example.com', '10.10.10.10', 'fe01:bad:code::cafe']
user: root@pam
pass: crazyPassw0rd!
# API Token method

22
debian/changelog vendored
View File

@@ -1,3 +1,25 @@
proxlb (1.1.6.1) stable; urgency=medium
* Validate for node presence when pinning VMs to avoid crashing. (Closes: #296)
-- Florian Paul Azim Hoberg <gyptazy@gyptazy.com> Thu, 04 Sep 2025 19:23:51 +0000
proxlb (1.1.6) stable; urgency=medium
* Add validation for provided API user token id to avoid confusions. (Closes: #291)
* Fix stacktrace output when validating permissions on non existing users in Proxmox. (Closes: #291)
* Fix Overprovisioning first node if anti_affinity_group has only one member. (Closes: #295)
* Validate for node presence when pinning guests to avoid crashing. (Closes: #296)
* Fix balancing evaluation of guest types (e.g., VM or CT). (Closes: #268)
-- Florian Paul Azim Hoberg <gyptazy@gyptazy.com> Thu, 04 Sep 2025 05:12:19 +0000
proxlb (1.1.5) stable; urgency=medium
* Allow custom API ports instead of fixed tcp/8006. (Closes: #260)
-- Florian Paul Azim Hoberg <gyptazy@gyptazy.com> Mon, 14 Jul 2025 11:07:34 +0000
proxlb (1.1.4) stable; urgency=medium
* Allow pinning of guests to a group of nodes. (Closes: #245)

View File

@@ -19,6 +19,7 @@
6. [Parallel Migrations](#parallel-migrations)
7. [Run as a Systemd-Service](#run-as-a-systemd-service)
8. [SSL Self-Signed Certificates](#ssl-self-signed-certificates)
9. [Node Maintenances](#node-maintenances)
## Authentication / User Accounts / Permissions
### Authentication
@@ -142,11 +143,15 @@ You can also repeat this step multiple times for different node names to create
**Note:** The given node names from the tag are validated. This means, ProxLB validated if the given node name is really part of the cluster. In case of a wrongly defined or unavailable node name it continous to use the regular processes to make sure the guest keeps running.
### API Loadbalancing
ProxLB supports API loadbalancing, where one or more host objects can be defined as a list. This ensures, that you can even operator ProxLB without further changes when one or more nodes are offline or in a maintenance. When defining multiple hosts, the first reachable one will be picked.
ProxLB supports API loadbalancing, where one or more host objects can be defined as a list. This ensures, that you can even operator ProxLB without further changes when one or more nodes are offline or in a maintenance. When defining multiple hosts, the first reachable one will be picked. You can speficy custom ports in the list. There are 4 ways of defining hosts with ports:
1. Hostname of IPv4 without port (in this case the default 8006 will be used)
2. Hostname or IPv4 with port
3. IPv6 in brackets with optional port
4. IPv6 without brackets, in this case the port is assumed after last colon
```
proxmox_api:
hosts: ['virt01.example.com', '10.10.10.10', 'fe01::bad:code::cafe']
hosts: ['virt01.example.com', '10.10.10.10', 'fe01::bad:code::cafe', 'virt01.example.com:443', '[fc00::1]', '[fc00::1]:443', 'fc00::1:8006']
```
### Ignore Host-Nodes or Guests
@@ -209,4 +214,25 @@ proxmox_api:
ssl_verification: False
```
*Note: Disabling SSL certificate validation is not recommended.*
*Note: Disabling SSL certificate validation is not recommended.*
### Node Maintenances
To exclude specific nodes from receiving any new workloads during the balancing process, the `maintenance_nodes` configuration option can be used. This option allows administrators to define a list of nodes that are currently undergoing maintenance or should otherwise not be used for running virtual machines or containers.
```yaml
maintenance_nodes:
- virt66.example.com
```
which can also be written as:
```yaml
maintenance_nodes: ['virt66.example.com']
```
The maintenance_nodes key must be defined as a list, even if it only includes a single node. Each entry in the list must exactly match the node name as it is known within the Proxmox VE cluster. Do not use IP addresses, alternative DNS names, or aliases—only the actual cluster node names are valid. Once a node is marked as being in maintenance mode:
* No new workloads will be balanced or migrated onto it.
* Any existing workloads currently running on the node will be migrated away in accordance with the configured balancing strategies, assuming resources on other nodes allow.
This feature is particularly useful during planned maintenance, upgrades, or troubleshooting, ensuring that services continue to run with minimal disruption while the specified node is being worked on.

24
helm/proxlb/Chart.yaml Normal file
View File

@@ -0,0 +1,24 @@
apiVersion: v3
name: proxlb
description: A Helm chart for self-hosted ProxLB
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: "1.1.6.1"
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v1.1.6.1"

View File

@@ -0,0 +1,13 @@
{{- define "proxlb.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{ define "proxlb.labels" }}
app.kubernetes.io/name: {{ .Release.Name }}
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/component: proxlb
{{- if .Values.labels }}
{{ toYaml .Values.labels }}
{{- end }}
{{ end }}

View File

@@ -0,0 +1,11 @@
{{- if .Values.configmap.create }}
apiVersion: v1
kind: ConfigMap
metadata:
name: proxlb-config
labels:
{{- include "proxlb.labels" . | nindent 4 }}
data:
proxlb.yaml: |
{{ toYaml .Values.configmap.config | indent 4 }}
{{ end }}

View File

@@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
labels:
{{- include "proxlb.labels" . | nindent 4 }}
spec:
replicas: 1 # Number of replicas cannot be more than 1
selector:
matchLabels:
{{- include "proxlb.labels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "proxlb.labels" . | nindent 8 }}
spec:
{{- with .Values.image.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
# not interacting with the k8s cluster
automountServiceAccountToken: False
containers:
- name: proxlb
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
{{- if .Values.extraArgs.dryRun }}
- --dry-run
{{- end }}
volumeMounts:
- name: config
mountPath: /etc/proxlb/proxlb.yaml
subPath: proxlb.yaml
{{ if .Values.resources }}
resources:
{{ with .Values.resources }}
{{ toYaml . | nindent 10 }}
{{ end }}
{{ end }}
volumes:
- name: config
configMap:
name: proxlb-config

60
helm/proxlb/values.yaml Normal file
View File

@@ -0,0 +1,60 @@
image:
registry: cr.gyptazy.com
repository: proxlb/proxlb
tag: v1.1.6.1
pullPolicy: IfNotPresent
imagePullSecrets: [ ]
resources:
limits:
cpu: "1000m"
memory: "2Gi"
requests:
cpu: "100m"
memory: "100Mi"
labels: {}
extraArgs:
dryRun: false
configmap:
create: true
config:
proxmox_api:
hosts: []
#Can be either a user or a token
# user: ""
# pass: ""
# token_id: ""
# token_secret: ""
ssl_verification: True
timeout: 10
proxmox_cluster:
maintenance_nodes: [ ]
ignore_nodes: [ ]
overprovisioning: True
balancing:
enable: True
enforce_affinity: False
parallel: False
# If running parallel job, you can define
# the amount of prallel jobs (default: 5)
parallel_jobs: 1
live: True
with_local_disks: True
balance_types: [ 'vm', 'ct' ]
max_job_validation: 1800
balanciness: 5
method: memory
mode: used
service:
daemon: True
schedule:
interval: 12
format: "hours"
delay:
enable: False
time: 1
format: "hours"
log_level: INFO

View File

@@ -1,6 +1,12 @@
#!/usr/bin/env bash
VERSION="1.1.4"
VERSION="1.1.6.1"
# ProxLB
sed -i "s/^__version__ = .*/__version__ = \"$VERSION\"/" "proxlb/utils/version.py"
sed -i "s/version=\"[0-9]*\.[0-9]*\.[0-9]*\"/version=\"$VERSION\"/" setup.py
# Helm Chart
sed -i "s/^version: .*/version: \"$VERSION\"/" helm/proxlb/Chart.yaml
sed -i "s/^appVersion: .*/appVersion: \"v$VERSION\"/" helm/proxlb/Chart.yaml
echo "OK: Versions have been sucessfully set to $VERSION"

View File

@@ -90,11 +90,23 @@ class Balancing:
# VM Balancing
if guest_meta["type"] == "vm":
job_id = self.exec_rebalancing_vm(proxmox_api, proxlb_data, guest_name)
if 'vm' in proxlb_data["meta"]["balancing"].get("balance_types", []):
logger.debug("Balancing: Balancing for guest {guest_name} of type VM started.")
job_id = self.exec_rebalancing_vm(proxmox_api, proxlb_data, guest_name)
else:
logger.debug(
f"Balancing: Balancing for guest {guest_name} will not be performed. "
"Guest is of type VM which is not included in allowed balancing types.")
# CT Balancing
elif guest_meta["type"] == "ct":
job_id = self.exec_rebalancing_ct(proxmox_api, proxlb_data, guest_name)
if 'ct' in proxlb_data["meta"]["balancing"].get("balance_types", []):
logger.debug("Balancing: Balancing for guest {guest_name} of type CT started.")
job_id = self.exec_rebalancing_ct(proxmox_api, proxlb_data, guest_name)
else:
logger.debug(
f"Balancing: Balancing for guest {guest_name} will not be performed. "
"Guest is of type CT which is not included in allowed balancing types.")
# Just in case we get a new type of guest in the future
else:

View File

@@ -266,23 +266,28 @@ class Calculations:
if guest_name in proxlb_data["groups"]["anti_affinity"][group_name]['guests'] and not proxlb_data["guests"][guest_name]["processed"]:
logger.debug(f"Anti-Affinity: Guest: {guest_name} is included in anti-affinity group: {group_name}.")
# Iterate over all available nodes
for node_name in proxlb_data["nodes"].keys():
# Check if the group has only one member. If so skip new guest node assignment.
if proxlb_data["groups"]["anti_affinity"][group_name]["counter"] > 1:
logger.debug(f"Anti-Affinity: Group has more than 1 member.")
# Iterate over all available nodes
for node_name in proxlb_data["nodes"].keys():
# Only select node if it was not used before and is not in a
# maintenance mode. Afterwards, add it to the list of already
# used nodes for the current anti-affinity group
if node_name not in proxlb_data["groups"]["anti_affinity"][group_name]["used_nodes"]:
# Only select node if it was not used before and is not in a
# maintenance mode. Afterwards, add it to the list of already
# used nodes for the current anti-affinity group
if node_name not in proxlb_data["groups"]["anti_affinity"][group_name]["used_nodes"]:
if not proxlb_data["nodes"][node_name]["maintenance"]:
# If the node has not been used yet, we assign this node to the guest
proxlb_data["meta"]["balancing"]["balance_next_node"] = node_name
proxlb_data["groups"]["anti_affinity"][group_name]["used_nodes"].append(node_name)
logger.debug(f"Node: {node_name} marked as used for anti-affinity group: {group_name} with guest {guest_name}")
break
if not proxlb_data["nodes"][node_name]["maintenance"]:
# If the node has not been used yet, we assign this node to the guest
proxlb_data["meta"]["balancing"]["balance_next_node"] = node_name
proxlb_data["groups"]["anti_affinity"][group_name]["used_nodes"].append(node_name)
logger.debug(f"Node: {node_name} marked as used for anti-affinity group: {group_name} with guest {guest_name}")
break
else:
logger.critical(f"Node: {node_name} already got used for anti-affinity group:: {group_name}. (Tried for guest: {guest_name})")
else:
logger.critical(f"Node: {node_name} already got used for anti-affinity group:: {group_name}. (Tried for guest: {guest_name})")
else:
logger.debug(f"Anti-Affinity: Group has less than 2 members. Skipping node calculation for the group.")
else:
logger.debug(f"Guest: {guest_name} is not included in anti-affinity group: {group_name}. Skipping.")

View File

@@ -79,7 +79,7 @@ class Guests:
guests['guests'][guest['name']]['affinity_groups'] = Tags.get_affinity_groups(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['anti_affinity_groups'] = Tags.get_anti_affinity_groups(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['ignore'] = Tags.get_ignore(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags'], nodes)
guests['guests'][guest['name']]['type'] = 'vm'
logger.debug(f"Resources of Guest {guest['name']} (type VM) added: {guests['guests'][guest['name']]}")
@@ -107,7 +107,7 @@ class Guests:
guests['guests'][guest['name']]['affinity_groups'] = Tags.get_affinity_groups(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['anti_affinity_groups'] = Tags.get_anti_affinity_groups(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['ignore'] = Tags.get_ignore(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags'])
guests['guests'][guest['name']]['node_relationships'] = Tags.get_node_relationships(guests['guests'][guest['name']]['tags'], nodes)
guests['guests'][guest['name']]['type'] = 'ct'
logger.debug(f"Resources of Guest {guest['name']} (type CT) added: {guests['guests'][guest['name']]}")

View File

@@ -12,7 +12,9 @@ __license__ = "GPL-3.0"
import time
from typing import List
from typing import Dict, Any
from utils.logger import SystemdLogger
from utils.helper import Helper
logger = SystemdLogger()
@@ -153,7 +155,7 @@ class Tags:
return ignore_tag
@staticmethod
def get_node_relationships(tags: List[str]) -> str:
def get_node_relationships(tags: List[str], nodes: Dict[str, Any]) -> str:
"""
Get a node relationship tag for a guest from the Proxmox cluster by the API to pin
a guest to a node.
@@ -163,6 +165,7 @@ class Tags:
Args:
tags (List): A list holding all defined tags for a given guest.
nodes (Dict): A dictionary holding all available nodes in the cluster.
Returns:
Str: The related hypervisor node name.
@@ -174,7 +177,13 @@ class Tags:
for tag in tags:
if tag.startswith("plb_pin"):
node_relationship_tag = tag.replace("plb_pin_", "")
node_relationship_tags.append(node_relationship_tag)
# Validate if the node to pin is present in the cluster
if Helper.validate_node_presence(node_relationship_tag, nodes):
logger.info(f"Tag {node_relationship_tag} is valid! Defined node exists in the cluster.")
node_relationship_tags.append(node_relationship_tag)
else:
logger.warning(f"Tag {node_relationship_tag} is invalid! Defined node does not exist in the cluster. Not applying pinning.")
logger.debug("Finished: get_node_relationships.")
return node_relationship_tags

View File

@@ -10,6 +10,7 @@ __license__ = "GPL-3.0"
import json
import uuid
import re
import sys
import time
import utils.version
@@ -214,4 +215,77 @@ class Helper:
logger.debug("Starting: handle_sighup.")
logger.debug("Got SIGHUP signal. Reloading...")
Helper.proxlb_reload = True
logger.debug("Starting: handle_sighup.")
logger.debug("Finished: handle_sighup.")
@staticmethod
def get_host_port_from_string(host_object):
"""
Parses a string containing a host (IPv4, IPv6, or hostname) and an optional port, and returns a tuple of (host, port).
Supported formats:
- Hostname or IPv4 without port: "example.com" or "192.168.0.1"
- Hostname or IPv4 with port: "example.com:8006" or "192.168.0.1:8006"
- IPv6 in brackets with optional port: "[fc00::1]" or "[fc00::1]:8006"
- IPv6 without brackets, port is assumed after last colon: "fc00::1:8006"
If no port is specified, port 8006 is used as the default.
Args:
host_object (str): A string representing a host with or without a port.
Returns:
tuple: A tuple (host: str, port: int)
"""
logger.debug("Starting: get_host_port_from_string.")
# IPv6 (with or without port, written in brackets)
match = re.match(r'^\[(.+)\](?::(\d+))?$', host_object)
if match:
host = match.group(1)
port = int(match.group(2)) if match.group(2) else 8006
return host, port
# Count colons to identify IPv6 addresses without brackets
colon_count = host_object.count(':')
# IPv4 or hostname without port
if colon_count == 0:
return host_object, 8006
# IPv4 or hostname with port
elif colon_count == 1:
host, port = host_object.split(':')
return host, int(port)
# IPv6 (with or without port, assume last colon is port)
else:
parts = host_object.rsplit(':', 1)
try:
port = int(parts[1])
return parts[0], port
except ValueError:
return host_object, 8006
@staticmethod
def validate_node_presence(node: str, nodes: Dict[str, Any]) -> bool:
"""
Validates whether a given node exists in the provided cluster nodes dictionary.
Args:
node (str): The name of the node to validate.
nodes (Dict[str, Any]): A dictionary containing cluster information.
Must include a "nodes" key mapping to a dict of available nodes.
Returns:
bool: True if the node exists in the cluster, False otherwise.
"""
logger.debug("Starting: validate_node_presence.")
if node in nodes["nodes"].keys():
logger.info(f"Node {node} found in cluster. Applying pinning.")
logger.debug("Finished: validate_node_presence.")
return True
else:
logger.warning(f"Node {node} not found in cluster. Not applying pinning!")
logger.debug("Finished: validate_node_presence.")
return False

View File

@@ -33,6 +33,7 @@ try:
except ImportError:
URLLIB3_PRESENT = False
from typing import Dict, Any
from utils.helper import Helper
from utils.logger import SystemdLogger
@@ -134,6 +135,14 @@ class ProxmoxApi:
proxlb_credentials = proxlb_config["proxmox_api"]
present_auth_pass = "pass" in proxlb_credentials
present_auth_secret = "token_secret" in proxlb_credentials
token_id = proxlb_credentials.get("token_id", None)
if token_id:
non_allowed_chars = ["@", "!"]
for char in non_allowed_chars:
if char in token_id:
logger.error(f"Wrong user/token format defined. User and token id must be splitted! Please see: https://github.com/gyptazy/ProxLB/blob/main/docs/03_configuration.md#required-permissions-for-a-user")
sys.exit(1)
if present_auth_pass and present_auth_secret:
logger.critical(f"Username/password and API token authentication are mutal exclusive. Please use only one!")
@@ -189,9 +198,9 @@ class ProxmoxApi:
api_connection_wait_time = proxlb_config["proxmox_api"].get("wait_time", 1)
for api_connection_attempt in range(api_connection_retries):
validated = self.test_api_proxmox_host(host)
if validated:
validated_api_hosts.append(validated)
validated_api_host, api_port = self.test_api_proxmox_host(host)
if validated_api_host:
validated_api_hosts.append(validated_api_host)
break
else:
logger.warning(f"Attempt {api_connection_attempt + 1}/{api_connection_retries} failed for host {host}. Retrying in {api_connection_wait_time} seconds...")
@@ -200,7 +209,7 @@ class ProxmoxApi:
if len(validated_api_hosts) > 0:
# Choose a random host to distribute the load across the cluster
# as a simple load balancing mechanism.
return random.choice(validated_api_hosts)
return random.choice(validated_api_hosts), api_port
logger.critical("No valid Proxmox API hosts found.")
print("No valid Proxmox API hosts found.")
@@ -228,6 +237,10 @@ class ProxmoxApi:
"""
logger.debug("Starting: test_api_proxmox_host.")
# Validate for custom ports in API hosts which might indicate
# that an external loadbalancer will be used.
host, port = Helper.get_host_port_from_string(host)
# Try resolving DNS to IP and log non-resolvable ones
try:
ip = socket.getaddrinfo(host, None, socket.AF_UNSPEC)
@@ -239,12 +252,12 @@ class ProxmoxApi:
for address_type in ip:
if address_type[0] == socket.AF_INET:
logger.debug(f"{host} is type ipv4.")
if self.test_api_proxmox_host_ipv4(host):
return host
if self.test_api_proxmox_host_ipv4(host, port):
return host, port
elif address_type[0] == socket.AF_INET6:
logger.debug(f"{host} is type ipv6.")
if self.test_api_proxmox_host_ipv6(host):
return host
if self.test_api_proxmox_host_ipv6(host, port):
return host, port
else:
return False
@@ -331,7 +344,15 @@ class ProxmoxApi:
permissions_available = []
# Get the permissions for the current user/token from API
permissions = proxmox_api.access.permissions.get()
try:
permissions = proxmox_api.access.permissions.get()
except proxmoxer.core.ResourceException as api_error:
if "no such user" in str(api_error):
logger.error("Authentication to Proxmox API not possible: User not known - please check your username and config file.")
sys.exit(1)
else:
logger.error(f"Proxmox API error: {api_error}")
sys.exit(1)
# Get all available permissions of the current user/token
for path, permission in permissions.items():
@@ -378,7 +399,7 @@ class ProxmoxApi:
self.validate_config(proxlb_config)
# Get a valid Proxmox API endpoint
proxmox_api_endpoint = self.api_connect_get_hosts(proxlb_config, proxlb_config.get("proxmox_api", {}).get("hosts", []))
proxmox_api_endpoint, proxmox_api_port = self.api_connect_get_hosts(proxlb_config, proxlb_config.get("proxmox_api", {}).get("hosts", []))
# Disable warnings for SSL certificate validation
if not proxlb_config.get("proxmox_api").get("ssl_verification", True):
@@ -392,6 +413,7 @@ class ProxmoxApi:
if proxlb_config.get("proxmox_api").get("token_secret", False):
proxmox_api = proxmoxer.ProxmoxAPI(
proxmox_api_endpoint,
port=proxmox_api_port,
user=proxlb_config.get("proxmox_api").get("user", True),
token_name=proxlb_config.get("proxmox_api").get("token_id", True),
token_value=proxlb_config.get("proxmox_api").get("token_secret", True),
@@ -401,6 +423,7 @@ class ProxmoxApi:
else:
proxmox_api = proxmoxer.ProxmoxAPI(
proxmox_api_endpoint,
port=proxmox_api_port,
user=proxlb_config.get("proxmox_api").get("user", True),
password=proxlb_config.get("proxmox_api").get("pass", True),
verify_ssl=proxlb_config.get("proxmox_api").get("ssl_verification", True),
@@ -420,6 +443,5 @@ class ProxmoxApi:
sys.exit(2)
logger.info(f"API connection to host {proxmox_api_endpoint} succeeded.")
logger.debug("Finished: api_connect.")
return proxmox_api

View File

@@ -3,5 +3,5 @@ __app_desc__ = "A DRS alike loadbalancer for Proxmox clusters."
__author__ = "Florian Paul Azim Hoberg <gyptazy>"
__copyright__ = "Copyright (C) 2025 Florian Paul Azim Hoberg (@gyptazy)"
__license__ = "GPL-3.0"
__version__ = "1.1.4"
__version__ = "1.1.6.1"
__url__ = "https://github.com/gyptazy/ProxLB"

View File

@@ -2,7 +2,7 @@ from setuptools import setup
setup(
name="proxlb",
version="1.1.4",
version="1.1.6.1",
description="A DRS alike loadbalancer for Proxmox clusters.",
long_description="An advanced DRS alike loadbalancer for Proxmox clusters that also supports maintenance modes and affinity/anti-affinity rules.",
author="Florian Paul Azim Hoberg",