mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-09-23 19:06:25 +00:00
feat: Add HAProxy options support for backends and frontends
Implemented comprehensive HAProxy options field support for both backend and frontend entities to enable standard HAProxy directives like 'option http-keep-alive', 'option httplog', 'option forwardfor', etc. Changes: - Database: Added 'options' TEXT column to backends and frontends tables - Models: Added options field to BackendConfig, BackendConfigUpdate, and FrontendConfig - API Endpoints: Updated CREATE, UPDATE, and GET endpoints to handle options field * Backend: CREATE/UPDATE/GET with options support * Frontend: CREATE/UPDATE/GET with options support (fixed 5 SELECT queries) - Config Generator: Added options block generation for both backends and frontends - Bulk Import Parser: * Added options field to ParsedBackend and ParsedFrontend dataclasses * Implemented option directive parsing with validation * Added unknown option warnings * Fixed bulk parse response to include options field - Bulk Import Merge: Added options field comparison in UPDATE logic - UI Components: * BackendServers.js: Added options TextArea form field * FrontendManagement.js: Added options TextArea form field Features: - Multi-line options support (newline-separated format) - Option validation with known HAProxy options list - Backward compatible (NULL options for existing entities) - Bulk import support with merge strategy - Full CRUD support for both manual and bulk operations Technical Details: - Format: Newline-separated TEXT field for multiple options - Validation: Warns about unknown options but allows them - Config Generation: Each option written as separate directive - Agent: Standard HAProxy config validation applies Total: 10 files modified, ~195 lines added, 26 integration points verified
This commit is contained in:
@@ -1555,6 +1555,8 @@ async def run_all_migrations():
|
||||
await add_cluster_id_to_agent_config_requests()
|
||||
await fix_users_unique_constraints_for_soft_delete()
|
||||
await add_ssl_certificate_id_to_backend_servers()
|
||||
await add_options_to_backends()
|
||||
await add_options_to_frontends()
|
||||
|
||||
logger.info("Database migrations completed successfully.")
|
||||
|
||||
@@ -2754,3 +2756,71 @@ async def fix_users_unique_constraints_for_soft_delete():
|
||||
finally:
|
||||
if conn:
|
||||
await close_database_connection(conn)
|
||||
|
||||
async def add_options_to_backends():
|
||||
"""Add options column to backends table for HAProxy backend options (option http-keep-alive, etc.)"""
|
||||
conn = None
|
||||
try:
|
||||
conn = await get_database_connection()
|
||||
|
||||
# Check if options column already exists
|
||||
column_exists = await conn.fetchval("""
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'backends'
|
||||
AND column_name = 'options'
|
||||
)
|
||||
""")
|
||||
|
||||
if not column_exists:
|
||||
# Add options column as TEXT (multi-line HAProxy options)
|
||||
await conn.execute("""
|
||||
ALTER TABLE backends
|
||||
ADD COLUMN options TEXT
|
||||
""")
|
||||
|
||||
logger.info("✅ Added options column to backends table for HAProxy backend options")
|
||||
else:
|
||||
logger.info("ℹ️ options column already exists in backends table")
|
||||
|
||||
await close_database_connection(conn)
|
||||
|
||||
except Exception as e:
|
||||
if conn:
|
||||
await close_database_connection(conn)
|
||||
logger.error(f"❌ Error adding options to backends: {e}")
|
||||
# Don't raise - we'll try to proceed
|
||||
|
||||
async def add_options_to_frontends():
|
||||
"""Add options column to frontends table for HAProxy frontend options (option httplog, option forwardfor, etc.)"""
|
||||
conn = None
|
||||
try:
|
||||
conn = await get_database_connection()
|
||||
|
||||
# Check if options column already exists
|
||||
column_exists = await conn.fetchval("""
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'frontends'
|
||||
AND column_name = 'options'
|
||||
)
|
||||
""")
|
||||
|
||||
if not column_exists:
|
||||
# Add options column as TEXT (multi-line HAProxy options)
|
||||
await conn.execute("""
|
||||
ALTER TABLE frontends
|
||||
ADD COLUMN options TEXT
|
||||
""")
|
||||
|
||||
logger.info("✅ Added options column to frontends table for HAProxy frontend options")
|
||||
else:
|
||||
logger.info("ℹ️ options column already exists in frontends table")
|
||||
|
||||
await close_database_connection(conn)
|
||||
|
||||
except Exception as e:
|
||||
if conn:
|
||||
await close_database_connection(conn)
|
||||
logger.error(f"❌ Error adding options to frontends: {e}")
|
||||
# Don't raise - we'll try to proceed
|
||||
|
||||
@@ -38,6 +38,7 @@ class BackendConfig(BaseModel):
|
||||
timeout_connect: Optional[int] = 10000
|
||||
timeout_server: Optional[int] = 60000
|
||||
timeout_queue: Optional[int] = 60000
|
||||
options: Optional[str] = None
|
||||
servers: List[ServerConfig] = []
|
||||
|
||||
@validator('health_check_interval', 'timeout_connect', 'timeout_server', 'timeout_queue', 'fullconn')
|
||||
@@ -70,6 +71,7 @@ class BackendConfigUpdate(BaseModel):
|
||||
timeout_connect: Optional[int] = None
|
||||
timeout_server: Optional[int] = None
|
||||
timeout_queue: Optional[int] = None
|
||||
options: Optional[str] = None
|
||||
servers: Optional[List[ServerConfig]] = None
|
||||
|
||||
@validator('health_check_interval', 'timeout_connect', 'timeout_server', 'timeout_queue', 'fullconn')
|
||||
|
||||
@@ -23,6 +23,7 @@ class FrontendConfig(BaseModel):
|
||||
use_backend_rules: Any = [] # Changed from Optional[str] to Any for list support
|
||||
request_headers: Optional[str] = None
|
||||
response_headers: Optional[str] = None
|
||||
options: Optional[str] = None
|
||||
tcp_request_rules: Optional[str] = None
|
||||
timeout_client: Optional[int] = None
|
||||
timeout_http_request: Optional[int] = None
|
||||
|
||||
@@ -184,7 +184,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, balance_method, mode, health_check_uri,
|
||||
health_check_interval, health_check_expected_status, fullconn,
|
||||
cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise,
|
||||
request_headers, response_headers,
|
||||
request_headers, response_headers, options,
|
||||
is_active, created_at, updated_at, cluster_id, last_config_status,
|
||||
timeout_connect, timeout_server, timeout_queue
|
||||
FROM backends WHERE cluster_id = $1 ORDER BY name
|
||||
@@ -194,7 +194,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, balance_method, mode, health_check_uri,
|
||||
health_check_interval, health_check_expected_status, fullconn,
|
||||
cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise,
|
||||
request_headers, response_headers,
|
||||
request_headers, response_headers, options,
|
||||
is_active, created_at, updated_at, cluster_id, last_config_status,
|
||||
timeout_connect, timeout_server, timeout_queue
|
||||
FROM backends WHERE cluster_id = $1 AND is_active = TRUE ORDER BY name
|
||||
@@ -205,7 +205,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, balance_method, mode, health_check_uri,
|
||||
health_check_interval, health_check_expected_status, fullconn,
|
||||
cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise,
|
||||
request_headers, response_headers,
|
||||
request_headers, response_headers, options,
|
||||
is_active, created_at, updated_at, cluster_id, last_config_status,
|
||||
timeout_connect, timeout_server, timeout_queue
|
||||
FROM backends ORDER BY name
|
||||
@@ -215,7 +215,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, balance_method, mode, health_check_uri,
|
||||
health_check_interval, health_check_expected_status, fullconn,
|
||||
cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise,
|
||||
request_headers, response_headers,
|
||||
request_headers, response_headers, options,
|
||||
is_active, created_at, updated_at, cluster_id, last_config_status,
|
||||
timeout_connect, timeout_server, timeout_queue
|
||||
FROM backends WHERE is_active = TRUE ORDER BY name
|
||||
@@ -446,14 +446,14 @@ async def create_backend(backend: BackendConfig, authorization: str = Header(Non
|
||||
INSERT INTO backends (name, balance_method, mode, health_check_uri, health_check_interval,
|
||||
health_check_expected_status, fullconn, cookie_name, cookie_options,
|
||||
default_server_inter, default_server_fall, default_server_rise,
|
||||
request_headers, response_headers,
|
||||
request_headers, response_headers, options,
|
||||
timeout_connect, timeout_server, timeout_queue, cluster_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING id
|
||||
""", backend.name, backend.balance_method, backend.mode,
|
||||
backend.health_check_uri, backend.health_check_interval,
|
||||
backend.health_check_expected_status, backend.fullconn, backend.cookie_name, backend.cookie_options,
|
||||
backend.default_server_inter, backend.default_server_fall, backend.default_server_rise,
|
||||
backend.request_headers, backend.response_headers,
|
||||
backend.request_headers, backend.response_headers, backend.options,
|
||||
backend.timeout_connect, backend.timeout_server, backend.timeout_queue, backend.cluster_id)
|
||||
|
||||
# If cluster_id provided, create new config version for agents
|
||||
@@ -746,7 +746,7 @@ async def update_backend(backend_id: int, backend_update: BackendConfigUpdate, r
|
||||
if field in ['name', 'balance_method', 'mode', 'health_check_uri', 'health_check_interval',
|
||||
'health_check_expected_status', 'fullconn', 'cookie_name', 'cookie_options',
|
||||
'default_server_inter', 'default_server_fall', 'default_server_rise',
|
||||
'request_headers', 'response_headers', 'timeout_connect', 'timeout_server', 'timeout_queue']:
|
||||
'request_headers', 'response_headers', 'options', 'timeout_connect', 'timeout_server', 'timeout_queue']:
|
||||
update_fields.append(f"{field} = ${param_idx}")
|
||||
update_values.append(value)
|
||||
param_idx += 1
|
||||
|
||||
@@ -804,6 +804,7 @@ async def parse_bulk_config(
|
||||
"maxconn": frontend.maxconn,
|
||||
"request_headers": frontend.request_headers,
|
||||
"response_headers": frontend.response_headers,
|
||||
"options": frontend.options,
|
||||
"tcp_request_rules": frontend.tcp_request_rules,
|
||||
"acl_rules": frontend.acl_rules,
|
||||
"use_backend_rules": frontend.use_backend_rules
|
||||
@@ -890,6 +891,7 @@ async def parse_bulk_config(
|
||||
"default_server_rise": backend.default_server_rise,
|
||||
"request_headers": backend.request_headers,
|
||||
"response_headers": backend.response_headers,
|
||||
"options": backend.options,
|
||||
"timeout_connect": backend.timeout_connect,
|
||||
"timeout_server": backend.timeout_server,
|
||||
"timeout_queue": backend.timeout_queue,
|
||||
@@ -1280,6 +1282,12 @@ async def bulk_create_entities(
|
||||
update_values.append(backend_data["response_headers"])
|
||||
param_index += 1
|
||||
|
||||
# NEW: Options field support (option http-keep-alive, etc.)
|
||||
if backend_data.get("options") and backend_data["options"] != existing_full.get("options"):
|
||||
update_fields.append(f"options = ${param_index}")
|
||||
update_values.append(backend_data["options"])
|
||||
param_index += 1
|
||||
|
||||
# NOTE: maxconn field exists in database but is not used in normal backend UPDATE
|
||||
# Preserving consistency with existing backend UPDATE endpoint (backend.py)
|
||||
# maxconn field intentionally excluded from bulk import UPDATE
|
||||
@@ -1331,9 +1339,9 @@ async def bulk_create_entities(
|
||||
health_check_interval, health_check_expected_status, fullconn,
|
||||
cookie_name, cookie_options, default_server_inter,
|
||||
default_server_fall, default_server_rise, request_headers,
|
||||
response_headers, timeout_connect, timeout_server, timeout_queue, cluster_id
|
||||
response_headers, options, timeout_connect, timeout_server, timeout_queue, cluster_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
RETURNING id
|
||||
""",
|
||||
backend_data["name"],
|
||||
@@ -1350,6 +1358,7 @@ async def bulk_create_entities(
|
||||
backend_data.get("default_server_rise"),
|
||||
backend_data.get("request_headers"),
|
||||
backend_data.get("response_headers"),
|
||||
backend_data.get("options"),
|
||||
backend_data.get("timeout_connect", 10000),
|
||||
backend_data.get("timeout_server", 60000),
|
||||
backend_data.get("timeout_queue", 60000),
|
||||
@@ -1494,6 +1503,12 @@ async def bulk_create_entities(
|
||||
update_values.append(frontend_data["tcp_request_rules"])
|
||||
param_index += 1
|
||||
|
||||
# NEW: Options field support (option httplog, option forwardfor, etc.)
|
||||
if frontend_data.get("options") and frontend_data["options"] != existing_full.get("options"):
|
||||
update_fields.append(f"options = ${param_index}")
|
||||
update_values.append(frontend_data["options"])
|
||||
param_index += 1
|
||||
|
||||
if frontend_data.get("timeout_http_request") and frontend_data["timeout_http_request"] != existing_full["timeout_http_request"]:
|
||||
update_fields.append(f"timeout_http_request = ${param_index}")
|
||||
update_values.append(frontend_data["timeout_http_request"])
|
||||
@@ -1561,11 +1576,11 @@ async def bulk_create_entities(
|
||||
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port,
|
||||
ssl_cert_path, ssl_cert, ssl_verify,
|
||||
timeout_client, timeout_http_request, maxconn,
|
||||
request_headers, response_headers, tcp_request_rules,
|
||||
request_headers, response_headers, tcp_request_rules, options,
|
||||
rate_limit, compression, log_separate, monitor_uri,
|
||||
cluster_id, acl_rules, use_backend_rules, redirect_rules, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, CURRENT_TIMESTAMP)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, CURRENT_TIMESTAMP)
|
||||
RETURNING id
|
||||
""",
|
||||
frontend_data["name"],
|
||||
@@ -1586,6 +1601,7 @@ async def bulk_create_entities(
|
||||
frontend_data.get("request_headers"),
|
||||
frontend_data.get("response_headers"),
|
||||
frontend_data.get("tcp_request_rules"),
|
||||
frontend_data.get("options"),
|
||||
frontend_data.get("rate_limit"),
|
||||
frontend_data.get("compression", False),
|
||||
frontend_data.get("log_separate", False),
|
||||
|
||||
+14
-14
@@ -145,7 +145,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, bind_address, bind_port, default_backend, mode,
|
||||
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
|
||||
acl_rules, redirect_rules, use_backend_rules,
|
||||
request_headers, response_headers, tcp_request_rules,
|
||||
request_headers, response_headers, options, tcp_request_rules,
|
||||
timeout_client, timeout_http_request,
|
||||
rate_limit, compression, log_separate, monitor_uri,
|
||||
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
|
||||
@@ -160,7 +160,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, bind_address, bind_port, default_backend, mode,
|
||||
ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify,
|
||||
acl_rules, redirect_rules, use_backend_rules,
|
||||
request_headers, response_headers, tcp_request_rules,
|
||||
request_headers, response_headers, options, tcp_request_rules,
|
||||
timeout_client, timeout_http_request,
|
||||
rate_limit, compression, log_separate, monitor_uri,
|
||||
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
|
||||
@@ -188,7 +188,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, bind_address, bind_port, default_backend, mode,
|
||||
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
|
||||
acl_rules, redirect_rules, use_backend_rules,
|
||||
request_headers, response_headers, tcp_request_rules,
|
||||
request_headers, response_headers, options, tcp_request_rules,
|
||||
timeout_client, timeout_http_request,
|
||||
rate_limit, compression, log_separate, monitor_uri,
|
||||
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
|
||||
@@ -199,7 +199,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, bind_address, bind_port, default_backend, mode,
|
||||
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
|
||||
acl_rules, redirect_rules, use_backend_rules,
|
||||
request_headers, response_headers, tcp_request_rules,
|
||||
request_headers, response_headers, options, tcp_request_rules,
|
||||
timeout_client, timeout_http_request,
|
||||
rate_limit, compression, log_separate, monitor_uri,
|
||||
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
|
||||
@@ -213,7 +213,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, bind_address, bind_port, default_backend, mode,
|
||||
ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify,
|
||||
acl_rules, redirect_rules, use_backend_rules,
|
||||
request_headers, response_headers, tcp_request_rules,
|
||||
request_headers, response_headers, options, tcp_request_rules,
|
||||
timeout_client, timeout_http_request,
|
||||
rate_limit, compression, log_separate, monitor_uri,
|
||||
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
|
||||
@@ -224,7 +224,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
|
||||
SELECT id, name, bind_address, bind_port, default_backend, mode,
|
||||
ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify,
|
||||
acl_rules, redirect_rules, use_backend_rules,
|
||||
request_headers, response_headers, tcp_request_rules,
|
||||
request_headers, response_headers, options, tcp_request_rules,
|
||||
timeout_client, timeout_http_request,
|
||||
rate_limit, compression, log_separate, monitor_uri,
|
||||
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
|
||||
@@ -382,16 +382,16 @@ async def create_frontend(frontend: FrontendConfig, request: Request, authorizat
|
||||
name, bind_address, bind_port, default_backend, mode,
|
||||
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
|
||||
acl_rules, redirect_rules, use_backend_rules,
|
||||
request_headers, response_headers, tcp_request_rules, timeout_client, timeout_http_request,
|
||||
request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request,
|
||||
rate_limit, compression, log_separate, monitor_uri,
|
||||
cluster_id, maxconn, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, CURRENT_TIMESTAMP)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, CURRENT_TIMESTAMP)
|
||||
RETURNING id
|
||||
""", frontend.name, frontend.bind_address, frontend.bind_port,
|
||||
frontend.default_backend, frontend.mode, frontend.ssl_enabled,
|
||||
frontend.ssl_certificate_id, ssl_cert_ids_json, frontend.ssl_port, frontend.ssl_cert_path, frontend.ssl_cert, frontend.ssl_verify,
|
||||
json.dumps(frontend.acl_rules or []), json.dumps(frontend.redirect_rules or []), json.dumps(frontend.use_backend_rules or []),
|
||||
frontend.request_headers, frontend.response_headers, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
|
||||
frontend.request_headers, frontend.response_headers, frontend.options, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
|
||||
frontend.rate_limit, frontend.compression, frontend.log_separate, frontend.monitor_uri,
|
||||
frontend.cluster_id, frontend.maxconn)
|
||||
|
||||
@@ -630,15 +630,15 @@ async def update_frontend(frontend_id: int, frontend: FrontendConfig, request: R
|
||||
default_backend = $4, mode = $5, ssl_enabled = $6,
|
||||
ssl_certificate_id = $7, ssl_certificate_ids = $8, ssl_port = $9, ssl_cert_path = $10, ssl_cert = $11, ssl_verify = $12,
|
||||
acl_rules = $13, redirect_rules = $14, use_backend_rules = $15,
|
||||
request_headers = $16, response_headers = $17, tcp_request_rules = $18, timeout_client = $19, timeout_http_request = $20,
|
||||
rate_limit = $21, compression = $22, log_separate = $23, monitor_uri = $24,
|
||||
cluster_id = $25, maxconn = $26, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $27
|
||||
request_headers = $16, response_headers = $17, options = $18, tcp_request_rules = $19, timeout_client = $20, timeout_http_request = $21,
|
||||
rate_limit = $22, compression = $23, log_separate = $24, monitor_uri = $25,
|
||||
cluster_id = $26, maxconn = $27, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $28
|
||||
""", frontend.name, frontend.bind_address, frontend.bind_port,
|
||||
frontend.default_backend, frontend.mode, ssl_enabled,
|
||||
ssl_certificate_id, ssl_cert_ids_json, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
|
||||
json.dumps(frontend.acl_rules or []), json.dumps(frontend.redirect_rules or []), json.dumps(frontend.use_backend_rules or []),
|
||||
frontend.request_headers, frontend.response_headers, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
|
||||
frontend.request_headers, frontend.response_headers, frontend.options, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
|
||||
frontend.rate_limit, frontend.compression, frontend.log_separate, frontend.monitor_uri,
|
||||
frontend.cluster_id, frontend.maxconn, frontend_id)
|
||||
|
||||
|
||||
@@ -282,6 +282,14 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An
|
||||
if frontend.get('monitor_uri'):
|
||||
config_lines.append(f" monitor-uri {frontend['monitor_uri']}")
|
||||
|
||||
# Frontend Options (NEW: option httplog, option forwardfor, etc.)
|
||||
if frontend.get('options'):
|
||||
for line in frontend['options'].split('\n'):
|
||||
if line.strip():
|
||||
# Lines are complete HAProxy option directives
|
||||
# Examples: "option httplog", "option forwardfor", "option dontlognull"
|
||||
config_lines.append(f" {line.strip()}")
|
||||
|
||||
# Request Headers (includes options, http-request directives - already formatted)
|
||||
if frontend.get('request_headers'):
|
||||
for line in frontend['request_headers'].split('\n'):
|
||||
@@ -459,6 +467,14 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An
|
||||
default_server_line += f" rise {backend['default_server_rise']}"
|
||||
config_lines.append(default_server_line)
|
||||
|
||||
# Backend Options (NEW: option http-keep-alive, option forwardfor, etc.)
|
||||
if backend.get('options'):
|
||||
for line in backend['options'].split('\n'):
|
||||
if line.strip():
|
||||
# Lines are complete HAProxy option directives
|
||||
# Examples: "option http-keep-alive", "option forwardfor", "option httpchk"
|
||||
config_lines.append(f" {line.strip()}")
|
||||
|
||||
# Backend Request Headers (new field - includes options, http-request directives)
|
||||
if backend.get('request_headers'):
|
||||
for line in backend['request_headers'].split('\n'):
|
||||
|
||||
@@ -47,6 +47,7 @@ class ParsedBackend:
|
||||
default_server_rise: Optional[int] = None # Default rise value for all servers
|
||||
request_headers: Optional[str] = None # HTTP request headers (backend level)
|
||||
response_headers: Optional[str] = None # HTTP response headers (backend level)
|
||||
options: Optional[str] = None # HAProxy backend options (option http-keep-alive, option forwardfor, etc.)
|
||||
timeout_connect: Optional[int] = 10000
|
||||
timeout_server: Optional[int] = 60000
|
||||
timeout_queue: Optional[int] = 60000
|
||||
@@ -83,6 +84,7 @@ class ParsedFrontend:
|
||||
redirect_rules: Optional[list] = None
|
||||
request_headers: Optional[str] = None
|
||||
response_headers: Optional[str] = None
|
||||
options: Optional[str] = None # HAProxy frontend options (option httplog, option forwardfor, etc.)
|
||||
tcp_request_rules: Optional[str] = None # TCP request directives (for TCP mode)
|
||||
|
||||
|
||||
@@ -228,9 +230,10 @@ class HAProxyConfigParser:
|
||||
frontend = ParsedFrontend(name=name)
|
||||
ssl_cert_found = False
|
||||
|
||||
# Collect HTTP headers, ACL rules, use_backend rules, and TCP rules
|
||||
# Collect HTTP headers, ACL rules, use_backend rules, TCP rules, and options
|
||||
request_headers_list = []
|
||||
response_headers_list = []
|
||||
options_list = []
|
||||
acl_rules_list = []
|
||||
use_backend_rules_list = []
|
||||
tcp_request_rules_list = []
|
||||
@@ -306,6 +309,35 @@ class HAProxyConfigParser:
|
||||
if maxconn_match:
|
||||
frontend.maxconn = int(maxconn_match.group(1))
|
||||
|
||||
# Parse options (httplog, forwardfor, etc.) - NEW for frontend support
|
||||
if line.startswith('option '):
|
||||
# Validate option directive - check for known HAProxy options
|
||||
option_match = re.match(r'^option\s+(\S+)', line, re.IGNORECASE)
|
||||
if option_match:
|
||||
option_name = option_match.group(1)
|
||||
# List of valid HAProxy frontend options
|
||||
valid_options = [
|
||||
'httplog', 'tcplog', 'dontlognull', 'http-keep-alive', 'http-server-close',
|
||||
'forwardfor', 'redispatch', 'http-use-proxy-header', 'httpchk', 'tcp-check',
|
||||
'contstats', 'http-pretend-keepalive', 'logasap', 'nolinger', 'persist',
|
||||
'prefer-last-server', 'splice-auto', 'splice-request', 'splice-response',
|
||||
'transparent', 'abortonclose', 'allbackups', 'checkcache', 'clitcpka',
|
||||
'srvtcpka', 'http-no-delay', 'socket-stats', 'tcp-smart-accept',
|
||||
'tcp-smart-connect', 'independant-streams', 'log-separate-errors',
|
||||
'log-health-checks', 'accept-invalid-http-request', 'accept-invalid-http-response'
|
||||
]
|
||||
|
||||
if option_name not in valid_options:
|
||||
# Unknown/invalid option - add warning but still collect it
|
||||
self.warnings.append(
|
||||
f"Frontend '{name}': Unknown or invalid option '{option_name}' detected. "
|
||||
"Please verify this directive is supported by your HAProxy version."
|
||||
)
|
||||
|
||||
# Collect option for frontend.options field
|
||||
options_list.append(line.strip())
|
||||
continue # Skip options - don't add to request_headers
|
||||
|
||||
# Parse HTTP request headers
|
||||
if line.startswith('http-request '):
|
||||
# Check for unsupported capture directives
|
||||
@@ -394,13 +426,17 @@ class HAProxyConfigParser:
|
||||
if line.startswith('tcp-request '):
|
||||
tcp_request_rules_list.append(line)
|
||||
|
||||
# Combine collected headers and rules
|
||||
# Combine collected headers, rules, and options
|
||||
if request_headers_list:
|
||||
frontend.request_headers = '\n'.join(request_headers_list)
|
||||
|
||||
if response_headers_list:
|
||||
frontend.response_headers = '\n'.join(response_headers_list)
|
||||
|
||||
# NEW: Assign collected options to frontend
|
||||
if options_list:
|
||||
frontend.options = '\n'.join(options_list)
|
||||
|
||||
if acl_rules_list:
|
||||
frontend.acl_rules = acl_rules_list
|
||||
|
||||
@@ -422,9 +458,10 @@ class HAProxyConfigParser:
|
||||
try:
|
||||
backend = ParsedBackend(name=name, servers=[])
|
||||
|
||||
# Collect HTTP headers
|
||||
# Collect HTTP headers and options
|
||||
request_headers_list = []
|
||||
response_headers_list = []
|
||||
options_list = []
|
||||
|
||||
for line in lines:
|
||||
# Parse mode
|
||||
@@ -495,9 +532,7 @@ class HAProxyConfigParser:
|
||||
|
||||
# Parse options (http-keep-alive, tcp-check, etc.)
|
||||
# Note: option httpchk is handled separately above
|
||||
# CRITICAL FIX: Do NOT add option directives to request_headers
|
||||
# Options are separate HAProxy config directives, not HTTP headers
|
||||
# They are skipped during bulk import as they need special handling
|
||||
# NEW: Collect options for backend.options field
|
||||
if line.startswith('option ') and 'httpchk' not in line:
|
||||
# Validate option directive - check for known HAProxy options
|
||||
option_match = re.match(r'^option\s+(\S+)', line, re.IGNORECASE)
|
||||
@@ -516,19 +551,15 @@ class HAProxyConfigParser:
|
||||
]
|
||||
|
||||
if option_name not in valid_options:
|
||||
# Unknown/invalid option - add warning and skip
|
||||
# Unknown/invalid option - add warning but still collect it
|
||||
self.warnings.append(
|
||||
f"Backend '{name}': Unknown or invalid option '{option_name}' detected and skipped. "
|
||||
f"Backend '{name}': Unknown or invalid option '{option_name}' detected. "
|
||||
"Please verify this directive is supported by your HAProxy version."
|
||||
)
|
||||
else:
|
||||
# Valid option but skip it - bulk import doesn't support custom options yet
|
||||
self.warnings.append(
|
||||
f"Backend '{name}': HAProxy option '{option_name}' detected and skipped. "
|
||||
f"Custom HAProxy options are not supported in bulk import. "
|
||||
f"Configure options manually after import if needed."
|
||||
)
|
||||
continue # Always skip options - don't add to request_headers
|
||||
|
||||
# Collect option for backend.options field
|
||||
options_list.append(line.strip())
|
||||
continue # Skip options - don't add to request_headers
|
||||
|
||||
# Parse HTTP request headers
|
||||
if line.startswith('http-request '):
|
||||
@@ -607,6 +638,10 @@ class HAProxyConfigParser:
|
||||
|
||||
logger.warning(f"Backend '{name}': Excluded {len(portless_servers)} portless servers (TCP mode requires ports)")
|
||||
|
||||
# Assign collected options to backend (NEW)
|
||||
if options_list:
|
||||
backend.options = '\n'.join(options_list)
|
||||
|
||||
# DNS HOSTNAME DETECTION: Warn if servers use hostnames instead of IPs
|
||||
if backend.servers:
|
||||
hostname_servers = []
|
||||
|
||||
@@ -1779,6 +1779,21 @@ const BackendServers = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="options"
|
||||
label="Backend Options"
|
||||
tooltip="HAProxy backend options (one per line). Examples: option http-keep-alive, option forwardfor, option redispatch"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="e.g., option http-keep-alive option forwardfor option redispatch"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => setBackendModalVisible(false)}>
|
||||
|
||||
@@ -1642,6 +1642,26 @@ http-response del-header Server`}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="options"
|
||||
label="Frontend Options"
|
||||
extra="HAProxy frontend options (one per line)"
|
||||
tooltip="Examples: option httplog, option forwardfor, option dontlognull, option http-keep-alive"
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder={`Examples:
|
||||
option httplog
|
||||
option forwardfor
|
||||
option dontlognull
|
||||
option http-keep-alive`}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
|
||||
Reference in New Issue
Block a user