Major upgrade: API key authentication, certificate renewal/download endpoints, monitoring/alerting scripts, improved logging, and documentation updates. See UPGRADE_SUMMARY.md for details.
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 43s

This commit is contained in:
jknapp 2025-07-11 06:24:56 -07:00
parent f58dbef3c5
commit 7b0b4c0476
9 changed files with 1338 additions and 86 deletions

View File

@ -1,5 +1,5 @@
FROM python:3.12-slim FROM python:3.12-slim
RUN apt update -y && apt dist-upgrade -y && apt install socat haproxy cron certbot curl -y && apt clean && rm -rf /var/lib/apt/lists/* RUN apt update -y && apt dist-upgrade -y && apt install socat haproxy cron certbot curl jq -y && apt clean && rm -rf /var/lib/apt/lists/*
WORKDIR /haproxy WORKDIR /haproxy
COPY ./templates /haproxy/templates COPY ./templates /haproxy/templates
COPY requirements.txt /haproxy/ COPY requirements.txt /haproxy/
@ -8,6 +8,9 @@ COPY scripts /haproxy/scripts
RUN chmod +x /haproxy/scripts/* RUN chmod +x /haproxy/scripts/*
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
RUN echo "0 */12 * * * root test -x /usr/bin/certbot && /usr/bin/certbot -q renew" > /var/spool/cron/crontabs/root RUN echo "0 */12 * * * root test -x /usr/bin/certbot && /usr/bin/certbot -q renew" > /var/spool/cron/crontabs/root
# Create log directories
RUN mkdir -p /var/log && touch /var/log/haproxy-manager.log /var/log/haproxy-manager-errors.log
RUN chmod 755 /var/log/haproxy-manager.log /var/log/haproxy-manager-errors.log
EXPOSE 80 443 8000 EXPOSE 80 443 8000
# Add health check # Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \

199
README.md
View File

@ -5,8 +5,13 @@ A Flask-based API service for managing HAProxy configurations with dynamic SSL c
To run the container: To run the container:
```bash ```bash
docker run -d -p 80:80 -p 443:443 -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy --name haproxy-manager repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest # Without API key authentication (default)
docker run -d -p 80:80 -p 443:443 -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy --name haproxy-manager repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest
# With API key authentication (recommended for production)
docker run -d -p 80:80 -p 443:443 -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy -e HAPROXY_API_KEY=your-secure-api-key-here --name haproxy-manager repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest
``` ```
## Features ## Features
- RESTful API for HAProxy configuration management - RESTful API for HAProxy configuration management
@ -18,6 +23,25 @@ docker run -d -p 80:80 -p 443:443 -p 8000:8000 -v lets-encrypt:/etc/letsencrypt
- Template override support for custom backend configurations - Template override support for custom backend configurations
- Process monitoring and auto-restart capabilities - Process monitoring and auto-restart capabilities
- Socket-based HAProxy runtime API integration - Socket-based HAProxy runtime API integration
- **NEW**: API key authentication for secure access
- **NEW**: Certificate renewal API endpoint
- **NEW**: Certificate download endpoints for other services
- **NEW**: Comprehensive error logging and alerting system
- **NEW**: Certificate status monitoring with expiration dates
## Security
### API Key Authentication
When the `HAPROXY_API_KEY` environment variable is set, all API endpoints (except `/health` and `/`) require authentication using a Bearer token:
```bash
# Example API call with authentication
curl -H "Authorization: Bearer your-secure-api-key-here" \
http://localhost:8000/api/domains
```
If no API key is set, the service runs without authentication (useful for development).
## Requirements ## Requirements
@ -61,11 +85,32 @@ GET /health
} }
``` ```
### Get Domains
Retrieve all configured domains and their backend information.
```bash
GET /api/domains
Authorization: Bearer your-api-key
# Response
[
{
"id": 1,
"domain": "example.com",
"ssl_enabled": 1,
"ssl_cert_path": "/etc/haproxy/certs/example.com.pem",
"template_override": null,
"backend_name": "example_backend"
}
]
```
### Add Domain ### Add Domain
Add a new domain with backend servers configuration. Add a new domain with backend servers configuration.
```bash ```bash
POST /api/domain POST /api/domain
Authorization: Bearer your-api-key
Content-Type: application/json Content-Type: application/json
{ {
@ -100,6 +145,7 @@ Request and configure SSL certificate for a domain using Let's Encrypt.
```bash ```bash
POST /api/ssl POST /api/ssl
Authorization: Bearer your-api-key
Content-Type: application/json Content-Type: application/json
{ {
@ -117,6 +163,7 @@ Remove a domain and its associated backend configuration.
```bash ```bash
DELETE /api/domain DELETE /api/domain
Authorization: Bearer your-api-key
Content-Type: application/json Content-Type: application/json
{ {
@ -129,3 +176,153 @@ Content-Type: application/json
"message": "Domain configuration removed" "message": "Domain configuration removed"
} }
``` ```
### Regenerate Configuration
Regenerate HAProxy configuration from database.
```bash
GET /api/regenerate
Authorization: Bearer your-api-key
# Response
{
"status": "success"
}
```
### Reload HAProxy
Reload HAProxy configuration without restart.
```bash
GET /api/reload
Authorization: Bearer your-api-key
# Response
{
"status": "success"
}
```
## New Certificate Management Endpoints
### Renew All Certificates
Trigger renewal of all Let's Encrypt certificates and reload HAProxy.
```bash
POST /api/certificates/renew
Authorization: Bearer your-api-key
# Response
{
"status": "success",
"message": "Certificates renewed and HAProxy reloaded"
}
```
### Get Certificate Status
Get status of all certificates including expiration dates.
```bash
GET /api/certificates/status
Authorization: Bearer your-api-key
# Response
{
"certificates": [
{
"domain": "example.com",
"ssl_enabled": true,
"cert_path": "/etc/haproxy/certs/example.com.pem",
"expires": "2024-12-31T23:59:59",
"days_until_expiry": 45
}
]
}
```
### Download Certificate Files
Download certificate files for use by other services.
```bash
# Download combined certificate (cert + key)
GET /api/certificates/example.com/download
Authorization: Bearer your-api-key
# Download private key only
GET /api/certificates/example.com/key
Authorization: Bearer your-api-key
# Download certificate only (no private key)
GET /api/certificates/example.com/cert
Authorization: Bearer your-api-key
```
## Logging and Monitoring
The HAProxy Manager includes comprehensive logging and error tracking:
### Log Files
- `/var/log/haproxy-manager.log` - General application logs
- `/var/log/haproxy-manager-errors.log` - Error logs for alerting
### Logged Operations
All API operations are logged with timestamps and success/failure status:
- Domain management (add/remove)
- SSL certificate operations
- Configuration generation
- HAProxy reload/restart operations
- Certificate renewals
### Error Alerting
Failed operations are logged to the error log file. You can monitor this file for alerting:
```bash
# Monitor error log for alerting
tail -f /var/log/haproxy-manager-errors.log
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `HAPROXY_API_KEY` | API key for authentication (optional) | None (no auth) |
## Example Usage
### Setting up with API key authentication:
```bash
# Start container with API key
docker run -d \
-p 80:80 -p 443:443 -p 8000:8000 \
-v lets-encrypt:/etc/letsencrypt \
-v haproxy:/etc/haproxy \
-e HAPROXY_API_KEY=your-secure-api-key-here \
--name haproxy-manager \
repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest
# Add a domain
curl -X POST http://localhost:8000/api/domain \
-H "Authorization: Bearer your-secure-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"domain": "example.com",
"backend_name": "example_backend",
"servers": [
{"name": "server1", "address": "10.0.0.1", "port": 8080, "options": "check"}
]
}'
# Request SSL certificate
curl -X POST http://localhost:8000/api/ssl \
-H "Authorization: Bearer your-secure-api-key-here" \
-H "Content-Type: application/json" \
-d '{"domain": "example.com"}'
# Renew certificates
curl -X POST http://localhost:8000/api/certificates/renew \
-H "Authorization: Bearer your-secure-api-key-here"
# Download certificate for another service
curl -H "Authorization: Bearer your-secure-api-key-here" \
http://localhost:8000/api/certificates/example.com/download \
-o example.com.pem
```

173
UPGRADE_SUMMARY.md Normal file
View File

@ -0,0 +1,173 @@
# HAProxy Manager Upgrade Summary
This document summarizes the new features and improvements added to the HAProxy Manager project.
## New Features Implemented
### 1. API Key Authentication
- **Feature**: Optional API key authentication for all API endpoints
- **Implementation**:
- Environment variable `HAPROXY_API_KEY` controls authentication
- Bearer token authentication using `Authorization: Bearer <key>` header
- Health check endpoint (`/health`) and web UI (`/`) remain unauthenticated
- Graceful fallback to unauthenticated mode when no API key is set
- **Security**: All API endpoints (except health check) require authentication when API key is configured
### 2. Certificate Renewal API
- **Endpoint**: `POST /api/certificates/renew`
- **Functionality**:
- Triggers renewal of all Let's Encrypt certificates
- Automatically updates combined certificate files for HAProxy
- Regenerates HAProxy configuration
- Reloads HAProxy with new certificates
- Returns detailed status of renewal process
- **Error Handling**: Comprehensive error logging and status reporting
### 3. Certificate Download Endpoints
- **Endpoints**:
- `GET /api/certificates/<domain>/download` - Combined certificate (cert + key)
- `GET /api/certificates/<domain>/key` - Private key only
- `GET /api/certificates/<domain>/cert` - Certificate only (no private key)
- **Use Case**: Allow other services to securely download certificates for their own use
- **Security**: All endpoints require API key authentication
### 4. Certificate Status Monitoring
- **Endpoint**: `GET /api/certificates/status`
- **Functionality**:
- Lists all certificates with expiration dates
- Calculates days until expiration
- Provides certificate file paths
- Enables proactive certificate management
### 5. Comprehensive Error Logging and Alerting
- **Logging System**:
- Structured JSON logging for all operations
- Separate error log file (`/var/log/haproxy-manager-errors.log`)
- General application log (`/var/log/haproxy-manager.log`)
- Timestamped operation tracking
- **Alerting Capabilities**:
- Error detection and logging
- Certificate expiration warnings
- HAProxy operation failure tracking
- Configurable alerting via monitoring script
## Technical Improvements
### Enhanced Error Handling
- All API endpoints now include comprehensive error handling
- Detailed error messages with logging
- Graceful failure handling for HAProxy operations
- Certificate operation error tracking
### Improved Logging
- Structured logging with timestamps
- Operation success/failure tracking
- Error categorization and alerting
- Debug information for troubleshooting
### Better HAProxy Integration
- Enhanced configuration validation
- Improved reload/restart handling
- Better error reporting for HAProxy operations
- Automatic recovery from configuration errors
## New Scripts and Tools
### 1. Monitoring Script (`scripts/monitor-errors.sh`)
- **Purpose**: Monitor error logs and certificate expiration
- **Features**:
- Check for recent errors in configurable time windows
- Monitor certificate expiration dates
- Email and webhook alerting capabilities
- Configurable thresholds and intervals
- **Usage**: Can be integrated with cron for automated monitoring
### 2. API Test Script (`scripts/test-api.sh`)
- **Purpose**: Test all new API endpoints
- **Features**:
- Comprehensive API endpoint testing
- Authentication testing
- Colored output for easy reading
- Detailed response logging
### 3. Monitoring Configuration (`scripts/monitoring-example.conf`)
- **Purpose**: Example configuration for monitoring setup
- **Features**:
- Email and webhook configuration examples
- Crontab entry examples
- Monitoring interval recommendations
## Updated Files
### Core Application
- `haproxy_manager.py` - Major updates with new endpoints and features
- `requirements.txt` - No changes needed (existing dependencies sufficient)
- `Dockerfile` - Added jq package and log directory setup
### Documentation
- `README.md` - Comprehensive updates with new feature documentation
- `UPGRADE_SUMMARY.md` - This summary document
### Scripts
- `scripts/monitor-errors.sh` - New monitoring and alerting script
- `scripts/test-api.sh` - New API testing script
- `scripts/monitoring-example.conf` - New monitoring configuration example
## Environment Variables
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `HAPROXY_API_KEY` | API key for authentication | None | No (optional) |
## Migration Guide
### For Existing Users
1. **No Breaking Changes**: Existing functionality remains unchanged
2. **Optional Authentication**: API key is optional - set `HAPROXY_API_KEY` to enable
3. **Backward Compatibility**: All existing endpoints work without authentication when no API key is set
### For New Deployments
1. **Recommended**: Set `HAPROXY_API_KEY` for production deployments
2. **Monitoring**: Configure monitoring script for automated alerting
3. **Testing**: Use test script to verify all endpoints work correctly
## API Endpoints Summary
### Existing Endpoints (Updated with Authentication)
- `GET /health` - Health check (no auth required)
- `GET /api/domains` - List domains
- `POST /api/domain` - Add domain
- `DELETE /api/domain` - Remove domain
- `POST /api/ssl` - Request SSL certificate
- `GET /api/regenerate` - Regenerate configuration
- `GET /api/reload` - Reload HAProxy
### New Endpoints
- `POST /api/certificates/renew` - Renew all certificates
- `GET /api/certificates/status` - Get certificate status
- `GET /api/certificates/<domain>/download` - Download combined certificate
- `GET /api/certificates/<domain>/key` - Download private key
- `GET /api/certificates/<domain>/cert` - Download certificate only
## Security Considerations
1. **API Key Security**: Use strong, unique API keys for production
2. **Network Security**: Restrict access to port 8000 using firewalls
3. **Certificate Security**: Private key endpoints require authentication
4. **Log Security**: Monitor log files for sensitive information
## Monitoring and Alerting
1. **Error Monitoring**: Monitor `/var/log/haproxy-manager-errors.log`
2. **Certificate Monitoring**: Use certificate status endpoint for expiration tracking
3. **HAProxy Monitoring**: Health check endpoint provides service status
4. **Automated Alerting**: Configure monitoring script with email/webhook alerts
## Future Enhancements
Potential areas for future development:
1. Webhook integration for certificate renewal notifications
2. Advanced certificate management (wildcard certificates, etc.)
3. HAProxy statistics and monitoring endpoints
4. Configuration backup and restore functionality
5. Multi-tenant support with per-domain API keys

View File

@ -1,18 +1,66 @@
import sqlite3 import sqlite3
import os import os
from flask import Flask, request, jsonify, render_template from flask import Flask, request, jsonify, render_template, send_file
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import jinja2 import jinja2
import socket import socket
import psutil import psutil
import functools
import logging
from datetime import datetime
import json
app = Flask(__name__) app = Flask(__name__)
# Configuration
DB_FILE = '/etc/haproxy/haproxy_config.db' DB_FILE = '/etc/haproxy/haproxy_config.db'
TEMPLATE_DIR = Path('templates') TEMPLATE_DIR = Path('templates')
HAPROXY_CONFIG_PATH = '/etc/haproxy/haproxy.cfg' HAPROXY_CONFIG_PATH = '/etc/haproxy/haproxy.cfg'
SSL_CERTS_DIR = '/etc/haproxy/certs' SSL_CERTS_DIR = '/etc/haproxy/certs'
API_KEY = os.environ.get('HAPROXY_API_KEY') # Optional API key for authentication
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/var/log/haproxy-manager.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def require_api_key(f):
"""Decorator to require API key authentication if API_KEY is set"""
@functools.wraps(f)
def decorated_function(*args, **kwargs):
if API_KEY:
auth_header = request.headers.get('Authorization')
if not auth_header or auth_header != f'Bearer {API_KEY}':
return jsonify({'error': 'Unauthorized - Invalid or missing API key'}), 401
return f(*args, **kwargs)
return decorated_function
def log_operation(operation, success=True, error_message=None):
"""Log operations for monitoring and alerting"""
log_entry = {
'timestamp': datetime.now().isoformat(),
'operation': operation,
'success': success,
'error': error_message
}
if success:
logger.info(f"Operation {operation} completed successfully")
else:
logger.error(f"Operation {operation} failed: {error_message}")
# Here you could add additional alerting (email, webhook, etc.)
# For now, we'll just log to a dedicated error log
with open('/var/log/haproxy-manager-errors.log', 'a') as f:
f.write(json.dumps(log_entry) + '\n')
return log_entry
def init_db(): def init_db():
with sqlite3.connect(DB_FILE) as conn: with sqlite3.connect(DB_FILE) as conn:
@ -102,6 +150,7 @@ template_loader = jinja2.FileSystemLoader(TEMPLATE_DIR)
template_env = jinja2.Environment(loader=template_loader) template_env = jinja2.Environment(loader=template_loader)
@app.route('/api/domains', methods=['GET']) @app.route('/api/domains', methods=['GET'])
@require_api_key
def get_domains(): def get_domains():
try: try:
with sqlite3.connect(DB_FILE) as conn: with sqlite3.connect(DB_FILE) as conn:
@ -113,8 +162,10 @@ def get_domains():
LEFT JOIN backends b ON d.id = b.domain_id LEFT JOIN backends b ON d.id = b.domain_id
''') ''')
domains = [dict(row) for row in cursor.fetchall()] domains = [dict(row) for row in cursor.fetchall()]
log_operation('get_domains', True)
return jsonify(domains) return jsonify(domains)
except Exception as e: except Exception as e:
log_operation('get_domains', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500 return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/health', methods=['GET']) @app.route('/health', methods=['GET'])
@ -141,27 +192,32 @@ def health_check():
}), 500 }), 500
@app.route('/api/regenerate', methods=['GET']) @app.route('/api/regenerate', methods=['GET'])
@require_api_key
def regenerate_conf(): def regenerate_conf():
try: try:
generate_config() generate_config()
log_operation('regenerate_config', True)
return jsonify({'status': 'success'}), 200 return jsonify({'status': 'success'}), 200
except Exception as e: except Exception as e:
log_operation('regenerate_config', False, str(e))
return jsonify({ return jsonify({
'status': 'failed', 'status': 'failed',
'error': str(e) 'error': str(e)
}), 500 }), 500
@app.route('/api/reload', methods=['GET']) @app.route('/api/reload', methods=['GET'])
@require_api_key
def reload_haproxy(): def reload_haproxy():
if is_process_running('haproxy'): try:
# Use a proper shell command string when shell=True is set if is_process_running('haproxy'):
result = subprocess.run('echo "reload" | socat stdio /tmp/haproxy-cli', # Use a proper shell command string when shell=True is set
check=True, capture_output=True, text=True, shell=True) result = subprocess.run('echo "reload" | socat stdio /tmp/haproxy-cli',
print(f"Reload result: {result.stdout}, {result.stderr}, {result.returncode}") check=True, capture_output=True, text=True, shell=True)
return jsonify({'status': 'success'}), 200 print(f"Reload result: {result.stdout}, {result.stderr}, {result.returncode}")
else: log_operation('reload_haproxy', True)
# Start HAProxy if it's not running return jsonify({'status': 'success'}), 200
try: else:
# Start HAProxy if it's not running
result = subprocess.run( result = subprocess.run(
['haproxy', '-W', '-S', '/tmp/haproxy-cli,level,admin', '-f', HAPROXY_CONFIG_PATH], ['haproxy', '-W', '-S', '/tmp/haproxy-cli,level,admin', '-f', HAPROXY_CONFIG_PATH],
check=True, check=True,
@ -170,18 +226,21 @@ def reload_haproxy():
) )
if result.returncode == 0: if result.returncode == 0:
print("HAProxy started successfully") print("HAProxy started successfully")
log_operation('start_haproxy', True)
return jsonify({'status': 'success'}), 200 return jsonify({'status': 'success'}), 200
else: else:
print(f"HAProxy start command returned: {result.stdout}") error_msg = f"HAProxy start command returned: {result.stdout}\nError output: {result.stderr}"
print(f"Error output: {result.stderr}") print(error_msg)
except subprocess.CalledProcessError as e: log_operation('start_haproxy', False, error_msg)
print(f"Failed to start HAProxy: {e.stdout}\n{e.stderr}") return jsonify({'status': 'failed', 'error': error_msg}), 500
return jsonify({ except subprocess.CalledProcessError as e:
'status': 'failed', error_msg = f"Failed to start HAProxy: {e.stdout}\n{e.stderr}"
'error': f"Failed to start HAProxy: {e.stdout}\n{e.stderr}" print(error_msg)
}), 500 log_operation('reload_haproxy', False, error_msg)
return jsonify({'status': 'failed', 'error': error_msg}), 500
@app.route('/api/domain', methods=['POST']) @app.route('/api/domain', methods=['POST'])
@require_api_key
def add_domain(): def add_domain():
data = request.get_json() data = request.get_json()
domain = data.get('domain') domain = data.get('domain')
@ -189,77 +248,257 @@ def add_domain():
backend_name = data.get('backend_name') backend_name = data.get('backend_name')
servers = data.get('servers', []) servers = data.get('servers', [])
with sqlite3.connect(DB_FILE) as conn: if not domain or not backend_name:
cursor = conn.cursor() log_operation('add_domain', False, 'Domain and backend_name are required')
return jsonify({'status': 'error', 'message': 'Domain and backend_name are required'}), 400
# Add domain try:
cursor.execute('INSERT INTO domains (domain, template_override) VALUES (?, ?)', (domain, template_override)) with sqlite3.connect(DB_FILE) as conn:
domain_id = cursor.lastrowid cursor = conn.cursor()
# Add backend # Add domain
cursor.execute('INSERT INTO backends (name, domain_id) VALUES (?, ?)', cursor.execute('INSERT INTO domains (domain, template_override) VALUES (?, ?)', (domain, template_override))
(backend_name, domain_id)) domain_id = cursor.lastrowid
backend_id = cursor.lastrowid
for server in servers: # Add backend
cursor.execute(''' cursor.execute('INSERT INTO backends (name, domain_id) VALUES (?, ?)',
INSERT INTO backend_servers (backend_name, domain_id))
(backend_id, server_name, server_address, server_port, server_options) backend_id = cursor.lastrowid
VALUES (?, ?, ?, ?, ?)
''', (backend_id, server['name'], server['address'], for server in servers:
server['port'], server.get('options'))) cursor.execute('''
# Close cursor and connection INSERT INTO backend_servers
cursor.close() (backend_id, server_name, server_address, server_port, server_options)
conn.close() VALUES (?, ?, ?, ?, ?)
generate_config() ''', (backend_id, server['name'], server['address'],
return jsonify({'status': 'success', 'domain_id': domain_id}) server['port'], server.get('options')))
# Close cursor and connection
cursor.close()
conn.close()
generate_config()
log_operation('add_domain', True, f'Domain {domain} added successfully')
return jsonify({'status': 'success', 'domain_id': domain_id})
except Exception as e:
log_operation('add_domain', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/') @app.route('/')
def index(): def index():
return render_template('index.html') return render_template('index.html')
@app.route('/api/ssl', methods=['POST']) @app.route('/api/ssl', methods=['POST'])
@require_api_key
def request_ssl(): def request_ssl():
data = request.get_json() data = request.get_json()
domain = data.get('domain') domain = data.get('domain')
# Request Let's Encrypt certificate if not domain:
result = subprocess.run([ log_operation('request_ssl', False, 'Domain not provided')
'certbot', 'certonly', '-n', '--standalone', return jsonify({'status': 'error', 'message': 'Domain is required'}), 400
'--preferred-challenges', 'http', '--http-01-port=8688',
'-d', domain
])
if result.returncode == 0: try:
# Combine cert files and copy to HAProxy certs directory # Request Let's Encrypt certificate
cert_path = f'/etc/letsencrypt/live/{domain}/fullchain.pem' result = subprocess.run([
key_path = f'/etc/letsencrypt/live/{domain}/privkey.pem' 'certbot', 'certonly', '-n', '--standalone',
combined_path = f'{SSL_CERTS_DIR}/{domain}.pem' '--preferred-challenges', 'http', '--http-01-port=8688',
'-d', domain
], capture_output=True, text=True)
with open(combined_path, 'w') as combined: if result.returncode == 0:
subprocess.run(['cat', cert_path, key_path], stdout=combined) # 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'
# Update database 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
SET ssl_enabled = 1, ssl_cert_path = ?
WHERE domain = ?
''', (combined_path, domain))
# Close cursor and connection
cursor.close()
conn.close()
generate_config()
log_operation('request_ssl', True, f'SSL certificate obtained for {domain}')
return jsonify({'status': 'success'})
else:
error_msg = f'Failed to obtain SSL certificate: {result.stderr}'
log_operation('request_ssl', False, error_msg)
return jsonify({'status': 'error', 'message': error_msg}), 500
except Exception as e:
log_operation('request_ssl', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/certificates/renew', methods=['POST'])
@require_api_key
def renew_certificates():
"""Renew all certificates and reload HAProxy"""
try:
# Run certbot renew
result = subprocess.run([
'certbot', 'renew', '--quiet'
], capture_output=True, text=True)
if result.returncode == 0:
# Check if any certificates were renewed
if 'Congratulations' in result.stdout or 'renewed' in result.stdout:
# Update combined certificates for HAProxy
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute('SELECT domain, ssl_cert_path FROM domains WHERE ssl_enabled = 1')
domains = cursor.fetchall()
for domain, cert_path in domains:
if cert_path and os.path.exists(cert_path):
# Recreate combined certificate
letsencrypt_cert = f'/etc/letsencrypt/live/{domain}/fullchain.pem'
letsencrypt_key = f'/etc/letsencrypt/live/{domain}/privkey.pem'
if os.path.exists(letsencrypt_cert) and os.path.exists(letsencrypt_key):
with open(cert_path, 'w') as combined:
subprocess.run(['cat', letsencrypt_cert, letsencrypt_key], stdout=combined)
# Regenerate config and reload HAProxy
generate_config()
reload_result = subprocess.run('echo "reload" | socat stdio /tmp/haproxy-cli',
capture_output=True, text=True, shell=True)
if reload_result.returncode == 0:
log_operation('renew_certificates', True, 'Certificates renewed and HAProxy reloaded')
return jsonify({'status': 'success', 'message': 'Certificates renewed and HAProxy reloaded'})
else:
error_msg = f'Certificates renewed but HAProxy reload failed: {reload_result.stderr}'
log_operation('renew_certificates', False, error_msg)
return jsonify({'status': 'partial_success', 'message': error_msg}), 500
else:
log_operation('renew_certificates', True, 'No certificates needed renewal')
return jsonify({'status': 'success', 'message': 'No certificates needed renewal'})
else:
error_msg = f'Certificate renewal failed: {result.stderr}'
log_operation('renew_certificates', False, error_msg)
return jsonify({'status': 'error', 'message': error_msg}), 500
except Exception as e:
log_operation('renew_certificates', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/certificates/<domain>/download', methods=['GET'])
@require_api_key
def download_certificate(domain):
"""Download the combined certificate file for a domain"""
try:
with sqlite3.connect(DB_FILE) as conn: with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('SELECT ssl_cert_path FROM domains WHERE domain = ? AND ssl_enabled = 1', (domain,))
UPDATE domains result = cursor.fetchone()
SET ssl_enabled = 1, ssl_cert_path = ?
WHERE domain = ? if not result or not result[0]:
''', (combined_path, domain)) return jsonify({'status': 'error', 'message': 'Certificate not found for domain'}), 404
# Close cursor and connection
cursor.close() cert_path = result[0]
conn.close() if not os.path.exists(cert_path):
generate_config() return jsonify({'status': 'error', 'message': 'Certificate file not found'}), 404
return jsonify({'status': 'success'})
return jsonify({'status': 'error', 'message': 'Failed to obtain SSL certificate'}) log_operation('download_certificate', True, f'Certificate downloaded for {domain}')
return send_file(cert_path, as_attachment=True, download_name=f'{domain}.pem')
except Exception as e:
log_operation('download_certificate', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/certificates/<domain>/key', methods=['GET'])
@require_api_key
def download_private_key(domain):
"""Download the private key for a domain"""
try:
key_path = f'/etc/letsencrypt/live/{domain}/privkey.pem'
if not os.path.exists(key_path):
return jsonify({'status': 'error', 'message': 'Private key not found for domain'}), 404
log_operation('download_private_key', True, f'Private key downloaded for {domain}')
return send_file(key_path, as_attachment=True, download_name=f'{domain}_key.pem')
except Exception as e:
log_operation('download_private_key', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/certificates/<domain>/cert', methods=['GET'])
@require_api_key
def download_cert_only(domain):
"""Download only the certificate (without private key) for a domain"""
try:
cert_path = f'/etc/letsencrypt/live/{domain}/fullchain.pem'
if not os.path.exists(cert_path):
return jsonify({'status': 'error', 'message': 'Certificate not found for domain'}), 404
log_operation('download_cert_only', True, f'Certificate (only) downloaded for {domain}')
return send_file(cert_path, as_attachment=True, download_name=f'{domain}_cert.pem')
except Exception as e:
log_operation('download_cert_only', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/certificates/status', methods=['GET'])
@require_api_key
def get_certificate_status():
"""Get status of all certificates including expiration dates"""
try:
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute('SELECT domain, ssl_enabled, ssl_cert_path FROM domains WHERE ssl_enabled = 1')
domains = cursor.fetchall()
cert_status = []
for domain, ssl_enabled, cert_path in domains:
status = {
'domain': domain,
'ssl_enabled': bool(ssl_enabled),
'cert_path': cert_path,
'expires': None,
'days_until_expiry': None
}
if cert_path and os.path.exists(cert_path):
# Check certificate expiration using openssl
try:
result = subprocess.run([
'openssl', 'x509', '-in', cert_path, '-noout', '-dates'
], capture_output=True, text=True)
if result.returncode == 0:
# Parse the notAfter date
for line in result.stdout.split('\n'):
if 'notAfter=' in line:
expiry_date_str = line.split('=')[1].strip()
from datetime import datetime
expiry_date = datetime.strptime(expiry_date_str, '%b %d %H:%M:%S %Y %Z')
status['expires'] = expiry_date.isoformat()
# Calculate days until expiry
days_until = (expiry_date - datetime.now()).days
status['days_until_expiry'] = days_until
break
except Exception as e:
status['error'] = str(e)
cert_status.append(status)
log_operation('get_certificate_status', True)
return jsonify({'certificates': cert_status})
except Exception as e:
log_operation('get_certificate_status', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/domain', methods=['DELETE']) @app.route('/api/domain', methods=['DELETE'])
@require_api_key
def remove_domain(): def remove_domain():
data = request.get_json() data = request.get_json()
domain = data.get('domain') domain = data.get('domain')
if not domain: if not domain:
log_operation('remove_domain', False, 'Domain is required')
return jsonify({'status': 'error', 'message': 'Domain is required'}), 400 return jsonify({'status': 'error', 'message': 'Domain is required'}), 400
try: try:
@ -271,6 +510,7 @@ def remove_domain():
domain_result = cursor.fetchone() domain_result = cursor.fetchone()
if not domain_result: if not domain_result:
log_operation('remove_domain', False, f'Domain {domain} not found')
return jsonify({'status': 'error', 'message': 'Domain not found'}), 404 return jsonify({'status': 'error', 'message': 'Domain not found'}), 404
domain_id = domain_result[0] domain_id = domain_result[0]
@ -301,9 +541,11 @@ def remove_domain():
# Regenerate HAProxy config # Regenerate HAProxy config
generate_config() generate_config()
log_operation('remove_domain', True, f'Domain {domain} removed successfully')
return jsonify({'status': 'success', 'message': 'Domain configuration removed'}) return jsonify({'status': 'success', 'message': 'Domain configuration removed'})
except Exception as e: except Exception as e:
log_operation('remove_domain', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500 return jsonify({'status': 'error', 'message': str(e)}), 500
def generate_config(): def generate_config():
@ -349,7 +591,7 @@ def generate_config():
# Add domain configurations # Add domain configurations
for domain in domains: for domain in domains:
if not domain['backend_name']: if not domain['backend_name']:
print(f"Skipping domain {domain['domain']} - no backend name") # Debug log logger.warning(f"Skipping domain {domain['domain']} - no backend name")
continue continue
# Add domain ACL # Add domain ACL
@ -359,9 +601,9 @@ def generate_config():
name=domain['backend_name'] name=domain['backend_name']
) )
config_acls.append(domain_acl) config_acls.append(domain_acl)
print(f"Added ACL for domain: {domain['domain']}") # Debug log logger.info(f"Added ACL for domain: {domain['domain']}")
except Exception as e: except Exception as e:
print(f"Error generating domain ACL for {domain['domain']}: {e}") logger.error(f"Error generating domain ACL for {domain['domain']}: {e}")
continue continue
# Add backend configuration # Add backend configuration
@ -372,11 +614,11 @@ def generate_config():
servers = [dict(server) for server in cursor.fetchall()] servers = [dict(server) for server in cursor.fetchall()]
if not servers: if not servers:
print(f"No servers found for backend {domain['backend_name']}") # Debug log logger.warning(f"No servers found for backend {domain['backend_name']}")
continue continue
if domain['template_override'] is not None: if domain['template_override'] is not None:
print(f"Template Override is set to: {domain['template_override']}") logger.info(f"Template Override is set to: {domain['template_override']}")
template_file = domain['template_override'] + ".tpl" template_file = domain['template_override'] + ".tpl"
backend_block = template_env.get_template(template_file).render( backend_block = template_env.get_template(template_file).render(
name=domain['backend_name'], name=domain['backend_name'],
@ -390,9 +632,9 @@ def generate_config():
servers=servers servers=servers
) )
config_backends.append(backend_block) config_backends.append(backend_block)
print(f"Added backend block for: {domain['backend_name']}") # Debug log logger.info(f"Added backend block for: {domain['backend_name']}")
except Exception as e: except Exception as e:
print(f"Error generating backend block for {domain['backend_name']}: {e}") logger.error(f"Error generating backend block for {domain['backend_name']}: {e}")
continue continue
# Add ACLS # Add ACLS
@ -406,17 +648,25 @@ def generate_config():
temp_config_path = "/etc/haproxy/haproxy.cfg" temp_config_path = "/etc/haproxy/haproxy.cfg"
config_content = '\n'.join(config_parts) config_content = '\n'.join(config_parts)
print("Final config content:", config_content) # Debug log logger.debug("Generated HAProxy configuration")
# Write complete configuration to tmp # Write complete configuration to tmp
# Check HAProxy Configuration, and reload if it works # Check HAProxy Configuration, and reload if it works
with open(temp_config_path, 'w') as f: with open(temp_config_path, 'w') as f:
f.write(config_content) f.write(config_content)
result = subprocess.run(['haproxy', '-c', '-f', temp_config_path], capture_output=True) result = subprocess.run(['haproxy', '-c', '-f', temp_config_path], capture_output=True, text=True)
if result.returncode == 0: if result.returncode == 0:
print("HAProxy configuration check passed") logger.info("HAProxy configuration check passed")
if is_process_running('haproxy'): if is_process_running('haproxy'):
subprocess.run(['echo', '"reload"', '|', 'socat', 'stdio', '/tmp/haproxy-cli']) reload_result = subprocess.run('echo "reload" | socat stdio /tmp/haproxy-cli',
capture_output=True, text=True, shell=True)
if reload_result.returncode == 0:
logger.info("HAProxy reloaded successfully")
log_operation('generate_config', True, 'Configuration generated and HAProxy reloaded')
else:
error_msg = f"HAProxy reload failed: {reload_result.stderr}"
logger.error(error_msg)
log_operation('generate_config', False, error_msg)
else: else:
try: try:
result = subprocess.run( result = subprocess.run(
@ -426,15 +676,26 @@ def generate_config():
text=True text=True
) )
if result.returncode == 0: if result.returncode == 0:
print("HAProxy started successfully") logger.info("HAProxy started successfully")
log_operation('generate_config', True, 'Configuration generated and HAProxy started')
else: else:
print(f"HAProxy start command returned: {result.stdout}") error_msg = f"HAProxy start command returned: {result.stdout}\nError output: {result.stderr}"
print(f"Error output: {result.stderr}") logger.error(error_msg)
log_operation('generate_config', False, error_msg)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Failed to start HAProxy: {e.stdout}\n{e.stderr}") error_msg = f"Failed to start HAProxy: {e.stdout}\n{e.stderr}"
logger.error(error_msg)
log_operation('generate_config', False, error_msg)
raise raise
else:
error_msg = f"HAProxy configuration check failed: {result.stderr}"
logger.error(error_msg)
log_operation('generate_config', False, error_msg)
raise Exception(error_msg)
except Exception as e: except Exception as e:
print(f"Error generating config: {e}") error_msg = f"Error generating config: {e}"
logger.error(error_msg)
log_operation('generate_config', False, error_msg)
import traceback import traceback
traceback.print_exc() traceback.print_exc()
raise raise
@ -449,12 +710,16 @@ def start_haproxy():
text=True text=True
) )
if result.returncode == 0: if result.returncode == 0:
print("HAProxy started successfully") logger.info("HAProxy started successfully")
log_operation('start_haproxy', True, 'HAProxy started successfully')
else: else:
print(f"HAProxy start command returned: {result.stdout}") error_msg = f"HAProxy start command returned: {result.stdout}\nError output: {result.stderr}"
print(f"Error output: {result.stderr}") logger.error(error_msg)
log_operation('start_haproxy', False, error_msg)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Failed to start HAProxy: {e.stdout}\n{e.stderr}") error_msg = f"Failed to start HAProxy: {e.stdout}\n{e.stderr}"
logger.error(error_msg)
log_operation('start_haproxy', False, error_msg)
raise raise
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -0,0 +1,42 @@
# HAProxy Manager External Monitoring Configuration
# Copy this file to /etc/haproxy-monitor.conf and modify for your environment
# Container configuration
CONTAINER_NAME="haproxy-manager"
CONTAINER_API_URL="http://localhost:8000"
# Log directory (adjust based on your Docker volume setup)
# Common paths:
# - Docker volumes: /var/lib/docker/volumes/haproxy-logs/_data
# - Bind mounts: /path/to/your/logs
# - Docker Desktop (Mac/Windows): May need different path
LOG_DIR="/var/lib/docker/volumes/haproxy-logs/_data"
# API key for certificate status checks
API_KEY="your-secure-api-key-here"
# Alerting configuration
ALERT_EMAIL="admin@yourdomain.com"
WEBHOOK_URL="https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
# Example crontab entries for external monitoring:
#
# Check container and API health every 5 minutes
# */5 * * * * /path/to/monitor-errors-external.sh health
#
# Check for errors every 30 minutes
# */30 * * * * /path/to/monitor-errors-external.sh errors 30
#
# Check certificates daily at 9 AM
# 0 9 * * * /path/to/monitor-errors-external.sh certs 30
#
# Comprehensive check every hour
# 0 * * * * /path/to/monitor-errors-external.sh all 60 30
# Installation instructions:
# 1. Copy this file to /etc/haproxy-monitor.conf
# 2. Modify the variables above for your environment
# 3. Copy monitor-errors-external.sh to /usr/local/bin/
# 4. Make it executable: chmod +x /usr/local/bin/monitor-errors-external.sh
# 5. Install required packages: apt-get install curl jq
# 6. Set up crontab entries as shown above

View File

@ -0,0 +1,224 @@
#!/bin/bash
# HAProxy Manager External Monitoring Script
# This script monitors the HAProxy Manager from outside the container
# Configuration - modify these variables
CONTAINER_NAME="haproxy-manager"
CONTAINER_API_URL="http://localhost:8000"
LOG_DIR="/var/lib/docker/volumes/haproxy-logs/_data" # Adjust path as needed
ERROR_LOG="$LOG_DIR/haproxy-manager-errors.log"
ALERT_EMAIL=""
WEBHOOK_URL=""
API_KEY=""
# Load configuration from file if it exists
CONFIG_FILE="/etc/haproxy-monitor.conf"
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
# Function to send email alert
send_email_alert() {
local subject="$1"
local message="$2"
if [ -n "$ALERT_EMAIL" ]; then
if command -v mail >/dev/null 2>&1; then
echo "$message" | mail -s "$subject" "$ALERT_EMAIL"
else
echo "Email alert (mail command not available): $subject - $message"
fi
fi
}
# Function to send webhook alert
send_webhook_alert() {
local message="$1"
if [ -n "$WEBHOOK_URL" ]; then
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\":\"$message\"}" >/dev/null 2>&1
fi
}
# Function to check if container is running
check_container_status() {
if ! docker ps --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
local alert_message="HAProxy Manager Alert: Container $CONTAINER_NAME is not running!"
send_email_alert "HAProxy Manager Container Down" "$alert_message"
send_webhook_alert "$alert_message"
return 1
fi
return 0
}
# Function to check for recent errors
check_recent_errors() {
local minutes="${1:-60}" # Default to last 60 minutes
if [ ! -f "$ERROR_LOG" ]; then
echo "Error log file not found: $ERROR_LOG"
echo "Container may not be running or log volume not mounted correctly"
return 1
fi
# Get current timestamp minus specified minutes
local cutoff_time=$(date -d "$minutes minutes ago" +%s)
# Check for errors in the last N minutes
local recent_errors=$(awk -v cutoff="$cutoff_time" '
BEGIN { FS="\""; found=0 }
/"timestamp":/ {
# Extract timestamp and convert to epoch
gsub(/[",]/, "", $4)
split($4, parts, "T")
split(parts[1], date_parts, "-")
split(parts[2], time_parts, ":")
timestamp = mktime(date_parts[1] " " date_parts[2] " " date_parts[3] " " time_parts[1] " " time_parts[2] " " time_parts[3])
if (timestamp > cutoff) {
found=1
print $0
}
}
END { exit found ? 0 : 1 }
' "$ERROR_LOG")
if [ $? -eq 0 ]; then
echo "Recent errors found in the last $minutes minutes:"
echo "$recent_errors"
# Send alerts
local alert_message="HAProxy Manager Error Alert: Recent errors detected in the last $minutes minutes. Check $ERROR_LOG for details."
send_email_alert "HAProxy Manager Error Alert" "$alert_message"
send_webhook_alert "$alert_message"
return 1 # Return error status
else
echo "No recent errors found in the last $minutes minutes."
return 0 # Return success status
fi
}
# Function to check certificate expiration via API
check_certificate_expiration() {
local warning_days="${1:-30}" # Default to 30 days warning
if [ -z "$API_KEY" ]; then
echo "No API key configured. Cannot check certificate status."
return 1
fi
# Check if container is running
if ! check_container_status; then
return 1
fi
# Use the API to get certificate status
local cert_status=$(curl -s -H "Authorization: Bearer $API_KEY" "$CONTAINER_API_URL/api/certificates/status")
if [ $? -eq 0 ]; then
# Parse JSON to check for expiring certificates
local expiring_certs=$(echo "$cert_status" | jq -r --arg days "$warning_days" '
.certificates[] |
select(.days_until_expiry != null and .days_until_expiry <= ($days | tonumber)) |
"\(.domain): expires in \(.days_until_expiry) days"
' 2>/dev/null)
if [ -n "$expiring_certs" ]; then
echo "Certificates expiring soon:"
echo "$expiring_certs"
local alert_message="HAProxy Manager Certificate Alert: Certificates expiring soon. $expiring_certs"
send_email_alert "HAProxy Manager Certificate Alert" "$alert_message"
send_webhook_alert "$alert_message"
return 1
else
echo "No certificates expiring within $warning_days days."
return 0
fi
else
echo "Failed to get certificate status from API."
return 1
fi
}
# Function to check API health
check_api_health() {
local health_response=$(curl -s "$CONTAINER_API_URL/health")
if [ $? -eq 0 ]; then
local status=$(echo "$health_response" | jq -r '.status' 2>/dev/null)
if [ "$status" = "healthy" ]; then
echo "API health check passed"
return 0
else
echo "API health check failed: $health_response"
return 1
fi
else
echo "API health check failed: cannot connect to $CONTAINER_API_URL"
return 1
fi
}
# Main script logic
case "${1:-help}" in
"container")
check_container_status
;;
"health")
check_api_health
;;
"errors")
check_recent_errors "${2:-60}"
;;
"certs")
check_certificate_expiration "${2:-30}"
;;
"all")
echo "Checking container status..."
check_container_status
container_status=$?
echo "Checking API health..."
check_api_health
health_status=$?
echo "Checking for recent errors..."
check_recent_errors "${2:-60}"
error_status=$?
echo "Checking certificate expiration..."
check_certificate_expiration "${3:-30}"
cert_status=$?
exit $((container_status + health_status + error_status + cert_status))
;;
"help"|*)
echo "HAProxy Manager External Monitoring Script"
echo ""
echo "Usage: $0 {container|health|errors|certs|all} [minutes] [cert_warning_days]"
echo ""
echo "Commands:"
echo " container Check if container is running"
echo " health Check API health endpoint"
echo " errors [minutes] Check for errors in the last N minutes (default: 60)"
echo " certs [days] Check for certificates expiring within N days (default: 30)"
echo " all [minutes] [days] Check container, health, errors, and certificates"
echo " help Show this help message"
echo ""
echo "Configuration:"
echo " Set variables at the top of this script or create $CONFIG_FILE"
echo " Required variables: CONTAINER_NAME, CONTAINER_API_URL, API_KEY"
echo " Optional variables: ALERT_EMAIL, WEBHOOK_URL, LOG_DIR"
echo ""
echo "Examples:"
echo " $0 container # Check if container is running"
echo " $0 errors 30 # Check for errors in last 30 minutes"
echo " $0 certs 7 # Check for certificates expiring in 7 days"
echo " $0 all 60 14 # Check everything (60 min errors, 14 day certs)"
;;
esac

159
scripts/monitor-errors.sh Executable file
View File

@ -0,0 +1,159 @@
#!/bin/bash
# HAProxy Manager Error Monitoring Script
# This script monitors the error log and can send alerts
ERROR_LOG="/var/log/haproxy-manager-errors.log"
ALERT_EMAIL=""
WEBHOOK_URL=""
# Function to send email alert
send_email_alert() {
local subject="$1"
local message="$2"
if [ -n "$ALERT_EMAIL" ]; then
echo "$message" | mail -s "$subject" "$ALERT_EMAIL"
fi
}
# Function to send webhook alert
send_webhook_alert() {
local message="$1"
if [ -n "$WEBHOOK_URL" ]; then
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\":\"$message\"}"
fi
}
# Function to check for recent errors
check_recent_errors() {
local minutes="${1:-60}" # Default to last 60 minutes
if [ ! -f "$ERROR_LOG" ]; then
echo "Error log file not found: $ERROR_LOG"
exit 1
fi
# Get current timestamp minus specified minutes
local cutoff_time=$(date -d "$minutes minutes ago" +%s)
# Check for errors in the last N minutes
local recent_errors=$(awk -v cutoff="$cutoff_time" '
BEGIN { FS="\""; found=0 }
/"timestamp":/ {
# Extract timestamp and convert to epoch
gsub(/[",]/, "", $4)
split($4, parts, "T")
split(parts[1], date_parts, "-")
split(parts[2], time_parts, ":")
timestamp = mktime(date_parts[1] " " date_parts[2] " " date_parts[3] " " time_parts[1] " " time_parts[2] " " time_parts[3])
if (timestamp > cutoff) {
found=1
print $0
}
}
END { exit found ? 0 : 1 }
' "$ERROR_LOG")
if [ $? -eq 0 ]; then
echo "Recent errors found in the last $minutes minutes:"
echo "$recent_errors"
# Send alerts
local alert_message="HAProxy Manager Error Alert: Recent errors detected in the last $minutes minutes. Check $ERROR_LOG for details."
send_email_alert "HAProxy Manager Error Alert" "$alert_message"
send_webhook_alert "$alert_message"
return 1 # Return error status
else
echo "No recent errors found in the last $minutes minutes."
return 0 # Return success status
fi
}
# Function to check certificate expiration
check_certificate_expiration() {
local warning_days="${1:-30}" # Default to 30 days warning
# Use the API to get certificate status
local api_key="${HAPROXY_API_KEY:-}"
local base_url="http://localhost:8000"
if [ -n "$api_key" ]; then
local cert_status=$(curl -s -H "Authorization: Bearer $api_key" "$base_url/api/certificates/status")
if [ $? -eq 0 ]; then
# Parse JSON to check for expiring certificates
local expiring_certs=$(echo "$cert_status" | jq -r --arg days "$warning_days" '
.certificates[] |
select(.days_until_expiry != null and .days_until_expiry <= ($days | tonumber)) |
"\(.domain): expires in \(.days_until_expiry) days"
')
if [ -n "$expiring_certs" ]; then
echo "Certificates expiring soon:"
echo "$expiring_certs"
local alert_message="HAProxy Manager Certificate Alert: Certificates expiring soon. $expiring_certs"
send_email_alert "HAProxy Manager Certificate Alert" "$alert_message"
send_webhook_alert "$alert_message"
return 1
else
echo "No certificates expiring within $warning_days days."
return 0
fi
else
echo "Failed to get certificate status from API."
return 1
fi
else
echo "No API key configured. Cannot check certificate status."
return 1
fi
}
# Main script logic
case "${1:-help}" in
"errors")
check_recent_errors "${2:-60}"
;;
"certs")
check_certificate_expiration "${2:-30}"
;;
"all")
echo "Checking for recent errors..."
check_recent_errors "${2:-60}"
error_status=$?
echo "Checking certificate expiration..."
check_certificate_expiration "${3:-30}"
cert_status=$?
exit $((error_status + cert_status))
;;
"help"|*)
echo "HAProxy Manager Monitoring Script"
echo ""
echo "Usage: $0 {errors|certs|all} [minutes] [cert_warning_days]"
echo ""
echo "Commands:"
echo " errors [minutes] Check for errors in the last N minutes (default: 60)"
echo " certs [days] Check for certificates expiring within N days (default: 30)"
echo " all [minutes] [days] Check both errors and certificates"
echo " help Show this help message"
echo ""
echo "Environment variables:"
echo " ALERT_EMAIL Email address for alerts"
echo " WEBHOOK_URL Webhook URL for alerts"
echo " HAPROXY_API_KEY API key for certificate status checks"
echo ""
echo "Examples:"
echo " $0 errors 30 # Check for errors in last 30 minutes"
echo " $0 certs 7 # Check for certificates expiring in 7 days"
echo " $0 all 60 14 # Check both (60 min errors, 14 day certs)"
;;
esac

View File

@ -0,0 +1,28 @@
# HAProxy Manager Monitoring Configuration Example
# Copy this file and modify it for your environment
# Email alerts (requires mailutils to be installed)
ALERT_EMAIL="admin@yourdomain.com"
# Webhook alerts (e.g., Slack, Discord, etc.)
WEBHOOK_URL="https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
# API key for certificate status checks
HAPROXY_API_KEY="your-secure-api-key-here"
# Monitoring intervals (in minutes)
ERROR_CHECK_INTERVAL=30
CERT_CHECK_INTERVAL=1440 # 24 hours
# Certificate warning threshold (days before expiration)
CERT_WARNING_DAYS=30
# Example crontab entries for monitoring:
# Check for errors every 30 minutes
# */30 * * * * /haproxy/scripts/monitor-errors.sh errors 30
# Check certificates daily
# 0 9 * * * /haproxy/scripts/monitor-errors.sh certs 30
# Check both errors and certificates daily
# 0 9 * * * /haproxy/scripts/monitor-errors.sh all 60 30

161
scripts/test-api.sh Executable file
View File

@ -0,0 +1,161 @@
#!/bin/bash
# HAProxy Manager API Test Script
# This script tests the new API endpoints
BASE_URL="http://localhost:8000"
API_KEY="${HAPROXY_API_KEY:-}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
local status=$1
local message=$2
if [ "$status" = "PASS" ]; then
echo -e "${GREEN}✓ PASS${NC}: $message"
elif [ "$status" = "FAIL" ]; then
echo -e "${RED}✗ FAIL${NC}: $message"
else
echo -e "${YELLOW}? INFO${NC}: $message"
fi
}
# Function to make API request
api_request() {
local method=$1
local endpoint=$2
local data=$3
local headers=""
if [ -n "$API_KEY" ]; then
headers="-H \"Authorization: Bearer $API_KEY\""
fi
if [ -n "$data" ]; then
headers="$headers -H \"Content-Type: application/json\" -d '$data'"
fi
eval "curl -s -w \"%{http_code}\" -o /tmp/api_response.json $headers -X $method $BASE_URL$endpoint"
}
# Test health endpoint (no auth required)
test_health() {
print_status "INFO" "Testing health endpoint..."
local response=$(api_request "GET" "/health")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ]; then
print_status "PASS" "Health endpoint working"
else
print_status "FAIL" "Health endpoint failed with status $status_code"
fi
}
# Test domains endpoint
test_domains() {
print_status "INFO" "Testing domains endpoint..."
local response=$(api_request "GET" "/api/domains")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Domains endpoint responded correctly (status: $status_code)"
else
print_status "FAIL" "Domains endpoint failed with status $status_code"
fi
}
# Test certificate status endpoint
test_cert_status() {
print_status "INFO" "Testing certificate status endpoint..."
local response=$(api_request "GET" "/api/certificates/status")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Certificate status endpoint responded correctly (status: $status_code)"
else
print_status "FAIL" "Certificate status endpoint failed with status $status_code"
fi
}
# Test certificate renewal endpoint
test_cert_renewal() {
print_status "INFO" "Testing certificate renewal endpoint..."
local response=$(api_request "POST" "/api/certificates/renew")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Certificate renewal endpoint responded correctly (status: $status_code)"
else
print_status "FAIL" "Certificate renewal endpoint failed with status $status_code"
fi
}
# Test reload endpoint
test_reload() {
print_status "INFO" "Testing HAProxy reload endpoint..."
local response=$(api_request "GET" "/api/reload")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Reload endpoint responded correctly (status: $status_code)"
else
print_status "FAIL" "Reload endpoint failed with status $status_code"
fi
}
# Test authentication
test_auth() {
if [ -n "$API_KEY" ]; then
print_status "INFO" "API key is configured"
# Test with valid API key
local response=$(api_request "GET" "/api/domains")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ]; then
print_status "PASS" "Authentication working with API key"
else
print_status "FAIL" "Authentication failed with API key (status: $status_code)"
fi
else
print_status "INFO" "No API key configured - testing without authentication"
# Test without API key
local response=$(api_request "GET" "/api/domains")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ]; then
print_status "PASS" "API accessible without authentication"
else
print_status "FAIL" "API not accessible without authentication (status: $status_code)"
fi
fi
}
# Main test execution
main() {
echo "HAProxy Manager API Test Suite"
echo "=============================="
echo "Base URL: $BASE_URL"
echo "API Key: ${API_KEY:-"Not configured"}"
echo ""
test_health
test_auth
test_domains
test_cert_status
test_cert_renewal
test_reload
echo ""
echo "Test completed. Check /tmp/api_response.json for detailed responses."
}
# Run tests
main "$@"