From 7b0b4c0476bd0fc6d91dfb7706ee9d4da1f279ad Mon Sep 17 00:00:00 2001 From: jknapp Date: Fri, 11 Jul 2025 06:24:56 -0700 Subject: [PATCH] Major upgrade: API key authentication, certificate renewal/download endpoints, monitoring/alerting scripts, improved logging, and documentation updates. See UPGRADE_SUMMARY.md for details. --- Dockerfile | 5 +- README.md | 199 ++++++++++- UPGRADE_SUMMARY.md | 173 +++++++++ haproxy_manager.py | 433 ++++++++++++++++++----- scripts/external-monitoring-example.conf | 42 +++ scripts/monitor-errors-external.sh | 224 ++++++++++++ scripts/monitor-errors.sh | 159 +++++++++ scripts/monitoring-example.conf | 28 ++ scripts/test-api.sh | 161 +++++++++ 9 files changed, 1338 insertions(+), 86 deletions(-) create mode 100644 UPGRADE_SUMMARY.md create mode 100644 scripts/external-monitoring-example.conf create mode 100755 scripts/monitor-errors-external.sh create mode 100755 scripts/monitor-errors.sh create mode 100644 scripts/monitoring-example.conf create mode 100755 scripts/test-api.sh diff --git a/Dockerfile b/Dockerfile index 26030ef..15673c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ 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 COPY ./templates /haproxy/templates COPY requirements.txt /haproxy/ @@ -8,6 +8,9 @@ COPY scripts /haproxy/scripts RUN chmod +x /haproxy/scripts/* 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 +# 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 # Add health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ diff --git a/README.md b/README.md index d8d88e1..552f43e 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,13 @@ A Flask-based API service for managing HAProxy configurations with dynamic SSL c To run the container: ```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 - 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 - Process monitoring and auto-restart capabilities - 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 @@ -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 a new domain with backend servers configuration. ```bash POST /api/domain +Authorization: Bearer your-api-key Content-Type: application/json { @@ -100,6 +145,7 @@ Request and configure SSL certificate for a domain using Let's Encrypt. ```bash POST /api/ssl +Authorization: Bearer your-api-key Content-Type: application/json { @@ -117,6 +163,7 @@ Remove a domain and its associated backend configuration. ```bash DELETE /api/domain +Authorization: Bearer your-api-key Content-Type: application/json { @@ -129,3 +176,153 @@ Content-Type: application/json "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 +``` diff --git a/UPGRADE_SUMMARY.md b/UPGRADE_SUMMARY.md new file mode 100644 index 0000000..1818e0b --- /dev/null +++ b/UPGRADE_SUMMARY.md @@ -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 ` 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//download` - Combined certificate (cert + key) + - `GET /api/certificates//key` - Private key only + - `GET /api/certificates//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//download` - Download combined certificate +- `GET /api/certificates//key` - Download private key +- `GET /api/certificates//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 \ No newline at end of file diff --git a/haproxy_manager.py b/haproxy_manager.py index b6c3e94..0214b08 100644 --- a/haproxy_manager.py +++ b/haproxy_manager.py @@ -1,18 +1,66 @@ import sqlite3 import os -from flask import Flask, request, jsonify, render_template +from flask import Flask, request, jsonify, render_template, send_file from pathlib import Path import subprocess import jinja2 import socket import psutil +import functools +import logging +from datetime import datetime +import json app = Flask(__name__) +# Configuration DB_FILE = '/etc/haproxy/haproxy_config.db' TEMPLATE_DIR = Path('templates') HAPROXY_CONFIG_PATH = '/etc/haproxy/haproxy.cfg' 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(): with sqlite3.connect(DB_FILE) as conn: @@ -102,6 +150,7 @@ template_loader = jinja2.FileSystemLoader(TEMPLATE_DIR) template_env = jinja2.Environment(loader=template_loader) @app.route('/api/domains', methods=['GET']) +@require_api_key def get_domains(): try: with sqlite3.connect(DB_FILE) as conn: @@ -113,8 +162,10 @@ def get_domains(): LEFT JOIN backends b ON d.id = b.domain_id ''') domains = [dict(row) for row in cursor.fetchall()] + log_operation('get_domains', True) return jsonify(domains) except Exception as e: + log_operation('get_domains', False, str(e)) return jsonify({'status': 'error', 'message': str(e)}), 500 @app.route('/health', methods=['GET']) @@ -141,27 +192,32 @@ def health_check(): }), 500 @app.route('/api/regenerate', methods=['GET']) +@require_api_key def regenerate_conf(): try: generate_config() + log_operation('regenerate_config', True) return jsonify({'status': 'success'}), 200 except Exception as e: + log_operation('regenerate_config', False, str(e)) return jsonify({ 'status': 'failed', 'error': str(e) }), 500 @app.route('/api/reload', methods=['GET']) +@require_api_key def reload_haproxy(): - if is_process_running('haproxy'): - # Use a proper shell command string when shell=True is set - result = subprocess.run('echo "reload" | socat stdio /tmp/haproxy-cli', - check=True, capture_output=True, text=True, shell=True) - print(f"Reload result: {result.stdout}, {result.stderr}, {result.returncode}") - return jsonify({'status': 'success'}), 200 - else: - # Start HAProxy if it's not running - try: + try: + if is_process_running('haproxy'): + # Use a proper shell command string when shell=True is set + result = subprocess.run('echo "reload" | socat stdio /tmp/haproxy-cli', + check=True, capture_output=True, text=True, shell=True) + print(f"Reload result: {result.stdout}, {result.stderr}, {result.returncode}") + log_operation('reload_haproxy', True) + return jsonify({'status': 'success'}), 200 + else: + # Start HAProxy if it's not running result = subprocess.run( ['haproxy', '-W', '-S', '/tmp/haproxy-cli,level,admin', '-f', HAPROXY_CONFIG_PATH], check=True, @@ -170,18 +226,21 @@ def reload_haproxy(): ) if result.returncode == 0: print("HAProxy started successfully") + log_operation('start_haproxy', True) return jsonify({'status': 'success'}), 200 else: - print(f"HAProxy start command returned: {result.stdout}") - print(f"Error output: {result.stderr}") - except subprocess.CalledProcessError as e: - print(f"Failed to start HAProxy: {e.stdout}\n{e.stderr}") - return jsonify({ - 'status': 'failed', - 'error': f"Failed to start HAProxy: {e.stdout}\n{e.stderr}" - }), 500 + error_msg = f"HAProxy start command returned: {result.stdout}\nError output: {result.stderr}" + print(error_msg) + log_operation('start_haproxy', False, error_msg) + return jsonify({'status': 'failed', 'error': error_msg}), 500 + except subprocess.CalledProcessError as e: + error_msg = f"Failed to start HAProxy: {e.stdout}\n{e.stderr}" + print(error_msg) + log_operation('reload_haproxy', False, error_msg) + return jsonify({'status': 'failed', 'error': error_msg}), 500 @app.route('/api/domain', methods=['POST']) +@require_api_key def add_domain(): data = request.get_json() domain = data.get('domain') @@ -189,77 +248,257 @@ def add_domain(): backend_name = data.get('backend_name') servers = data.get('servers', []) - with sqlite3.connect(DB_FILE) as conn: - cursor = conn.cursor() + if not domain or not backend_name: + 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 - cursor.execute('INSERT INTO domains (domain, template_override) VALUES (?, ?)', (domain, template_override)) - domain_id = cursor.lastrowid + try: + with sqlite3.connect(DB_FILE) as conn: + cursor = conn.cursor() - # Add backend - cursor.execute('INSERT INTO backends (name, domain_id) VALUES (?, ?)', - (backend_name, domain_id)) - backend_id = cursor.lastrowid + # Add domain + cursor.execute('INSERT INTO domains (domain, template_override) VALUES (?, ?)', (domain, template_override)) + domain_id = cursor.lastrowid - for server in servers: - cursor.execute(''' - INSERT INTO backend_servers - (backend_id, server_name, server_address, server_port, server_options) - VALUES (?, ?, ?, ?, ?) - ''', (backend_id, server['name'], server['address'], - server['port'], server.get('options'))) - # Close cursor and connection - cursor.close() - conn.close() - generate_config() - return jsonify({'status': 'success', 'domain_id': domain_id}) + # Add backend + cursor.execute('INSERT INTO backends (name, domain_id) VALUES (?, ?)', + (backend_name, domain_id)) + backend_id = cursor.lastrowid + + for server in servers: + cursor.execute(''' + INSERT INTO backend_servers + (backend_id, server_name, server_address, server_port, server_options) + VALUES (?, ?, ?, ?, ?) + ''', (backend_id, server['name'], server['address'], + 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('/') def index(): return render_template('index.html') @app.route('/api/ssl', methods=['POST']) +@require_api_key def request_ssl(): data = request.get_json() domain = data.get('domain') - # Request Let's Encrypt certificate - result = subprocess.run([ - 'certbot', 'certonly', '-n', '--standalone', - '--preferred-challenges', 'http', '--http-01-port=8688', - '-d', domain - ]) + if not domain: + log_operation('request_ssl', False, 'Domain not provided') + return jsonify({'status': 'error', 'message': 'Domain is required'}), 400 - 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' + try: + # Request Let's Encrypt certificate + result = subprocess.run([ + 'certbot', 'certonly', '-n', '--standalone', + '--preferred-challenges', 'http', '--http-01-port=8688', + '-d', domain + ], capture_output=True, text=True) - with open(combined_path, 'w') as combined: - subprocess.run(['cat', cert_path, key_path], stdout=combined) + 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' - # 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//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: 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() - return jsonify({'status': 'success'}) - return jsonify({'status': 'error', 'message': 'Failed to obtain SSL certificate'}) + cursor.execute('SELECT ssl_cert_path FROM domains WHERE domain = ? AND ssl_enabled = 1', (domain,)) + result = cursor.fetchone() + + if not result or not result[0]: + return jsonify({'status': 'error', 'message': 'Certificate not found for domain'}), 404 + + cert_path = result[0] + if not os.path.exists(cert_path): + return jsonify({'status': 'error', 'message': 'Certificate file not found'}), 404 + + 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//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//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']) +@require_api_key def remove_domain(): data = request.get_json() domain = data.get('domain') if not domain: + log_operation('remove_domain', False, 'Domain is required') return jsonify({'status': 'error', 'message': 'Domain is required'}), 400 try: @@ -271,6 +510,7 @@ def remove_domain(): domain_result = cursor.fetchone() if not domain_result: + log_operation('remove_domain', False, f'Domain {domain} not found') return jsonify({'status': 'error', 'message': 'Domain not found'}), 404 domain_id = domain_result[0] @@ -301,9 +541,11 @@ def remove_domain(): # Regenerate HAProxy config generate_config() + log_operation('remove_domain', True, f'Domain {domain} removed successfully') return jsonify({'status': 'success', 'message': 'Domain configuration removed'}) except Exception as e: + log_operation('remove_domain', False, str(e)) return jsonify({'status': 'error', 'message': str(e)}), 500 def generate_config(): @@ -349,7 +591,7 @@ def generate_config(): # Add domain configurations for domain in domains: 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 # Add domain ACL @@ -359,9 +601,9 @@ def generate_config(): name=domain['backend_name'] ) 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: - print(f"Error generating domain ACL for {domain['domain']}: {e}") + logger.error(f"Error generating domain ACL for {domain['domain']}: {e}") continue # Add backend configuration @@ -372,11 +614,11 @@ def generate_config(): servers = [dict(server) for server in cursor.fetchall()] 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 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" backend_block = template_env.get_template(template_file).render( name=domain['backend_name'], @@ -390,9 +632,9 @@ def generate_config(): servers=servers ) 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: - print(f"Error generating backend block for {domain['backend_name']}: {e}") + logger.error(f"Error generating backend block for {domain['backend_name']}: {e}") continue # Add ACLS @@ -406,17 +648,25 @@ def generate_config(): temp_config_path = "/etc/haproxy/haproxy.cfg" config_content = '\n'.join(config_parts) - print("Final config content:", config_content) # Debug log + logger.debug("Generated HAProxy configuration") # Write complete configuration to tmp # Check HAProxy Configuration, and reload if it works with open(temp_config_path, 'w') as f: 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: - print("HAProxy configuration check passed") + logger.info("HAProxy configuration check passed") 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: try: result = subprocess.run( @@ -426,15 +676,26 @@ def generate_config(): text=True ) if result.returncode == 0: - print("HAProxy started successfully") + logger.info("HAProxy started successfully") + log_operation('generate_config', True, 'Configuration generated and HAProxy started') else: - print(f"HAProxy start command returned: {result.stdout}") - print(f"Error output: {result.stderr}") + error_msg = f"HAProxy start command returned: {result.stdout}\nError output: {result.stderr}" + logger.error(error_msg) + log_operation('generate_config', False, error_msg) 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 + 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: - 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 traceback.print_exc() raise @@ -449,12 +710,16 @@ def start_haproxy(): text=True ) if result.returncode == 0: - print("HAProxy started successfully") + logger.info("HAProxy started successfully") + log_operation('start_haproxy', True, 'HAProxy started successfully') else: - print(f"HAProxy start command returned: {result.stdout}") - print(f"Error output: {result.stderr}") + error_msg = f"HAProxy start command returned: {result.stdout}\nError output: {result.stderr}" + logger.error(error_msg) + log_operation('start_haproxy', False, error_msg) 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 if __name__ == '__main__': diff --git a/scripts/external-monitoring-example.conf b/scripts/external-monitoring-example.conf new file mode 100644 index 0000000..72d2a18 --- /dev/null +++ b/scripts/external-monitoring-example.conf @@ -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 \ No newline at end of file diff --git a/scripts/monitor-errors-external.sh b/scripts/monitor-errors-external.sh new file mode 100755 index 0000000..88911f3 --- /dev/null +++ b/scripts/monitor-errors-external.sh @@ -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 \ No newline at end of file diff --git a/scripts/monitor-errors.sh b/scripts/monitor-errors.sh new file mode 100755 index 0000000..4aff58e --- /dev/null +++ b/scripts/monitor-errors.sh @@ -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 \ No newline at end of file diff --git a/scripts/monitoring-example.conf b/scripts/monitoring-example.conf new file mode 100644 index 0000000..57818b2 --- /dev/null +++ b/scripts/monitoring-example.conf @@ -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 \ No newline at end of file diff --git a/scripts/test-api.sh b/scripts/test-api.sh new file mode 100755 index 0000000..6f3c82b --- /dev/null +++ b/scripts/test-api.sh @@ -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 "$@" \ No newline at end of file