mirror of
https://github.com/gyptazy/ProxLB.git
synced 2026-04-06 04:41:58 +02:00
Compare commits
55 Commits
v1.1.4
...
feature/21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020ed6a237 | ||
|
|
8c473b416c | ||
|
|
51c8afe5c5 | ||
|
|
a8a154abde | ||
|
|
554a3eaf72 | ||
|
|
0b35987403 | ||
|
|
d93048db69 | ||
|
|
2aba7dbe23 | ||
|
|
ba388dfd7c | ||
|
|
5aa8257d40 | ||
|
|
99fefe20bf | ||
|
|
b9fb3a60e1 | ||
|
|
88b3288eb7 | ||
|
|
fa0113f112 | ||
|
|
0039ae9093 | ||
|
|
e3bbf31fdd | ||
|
|
bf393c6bbf | ||
|
|
7e5b72cfc7 | ||
|
|
0ba76f80f3 | ||
|
|
b48ff9d677 | ||
|
|
b5c11af474 | ||
|
|
af2992747d | ||
|
|
fb8dc40c16 | ||
|
|
34f1de8367 | ||
|
|
0e992e99de | ||
|
|
f5d073dc02 | ||
|
|
70ba1f2dfc | ||
|
|
c9855f1991 | ||
|
|
9bd29158b9 | ||
|
|
1ff0c5d96e | ||
|
|
3eb4038723 | ||
|
|
47e7dd3c56 | ||
|
|
bb8cf9033d | ||
|
|
756b4efcbd | ||
|
|
8630333e4b | ||
|
|
7bd9a9b038 | ||
|
|
16651351de | ||
|
|
63805f1f50 | ||
|
|
c0ff1b5273 | ||
|
|
07f8596fc5 | ||
|
|
affbe433f9 | ||
|
|
7bda22e754 | ||
|
|
253dcf8eb9 | ||
|
|
6212d23268 | ||
|
|
cf8c06393f | ||
|
|
5c23fd3433 | ||
|
|
0fb732fc8c | ||
|
|
f36d96c72a | ||
|
|
9cc03717ef | ||
|
|
4848887ccc | ||
|
|
04476feeaf | ||
|
|
b3765bf0ae | ||
|
|
806b728a14 | ||
|
|
2c34ec91b1 | ||
|
|
08b746a53b |
2
.changelogs/1.1.5/260_allow_custom_api_ports.yml
Normal file
2
.changelogs/1.1.5/260_allow_custom_api_ports.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
added:
|
||||
- Allow custom API ports instead of fixed tcp/8006 (@gyptazy). [#260]
|
||||
1
.changelogs/1.1.5/release_meta.yml
Normal file
1
.changelogs/1.1.5/release_meta.yml
Normal file
@@ -0,0 +1 @@
|
||||
date: 2025-07-14
|
||||
2
.changelogs/1.1.6/268_fix_balancing_type_eval.yml
Normal file
2
.changelogs/1.1.6/268_fix_balancing_type_eval.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
fixed:
|
||||
- Fix balancing evaluation of guest types (e.g., VM or CT) (@gyptazy). [#268]
|
||||
2
.changelogs/1.1.6/290_validate_user_token_syntax.yml
Normal file
2
.changelogs/1.1.6/290_validate_user_token_syntax.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
added:
|
||||
- Add validation for provided API user token id to avoid confusions (@gyptazy). [#291]
|
||||
@@ -0,0 +1,2 @@
|
||||
fixed:
|
||||
- Fix stacktrace output when validating permissions on non existing users in Proxmox (@gyptazy). [#291]
|
||||
@@ -0,0 +1,3 @@
|
||||
fixed:
|
||||
- Fix Overprovisioning first node if anti_affinity_group has only one member (@MiBUl-eu). [#295]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fixed:
|
||||
- Validate for node presence when pinning guests to avoid crashing (@gyptazy). [#296]
|
||||
|
||||
1
.changelogs/1.1.6/release_meta.yml
Normal file
1
.changelogs/1.1.6/release_meta.yml
Normal file
@@ -0,0 +1 @@
|
||||
date: 2025-09-04
|
||||
2
.changelogs/1.1.7/304_add_graceful_shutdown_sigint.yml
Normal file
2
.changelogs/1.1.7/304_add_graceful_shutdown_sigint.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
added:
|
||||
- Add graceful shutdown for SIGINT (e.g., CTRL + C abort). (@gyptazy). [#304]
|
||||
@@ -0,0 +1,2 @@
|
||||
added:
|
||||
- Add conntrack state aware migrations of VMs (@gyptazy). [#305]
|
||||
2
.changelogs/1.1.7/308_fix_only_validate_valid_jobids.yml
Normal file
2
.changelogs/1.1.7/308_fix_only_validate_valid_jobids.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
fixed:
|
||||
- Fix crash when validating absent migration job ids. (@gyptazy). [#308]
|
||||
@@ -0,0 +1,2 @@
|
||||
fixed:
|
||||
- Fix guest object names are not being evaluated in debug log. (@gyptazy). [#310]
|
||||
1
.changelogs/1.1.7/release_meta.yml
Normal file
1
.changelogs/1.1.7/release_meta.yml
Normal file
@@ -0,0 +1 @@
|
||||
date: 2025-09-19
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -5,6 +5,45 @@ 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.7] - 2025-09-19
|
||||
|
||||
### Added
|
||||
|
||||
- Add conntrack state aware migrations of VMs (@gyptazy). [#305]
|
||||
- Add graceful shutdown for SIGINT (e.g., CTRL + C abort). (@gyptazy). [#304]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash when validating absent migration job ids. (@gyptazy). [#308]
|
||||
- Fix guest object names are not being evaluated in debug log. (@gyptazy). [#310]
|
||||
|
||||
## [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
|
||||
|
||||
25
README.md
25
README.md
@@ -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>
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
|
||||
## Introduction
|
||||
ProxLB is an advanced load balancing solution specifically designed for Proxmox clusters, addressing the absence of a Dynamic Resource Scheduler (DRS) that is familiar to VMware users. As a third-party solution, ProxLB enhances the management and efficiency of Proxmox clusters by intelligently distributing workloads across available nodes. Workloads can be balanced by different times like the guest's memory, CPU or disk usage or their assignment to avoid overprovisioning and ensuring resources.
|
||||
ProxLB is an advanced load balancing solution specifically designed for Proxmox clusters, addressing the absence of an intelligent and more advanced resource scheduler. As a third-party solution, ProxLB enhances the management and efficiency of Proxmox clusters by intelligently distributing workloads across available nodes. Workloads can be balanced by different times like the guest's memory, CPU or disk usage or their assignment to avoid overprovisioning and ensuring resources.
|
||||
|
||||
One of the key advantages of ProxLB is that it is fully open-source and free, making it accessible for anyone to use, modify, and contribute to. This ensures transparency and fosters community-driven improvements. ProxLB supports filtering and ignoring specific nodes and guests through configuration files and API calls, providing administrators with the flexibility to tailor the load balancing behavior to their specific needs.
|
||||
|
||||
@@ -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,10 @@ docker run -it --rm -v $(pwd)/proxlb.yaml:/etc/proxlb/proxlb.yaml proxlb
|
||||
| Version | Image |
|
||||
|------|:------:|
|
||||
| latest | cr.gyptazy.com/proxlb/proxlb:latest |
|
||||
| v1.1.7 | cr.gyptazy.com/proxlb/proxlb:v1.1.7 |
|
||||
| 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 +248,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 +258,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` | | | | | |
|
||||
@@ -260,11 +268,15 @@ The following options can be set in the configuration file `proxlb.yaml`:
|
||||
| | parallel_jobs | | 5 | `Int` | The amount if parallel jobs when migrating guests. (default: `5`)|
|
||||
| | live | | True | `Bool` | If guests should be moved live or shutdown.|
|
||||
| | with_local_disks | | True | `Bool` | If balancing of guests should include local disks.|
|
||||
| | with_conntrack_state | | True | `Bool` | If balancing of guests should including the conntrack state.|
|
||||
| | balance_types | | ['vm', 'ct'] | `List` | Defined the types of guests that should be honored. [values: `vm`, `ct`]|
|
||||
| | max_job_validation | | 1800 | `Int` | How long a job validation may take in seconds. (default: 1800) |
|
||||
| | balanciness | | 10 | `Int` | The maximum delta of resource usage between node with highest and lowest usage. |
|
||||
| | method | | memory | `Str` | The balancing method that should be used. [values: `memory` (default), `cpu`, `disk`]|
|
||||
| | mode | | used | `Str` | The balancing mode that should be used. [values: `used` (default), `assigned`] |
|
||||
| `patching` | | | | | |
|
||||
| | enable | | True | `Bool` | Enables the guest balancing.|
|
||||
| | maximum_nodes | | 1 | `Int` | How many nodes may be patched at the same time during a ProxLB run. |
|
||||
| `service` | | | | | |
|
||||
| | daemon | | True | `Bool` | If daemon mode should be activated. |
|
||||
| | `schedule` | | | `Dict` | Schedule config block for rebalancing. |
|
||||
@@ -280,7 +292,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
|
||||
@@ -303,6 +315,7 @@ balancing:
|
||||
parallel: False
|
||||
live: True
|
||||
with_local_disks: True
|
||||
with_conntrack_state: True
|
||||
balance_types: ['vm', 'ct']
|
||||
max_job_validation: 1800
|
||||
balanciness: 5
|
||||
|
||||
@@ -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
|
||||
@@ -25,12 +25,17 @@ balancing:
|
||||
parallel_jobs: 1
|
||||
live: True
|
||||
with_local_disks: True
|
||||
with_conntrack_state: True
|
||||
balance_types: ['vm', 'ct']
|
||||
max_job_validation: 1800
|
||||
balanciness: 5
|
||||
method: memory
|
||||
mode: used
|
||||
|
||||
patching:
|
||||
enable: True
|
||||
maximum_nodes: 1
|
||||
|
||||
service:
|
||||
daemon: True
|
||||
schedule:
|
||||
|
||||
32
debian/changelog
vendored
32
debian/changelog
vendored
@@ -1,3 +1,35 @@
|
||||
proxlb (1.1.7) stable; urgency=medium
|
||||
|
||||
* Add conntrack state aware migrations of VMs. (Closes: #305)
|
||||
* Add graceful shutdown for SIGINT command. (Closes: #304)
|
||||
* Fix crash when validating absent migration job ids. (Closes: #308)
|
||||
* Fix guest object names are not being evaluated in debug log. (Closes: #310)
|
||||
* Note: Have a great Dutch Proxmox Day 2025!
|
||||
|
||||
-- Florian Paul Azim Hoberg <gyptazy@gyptazy.com> Thu, 04 Sep 2025 19:23:51 +0000
|
||||
|
||||
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)
|
||||
|
||||
4
debian/control
vendored
4
debian/control
vendored
@@ -8,5 +8,5 @@ Build-Depends: debhelper-compat (= 13), dh-python, python3-all, python3-setuptoo
|
||||
Package: proxlb
|
||||
Architecture: all
|
||||
Depends: ${python3:Depends}, ${misc:Depends}, python3-requests, python3-urllib3, python3-proxmoxer, python3-yaml
|
||||
Description: A DRS alike Load Balancer for Proxmox Clusters
|
||||
An advanced DRS alike loadbalancer for Proxmox clusters that also supports maintenance modes and affinity/anti-affinity rules.
|
||||
Description: An advanced resource scheduler and load balancer for Proxmox clusters
|
||||
An advanced resource scheduler and load balancer for Proxmox clusters that also supports maintenance mode and affinity/anti-affinity rules.
|
||||
|
||||
@@ -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.
|
||||
6
helm/proxlb/Chart.yaml
Normal file
6
helm/proxlb/Chart.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
name: proxlb
|
||||
description: A Helm chart for self-hosted ProxLB
|
||||
type: application
|
||||
version: "1.1.7"
|
||||
appVersion: "v1.1.7"
|
||||
13
helm/proxlb/templates/_helpers.yaml
Normal file
13
helm/proxlb/templates/_helpers.yaml
Normal 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 }}
|
||||
11
helm/proxlb/templates/configmap.yaml
Normal file
11
helm/proxlb/templates/configmap.yaml
Normal 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 }}
|
||||
44
helm/proxlb/templates/deployment.yaml
Normal file
44
helm/proxlb/templates/deployment.yaml
Normal 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
|
||||
61
helm/proxlb/values.yaml
Normal file
61
helm/proxlb/values.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
image:
|
||||
registry: cr.gyptazy.com
|
||||
repository: proxlb/proxlb
|
||||
tag: v1.1.7
|
||||
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
|
||||
with_conntrack_state: 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
|
||||
@@ -1,6 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
VERSION="1.1.4"
|
||||
VERSION="1.1.7"
|
||||
|
||||
# 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"
|
||||
|
||||
@@ -23,6 +23,7 @@ from models.guests import Guests
|
||||
from models.groups import Groups
|
||||
from models.calculations import Calculations
|
||||
from models.balancing import Balancing
|
||||
from models.patching import Patching
|
||||
from utils.helper import Helper
|
||||
|
||||
|
||||
@@ -33,8 +34,9 @@ def main():
|
||||
# Initialize logging handler
|
||||
logger = SystemdLogger(level=logging.INFO)
|
||||
|
||||
# Signal handler for SIGHUP
|
||||
# Initialize handlers
|
||||
signal.signal(signal.SIGHUP, Helper.handler_sighup)
|
||||
signal.signal(signal.SIGINT, Helper.handler_sigint)
|
||||
|
||||
# Parses arguments passed from the CLI
|
||||
cli_parser = CliParser()
|
||||
@@ -77,6 +79,10 @@ def main():
|
||||
proxlb_data = {**meta, **nodes, **guests, **groups}
|
||||
Helper.log_node_metrics(proxlb_data)
|
||||
|
||||
# Perform preparing patching actions via Proxmox API
|
||||
if proxlb_data["meta"]["patching"].get("enable", False):
|
||||
Patching(proxmox_api, proxlb_data)
|
||||
|
||||
# Update the initial node resource assignments
|
||||
# by the previously created groups.
|
||||
Calculations.set_node_assignments(proxlb_data)
|
||||
@@ -94,6 +100,11 @@ def main():
|
||||
# Validate if the JSON output should be
|
||||
# printed to stdout
|
||||
Helper.print_json(proxlb_data, cli_args.json)
|
||||
|
||||
# Perform patching actions via Proxmox API
|
||||
if proxlb_data["meta"]["patching"].get("enable", False):
|
||||
Patching(proxmox_api, proxlb_data, calculations_done=True)
|
||||
|
||||
# Validate daemon mode
|
||||
Helper.get_daemon_mode(proxlb_config)
|
||||
|
||||
|
||||
@@ -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(f"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(f"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:
|
||||
@@ -110,7 +122,8 @@ class Balancing:
|
||||
|
||||
# Wait for all jobs in the current chunk to complete
|
||||
for guest_name, node, job_id in jobs_to_wait:
|
||||
self.get_rebalancing_job_status(proxmox_api, proxlb_data, guest_name, node, job_id)
|
||||
if job_id:
|
||||
self.get_rebalancing_job_status(proxmox_api, proxlb_data, guest_name, node, job_id)
|
||||
|
||||
def exec_rebalancing_vm(self, proxmox_api: any, proxlb_data: Dict[str, Any], guest_name: str) -> None:
|
||||
"""
|
||||
@@ -131,6 +144,7 @@ class Balancing:
|
||||
guest_id = proxlb_data["guests"][guest_name]["id"]
|
||||
guest_node_current = proxlb_data["guests"][guest_name]["node_current"]
|
||||
guest_node_target = proxlb_data["guests"][guest_name]["node_target"]
|
||||
job_id = None
|
||||
|
||||
if proxlb_data["meta"]["balancing"].get("live", True):
|
||||
online_migration = 1
|
||||
@@ -142,10 +156,16 @@ class Balancing:
|
||||
else:
|
||||
with_local_disks = 0
|
||||
|
||||
if proxlb_data["meta"]["balancing"].get("with_conntrack_state", True):
|
||||
with_conntrack_state = 1
|
||||
else:
|
||||
with_conntrack_state = 0
|
||||
|
||||
migration_options = {
|
||||
'target': {guest_node_target},
|
||||
'target': guest_node_target,
|
||||
'online': online_migration,
|
||||
'with-local-disks': with_local_disks
|
||||
'with-local-disks': with_local_disks,
|
||||
'with-conntrack-state': with_conntrack_state,
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -154,6 +174,7 @@ class Balancing:
|
||||
except proxmoxer.core.ResourceException as proxmox_api_error:
|
||||
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.")
|
||||
logger.debug(f"Balancing: Failed to migrate guest {guest_name} of type VM due to some Proxmox errors: {proxmox_api_error}")
|
||||
|
||||
logger.debug("Finished: exec_rebalancing_vm.")
|
||||
return job_id
|
||||
|
||||
@@ -176,6 +197,7 @@ class Balancing:
|
||||
guest_id = proxlb_data["guests"][guest_name]["id"]
|
||||
guest_node_current = proxlb_data["guests"][guest_name]["node_current"]
|
||||
guest_node_target = proxlb_data["guests"][guest_name]["node_target"]
|
||||
job_id = None
|
||||
|
||||
try:
|
||||
logger.info(f"Balancing: Starting to migrate CT guest {guest_name} from {guest_node_current} to {guest_node_target}.")
|
||||
@@ -183,6 +205,7 @@ class Balancing:
|
||||
except proxmoxer.core.ResourceException as proxmox_api_error:
|
||||
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.")
|
||||
logger.debug(f"Balancing: Failed to migrate guest {guest_name} of type CT due to some Proxmox errors: {proxmox_api_error}")
|
||||
|
||||
logger.debug("Finished: exec_rebalancing_ct.")
|
||||
return job_id
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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']]}")
|
||||
|
||||
@@ -61,6 +61,7 @@ class Nodes:
|
||||
nodes["nodes"][node["node"]] = {}
|
||||
nodes["nodes"][node["node"]]["name"] = node["node"]
|
||||
nodes["nodes"][node["node"]]["maintenance"] = False
|
||||
nodes["nodes"][node["node"]]["patching"] = False
|
||||
nodes["nodes"][node["node"]]["cpu_total"] = node["maxcpu"]
|
||||
nodes["nodes"][node["node"]]["cpu_assigned"] = 0
|
||||
nodes["nodes"][node["node"]]["cpu_used"] = node["cpu"] * node["maxcpu"]
|
||||
|
||||
176
proxlb/models/patching.py
Normal file
176
proxlb/models/patching.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
The Patching class is responsible for orchestrating the patching process of nodes in a Proxmox cluster,
|
||||
based on the provided ProxLB data and using the Proxmox API. It determines which nodes require
|
||||
patching, selects nodes for patching according to configuration, and executes patching actions
|
||||
while ensuring no running guests are present.
|
||||
"""
|
||||
|
||||
|
||||
__author__ = "Florian Paul Azim Hoberg <gyptazy>"
|
||||
__copyright__ = "Copyright (C) 2025 Florian Paul Azim Hoberg (@gyptazy)"
|
||||
__license__ = "GPL-3.0"
|
||||
|
||||
|
||||
from utils.logger import SystemdLogger
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = SystemdLogger()
|
||||
|
||||
|
||||
class Patching:
|
||||
"""
|
||||
Patching
|
||||
|
||||
This class is responsible for orchestrating the patching process of nodes in a Proxmox cluster,
|
||||
based on the provided ProxLB data and using the Proxmox API. It determines which nodes require
|
||||
patching, selects nodes for patching according to configuration, and executes patching actions
|
||||
while ensuring no running guests are present.
|
||||
|
||||
Functions:
|
||||
-----------
|
||||
__init__(self, proxmox_api: any, proxlb_data: Dict[str, Any], calculations_done: bool = False)
|
||||
- Initializes the Patching class and triggers either patch preparation or execution based on the calculations_done flag.
|
||||
- Inputs:
|
||||
- proxmox_api: Proxmox API client instance.
|
||||
- proxlb_data: Dictionary containing cluster and node information.
|
||||
- calculations_done: Boolean flag to determine operation mode.
|
||||
- Outputs: None
|
||||
|
||||
val_nodes_packages(self, proxmox_api: any, proxlb_data: Dict[str, Any]) -> Dict[str, Any]
|
||||
- Checks each node for available package updates and updates their patching status.
|
||||
- Inputs:
|
||||
- proxmox_api: Proxmox API client instance.
|
||||
- proxlb_data: Dictionary with node and maintenance information.
|
||||
- Outputs:
|
||||
- Updated proxlb_data dictionary with patching status for each node.
|
||||
|
||||
get_nodes_to_patch(self, proxlb_data: Dict[str, Any]) -> Dict[str, Any]
|
||||
- Selects nodes to patch in the current run based on configuration and node status.
|
||||
- Inputs:
|
||||
- proxlb_data: Dictionary with ProxLB configuration and node information.
|
||||
- Outputs:
|
||||
- Updated proxlb_data with selected nodes for patching in this run.
|
||||
|
||||
patch_node(self, proxmox_api: any, proxlb_data: Dict[str, Any])
|
||||
- Executes the patching process for selected nodes, ensuring no running guests are present before proceeding.
|
||||
- Inputs:
|
||||
- proxmox_api: Proxmox API client instance.
|
||||
- proxlb_data: Dictionary with metadata and list of nodes to patch.
|
||||
- Outputs: None
|
||||
"""
|
||||
def __init__(self, proxmox_api: any, proxlb_data: Dict[str, Any], calculations_done: bool = False):
|
||||
"""
|
||||
Initializes the Patching class with the provided ProxLB data.
|
||||
"""
|
||||
if not calculations_done:
|
||||
logger.debug("Starting: Patching preparations.")
|
||||
self.val_nodes_packages(proxmox_api, proxlb_data)
|
||||
self.get_nodes_to_patch(proxlb_data)
|
||||
logger.debug("Finished: Patching preparations.")
|
||||
else:
|
||||
logger.debug("Starting: Patching executions.")
|
||||
self.patch_node(proxmox_api, proxlb_data)
|
||||
logger.debug("Finished: Patching executions.")
|
||||
|
||||
def val_nodes_packages(self, proxmox_api: any, proxlb_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Checks each node in the provided ProxLB data for available package updates using the Proxmox API,
|
||||
and updates the node's patching status accordingly.
|
||||
|
||||
Args:
|
||||
proxmox_api (Any): An instance of the Proxmox API client used to query node package updates.
|
||||
proxlb_data (Dict[str, Any]): A dictionary containing node information, including maintenance status.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The updated proxlb_data dictionary with patching status set for each node.
|
||||
"""
|
||||
logger.debug("Starting: val_nodes_packages.")
|
||||
|
||||
for node in proxlb_data['nodes'].keys():
|
||||
if proxlb_data['nodes'][node]['maintenance'] is False:
|
||||
node_pkgs = proxmox_api.nodes(node).apt.update.get()
|
||||
|
||||
if len(node_pkgs) > 0:
|
||||
proxlb_data['nodes'][node]['patching'] = True
|
||||
logger.debug(f"Node {node} has {len(node_pkgs)} packages to update.")
|
||||
else:
|
||||
logger.debug(f"Node {node} is up to date and has no packages to update.")
|
||||
|
||||
logger.debug("Finished: val_nodes_packages.")
|
||||
return proxlb_data
|
||||
|
||||
def get_nodes_to_patch(self, proxlb_data: Dict[str, Any]):
|
||||
"""
|
||||
Determines which nodes should be patched in the current run based on the ProxLB configuration and node status.
|
||||
|
||||
Args:
|
||||
proxlb_data (Dict[str, Any]): A dictionary containing ProxLB configuration, metadata, and node information.
|
||||
- proxlb_data["meta"]["patching"]["maximum_nodes"]: Maximum number of nodes to patch in this run (default is 1).
|
||||
- proxlb_data["nodes"]: Dictionary of node objects, each with a "patching" status and "name".
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The updated proxlb_data dictionary with:
|
||||
- proxlb_data["meta"]["patching"]: List of node names selected for patching in this run.
|
||||
- proxlb_data["nodes"]: Updated node objects with "patching" status set to True for selected nodes.
|
||||
"""
|
||||
logger.debug("Starting: get_node_patching.")
|
||||
|
||||
nodes_patching_execution = []
|
||||
nodes_patching_count = proxlb_data["meta"].get("patching", {}).get("maximum_nodes", 1)
|
||||
nodes_patching = [node for node in proxlb_data["nodes"].values() if node["patching"]]
|
||||
nodes_patching_sorted = sorted(nodes_patching, key=lambda x: x["name"])
|
||||
logger.debug(f"{len(nodes_patching)} nodes are pending for patching. Patching up to {nodes_patching_count} nodes in this run.")
|
||||
|
||||
if len(nodes_patching_sorted) > 0:
|
||||
nodes = nodes_patching_sorted[:nodes_patching_count]
|
||||
for node in nodes:
|
||||
nodes_patching_execution.append(node["name"])
|
||||
proxlb_data['nodes'][node['name']]['patching'] = True
|
||||
logger.info(f"Node {node['name']} is going to be patched.")
|
||||
logger.info(f"Node {node['name']} is set to maintenance.")
|
||||
|
||||
proxlb_data["meta"]["patching"] = nodes_patching_execution
|
||||
|
||||
logger.debug("Finished: get_node_patching.")
|
||||
return proxlb_data
|
||||
|
||||
def patch_node(self, proxmox_api: any, proxlb_data: Dict[str, Any]):
|
||||
"""
|
||||
Patches Proxmox nodes if no running guests are detected.
|
||||
|
||||
This method iterates over the nodes specified in the `proxlb_data` dictionary under the "meta" -> "patching" key.
|
||||
For each node, it checks for running QEMU (VM) and LXC (container) guests using the provided Proxmox API client.
|
||||
If any guests are running, patching is skipped for that node and a warning is logged.
|
||||
If no guests are running, the method proceeds to patch the node (API calls are commented out) and logs the actions.
|
||||
Rebooting the node after patching is also logged (API call commented out).
|
||||
|
||||
Args:
|
||||
proxmox_api (Any): An instance of the Proxmox API client used to interact with the cluster.
|
||||
proxlb_data (Dict[str, Any]): A dictionary containing metadata, including the list of nodes to patch under "meta" -> "patching".
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
logger.debug("Starting: patch_node.")
|
||||
|
||||
for node in proxlb_data["meta"]["patching"]:
|
||||
node_guests = []
|
||||
guests_vm = proxmox_api.nodes(node).qemu.get()
|
||||
guests_ct = proxmox_api.nodes(node).lxc.get()
|
||||
guests_vm = [vm for vm in guests_vm if vm["status"] == "running"]
|
||||
guests_ct = [ct for ct in guests_ct if ct["status"] == "running"]
|
||||
guests_count = len(guests_vm) + len(guests_ct)
|
||||
|
||||
# Do not proceed when we still have someho guests running on the node
|
||||
if guests_vm or guests_ct:
|
||||
logger.warning(f"Node {node} has {guests_count} running guest(s). Patching will be skipped.")
|
||||
else:
|
||||
logger.debug(f"Node {node} has no running guests. Proceeding with patching.")
|
||||
# Upgrading a node by API requires the patched 'pve-manager' package
|
||||
# from gyptazy including the new 'upgrade' endpoint.
|
||||
# proxmox_api.nodes(node).apt.upgrade.post()
|
||||
logger.debug(f"Node {node} has been patched.")
|
||||
logger.debug(f"Node {node} is going to reboot.")
|
||||
# proxmox_api.nodes(node).status.reboot.post()
|
||||
|
||||
logger.debug("Finished: patch_node.")
|
||||
@@ -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
|
||||
|
||||
@@ -10,11 +10,13 @@ __license__ = "GPL-3.0"
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import utils.version
|
||||
from utils.logger import SystemdLogger
|
||||
from typing import Dict, Any
|
||||
from types import FrameType
|
||||
|
||||
logger = SystemdLogger()
|
||||
|
||||
@@ -199,7 +201,7 @@ class Helper:
|
||||
logger.debug("Finished: print_json.")
|
||||
|
||||
@staticmethod
|
||||
def handler_sighup(signum, frame):
|
||||
def handler_sighup(signum: int, frame: FrameType) -> None:
|
||||
"""
|
||||
Signal handler for SIGHUP.
|
||||
|
||||
@@ -214,4 +216,94 @@ 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 handler_sigint(signum: int, frame: FrameType) -> None:
|
||||
"""
|
||||
Signal handler for SIGINT. (triggered by CTRL+C).
|
||||
|
||||
Args:
|
||||
signum (int): The signal number (e.g., SIGINT).
|
||||
frame (FrameType): The current stack frame when the signal was received.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
exit_message = "ProxLB has been successfully terminated by user."
|
||||
logger.debug(exit_message)
|
||||
print(f"\n {exit_message}")
|
||||
sys.exit(0)
|
||||
|
||||
@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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
__app_name__ = "ProxLB"
|
||||
__app_desc__ = "A DRS alike loadbalancer for Proxmox clusters."
|
||||
__app_desc__ = "An advanced resource scheduler and load balancer 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.7"
|
||||
__url__ = "https://github.com/gyptazy/ProxLB"
|
||||
|
||||
6
setup.py
6
setup.py
@@ -2,9 +2,9 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="proxlb",
|
||||
version="1.1.4",
|
||||
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.",
|
||||
version="1.1.7",
|
||||
description="An advanced resource scheduler and load balancer for Proxmox clusters.",
|
||||
long_description="An advanced resource scheduler and load balancer for Proxmox clusters that also supports maintenance modes and affinity/anti-affinity rules.",
|
||||
author="Florian Paul Azim Hoberg",
|
||||
author_email="gyptazy@gyptazy.com",
|
||||
maintainer="Florian Paul Azim Hoberg",
|
||||
|
||||
Reference in New Issue
Block a user