Adding web interface
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 1m18s

This commit is contained in:
jknapp 2025-03-06 16:51:29 -08:00
parent de2957fcd2
commit 9621786175
2 changed files with 306 additions and 14 deletions

View File

@ -1,6 +1,6 @@
import sqlite3
import os
from flask import Flask, request, jsonify
from flask import Flask, request, jsonify, render_template
from pathlib import Path
import subprocess
import jinja2
@ -25,7 +25,7 @@ def init_db():
domain TEXT UNIQUE NOT NULL,
ssl_enabled BOOLEAN DEFAULT 0,
ssl_cert_path TEXT,
template_override TEXT
template_override TEXT
)
''')
@ -101,18 +101,34 @@ def is_process_running(process_name):
template_loader = jinja2.FileSystemLoader(TEMPLATE_DIR)
template_env = jinja2.Environment(loader=template_loader)
@app.route('/api/domains', methods=['GET'])
def get_domains():
try:
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('''
SELECT d.*, b.name as backend_name
FROM domains d
LEFT JOIN backends b ON d.id = b.domain_id
''')
domains = [dict(row) for row in cursor.fetchall()]
return jsonify(domains)
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@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',
@ -123,7 +139,7 @@ def health_check():
'status': 'unhealthy',
'error': str(e)
}), 500
@app.route('/api/regenerate', methods=['GET'])
def regenerate_conf():
try:
@ -155,7 +171,6 @@ def add_domain():
(backend_name, domain_id))
backend_id = cursor.lastrowid
# Add servers
for server in servers:
cursor.execute('''
INSERT INTO backend_servers
@ -169,6 +184,10 @@ def add_domain():
generate_config()
return jsonify({'status': 'success', 'domain_id': domain_id})
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/ssl', methods=['POST'])
def request_ssl():
data = request.get_json()
@ -178,7 +197,7 @@ def request_ssl():
result = subprocess.run([
'certbot', 'certonly', '-n', '--standalone',
'--preferred-challenges', 'http', '--http-01-port=8688',
'-d', domain
'-d', domain
])
if result.returncode == 0:
@ -216,14 +235,14 @@ def remove_domain():
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
@ -325,14 +344,14 @@ def generate_config():
if not servers:
print(f"No servers found for backend {domain['backend_name']}") # Debug log
continue
if domain['template_override'] is not None:
print(f"Template Override is set to: {domain['template_override']}")
template_file = domain['template_override'] + ".tpl"
backend_block = template_env.get_template(template_file).render(
name=domain['backend_name'],
servers=servers
)
else:
backend_block = template_env.get_template('hap_backend.tpl').render(
@ -345,7 +364,7 @@ def generate_config():
except Exception as e:
print(f"Error generating backend block for {domain['backend_name']}: {e}")
continue
# Add ACLS
config_parts.append('\n' .join(config_acls))
# Add LetsEncrypt Backend
@ -353,7 +372,7 @@ def generate_config():
config_parts.append(letsencrypt_backend)
# Add Backends
config_parts.append('\n' .join(config_backends) + '\n')
# Write complete configuration to tmp
# Write complete configuration to tmp
temp_config_path = "/etc/haproxy/haproxy.cfg"
config_content = '\n'.join(config_parts)

273
templates/index.html Normal file
View File

@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HAProxy Manager</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.server-list {
margin-top: 10px;
}
.server-item {
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
}
.delete-btn {
background-color: #f44336;
}
.delete-btn:hover {
background-color: #da190b;
}
.domain-list {
margin-top: 20px;
}
.ssl-btn {
background-color: #2196F3;
}
.ssl-btn:hover {
background-color: #0b7dda;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.status.success {
background-color: #dff0d8;
color: #3c763d;
}
.status.error {
background-color: #f2dede;
color: #a94442;
}
</style>
</head>
<body>
<div class="container">
<h1>HAProxy Domain Manager</h1>
<!-- Add Domain Form -->
<h2>Add New Domain</h2>
<form id="domainForm">
<div class="form-group">
<label for="domain">Domain:</label>
<input type="text" id="domain" required placeholder="example.com">
</div>
<div class="form-group">
<label for="backendName">Backend Name:</label>
<input type="text" id="backendName" required placeholder="example_backend">
</div>
<div class="form-group">
<label for="templateOverride">Template Override (optional):</label>
<input type="text" id="templateOverride" placeholder="custom_template">
</div>
<h3>Backend Servers</h3>
<div id="serverList" class="server-list"></div>
<button type="button" onclick="addServerField()">Add Server</button>
<button type="submit">Add Domain</button>
</form>
<!-- Domain List -->
<h2>Existing Domains</h2>
<div id="domainList" class="domain-list">
<!-- Domains will be listed here -->
</div>
</div>
<script>
// Add server field to the form
function addServerField() {
const serverList = document.getElementById('serverList');
const serverDiv = document.createElement('div');
serverDiv.className = 'server-item';
serverDiv.innerHTML = `
<div class="form-group">
<label>Server Name:</label>
<input type="text" class="server-name" required placeholder="server1">
</div>
<div class="form-group">
<label>Server Address:</label>
<input type="text" class="server-address" required placeholder="192.168.1.100">
</div>
<div class="form-group">
<label>Server Port:</label>
<input type="number" class="server-port" required placeholder="8080">
</div>
<div class="form-group">
<label>Server Options:</label>
<input type="text" class="server-options" placeholder="check">
</div>
<button type="button" class="delete-btn" onclick="this.parentElement.remove()">Remove Server</button>
`;
serverList.appendChild(serverDiv);
}
// Handle form submission
document.getElementById('domainForm').addEventListener('submit', async (e) => {
e.preventDefault();
const servers = Array.from(document.getElementsByClassName('server-item')).map(item => ({
name: item.querySelector('.server-name').value,
address: item.querySelector('.server-address').value,
port: parseInt(item.querySelector('.server-port').value),
options: item.querySelector('.server-options').value
}));
const data = {
domain: document.getElementById('domain').value,
backend_name: document.getElementById('backendName').value,
template_override: document.getElementById('templateOverride').value || null,
servers: servers
};
try {
const response = await fetch('/api/domain', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (response.ok) {
showStatus('Domain added successfully!', 'success');
document.getElementById('domainForm').reset();
loadDomains();
} else {
showStatus('Failed to add domain', 'error');
}
} catch (error) {
showStatus('Error: ' + error.message, 'error');
}
});
// Request SSL certificate
async function requestSSL(domain) {
try {
const response = await fetch('/api/ssl', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain })
});
if (response.ok) {
showStatus(`SSL certificate requested for ${domain}`, 'success');
loadDomains();
} else {
showStatus('Failed to request SSL certificate', 'error');
}
} catch (error) {
showStatus('Error: ' + error.message, 'error');
}
}
// Delete domain
async function deleteDomain(domain) {
if (!confirm(`Are you sure you want to delete ${domain}?`)) {
return;
}
try {
const response = await fetch('/api/domain', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain })
});
if (response.ok) {
showStatus(`Domain ${domain} deleted successfully`, 'success');
loadDomains();
} else {
showStatus('Failed to delete domain', 'error');
}
} catch (error) {
showStatus('Error: ' + error.message, 'error');
}
}
// Show status message
function showStatus(message, type) {
const statusDiv = document.createElement('div');
statusDiv.className = `status ${type}`;
statusDiv.textContent = message;
document.querySelector('.container').insertBefore(statusDiv, document.querySelector('.domain-list'));
setTimeout(() => statusDiv.remove(), 5000);
}
// Load existing domains
async function loadDomains() {
try {
const response = await fetch('/api/domains');
const domains = await response.json();
const domainList = document.getElementById('domainList');
domainList.innerHTML = domains.map(domain => `
<div class="server-item">
<h3>${domain.domain}</h3>
<p>Backend: ${domain.backend_name}</p>
<p>SSL: ${domain.ssl_enabled ? 'Enabled' : 'Disabled'}</p>
<button onclick="requestSSL('${domain.domain}')" class="ssl-btn">
${domain.ssl_enabled ? 'Renew SSL' : 'Enable SSL'}
</button>
<button onclick="deleteDomain('${domain.domain}')" class="delete-btn">Delete</button>
</div>
`).join('');
} catch (error) {
showStatus('Error loading domains: ' + error.message, 'error');
}
}
// Add initial server field and load domains
addServerField();
loadDomains();
</script>
</body>
</html>