secubox-openwrt/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl
CyberMind-FR 3a5655451e feat(haproxy): Add edit functionality for backends, servers, and vhosts
- Add showEditVhostModal() for editing virtual host properties
- Add showEditBackendModal() for editing backend configuration
- Add showEditServerModal() for editing server properties
- Modern card-based UI with inline edit/delete actions
- Toggle enable/disable for backends
- Fix haproxyctl to read server option from backend UCI sections
- Add debug logging to container startup script

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 05:56:03 +01:00

1035 lines
26 KiB
Bash

#!/bin/sh
# SecuBox HAProxy Controller
# Copyright (C) 2025 CyberMind.fr
# Source OpenWrt functions for UCI iteration
. /lib/functions.sh
CONFIG="haproxy"
LXC_NAME="haproxy"
# Paths
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
DATA_PATH="/srv/haproxy"
SHARE_PATH="/usr/share/haproxy"
CERTS_PATH="$DATA_PATH/certs"
CONFIG_PATH="$DATA_PATH/config"
# Logging
log_info() { echo "[INFO] $*"; logger -t haproxy "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t haproxy -p err "$*"; }
log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; }
# Helpers
require_root() {
[ "$(id -u)" -eq 0 ] || { log_error "Root required"; exit 1; }
}
has_lxc() { command -v lxc-start >/dev/null 2>&1; }
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
# Load configuration
load_config() {
http_port="$(uci_get main.http_port)" || http_port="80"
https_port="$(uci_get main.https_port)" || https_port="443"
stats_port="$(uci_get main.stats_port)" || stats_port="8404"
stats_enabled="$(uci_get main.stats_enabled)" || stats_enabled="1"
stats_user="$(uci_get main.stats_user)" || stats_user="admin"
stats_password="$(uci_get main.stats_password)" || stats_password="secubox"
data_path="$(uci_get main.data_path)" || data_path="$DATA_PATH"
memory_limit="$(uci_get main.memory_limit)" || memory_limit="256M"
maxconn="$(uci_get main.maxconn)" || maxconn="4096"
log_level="$(uci_get main.log_level)" || log_level="warning"
CERTS_PATH="$data_path/certs"
CONFIG_PATH="$data_path/config"
ensure_dir "$data_path"
ensure_dir "$CERTS_PATH"
ensure_dir "$CONFIG_PATH"
}
# Usage
usage() {
cat <<EOF
SecuBox HAProxy Controller
Usage: $(basename $0) <command> [options]
Container Commands:
install Setup HAProxy LXC container
uninstall Remove container (keeps config)
update Update HAProxy in container
status Show service status
Configuration:
generate Generate haproxy.cfg from UCI
validate Validate configuration
reload Reload HAProxy config (no downtime)
Virtual Hosts:
vhost list List all virtual hosts
vhost add <domain> Add virtual host
vhost remove <domain> Remove virtual host
vhost sync Sync vhosts to config
Backends:
backend list List all backends
backend add <name> Add backend
backend remove <name> Remove backend
Servers:
server list <backend> List servers in backend
server add <backend> <addr:port> Add server to backend
server remove <backend> <name> Remove server
Certificates:
cert list List certificates
cert add <domain> Request ACME certificate
cert import <domain> <cert> <key> Import certificate
cert renew [domain] Renew certificate(s)
cert remove <domain> Remove certificate
Service Commands:
service-run Run in foreground (for init)
service-stop Stop service
Stats:
stats Show HAProxy stats
connections Show active connections
EOF
}
# ===========================================
# LXC Container Management
# ===========================================
lxc_running() {
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}
lxc_exists() {
[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
}
lxc_stop() {
if lxc_running; then
log_info "Stopping HAProxy container..."
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
sleep 2
fi
}
lxc_create_rootfs() {
log_info "Creating Alpine rootfs for HAProxy..."
ensure_dir "$LXC_PATH/$LXC_NAME"
local arch="x86_64"
case "$(uname -m)" in
aarch64) arch="aarch64" ;;
armv7l) arch="armv7" ;;
esac
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/$arch/alpine-minirootfs-3.21.2-$arch.tar.gz"
local rootfs_tar="/tmp/alpine-haproxy.tar.gz"
log_info "Downloading Alpine rootfs..."
wget -q -O "$rootfs_tar" "$alpine_url" || {
log_error "Failed to download Alpine rootfs"
return 1
}
log_info "Extracting rootfs..."
ensure_dir "$LXC_ROOTFS"
tar -xzf "$rootfs_tar" -C "$LXC_ROOTFS" || {
log_error "Failed to extract rootfs"
return 1
}
rm -f "$rootfs_tar"
# Configure Alpine
cat > "$LXC_ROOTFS/etc/resolv.conf" << 'EOF'
nameserver 1.1.1.1
nameserver 8.8.8.8
EOF
cat > "$LXC_ROOTFS/etc/apk/repositories" << 'EOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
EOF
# Install HAProxy
log_info "Installing HAProxy..."
chroot "$LXC_ROOTFS" /bin/sh -c "
apk update
apk add --no-cache haproxy openssl curl socat lua5.4 lua5.4-socket
" || {
log_error "Failed to install HAProxy"
return 1
}
log_info "Rootfs created successfully"
}
lxc_create_config() {
load_config
local arch="x86_64"
case "$(uname -m)" in
aarch64) arch="aarch64" ;;
armv7l) arch="armhf" ;;
esac
local mem_bytes=$(echo "$memory_limit" | sed 's/M/000000/;s/G/000000000/')
cat > "$LXC_CONFIG" << EOF
# HAProxy LXC Configuration
lxc.uts.name = $LXC_NAME
lxc.rootfs.path = dir:$LXC_ROOTFS
lxc.arch = $arch
# Network: use host network for binding ports
lxc.net.0.type = none
# Mount points
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.mount.entry = $data_path opt/haproxy none bind,create=dir 0 0
# Environment
lxc.environment = HTTP_PORT=$http_port
lxc.environment = HTTPS_PORT=$https_port
lxc.environment = STATS_PORT=$stats_port
# Security
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time
# Resource limits (cgroup2)
lxc.cgroup2.memory.max = $mem_bytes
# Init command
lxc.init.cmd = /opt/start-haproxy.sh
EOF
log_info "LXC config created"
}
lxc_run() {
load_config
lxc_stop
if ! lxc_exists; then
log_error "Container not installed. Run: haproxyctl install"
return 1
fi
lxc_create_config
# Ensure start script exists
local start_script="$LXC_ROOTFS/opt/start-haproxy.sh"
cat > "$start_script" << 'STARTEOF'
#!/bin/sh
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
CONFIG_FILE="/opt/haproxy/config/haproxy.cfg"
LOG_FILE="/opt/haproxy/startup.log"
# Log all output
exec >>"$LOG_FILE" 2>&1
echo "=== HAProxy startup: $(date) ==="
echo "Config: $CONFIG_FILE"
ls -la /opt/haproxy/
ls -la /opt/haproxy/certs/ 2>/dev/null || echo "No certs dir"
# Wait for config
if [ ! -f "$CONFIG_FILE" ]; then
echo "[haproxy] Config not found, generating default..."
mkdir -p /opt/haproxy/config
cat > "$CONFIG_FILE" << 'CFGEOF'
global
log stdout format raw local0
maxconn 4096
stats socket /var/run/haproxy.sock mode 660 level admin expose-fd listeners
stats timeout 30s
defaults
mode http
log global
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
frontend stats
bind *:8404
mode http
stats enable
stats uri /stats
stats refresh 10s
stats admin if TRUE
frontend http-in
bind *:80
mode http
default_backend fallback
backend fallback
mode http
server local 127.0.0.1:8080 check
CFGEOF
fi
# Validate config first
echo "[haproxy] Validating config..."
haproxy -c -f "$CONFIG_FILE"
RC=$?
echo "[haproxy] Validation exit code: $RC"
if [ $RC -ne 0 ]; then
echo "[haproxy] Config validation failed!"
exit 1
fi
echo "[haproxy] Starting HAProxy..."
exec haproxy -f "$CONFIG_FILE" -W -db
STARTEOF
chmod +x "$start_script"
# Generate config before starting
generate_config
log_info "Starting HAProxy container..."
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
}
lxc_exec() {
if ! lxc_running; then
log_error "Container not running"
return 1
fi
lxc-attach -n "$LXC_NAME" -- "$@"
}
# ===========================================
# Configuration Generation
# ===========================================
generate_config() {
load_config
local cfg_file="$CONFIG_PATH/haproxy.cfg"
log_info "Generating HAProxy configuration..."
# Global section
cat > "$cfg_file" << EOF
# HAProxy Configuration - Generated by SecuBox
# DO NOT EDIT - Use UCI configuration
global
log stdout format raw local0 $log_level
maxconn $maxconn
stats socket /var/run/haproxy.sock mode 660 level admin expose-fd listeners
stats timeout 30s
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
tune.ssl.default-dh-param 2048
EOF
# Defaults section
local mode=$(uci_get defaults.mode) || mode="http"
local timeout_connect=$(uci_get defaults.timeout_connect) || timeout_connect="5s"
local timeout_client=$(uci_get defaults.timeout_client) || timeout_client="30s"
local timeout_server=$(uci_get defaults.timeout_server) || timeout_server="30s"
cat >> "$cfg_file" << EOF
defaults
mode $mode
log global
option httplog
option dontlognull
option forwardfor
timeout connect $timeout_connect
timeout client $timeout_client
timeout server $timeout_server
timeout http-request 10s
timeout http-keep-alive 10s
retries 3
EOF
# Stats frontend
if [ "$stats_enabled" = "1" ]; then
cat >> "$cfg_file" << EOF
frontend stats
bind *:$stats_port
mode http
stats enable
stats uri /stats
stats refresh 10s
stats auth $stats_user:$stats_password
stats admin if TRUE
EOF
fi
# Generate frontends from UCI
_generate_frontends >> "$cfg_file"
# Generate backends from UCI
_generate_backends >> "$cfg_file"
log_info "Configuration generated: $cfg_file"
}
_generate_frontends() {
# HTTP Frontend
cat << EOF
frontend http-in
bind *:$http_port
mode http
EOF
# Add HTTPS redirect rules for vhosts with ssl_redirect
config_load haproxy
config_foreach _add_ssl_redirect vhost
# Add vhost ACLs for HTTP
config_foreach _add_vhost_acl vhost "http"
echo " default_backend fallback"
echo ""
# HTTPS Frontend (if certificates exist)
# Use container path /opt/haproxy/certs/ (not host path)
local CONTAINER_CERTS_PATH="/opt/haproxy/certs"
if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then
cat << EOF
frontend https-in
bind *:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1
mode http
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
EOF
# Add vhost ACLs for HTTPS
config_foreach _add_vhost_acl vhost "https"
echo " default_backend fallback"
echo ""
fi
}
_add_ssl_redirect() {
local section="$1"
local enabled domain ssl_redirect
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
config_get domain "$section" domain
config_get ssl_redirect "$section" ssl_redirect "0"
[ -n "$domain" ] || return
[ "$ssl_redirect" = "1" ] || return
local acl_name=$(echo "$domain" | tr '.' '_' | tr '-' '_')
echo " acl host_${acl_name} hdr(host) -i $domain"
echo " http-request redirect scheme https code 301 if host_${acl_name} !{ ssl_fc }"
}
_add_vhost_acl() {
local section="$1"
local proto="$2"
local enabled domain backend ssl
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
config_get domain "$section" domain
config_get backend "$section" backend
config_get ssl "$section" ssl "0"
[ -n "$domain" ] || return
[ -n "$backend" ] || return
# For HTTP frontend, skip SSL-only vhosts
[ "$proto" = "http" ] && [ "$ssl" = "1" ] && return
local acl_name=$(echo "$domain" | tr '.' '_' | tr '-' '_')
echo " acl host_${acl_name} hdr(host) -i $domain"
echo " use_backend $backend if host_${acl_name}"
}
_generate_backends() {
config_load haproxy
# Generate each backend from UCI
config_foreach _generate_backend backend
# Only add default fallback if no "fallback" backend exists in UCI
if ! uci -q get haproxy.fallback >/dev/null 2>&1; then
cat << EOF
backend fallback
mode http
http-request deny deny_status 503
EOF
fi
}
_generate_backend() {
local section="$1"
local enabled name mode balance health_check
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
config_get name "$section" name "$section"
config_get mode "$section" mode "http"
config_get balance "$section" balance "roundrobin"
config_get health_check "$section" health_check ""
echo ""
echo "backend $name"
echo " mode $mode"
echo " balance $balance"
[ -n "$health_check" ] && echo " option $health_check"
# Add servers defined in backend section (handles both single and list)
local server_line
config_get server_line "$section" server ""
[ -n "$server_line" ] && echo " server $server_line"
# Add servers from separate server UCI sections
config_foreach _add_server_to_backend server "$name"
}
_add_server_to_backend() {
local section="$1"
local target_backend="$2"
local backend server_name address port weight check enabled
config_get backend "$section" backend
[ "$backend" = "$target_backend" ] || return
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
config_get server_name "$section" name "$section"
config_get address "$section" address
config_get port "$section" port "80"
config_get weight "$section" weight "100"
config_get check "$section" check "1"
[ -n "$address" ] || return
local check_opt=""
[ "$check" = "1" ] && check_opt="check"
echo " server $server_name $address:$port weight $weight $check_opt"
}
# ===========================================
# Certificate Management
# ===========================================
cmd_cert_list() {
load_config
echo "Certificates in $CERTS_PATH:"
echo "----------------------------"
if [ -d "$CERTS_PATH" ]; then
for cert in "$CERTS_PATH"/*.pem; do
[ -f "$cert" ] || continue
local name=$(basename "$cert" .pem)
local expiry=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2)
echo " $name - Expires: ${expiry:-Unknown}"
done
else
echo " No certificates found"
fi
}
cmd_cert_add() {
require_root
load_config
local domain="$1"
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
local email=$(uci_get acme.email)
local staging=$(uci_get acme.staging)
local key_type_raw=$(uci_get acme.key_type) || key_type_raw="ec-256"
# Convert key type for acme.sh (rsa-4096 → 4096, ec-256 stays ec-256)
local key_type="$key_type_raw"
case "$key_type_raw" in
rsa-*) key_type="${key_type_raw#rsa-}" ;; # rsa-4096 → 4096
RSA-*) key_type="${key_type_raw#RSA-}" ;;
esac
[ -z "$email" ] && { log_error "ACME email not configured. Set in LuCI > Services > HAProxy > Settings"; return 1; }
log_info "Requesting certificate for $domain..."
local staging_flag=""
[ "$staging" = "1" ] && staging_flag="--staging"
# Find acme.sh - check OpenWrt location first, then PATH
local ACME_SH=""
if [ -x "/usr/lib/acme/client/acme.sh" ]; then
ACME_SH="/usr/lib/acme/client/acme.sh"
elif command -v acme.sh >/dev/null 2>&1; then
ACME_SH="acme.sh"
fi
if [ -n "$ACME_SH" ]; then
# Set acme.sh home directory
export LE_WORKING_DIR="/etc/acme"
export LE_CONFIG_HOME="/etc/acme"
ensure_dir "$LE_WORKING_DIR"
# Register account if needed
if [ ! -f "$LE_WORKING_DIR/account.conf" ]; then
log_info "Registering ACME account..."
"$ACME_SH" --register-account -m "$email" $staging_flag --home "$LE_WORKING_DIR" || true
fi
# Check if HAProxy is using the port
local haproxy_was_running=0
if lxc_running; then
log_info "Temporarily stopping HAProxy for certificate issuance..."
haproxy_was_running=1
/etc/init.d/haproxy stop 2>/dev/null || true
sleep 2
fi
# Issue certificate using standalone mode
log_info "Issuing certificate (standalone mode on port $http_port)..."
local acme_result=0
"$ACME_SH" --issue -d "$domain" \
--standalone --httpport "$http_port" \
--keylength "$key_type" \
$staging_flag \
--home "$LE_WORKING_DIR" || acme_result=$?
# acme.sh returns 0 on success, 2 on "skip/already valid" - both are OK
# Install the certificate to our certs path
if [ "$acme_result" -eq 0 ] || [ "$acme_result" -eq 2 ]; then
log_info "Installing certificate..."
"$ACME_SH" --install-cert -d "$domain" \
--home "$LE_WORKING_DIR" \
--cert-file "$CERTS_PATH/$domain.crt" \
--key-file "$CERTS_PATH/$domain.key" \
--fullchain-file "$CERTS_PATH/$domain.pem" \
--reloadcmd "/etc/init.d/haproxy reload" 2>/dev/null || true
fi
# Restart HAProxy if it was running
if [ "$haproxy_was_running" = "1" ]; then
log_info "Restarting HAProxy..."
/etc/init.d/haproxy start 2>/dev/null || true
fi
# Check if certificate was created
if [ ! -f "$CERTS_PATH/$domain.pem" ]; then
log_error "Certificate issuance failed. Ensure port $http_port is accessible from internet and domain points to this IP."
return 1
fi
log_info "Certificate ready: $CERTS_PATH/$domain.pem"
elif command -v certbot >/dev/null 2>&1; then
certbot certonly --standalone -d "$domain" \
--email "$email" --agree-tos -n \
--http-01-port "$http_port" $staging_flag || {
log_error "Certbot failed"
return 1
}
# Copy to HAProxy certs dir
local le_path="/etc/letsencrypt/live/$domain"
cat "$le_path/fullchain.pem" "$le_path/privkey.pem" > "$CERTS_PATH/$domain.pem"
else
log_error "No ACME client found. Install: opkg install acme acme-acmesh"
return 1
fi
chmod 600 "$CERTS_PATH/$domain.pem"
# Add to UCI
local section="cert_$(echo "$domain" | tr '.-' '__')"
uci set haproxy.$section=certificate
uci set haproxy.$section.domain="$domain"
uci set haproxy.$section.type="acme"
uci set haproxy.$section.enabled="1"
uci commit haproxy
log_info "Certificate installed for $domain"
}
cmd_cert_import() {
require_root
load_config
local domain="$1"
local cert_file="$2"
local key_file="$3"
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
[ -z "$cert_file" ] && { log_error "Certificate file required"; return 1; }
[ -z "$key_file" ] && { log_error "Key file required"; return 1; }
[ -f "$cert_file" ] || { log_error "Certificate file not found"; return 1; }
[ -f "$key_file" ] || { log_error "Key file not found"; return 1; }
# Combine cert and key for HAProxy
cat "$cert_file" "$key_file" > "$CERTS_PATH/$domain.pem"
chmod 600 "$CERTS_PATH/$domain.pem"
# Add to UCI
uci set haproxy.cert_${domain//[.-]/_}=certificate
uci set haproxy.cert_${domain//[.-]/_}.domain="$domain"
uci set haproxy.cert_${domain//[.-]/_}.type="manual"
uci set haproxy.cert_${domain//[.-]/_}.enabled="1"
uci commit haproxy
log_info "Certificate imported for $domain"
}
# ===========================================
# Virtual Host Management
# ===========================================
cmd_vhost_list() {
load_config
echo "Virtual Hosts:"
echo "--------------"
config_load haproxy
config_foreach _print_vhost vhost
}
_print_vhost() {
local section="$1"
local enabled domain backend ssl ssl_redirect acme
config_get domain "$section" domain
config_get backend "$section" backend
config_get enabled "$section" enabled "0"
config_get ssl "$section" ssl "0"
config_get ssl_redirect "$section" ssl_redirect "0"
config_get acme "$section" acme "0"
local status="disabled"
[ "$enabled" = "1" ] && status="enabled"
local flags=""
[ "$ssl" = "1" ] && flags="${flags}SSL "
[ "$ssl_redirect" = "1" ] && flags="${flags}REDIRECT "
[ "$acme" = "1" ] && flags="${flags}ACME "
printf " %-30s -> %-20s [%s] %s\n" "$domain" "$backend" "$status" "$flags"
}
cmd_vhost_add() {
require_root
load_config
local domain="$1"
local backend="$2"
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
[ -z "$backend" ] && backend="fallback"
local section="vhost_${domain//[.-]/_}"
uci set haproxy.$section=vhost
uci set haproxy.$section.domain="$domain"
uci set haproxy.$section.backend="$backend"
uci set haproxy.$section.ssl="1"
uci set haproxy.$section.ssl_redirect="1"
uci set haproxy.$section.acme="1"
uci set haproxy.$section.enabled="1"
uci commit haproxy
log_info "Virtual host added: $domain -> $backend"
}
cmd_vhost_remove() {
require_root
local domain="$1"
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
local section="vhost_${domain//[.-]/_}"
uci delete haproxy.$section 2>/dev/null
uci commit haproxy
log_info "Virtual host removed: $domain"
}
# ===========================================
# Backend Management
# ===========================================
cmd_backend_list() {
load_config
echo "Backends:"
echo "---------"
config_load haproxy
config_foreach _print_backend backend
}
_print_backend() {
local section="$1"
local enabled name mode balance
config_get name "$section" name "$section"
config_get enabled "$section" enabled "0"
config_get mode "$section" mode "http"
config_get balance "$section" balance "roundrobin"
local status="disabled"
[ "$enabled" = "1" ] && status="enabled"
printf " %-20s mode=%-6s balance=%-12s [%s]\n" "$name" "$mode" "$balance" "$status"
}
cmd_backend_add() {
require_root
local name="$1"
[ -z "$name" ] && { log_error "Backend name required"; return 1; }
local section="backend_${name//[.-]/_}"
uci set haproxy.$section=backend
uci set haproxy.$section.name="$name"
uci set haproxy.$section.mode="http"
uci set haproxy.$section.balance="roundrobin"
uci set haproxy.$section.enabled="1"
uci commit haproxy
log_info "Backend added: $name"
}
cmd_server_add() {
require_root
local backend="$1"
local addr_port="$2"
local server_name="$3"
[ -z "$backend" ] && { log_error "Backend name required"; return 1; }
[ -z "$addr_port" ] && { log_error "Address:port required"; return 1; }
local address=$(echo "$addr_port" | cut -d: -f1)
local port=$(echo "$addr_port" | cut -d: -f2)
[ -z "$port" ] && port="80"
[ -z "$server_name" ] && server_name="srv_$(echo $address | tr '.' '_')_$port"
local section="server_${server_name//[.-]/_}"
uci set haproxy.$section=server
uci set haproxy.$section.backend="$backend"
uci set haproxy.$section.name="$server_name"
uci set haproxy.$section.address="$address"
uci set haproxy.$section.port="$port"
uci set haproxy.$section.weight="100"
uci set haproxy.$section.check="1"
uci set haproxy.$section.enabled="1"
uci commit haproxy
log_info "Server added: $server_name ($address:$port) to backend $backend"
}
# ===========================================
# Commands
# ===========================================
cmd_install() {
require_root
load_config
log_info "Installing HAProxy..."
has_lxc || { log_error "LXC not installed"; exit 1; }
if ! lxc_exists; then
lxc_create_rootfs || exit 1
fi
lxc_create_config || exit 1
log_info "Installation complete!"
log_info ""
log_info "Next steps:"
log_info " 1. Enable: uci set haproxy.main.enabled=1 && uci commit haproxy"
log_info " 2. Add vhost: haproxyctl vhost add example.com backend_name"
log_info " 3. Start: /etc/init.d/haproxy start"
}
cmd_status() {
load_config
local enabled=$(uci_get main.enabled)
local running="no"
lxc_running && running="yes"
cat << EOF
HAProxy Status
==============
Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")
Running: $running
HTTP Port: $http_port
HTTPS Port: $https_port
Stats Port: $stats_port
Stats URL: http://localhost:$stats_port/stats
Container: $LXC_NAME
Rootfs: $LXC_ROOTFS
Config: $CONFIG_PATH/haproxy.cfg
Certs: $CERTS_PATH
EOF
}
cmd_reload() {
require_root
if ! lxc_running; then
log_error "Container not running"
return 1
fi
generate_config
log_info "Reloading HAProxy configuration..."
# HAProxy in master-worker mode (-W) reloads gracefully on SIGUSR2
# Fallback to SIGHUP if USR2 fails
lxc_exec killall -USR2 haproxy 2>/dev/null || \
lxc_exec killall -HUP haproxy 2>/dev/null || \
log_error "Could not signal HAProxy for reload"
log_info "Reload complete"
}
cmd_validate() {
load_config
generate_config
log_info "Validating configuration..."
if lxc_running; then
lxc_exec haproxy -c -f /opt/haproxy/config/haproxy.cfg
else
# Validate locally if possible
if [ -f "$CONFIG_PATH/haproxy.cfg" ]; then
log_info "Config file: $CONFIG_PATH/haproxy.cfg"
head -50 "$CONFIG_PATH/haproxy.cfg"
fi
fi
}
cmd_stats() {
if ! lxc_running; then
log_error "Container not running"
return 1
fi
lxc_exec sh -c "echo 'show stat' | socat stdio /var/run/haproxy.sock" 2>/dev/null || \
curl -s "http://localhost:$stats_port/stats;csv"
}
cmd_service_run() {
require_root
load_config
has_lxc || { log_error "LXC not installed"; exit 1; }
lxc_run
}
cmd_service_stop() {
require_root
lxc_stop
}
# ===========================================
# Main
# ===========================================
case "${1:-}" in
install) shift; cmd_install "$@" ;;
uninstall) shift; lxc_stop; log_info "Uninstall: rm -rf $LXC_PATH/$LXC_NAME" ;;
update) shift; lxc_exec apk update && lxc_exec apk upgrade haproxy ;;
status) shift; cmd_status "$@" ;;
generate) shift; generate_config "$@" ;;
validate) shift; cmd_validate "$@" ;;
reload) shift; cmd_reload "$@" ;;
vhost)
shift
case "${1:-}" in
list) shift; cmd_vhost_list "$@" ;;
add) shift; cmd_vhost_add "$@" ;;
remove) shift; cmd_vhost_remove "$@" ;;
sync) shift; generate_config && cmd_reload ;;
*) echo "Usage: haproxyctl vhost {list|add|remove|sync}" ;;
esac
;;
backend)
shift
case "${1:-}" in
list) shift; cmd_backend_list "$@" ;;
add) shift; cmd_backend_add "$@" ;;
remove) shift; uci delete haproxy.backend_${2//[.-]/_} 2>/dev/null; uci commit haproxy ;;
*) echo "Usage: haproxyctl backend {list|add|remove}" ;;
esac
;;
server)
shift
case "${1:-}" in
list) shift; config_load haproxy; config_foreach _print_server server "$1" ;;
add) shift; cmd_server_add "$@" ;;
remove) shift; uci delete haproxy.server_${3//[.-]/_} 2>/dev/null; uci commit haproxy ;;
*) echo "Usage: haproxyctl server {list|add|remove} <backend> [addr:port]" ;;
esac
;;
cert)
shift
case "${1:-}" in
list) shift; cmd_cert_list "$@" ;;
add) shift; cmd_cert_add "$@" ;;
import) shift; cmd_cert_import "$@" ;;
renew) shift; cmd_cert_add "$@" ;;
remove) shift; rm -f "$CERTS_PATH/$1.pem"; uci delete haproxy.cert_${1//[.-]/_} 2>/dev/null ;;
*) echo "Usage: haproxyctl cert {list|add|import|renew|remove}" ;;
esac
;;
stats) shift; cmd_stats "$@" ;;
connections) shift; lxc_exec sh -c "echo 'show sess' | socat stdio /var/run/haproxy.sock" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
shell) shift; lxc_exec /bin/sh ;;
exec) shift; lxc_exec "$@" ;;
*) usage ;;
esac