haproxy manager

This commit is contained in:
2025-02-20 13:41:38 -08:00
parent 9c52edd53a
commit 305fffba42
6 changed files with 224 additions and 38 deletions

View File

@@ -5,7 +5,6 @@ from pathlib import Path
import subprocess
import jinja2
import socket
import shutil
import psutil
app = Flask(__name__)
@@ -18,7 +17,7 @@ SSL_CERTS_DIR = '/etc/haproxy/certs'
def init_db():
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
# Create domains table
cursor.execute('''
CREATE TABLE IF NOT EXISTS domains (
@@ -28,7 +27,7 @@ def init_db():
ssl_cert_path TEXT
)
''')
# Create backends table
cursor.execute('''
CREATE TABLE IF NOT EXISTS backends (
@@ -39,7 +38,7 @@ def init_db():
FOREIGN KEY (domain_id) REFERENCES domains (id)
)
''')
# Create backend_servers table
cursor.execute('''
CREATE TABLE IF NOT EXISTS backend_servers (
@@ -57,7 +56,7 @@ def init_db():
def generate_self_signed_cert(ssl_certs_dir):
"""Generate a self-signed certificate for a domain."""
self_sign_cert = os.path.join(ssl_certs_dir, "default_self_signed_cert.pem")
print(self_sign_cert)
print(self_sign_cert)
if os.path.exists(self_sign_cert):
print("Self Signed Cert Found")
return True
@@ -68,14 +67,14 @@ def generate_self_signed_cert(ssl_certs_dir):
DOMAIN = socket.gethostname()
# Generate private key and certificate
subprocess.run([
'openssl', 'req', '-x509', '-newkey', 'rsa:4096',
'openssl', 'req', '-x509', '-newkey', 'rsa:4096',
'-keyout', '/tmp/key.pem',
'-out', '/tmp/cert.pem',
'-days', '3650',
'-nodes', # No passphrase
'-subj', f'/CN={DOMAIN}'
], check=True)
# Combine cert and key for HAProxy
with open(self_sign_cert, 'wb') as combined:
for file in ['/tmp/cert.pem', '/tmp/key.pem']:
@@ -95,32 +94,55 @@ def is_process_running(process_name):
template_loader = jinja2.FileSystemLoader(TEMPLATE_DIR)
template_env = jinja2.Environment(loader=template_loader)
@app.route('/health', methods=['GET'])
def health_check():
try:
# Check if HAProxy is running
haproxy_running = is_process_running('haproxy')
# Check if database is accessible
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute('SELECT 1')
cursor.fetchone()
return jsonify({
'status': 'healthy',
'haproxy_status': 'running' if haproxy_running else 'stopped',
'database': 'connected'
}), 200
except Exception as e:
return jsonify({
'status': 'unhealthy',
'error': str(e)
}), 500
@app.route('/api/domain', methods=['POST'])
def add_domain():
data = request.get_json()
domain = data.get('domain')
backend_name = data.get('backend_name')
servers = data.get('servers', [])
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
# Add domain
cursor.execute('INSERT INTO domains (domain) VALUES (?)', (domain,))
domain_id = cursor.lastrowid
# Add backend
cursor.execute('INSERT INTO backends (name, domain_id) VALUES (?, ?)',
(backend_name, domain_id))
backend_id = cursor.lastrowid
# Add servers
for server in servers:
cursor.execute('''
INSERT INTO backend_servers
INSERT INTO backend_servers
(backend_id, server_name, server_address, server_port, server_options)
VALUES (?, ?, ?, ?, ?)
''', (backend_id, server['name'], server['address'],
''', (backend_id, server['name'], server['address'],
server['port'], server.get('options')))
# Close cursor and connection
cursor.close()
@@ -132,28 +154,28 @@ def add_domain():
def request_ssl():
data = request.get_json()
domain = data.get('domain')
# Request Let's Encrypt certificate
result = subprocess.run([
'certbot', 'certonly', '--standalone',
'certbot', 'certonly', '--standalone',
'--preferred-challenges', 'http',
'-d', domain, '--non-interactive --http-01-port=8688'
])
if result.returncode == 0:
# Combine cert files and copy to HAProxy certs directory
cert_path = f'/etc/letsencrypt/live/{domain}/fullchain.pem'
key_path = f'/etc/letsencrypt/live/{domain}/privkey.pem'
combined_path = f'{SSL_CERTS_DIR}/{domain}.pem'
with open(combined_path, 'w') as combined:
subprocess.run(['cat', cert_path, key_path], stdout=combined)
# Update database
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute('''
UPDATE domains
UPDATE domains
SET ssl_enabled = 1, ssl_cert_path = ?
WHERE domain = ?
''', (combined_path, domain))
@@ -164,15 +186,67 @@ def request_ssl():
return jsonify({'status': 'success'})
return jsonify({'status': 'error', 'message': 'Failed to obtain SSL certificate'})
@app.route('/api/domain', methods=['DELETE'])
def remove_domain():
data = request.get_json()
domain = data.get('domain')
if not domain:
return jsonify({'status': 'error', 'message': 'Domain is required'}), 400
try:
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
# Get domain ID and check if it exists
cursor.execute('SELECT id FROM domains WHERE domain = ?', (domain,))
domain_result = cursor.fetchone()
if not domain_result:
return jsonify({'status': 'error', 'message': 'Domain not found'}), 404
domain_id = domain_result[0]
# Get backend IDs associated with this domain
cursor.execute('SELECT id FROM backends WHERE domain_id = ?', (domain_id,))
backend_ids = [row[0] for row in cursor.fetchall()]
# Delete backend servers
for backend_id in backend_ids:
cursor.execute('DELETE FROM backend_servers WHERE backend_id = ?', (backend_id,))
# Delete backends
cursor.execute('DELETE FROM backends WHERE domain_id = ?', (domain_id,))
# Delete domain
cursor.execute('DELETE FROM domains WHERE id = ?', (domain_id,))
# Delete SSL certificate if it exists
cursor.execute('SELECT ssl_cert_path FROM domains WHERE id = ? AND ssl_enabled = 1', (domain_id,))
cert_result = cursor.fetchone()
if cert_result and cert_result[0]:
try:
os.remove(cert_result[0])
except OSError:
pass # Ignore errors if file doesn't exist
# Regenerate HAProxy config
generate_config()
return jsonify({'status': 'success', 'message': 'Domain configuration removed'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
def generate_config():
try:
conn = sqlite3.connect(DB_FILE)
# Enable dictionary-like access to rows
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query = '''
SELECT
SELECT
d.id as domain_id,
d.domain,
d.ssl_enabled,
@@ -183,7 +257,7 @@ def generate_config():
LEFT JOIN backends b ON d.id = b.domain_id
'''
cursor.execute(query)
# Fetch and immediately convert to list of dicts to avoid any cursor issues
domains = [dict(domain) for domain in cursor.fetchall()]
config_parts = []
@@ -226,7 +300,7 @@ def generate_config():
SELECT * FROM backend_servers WHERE backend_id = ?
''', (domain['backend_id'],))
servers = [dict(server) for server in cursor.fetchall()]
if not servers:
print(f"No servers found for backend {domain['backend_name']}") # Debug log
continue
@@ -241,19 +315,21 @@ def generate_config():
except Exception as e:
print(f"Error generating backend block for {domain['backend_name']}: {e}")
continue
# Write complete configuration to tmp
# Write complete configuration to tmp
temp_config_path = "/etc/haproxy/haproxy.cfg"
config_content = '\n'.join(config_parts)
print("Final config content:", config_content) # Debug log
# Write complete configuration to tmp
# Check HAProxy Configuration, and reload if it works
with open("/tmp/haproxy_temp.cfg", 'w') as f:
f.write('\n'.join(config_parts))
result = subprocess.run(['haproxy', '-c', '-f', "/tmp/haproxy_temp.cfg"], capture_output=True)
with open(temp_config_path, 'w') as f:
f.write(config_content)
result = subprocess.run(['haproxy', '-c', '-f', temp_config_path], capture_output=True)
if result.returncode == 0:
shutil.copyfile("/tmp/haproxy_temp.cfg", HAPROXY_CONFIG_PATH)
os.remove("/tmp/haproxy_temp.cfg")
print("HAProxy configuration check passed")
if is_process_running('haproxy'):
subprocess.run(['echo', '"reload"', '|', 'socat', 'stdio', '/tmp/haproxy-cli'])
else: