From 806b728a14a0e2267e1bdd3a97981505f0c3d5d4 Mon Sep 17 00:00:00 2001 From: Florian Paul Azim Hoberg Date: Tue, 8 Jul 2025 08:07:21 +0200 Subject: [PATCH] feature: Allow custom (instead of static tcp/8006) API ports for API hosts. Fixes: #260 --- .../1.1.5/260_allow_custom_api_ports.yml | 2 + proxlb/utils/helper.py | 52 ++++++++++++++++++- proxlb/utils/proxmox_api.py | 26 ++++++---- 3 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 .changelogs/1.1.5/260_allow_custom_api_ports.yml diff --git a/.changelogs/1.1.5/260_allow_custom_api_ports.yml b/.changelogs/1.1.5/260_allow_custom_api_ports.yml new file mode 100644 index 0000000..ae9531c --- /dev/null +++ b/.changelogs/1.1.5/260_allow_custom_api_ports.yml @@ -0,0 +1,2 @@ +added: + - Allow custom API ports instead of fixed tcp/8006 (@gyptazy). [#260] diff --git a/proxlb/utils/helper.py b/proxlb/utils/helper.py index 748cac7..7fff9f1 100644 --- a/proxlb/utils/helper.py +++ b/proxlb/utils/helper.py @@ -10,6 +10,7 @@ __license__ = "GPL-3.0" import json import uuid +import re import sys import time import utils.version @@ -214,4 +215,53 @@ 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 diff --git a/proxlb/utils/proxmox_api.py b/proxlb/utils/proxmox_api.py index 3abf2d7..4b341e5 100644 --- a/proxlb/utils/proxmox_api.py +++ b/proxlb/utils/proxmox_api.py @@ -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 @@ -189,9 +190,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 +201,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 +229,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 +244,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 @@ -378,7 +383,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 +397,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 +407,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 +427,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