mirror of
https://github.com/acedanger/shell.git
synced 2025-12-06 01:10:12 -08:00
feat: Implement comprehensive backup web application with Docker, systemd service, and Gunicorn support
This commit is contained in:
200
DEPLOYMENT-GUIDE.md
Normal file
200
DEPLOYMENT-GUIDE.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# Backup Web Application Deployment Guide
|
||||||
|
|
||||||
|
This guide covers multiple methods to keep the backup web application running perpetually on your server.
|
||||||
|
|
||||||
|
## Deployment Options
|
||||||
|
|
||||||
|
### 1. 🚀 Systemd Service (Recommended for Production)
|
||||||
|
|
||||||
|
**Best for:** Production environments, automatic startup on boot, proper logging, and system integration.
|
||||||
|
|
||||||
|
#### Setup Steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the service
|
||||||
|
sudo ./manage-backup-web-service.sh install
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
sudo ./manage-backup-web-service.sh start
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
./manage-backup-web-service.sh status
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
./manage-backup-web-service.sh logs
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Service Management:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start/Stop/Restart
|
||||||
|
sudo systemctl start backup-web-app
|
||||||
|
sudo systemctl stop backup-web-app
|
||||||
|
sudo systemctl restart backup-web-app
|
||||||
|
|
||||||
|
# Enable/Disable auto-start on boot
|
||||||
|
sudo systemctl enable backup-web-app
|
||||||
|
sudo systemctl disable backup-web-app
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
sudo journalctl -u backup-web-app -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 🐳 Docker (Recommended for Isolation)
|
||||||
|
|
||||||
|
**Best for:** Containerized environments, easy deployment, consistent runtime.
|
||||||
|
|
||||||
|
#### Using Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Rebuild and restart
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using Docker directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build image
|
||||||
|
docker build -t backup-web-app .
|
||||||
|
|
||||||
|
# Run container
|
||||||
|
docker run -d \
|
||||||
|
--name backup-web-app \
|
||||||
|
-p 5000:5000 \
|
||||||
|
-v /mnt/share/media/backups:/data/backups:ro \
|
||||||
|
-e BACKUP_ROOT=/data/backups \
|
||||||
|
--restart unless-stopped \
|
||||||
|
backup-web-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 📺 Screen Session (Quick & Simple)
|
||||||
|
|
||||||
|
**Best for:** Development, testing, quick deployments.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the application
|
||||||
|
./run-backup-web-screen.sh start
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
./run-backup-web-screen.sh status
|
||||||
|
|
||||||
|
# View logs (connect to session)
|
||||||
|
./run-backup-web-screen.sh logs
|
||||||
|
|
||||||
|
# Stop the application
|
||||||
|
./run-backup-web-screen.sh stop
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. ⚡ Production with Gunicorn
|
||||||
|
|
||||||
|
**Best for:** High-performance production deployments.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install gunicorn
|
||||||
|
pip install gunicorn
|
||||||
|
|
||||||
|
# Run with production settings
|
||||||
|
./run-production.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
- `BACKUP_ROOT`: Path to backup directory (default: `/mnt/share/media/backups`)
|
||||||
|
- `PORT`: Application port (default: `5000`)
|
||||||
|
- `FLASK_ENV`: Environment mode (`development` or `production`)
|
||||||
|
- `FLASK_DEBUG`: Enable debug mode (`true` or `false`)
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
1. **Firewall**: Ensure port 5000 is properly secured
|
||||||
|
2. **Reverse Proxy**: Consider using nginx for SSL termination
|
||||||
|
3. **Authentication**: Add authentication for production use
|
||||||
|
4. **File Permissions**: Ensure proper read permissions for backup directories
|
||||||
|
|
||||||
|
## Monitoring & Maintenance
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
The application provides a health endpoint:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Locations
|
||||||
|
|
||||||
|
- **Systemd**: `sudo journalctl -u backup-web-app`
|
||||||
|
- **Docker**: `docker-compose logs` or `docker logs backup-web-app`
|
||||||
|
- **Screen**: Connect to session with `screen -r backup-web-app`
|
||||||
|
- **Gunicorn**: `/tmp/backup-web-app-access.log` and `/tmp/backup-web-app-error.log`
|
||||||
|
|
||||||
|
### Automatic Restarts
|
||||||
|
|
||||||
|
- **Systemd**: Built-in restart on failure
|
||||||
|
- **Docker**: Use `--restart unless-stopped` or `restart: unless-stopped` in compose
|
||||||
|
- **Screen**: Manual restart required
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Port already in use**:
|
||||||
|
```bash
|
||||||
|
sudo lsof -i :5000
|
||||||
|
sudo netstat -tulpn | grep :5000
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Permission denied for backup directory**:
|
||||||
|
```bash
|
||||||
|
sudo chown -R acedanger:acedanger /mnt/share/media/backups
|
||||||
|
chmod -R 755 /mnt/share/media/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Service won't start**:
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u backup-web-app -n 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Tuning
|
||||||
|
|
||||||
|
1. **Gunicorn Workers**: Adjust in `gunicorn.conf.py`
|
||||||
|
2. **Memory Limits**: Set in systemd service or docker-compose
|
||||||
|
3. **Log Rotation**: Configure logrotate for production
|
||||||
|
|
||||||
|
## Quick Start Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For development/testing (Screen)
|
||||||
|
./run-backup-web-screen.sh start
|
||||||
|
|
||||||
|
# For production (Systemd)
|
||||||
|
sudo ./manage-backup-web-service.sh install
|
||||||
|
sudo ./manage-backup-web-service.sh start
|
||||||
|
|
||||||
|
# For containerized (Docker)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Check if running
|
||||||
|
curl http://localhost:5000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommended Setup
|
||||||
|
|
||||||
|
For a production server, use this combination:
|
||||||
|
|
||||||
|
1. **Primary**: Systemd service for reliability
|
||||||
|
2. **Backup**: Docker setup for easy maintenance
|
||||||
|
3. **Monitoring**: Set up log monitoring and alerts
|
||||||
|
4. **Security**: Add reverse proxy with SSL
|
||||||
|
|
||||||
|
Choose the method that best fits your infrastructure and requirements!
|
||||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Dockerfile for Backup Web Application
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements and install Python dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY backup-web-app.py .
|
||||||
|
COPY templates/ ./templates/
|
||||||
|
COPY static/ ./static/
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV FLASK_ENV=production
|
||||||
|
ENV BACKUP_ROOT=/data/backups
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:5000/health || exit 1
|
||||||
|
|
||||||
|
# Run application
|
||||||
|
CMD ["python", "backup-web-app.py"]
|
||||||
@@ -16,9 +16,8 @@ Author: Shell Repository
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from flask import Flask, render_template, jsonify, request, abort
|
||||||
from flask import Flask, render_template, jsonify, request, send_file, abort
|
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -48,10 +47,10 @@ def load_json_file(filepath):
|
|||||||
"""Safely load JSON file with error handling"""
|
"""Safely load JSON file with error handling"""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(filepath):
|
if os.path.exists(filepath):
|
||||||
with open(filepath, 'r') as f:
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception as e:
|
except (OSError, json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||||
logger.error(f"Error loading JSON file {filepath}: {e}")
|
logger.error("Error loading JSON file %s: %s", filepath, e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -206,7 +205,6 @@ def index():
|
|||||||
"""Main dashboard"""
|
"""Main dashboard"""
|
||||||
try:
|
try:
|
||||||
# Get all services with their metrics
|
# Get all services with their metrics
|
||||||
service_names = get_services()
|
|
||||||
services_data = []
|
services_data = []
|
||||||
|
|
||||||
# Status counters for summary
|
# Status counters for summary
|
||||||
@@ -259,8 +257,8 @@ def index():
|
|||||||
}
|
}
|
||||||
|
|
||||||
return render_template('dashboard.html', data=dashboard_data)
|
return render_template('dashboard.html', data=dashboard_data)
|
||||||
except Exception as e:
|
except (OSError, IOError, json.JSONDecodeError) as e:
|
||||||
logger.error(f"Error in index route: {e}")
|
logger.error("Error in index route: %s", e)
|
||||||
return f"Error: {e}", 500
|
return f"Error: {e}", 500
|
||||||
|
|
||||||
|
|
||||||
@@ -285,8 +283,9 @@ def api_service_details(service_name):
|
|||||||
'backup_files': backup_files,
|
'backup_files': backup_files,
|
||||||
'log_files': log_files
|
'log_files': log_files
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except (OSError, IOError, json.JSONDecodeError) as e:
|
||||||
logger.error(f"Error getting service details for {service_name}: {e}")
|
logger.error("Error getting service details for %s: %s",
|
||||||
|
service_name, e)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@@ -328,8 +327,8 @@ def service_detail(service_name):
|
|||||||
service_data['latest_backup'] = latest_backup['path']
|
service_data['latest_backup'] = latest_backup['path']
|
||||||
|
|
||||||
return render_template('service.html', service=service_data)
|
return render_template('service.html', service=service_data)
|
||||||
except Exception as e:
|
except (OSError, IOError, json.JSONDecodeError) as e:
|
||||||
logger.error(f"Error in service detail for {service_name}: {e}")
|
logger.error("Error in service detail for %s: %s", service_name, e)
|
||||||
return f"Error: {e}", 500
|
return f"Error: {e}", 500
|
||||||
|
|
||||||
|
|
||||||
@@ -369,8 +368,8 @@ def logs_view():
|
|||||||
})
|
})
|
||||||
|
|
||||||
return render_template('logs.html', logs=formatted_logs, filter_service=service_filter)
|
return render_template('logs.html', logs=formatted_logs, filter_service=service_filter)
|
||||||
except Exception as e:
|
except (OSError, IOError) as e:
|
||||||
logger.error(f"Error in logs view: {e}")
|
logger.error("Error in logs view: %s", e)
|
||||||
return f"Error: {e}", 500
|
return f"Error: {e}", 500
|
||||||
|
|
||||||
|
|
||||||
@@ -408,7 +407,7 @@ def view_log(filename):
|
|||||||
# Read last N lines for large files
|
# Read last N lines for large files
|
||||||
max_lines = int(request.args.get('lines', 1000))
|
max_lines = int(request.args.get('lines', 1000))
|
||||||
|
|
||||||
with open(log_path, 'r') as f:
|
with open(log_path, 'r', encoding='utf-8') as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
if len(lines) > max_lines:
|
if len(lines) > max_lines:
|
||||||
lines = lines[-max_lines:]
|
lines = lines[-max_lines:]
|
||||||
@@ -427,8 +426,8 @@ def view_log(filename):
|
|||||||
"%Y-%m-%d %H:%M:%S"),
|
"%Y-%m-%d %H:%M:%S"),
|
||||||
total_lines=len(lines),
|
total_lines=len(lines),
|
||||||
lines_shown=min(len(lines), max_lines))
|
lines_shown=min(len(lines), max_lines))
|
||||||
except Exception as e:
|
except (OSError, IOError, UnicodeDecodeError, ValueError) as e:
|
||||||
logger.error(f"Error viewing log {filename}: {e}")
|
logger.error("Error viewing log %s: %s", filename, e)
|
||||||
return f"Error: {e}", 500
|
return f"Error: {e}", 500
|
||||||
|
|
||||||
|
|
||||||
@@ -449,7 +448,8 @@ def api_refresh_metrics():
|
|||||||
env=env,
|
env=env,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=300 # 5 minute timeout
|
timeout=300, # 5 minute timeout
|
||||||
|
check=False
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
@@ -460,7 +460,7 @@ def api_refresh_metrics():
|
|||||||
'output': result.stdout
|
'output': result.stdout
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
logger.error(f"Metrics refresh failed: {result.stderr}")
|
logger.error("Metrics refresh failed: %s", result.stderr)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Metrics refresh failed',
|
'message': 'Metrics refresh failed',
|
||||||
@@ -477,8 +477,8 @@ def api_refresh_metrics():
|
|||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Metrics refresh timed out'
|
'message': 'Metrics refresh timed out'
|
||||||
}), 408
|
}), 408
|
||||||
except Exception as e:
|
except (OSError, subprocess.SubprocessError) as e:
|
||||||
logger.error(f"Error refreshing metrics: {e}")
|
logger.error("Error refreshing metrics: %s", e)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': str(e)
|
'message': str(e)
|
||||||
@@ -498,14 +498,14 @@ def health_check():
|
|||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(error):
|
def not_found(_error):
|
||||||
return render_template('error.html',
|
return render_template('error.html',
|
||||||
error_code=404,
|
error_code=404,
|
||||||
error_message="Page not found"), 404
|
error_message="Page not found"), 404
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def internal_error(error):
|
def internal_error(_error):
|
||||||
return render_template('error.html',
|
return render_template('error.html',
|
||||||
error_code=500,
|
error_code=500,
|
||||||
error_message="Internal server error"), 500
|
error_message="Internal server error"), 500
|
||||||
|
|||||||
24
backup-web-app.service
Normal file
24
backup-web-app.service
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Backup Web Application
|
||||||
|
After=network.target
|
||||||
|
Wants=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=acedanger
|
||||||
|
Group=acedanger
|
||||||
|
WorkingDirectory=/home/acedanger/shell
|
||||||
|
Environment=PATH=/usr/bin:/usr/local/bin
|
||||||
|
Environment=BACKUP_ROOT=/mnt/share/media/backups
|
||||||
|
Environment=FLASK_ENV=production
|
||||||
|
Environment=PORT=5000
|
||||||
|
ExecStart=/usr/bin/python3 /home/acedanger/shell/backup-web-app.py
|
||||||
|
ExecReload=/bin/kill -s HUP $MAINPID
|
||||||
|
KillMode=mixed
|
||||||
|
TimeoutStopSec=5
|
||||||
|
PrivateTmp=true
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backup-web-app:
|
||||||
|
build: .
|
||||||
|
container_name: backup-web-app
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- /mnt/share/media/backups:/data/backups:ro
|
||||||
|
- ./logs:/app/logs
|
||||||
|
environment:
|
||||||
|
- BACKUP_ROOT=/data/backups
|
||||||
|
- FLASK_ENV=production
|
||||||
|
- PORT=5000
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
61
gunicorn.conf.py
Normal file
61
gunicorn.conf.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Gunicorn configuration for backup web application
|
||||||
|
|
||||||
|
import os
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
# Server socket
|
||||||
|
bind = f"0.0.0.0:{os.environ.get('PORT', '5000')}"
|
||||||
|
backlog = 2048
|
||||||
|
|
||||||
|
# Worker processes
|
||||||
|
workers = multiprocessing.cpu_count() * 2 + 1
|
||||||
|
worker_class = "sync"
|
||||||
|
worker_connections = 1000
|
||||||
|
timeout = 30
|
||||||
|
keepalive = 2
|
||||||
|
|
||||||
|
# Restart workers after this many requests, to help prevent memory leaks
|
||||||
|
max_requests = 1000
|
||||||
|
max_requests_jitter = 50
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
accesslog = "/tmp/backup-web-app-access.log"
|
||||||
|
errorlog = "/tmp/backup-web-app-error.log"
|
||||||
|
loglevel = "info"
|
||||||
|
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
|
||||||
|
|
||||||
|
# Process naming
|
||||||
|
proc_name = "backup-web-app"
|
||||||
|
|
||||||
|
# Daemon mode
|
||||||
|
daemon = False
|
||||||
|
pidfile = "/tmp/backup-web-app.pid"
|
||||||
|
umask = 0
|
||||||
|
user = None
|
||||||
|
group = None
|
||||||
|
tmp_upload_dir = None
|
||||||
|
|
||||||
|
# SSL (if needed)
|
||||||
|
# keyfile = "/path/to/keyfile"
|
||||||
|
# certfile = "/path/to/certfile"
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
raw_env = [
|
||||||
|
f"BACKUP_ROOT={os.environ.get('BACKUP_ROOT', '/mnt/share/media/backups')}",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Preload app for better performance
|
||||||
|
preload_app = True
|
||||||
|
|
||||||
|
# Graceful timeout
|
||||||
|
graceful_timeout = 30
|
||||||
|
|
||||||
|
# Security
|
||||||
|
forwarded_allow_ips = "*"
|
||||||
|
secure_scheme_headers = {
|
||||||
|
'X-FORWARDED-PROTOCOL': 'ssl',
|
||||||
|
'X-FORWARDED-PROTO': 'https',
|
||||||
|
'X-FORWARDED-SSL': 'on'
|
||||||
|
}
|
||||||
197
manage-backup-web-service.sh
Executable file
197
manage-backup-web-service.sh
Executable file
@@ -0,0 +1,197 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Backup Web Application Service Manager
|
||||||
|
# Manages the backup web application as a systemd service
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVICE_NAME="backup-web-app"
|
||||||
|
SERVICE_FILE="/home/acedanger/shell/${SERVICE_NAME}.service"
|
||||||
|
SYSTEMD_DIR="/etc/systemd/system"
|
||||||
|
APP_USER="acedanger"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
print_status() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_root() {
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
print_error "This script must be run as root (use sudo)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_service() {
|
||||||
|
print_status "Installing backup web application service..."
|
||||||
|
|
||||||
|
# Check if service file exists
|
||||||
|
if [[ ! -f "$SERVICE_FILE" ]]; then
|
||||||
|
print_error "Service file not found: $SERVICE_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy service file to systemd directory
|
||||||
|
cp "$SERVICE_FILE" "$SYSTEMD_DIR/"
|
||||||
|
print_success "Service file copied to $SYSTEMD_DIR"
|
||||||
|
|
||||||
|
# Reload systemd daemon
|
||||||
|
systemctl daemon-reload
|
||||||
|
print_success "Systemd daemon reloaded"
|
||||||
|
|
||||||
|
# Enable service to start on boot
|
||||||
|
systemctl enable "$SERVICE_NAME"
|
||||||
|
print_success "Service enabled for auto-start on boot"
|
||||||
|
|
||||||
|
print_success "Service installation completed!"
|
||||||
|
print_status "Use 'sudo systemctl start $SERVICE_NAME' to start the service"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
print_status "Starting backup web application service..."
|
||||||
|
systemctl start "$SERVICE_NAME"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
print_success "Service started successfully"
|
||||||
|
systemctl status "$SERVICE_NAME" --no-pager -l
|
||||||
|
else
|
||||||
|
print_error "Failed to start service"
|
||||||
|
print_status "Check logs with: sudo journalctl -u $SERVICE_NAME -f"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
print_status "Stopping backup web application service..."
|
||||||
|
systemctl stop "$SERVICE_NAME"
|
||||||
|
print_success "Service stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_service() {
|
||||||
|
print_status "Restarting backup web application service..."
|
||||||
|
systemctl restart "$SERVICE_NAME"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
print_success "Service restarted successfully"
|
||||||
|
else
|
||||||
|
print_error "Failed to restart service"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
status_service() {
|
||||||
|
print_status "Service status:"
|
||||||
|
systemctl status "$SERVICE_NAME" --no-pager -l
|
||||||
|
}
|
||||||
|
|
||||||
|
logs_service() {
|
||||||
|
print_status "Following service logs (Ctrl+C to exit):"
|
||||||
|
journalctl -u "$SERVICE_NAME" -f
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstall_service() {
|
||||||
|
print_status "Uninstalling backup web application service..."
|
||||||
|
|
||||||
|
# Stop service if running
|
||||||
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
systemctl stop "$SERVICE_NAME"
|
||||||
|
print_status "Service stopped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disable service
|
||||||
|
if systemctl is-enabled --quiet "$SERVICE_NAME"; then
|
||||||
|
systemctl disable "$SERVICE_NAME"
|
||||||
|
print_status "Service disabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove service file
|
||||||
|
if [[ -f "$SYSTEMD_DIR/${SERVICE_NAME}.service" ]]; then
|
||||||
|
rm "$SYSTEMD_DIR/${SERVICE_NAME}.service"
|
||||||
|
print_status "Service file removed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload systemd daemon
|
||||||
|
systemctl daemon-reload
|
||||||
|
print_success "Service uninstalled successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo "Backup Web Application Service Manager"
|
||||||
|
echo
|
||||||
|
echo "Usage: $0 {install|start|stop|restart|status|logs|uninstall|help}"
|
||||||
|
echo
|
||||||
|
echo "Commands:"
|
||||||
|
echo " install - Install the service (requires root)"
|
||||||
|
echo " start - Start the service (requires root)"
|
||||||
|
echo " stop - Stop the service (requires root)"
|
||||||
|
echo " restart - Restart the service (requires root)"
|
||||||
|
echo " status - Show service status"
|
||||||
|
echo " logs - Follow service logs"
|
||||||
|
echo " uninstall - Remove the service (requires root)"
|
||||||
|
echo " help - Show this help message"
|
||||||
|
echo
|
||||||
|
echo "Examples:"
|
||||||
|
echo " sudo $0 install # Install and enable the service"
|
||||||
|
echo " sudo $0 start # Start the service"
|
||||||
|
echo " $0 status # Check service status"
|
||||||
|
echo " $0 logs # View live logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main script logic
|
||||||
|
case "${1:-}" in
|
||||||
|
install)
|
||||||
|
check_root
|
||||||
|
install_service
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
check_root
|
||||||
|
start_service
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
check_root
|
||||||
|
stop_service
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
check_root
|
||||||
|
restart_service
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status_service
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
logs_service
|
||||||
|
;;
|
||||||
|
uninstall)
|
||||||
|
check_root
|
||||||
|
uninstall_service
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Invalid command: ${1:-}"
|
||||||
|
echo
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Flask==2.3.3
|
||||||
|
Werkzeug==2.3.7
|
||||||
|
gunicorn==21.2.0
|
||||||
150
run-backup-web-screen.sh
Executable file
150
run-backup-web-screen.sh
Executable file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Simple script to run backup web app in a persistent screen session
|
||||||
|
|
||||||
|
SESSION_NAME="backup-web-app"
|
||||||
|
APP_DIR="/home/acedanger/shell"
|
||||||
|
PYTHON_CMD="python3"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_status() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_screen() {
|
||||||
|
if ! command -v screen &> /dev/null; then
|
||||||
|
print_error "Screen is not installed. Install it with: sudo apt install screen"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_app() {
|
||||||
|
check_screen
|
||||||
|
|
||||||
|
# Check if session already exists
|
||||||
|
if screen -list | grep -q "$SESSION_NAME"; then
|
||||||
|
print_warning "Session '$SESSION_NAME' already exists"
|
||||||
|
print_status "Use './run-backup-web-screen.sh status' to check or './run-backup-web-screen.sh stop' to stop"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "Starting backup web app in screen session '$SESSION_NAME'..."
|
||||||
|
|
||||||
|
# Start new detached screen session
|
||||||
|
cd "$APP_DIR" || exit 1
|
||||||
|
screen -dmS "$SESSION_NAME" bash -c "
|
||||||
|
export BACKUP_ROOT=/mnt/share/media/backups
|
||||||
|
export FLASK_ENV=production
|
||||||
|
$PYTHON_CMD backup-web-app.py
|
||||||
|
"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if screen -list | grep -q "$SESSION_NAME"; then
|
||||||
|
print_status "✅ Backup web app started successfully!"
|
||||||
|
print_status "Session: $SESSION_NAME"
|
||||||
|
print_status "URL: http://localhost:5000"
|
||||||
|
print_status ""
|
||||||
|
print_status "Commands:"
|
||||||
|
print_status " View logs: ./run-backup-web-screen.sh logs"
|
||||||
|
print_status " Stop app: ./run-backup-web-screen.sh stop"
|
||||||
|
print_status " Status: ./run-backup-web-screen.sh status"
|
||||||
|
else
|
||||||
|
print_error "Failed to start the application"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_app() {
|
||||||
|
if screen -list | grep -q "$SESSION_NAME"; then
|
||||||
|
print_status "Stopping backup web app..."
|
||||||
|
screen -S "$SESSION_NAME" -X quit
|
||||||
|
print_status "✅ Application stopped"
|
||||||
|
else
|
||||||
|
print_warning "No session '$SESSION_NAME' found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
status_app() {
|
||||||
|
if screen -list | grep -q "$SESSION_NAME"; then
|
||||||
|
print_status "✅ Backup web app is running"
|
||||||
|
print_status "Session details:"
|
||||||
|
screen -list | grep "$SESSION_NAME"
|
||||||
|
print_status ""
|
||||||
|
print_status "Access the session with: screen -r $SESSION_NAME"
|
||||||
|
print_status "Detach from session with: Ctrl+A, then D"
|
||||||
|
else
|
||||||
|
print_warning "❌ Backup web app is not running"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_logs() {
|
||||||
|
if screen -list | grep -q "$SESSION_NAME"; then
|
||||||
|
print_status "Connecting to session '$SESSION_NAME'..."
|
||||||
|
print_status "Press Ctrl+A, then D to detach from the session"
|
||||||
|
screen -r "$SESSION_NAME"
|
||||||
|
else
|
||||||
|
print_error "No session '$SESSION_NAME' found. App is not running."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_app() {
|
||||||
|
print_status "Restarting backup web app..."
|
||||||
|
stop_app
|
||||||
|
sleep 2
|
||||||
|
start_app
|
||||||
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo "Backup Web App Screen Manager"
|
||||||
|
echo
|
||||||
|
echo "Usage: $0 {start|stop|restart|status|logs|help}"
|
||||||
|
echo
|
||||||
|
echo "Commands:"
|
||||||
|
echo " start - Start the app in a screen session"
|
||||||
|
echo " stop - Stop the app"
|
||||||
|
echo " restart - Restart the app"
|
||||||
|
echo " status - Check if app is running"
|
||||||
|
echo " logs - Connect to the screen session to view logs"
|
||||||
|
echo " help - Show this help message"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
start)
|
||||||
|
start_app
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_app
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_app
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status_app
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
show_logs
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Invalid command: ${1:-}"
|
||||||
|
echo
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
59
run-production.sh
Executable file
59
run-production.sh
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Production runner for backup web application using Gunicorn
|
||||||
|
|
||||||
|
APP_DIR="/home/acedanger/shell"
|
||||||
|
APP_MODULE="backup-web-app:app"
|
||||||
|
CONFIG_FILE="gunicorn.conf.py"
|
||||||
|
VENV_PATH="/home/acedanger/shell/venv"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_status() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if we're in the right directory
|
||||||
|
cd "$APP_DIR" || {
|
||||||
|
print_error "Cannot change to app directory: $APP_DIR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for virtual environment
|
||||||
|
if [[ -d "$VENV_PATH" ]]; then
|
||||||
|
print_status "Activating virtual environment..."
|
||||||
|
source "$VENV_PATH/bin/activate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
export BACKUP_ROOT="/mnt/share/media/backups"
|
||||||
|
export FLASK_ENV="production"
|
||||||
|
|
||||||
|
# Check if gunicorn is installed
|
||||||
|
if ! command -v gunicorn &> /dev/null; then
|
||||||
|
print_error "Gunicorn is not installed"
|
||||||
|
print_status "Install with: pip install gunicorn"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "Starting backup web application with Gunicorn..."
|
||||||
|
print_status "Configuration: $CONFIG_FILE"
|
||||||
|
print_status "Module: $APP_MODULE"
|
||||||
|
print_status "Directory: $APP_DIR"
|
||||||
|
|
||||||
|
# Start Gunicorn
|
||||||
|
exec gunicorn \
|
||||||
|
--config "$CONFIG_FILE" \
|
||||||
|
"$APP_MODULE"
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import sys
|
|
||||||
|
|
||||||
# Set environment
|
# Set environment
|
||||||
os.environ['BACKUP_ROOT'] = '/home/acedanger/shell'
|
os.environ['BACKUP_ROOT'] = '/home/acedanger/shell'
|
||||||
@@ -13,9 +12,9 @@ def load_json_file(filepath):
|
|||||||
"""Safely load JSON file with error handling"""
|
"""Safely load JSON file with error handling"""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(filepath):
|
if os.path.exists(filepath):
|
||||||
with open(filepath, 'r') as f:
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception as e:
|
except (OSError, json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||||
print(f"Error loading JSON file {filepath}: {e}")
|
print(f"Error loading JSON file {filepath}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -25,35 +24,35 @@ def get_service_metrics(service_name):
|
|||||||
# Simple status file approach
|
# Simple status file approach
|
||||||
status_file = os.path.join(METRICS_DIR, f'{service_name}_status.json')
|
status_file = os.path.join(METRICS_DIR, f'{service_name}_status.json')
|
||||||
|
|
||||||
status = load_json_file(status_file)
|
service_status = load_json_file(status_file)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status': status,
|
'status': service_status,
|
||||||
'last_run': status.get('end_time') if status else None,
|
'last_run': service_status.get('end_time') if service_status else None,
|
||||||
'current_status': status.get('status', 'unknown') if status else 'never_run',
|
'current_status': service_status.get('status', 'unknown') if service_status else 'never_run',
|
||||||
'files_processed': status.get('files_processed', 0) if status else 0,
|
'files_processed': service_status.get('files_processed', 0) if service_status else 0,
|
||||||
'total_size': status.get('total_size_bytes', 0) if status else 0,
|
'total_size': service_status.get('total_size_bytes', 0) if service_status else 0,
|
||||||
'duration': status.get('duration_seconds', 0) if status else 0
|
'duration': service_status.get('duration_seconds', 0) if service_status else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_consolidated_metrics():
|
def get_consolidated_metrics():
|
||||||
"""Get consolidated metrics across all services"""
|
"""Get consolidated metrics across all services"""
|
||||||
# With simplified approach, we consolidate by reading all status files
|
# With simplified approach, we consolidate by reading all status files
|
||||||
services = {}
|
all_services = {}
|
||||||
|
|
||||||
if os.path.exists(METRICS_DIR):
|
if os.path.exists(METRICS_DIR):
|
||||||
for filename in os.listdir(METRICS_DIR):
|
for filename in os.listdir(METRICS_DIR):
|
||||||
if filename.endswith('_status.json'):
|
if filename.endswith('_status.json'):
|
||||||
service_name = filename.replace('_status.json', '')
|
service_name = filename.replace('_status.json', '')
|
||||||
status_file = os.path.join(METRICS_DIR, filename)
|
status_file = os.path.join(METRICS_DIR, filename)
|
||||||
status = load_json_file(status_file)
|
service_status = load_json_file(status_file)
|
||||||
if status:
|
if service_status:
|
||||||
services[service_name] = status
|
all_services[service_name] = service_status
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'services': services,
|
'services': all_services,
|
||||||
'total_services': len(services),
|
'total_services': len(all_services),
|
||||||
'last_updated': '2025-06-18T05:15:00-04:00'
|
'last_updated': '2025-06-18T05:15:00-04:00'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ if __name__ == "__main__":
|
|||||||
files = metrics['files_processed']
|
files = metrics['files_processed']
|
||||||
duration = metrics['duration']
|
duration = metrics['duration']
|
||||||
print(f' {service}: {status} ({files} files, {duration}s)')
|
print(f' {service}: {status} ({files} files, {duration}s)')
|
||||||
except Exception as e:
|
except (OSError, IOError, KeyError) as e:
|
||||||
print(f' {service}: Error - {e}')
|
print(f' {service}: Error - {e}')
|
||||||
|
|
||||||
# Test consolidated metrics
|
# Test consolidated metrics
|
||||||
@@ -82,7 +81,7 @@ if __name__ == "__main__":
|
|||||||
for name, status in services.items():
|
for name, status in services.items():
|
||||||
message = status.get('message', 'N/A')
|
message = status.get('message', 'N/A')
|
||||||
print(f' {name}: {status["status"]} - {message}')
|
print(f' {name}: {status["status"]} - {message}')
|
||||||
except Exception as e:
|
except (OSError, IOError, KeyError) as e:
|
||||||
print(f' Error: {e}')
|
print(f' Error: {e}')
|
||||||
|
|
||||||
print('\n✅ Web integration test completed successfully!')
|
print('\n✅ Web integration test completed successfully!')
|
||||||
|
|||||||
Reference in New Issue
Block a user