Build [v1.14.22]
This commit is contained in:
@@ -1,128 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Phase 6, Plan 02, Task 1: Automated Backup Script
|
||||
# Supports both Docker and Standalone deployment modes
|
||||
# Usage: ./backup.sh [daily|weekly|manual] [retention_days]
|
||||
|
||||
BACKUP_TYPE="${1:-manual}"
|
||||
RETENTION_DAYS="${2:-30}"
|
||||
BACKUP_DIR="./backups"
|
||||
DATA_DIR="./data"
|
||||
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
|
||||
BACKUP_FILE="$BACKUP_DIR/inventory-$TIMESTAMP.tar.gz"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Determine deployment mode
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
log_info "Docker deployment mode detected"
|
||||
IS_DOCKER=true
|
||||
|
||||
# Verify Docker daemon is running
|
||||
docker ps > /dev/null 2>&1 || log_error "Docker daemon not running"
|
||||
|
||||
# 1. Verify services are running
|
||||
log_info "Checking services..."
|
||||
if ! docker-compose ps | grep -q "Up"; then
|
||||
log_warn "Not all services running; attempting to start..."
|
||||
docker-compose up -d
|
||||
fi
|
||||
|
||||
# 2. Stop backend to ensure DB consistency
|
||||
log_info "Stopping backend service (DB consistency)..."
|
||||
docker-compose stop backend
|
||||
else
|
||||
log_info "Standalone deployment mode detected"
|
||||
IS_DOCKER=false
|
||||
|
||||
# For standalone mode, we'll just backup while running
|
||||
# (Process is single-threaded, so minimal locking risk)
|
||||
log_warn "Standalone mode: backing up with minimal service pause"
|
||||
fi
|
||||
|
||||
# Wait for graceful shutdown (Docker only)
|
||||
[[ "$IS_DOCKER" == "true" ]] && sleep 2
|
||||
|
||||
# 3. Create backup tarball
|
||||
log_info "Creating backup: $BACKUP_FILE"
|
||||
|
||||
tar --exclude="$DATA_DIR/caddy_*" \
|
||||
--exclude="$BACKUP_DIR" \
|
||||
--exclude=".git" \
|
||||
--exclude="node_modules" \
|
||||
--exclude=".next" \
|
||||
--exclude=".venv" \
|
||||
--exclude="__pycache__" \
|
||||
-czf "$BACKUP_FILE" \
|
||||
"$DATA_DIR/inventory.db" \
|
||||
"$DATA_DIR/inventory.db-wal" \
|
||||
"$DATA_DIR/inventory.db-shm" \
|
||||
"config/" \
|
||||
"inventory.env" \
|
||||
2>/dev/null || log_error "Backup creation failed"
|
||||
|
||||
# Restart backend (Docker only)
|
||||
if [[ "$IS_DOCKER" == "true" ]]; then
|
||||
log_info "Restarting backend service..."
|
||||
docker-compose start backend
|
||||
|
||||
# Wait for backend to be ready
|
||||
sleep 5
|
||||
|
||||
# Verify backend is healthy
|
||||
if curl -sf "http://localhost:8000/health" > /dev/null; then
|
||||
log_info "Backend restored and healthy"
|
||||
else
|
||||
log_warn "Backend not responding yet; check logs with 'docker-compose logs -f backend'"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. Verify backup integrity
|
||||
log_info "Verifying backup integrity..."
|
||||
if tar -tzf "$BACKUP_FILE" > /dev/null 2>&1; then
|
||||
log_info "✓ Backup verified"
|
||||
else
|
||||
log_error "Backup corrupted"
|
||||
fi
|
||||
|
||||
# 5. Log backup metadata
|
||||
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
BACKUP_VERSION=$(cat VERSION.json 2>/dev/null | grep version | cut -d'"' -f4 || echo "unknown")
|
||||
log_info "Backup size: $BACKUP_SIZE, Version: $BACKUP_VERSION"
|
||||
|
||||
# 6. Cleanup old backups
|
||||
log_info "Cleaning up backups older than $RETENTION_DAYS days..."
|
||||
find "$BACKUP_DIR" -name "inventory-*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
# 7. Create backup manifest
|
||||
cat > "$BACKUP_DIR/MANIFEST.txt" << EOF
|
||||
Backup Metadata
|
||||
===============
|
||||
Timestamp: $TIMESTAMP
|
||||
Type: $BACKUP_TYPE
|
||||
Retention: $RETENTION_DAYS days
|
||||
Size: $BACKUP_SIZE
|
||||
Version: $BACKUP_VERSION
|
||||
Deployment Mode: $([ "$IS_DOCKER" == "true" ] && echo "Docker" || echo "Standalone")
|
||||
Contents: DB, config, certificates (excluding certs)
|
||||
Restored: Not yet
|
||||
|
||||
Command to restore:
|
||||
./restore.sh $BACKUP_FILE
|
||||
EOF
|
||||
|
||||
log_info "Backup complete: $BACKUP_FILE"
|
||||
log_info "Retention policy: Delete after $RETENTION_DAYS days"
|
||||
log_info "Next backup: $(date -d '+1 day' '+%Y-%m-%d')"
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# scripts/init_data.sh
|
||||
# =============================================================================
|
||||
# First-run initialization script for TFM aInventory.
|
||||
# Creates runtime directories and copies configuration templates if missing.
|
||||
#
|
||||
# Called by:
|
||||
# - start_server.sh (local dev / systemd runs)
|
||||
# - backend/entrypoint.sh (Docker container startup)
|
||||
#
|
||||
# Environment variables (with defaults):
|
||||
# DATA_DIR — path to runtime data directory (default: <project_root>/data)
|
||||
# LOGS_DIR — path to runtime logs directory (default: <project_root>/logs)
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve project root relative to this script's location
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Use environment variables if set, otherwise default to in-repo directories
|
||||
DATA_DIR="${DATA_DIR:-$PROJECT_ROOT/data}"
|
||||
LOGS_DIR="${LOGS_DIR:-$PROJECT_ROOT/logs}"
|
||||
|
||||
echo " [init] DATA_DIR = $DATA_DIR"
|
||||
echo " [init] LOGS_DIR = $LOGS_DIR"
|
||||
|
||||
# 1. Create runtime directories (idempotent)
|
||||
mkdir -p "$DATA_DIR"
|
||||
mkdir -p "$LOGS_DIR"
|
||||
|
||||
# 2. Copy LDAP config from example template if no active config exists yet
|
||||
# NOTE: The application reads LDAP config from config/ldap_config.json
|
||||
# (see backend/routers/users.py → get_ldap_config())
|
||||
LDAP_CONFIG="$PROJECT_ROOT/config/ldap_config.json"
|
||||
LDAP_EXAMPLE="$PROJECT_ROOT/config/ldap_config.json.example"
|
||||
|
||||
if [ ! -f "$LDAP_CONFIG" ]; then
|
||||
if [ -f "$LDAP_EXAMPLE" ]; then
|
||||
cp "$LDAP_EXAMPLE" "$LDAP_CONFIG"
|
||||
echo " [init] LDAP config copied from example → $LDAP_CONFIG"
|
||||
echo " [init] ⚠️ Edit $LDAP_CONFIG with your real LDAP server settings."
|
||||
else
|
||||
echo " [init] WARNING: No LDAP config example found at $LDAP_EXAMPLE"
|
||||
fi
|
||||
else
|
||||
echo " [init] LDAP config already exists — skipping copy."
|
||||
fi
|
||||
|
||||
# 3. Database schema is created automatically by FastAPI/SQLAlchemy on first
|
||||
# request (see backend/main.py → Base.metadata.create_all). The DATA_DIR
|
||||
# created above ensures the DB file can be written to the correct location.
|
||||
|
||||
echo " [init] Runtime data initialization complete."
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Phase 6, Plan 02, Task 2: Restore from Backup
|
||||
# Supports both Docker and Standalone deployment modes
|
||||
# Usage: ./restore.sh <backup_file> [--validate]
|
||||
|
||||
BACKUP_FILE="${1:-}"
|
||||
VALIDATE="${2:---validate}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||
|
||||
# Validate input
|
||||
[[ -z "$BACKUP_FILE" ]] && log_error "Usage: ./restore.sh <backup_file>"
|
||||
[[ ! -f "$BACKUP_FILE" ]] && log_error "Backup file not found: $BACKUP_FILE"
|
||||
|
||||
log_info "Restoring from: $BACKUP_FILE"
|
||||
|
||||
# Triple confirmation (security requirement)
|
||||
echo "⚠️ WARNING: This will overwrite current data!"
|
||||
read -p "Type 'RESTORE' to confirm (1/3): " confirm1
|
||||
[[ "$confirm1" != "RESTORE" ]] && log_error "Restore cancelled"
|
||||
|
||||
read -p "Type 'RESTORE' again to confirm (2/3): " confirm2
|
||||
[[ "$confirm2" != "RESTORE" ]] && log_error "Restore cancelled"
|
||||
|
||||
read -p "Type 'RESTORE' one more time to confirm (3/3): " confirm3
|
||||
[[ "$confirm3" != "RESTORE" ]] && log_error "Restore cancelled"
|
||||
|
||||
log_warn "Proceeding with restore..."
|
||||
|
||||
# Determine deployment mode
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
log_info "Docker deployment mode detected"
|
||||
IS_DOCKER=true
|
||||
|
||||
# 1. Stop services
|
||||
log_info "Stopping services..."
|
||||
docker-compose down
|
||||
|
||||
# 2. Backup current data (safety copy)
|
||||
log_info "Creating safety backup of current data..."
|
||||
mkdir -p data/backups_before_restore
|
||||
tar -czf "data/backups_before_restore/backup-before-restore-$(date +%s).tar.gz" \
|
||||
data/inventory.db data/inventory.db-* 2>/dev/null || true
|
||||
|
||||
# 3. Extract backup
|
||||
log_info "Extracting backup..."
|
||||
tar -xzf "$BACKUP_FILE" -C . || log_error "Backup extraction failed"
|
||||
|
||||
# 4. Verify essential files
|
||||
log_info "Verifying restored files..."
|
||||
[[ -f "data/inventory.db" ]] || log_error "Database not found in backup"
|
||||
[[ -f "inventory.env" ]] || log_error "inventory.env not found in backup"
|
||||
|
||||
# 5. Restart services
|
||||
log_info "Restarting services..."
|
||||
docker-compose up -d
|
||||
|
||||
# 6. Wait for health
|
||||
log_info "Waiting for services to be healthy..."
|
||||
max_attempts=30
|
||||
attempt=0
|
||||
while [[ $attempt -lt $max_attempts ]]; do
|
||||
if curl -sf "http://localhost:8000/health" > /dev/null 2>&1; then
|
||||
log_info "Services healthy!"
|
||||
break
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ $attempt -eq $max_attempts ]]; then
|
||||
log_warn "Services did not become healthy within 60 seconds"
|
||||
docker-compose logs backend
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 7. Validation tests (if requested)
|
||||
if [[ "$VALIDATE" == "--validate" ]]; then
|
||||
log_info "Running validation tests..."
|
||||
|
||||
# Test 1: Database accessible
|
||||
DB_ITEMS=$(docker-compose exec backend sqlite3 /app/data/inventory.db \
|
||||
"SELECT COUNT(*) FROM items" 2>/dev/null || echo "0")
|
||||
log_info "✓ Database has $DB_ITEMS items"
|
||||
|
||||
# Test 2: API responsive
|
||||
if curl -sf "http://localhost:8000/health" > /dev/null; then
|
||||
log_info "✓ API health check passed"
|
||||
else
|
||||
log_error "API health check failed"
|
||||
fi
|
||||
|
||||
# Test 3: Frontend loads
|
||||
if curl -sf "http://localhost:3000" > /dev/null 2>&1; then
|
||||
log_info "✓ Frontend loads"
|
||||
else
|
||||
log_warn "⚠ Frontend check failed (normal on first startup)"
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
log_info "Standalone deployment mode detected"
|
||||
IS_DOCKER=false
|
||||
|
||||
# 1. Kill any running servers
|
||||
log_info "Stopping running services..."
|
||||
pkill -f "next start" || true
|
||||
pkill -f "uvicorn" || true
|
||||
sleep 2
|
||||
|
||||
# 2. Backup current data
|
||||
log_info "Creating safety backup of current data..."
|
||||
mkdir -p data/backups_before_restore
|
||||
tar -czf "data/backups_before_restore/backup-before-restore-$(date +%s).tar.gz" \
|
||||
data/inventory.db data/inventory.db-* 2>/dev/null || true
|
||||
|
||||
# 3. Extract backup
|
||||
log_info "Extracting backup..."
|
||||
tar -xzf "$BACKUP_FILE" -C . || log_error "Backup extraction failed"
|
||||
|
||||
# 4. Verify essential files
|
||||
log_info "Verifying restored files..."
|
||||
[[ -f "data/inventory.db" ]] || log_error "Database not found in backup"
|
||||
[[ -f "inventory.env" ]] || log_error "inventory.env not found in backup"
|
||||
|
||||
log_info "✓ Restore complete"
|
||||
log_warn "Remember to start services: ./start_server.sh"
|
||||
fi
|
||||
|
||||
log_info "Restore complete!"
|
||||
log_warn "Remember to verify data in the application before returning to production"
|
||||
134
scripts/restore_prod.py
Normal file
134
scripts/restore_prod.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TFM aInventory - Production Restore Script (v1.12.0)
|
||||
Converted from restore.sh to Python per Decision D-05.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
import argparse
|
||||
import tarfile
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
# Color codes
|
||||
class Colors:
|
||||
RED = '\033[0;31m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
BLUE = '\033[0;34m'
|
||||
NC = '\033[0m'
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO, format=f"{Colors.BLUE}[INFO]{Colors.NC} %(message)s")
|
||||
logger = logging.getLogger("restore_prod")
|
||||
|
||||
def load_yaml(file_path: str) -> Dict[str, Any]:
|
||||
if not os.path.exists(file_path):
|
||||
return {}
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse {file_path}: {e}")
|
||||
return {}
|
||||
|
||||
def confirm_action(prompt: str, required_text: str = "RESTORE") -> bool:
|
||||
print(f"{Colors.YELLOW}⚠️ WARNING: {prompt}{Colors.NC}")
|
||||
for i in range(1, 4):
|
||||
response = input(f"Type '{required_text}' to confirm ({i}/3): ")
|
||||
if response != required_text:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_docker_running() -> bool:
|
||||
try:
|
||||
subprocess.run(["docker", "ps"], capture_output=True, check=True)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="TFM aInventory Production Restore Tool")
|
||||
parser.add_argument("backup_file", help="Path to the tar.gz backup file")
|
||||
parser.add_argument("--force", action="store_true", help="Skip confirmation (DANGEROUS)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.backup_file):
|
||||
logger.error(f"Backup file not found: {args.backup_file}")
|
||||
sys.exit(1)
|
||||
|
||||
if not args.force:
|
||||
if not confirm_action(f"This will overwrite current data with content from {args.backup_file}!"):
|
||||
logger.error("Restore cancelled by user.")
|
||||
sys.exit(1)
|
||||
|
||||
# Load config to find data directory
|
||||
backend_cfg = load_yaml("config/backend.yaml")
|
||||
app_cfg = backend_cfg.get("application", {})
|
||||
data_dir = app_cfg.get("data_dir", "./data")
|
||||
|
||||
# 1. Detect deployment mode and stop services
|
||||
docker_mode = os.path.exists("docker-compose.yml") and is_docker_running()
|
||||
|
||||
if docker_mode:
|
||||
logger.info("Docker mode detected. Stopping services...")
|
||||
subprocess.run(["docker-compose", "down"], check=True)
|
||||
else:
|
||||
logger.info("Standalone mode detected. Ensuring services are stopped...")
|
||||
# Try to use run_standalone.py if available
|
||||
launcher = "scripts/run_standalone.py"
|
||||
if os.path.exists(launcher):
|
||||
subprocess.run([sys.executable, launcher, "stop"], capture_output=True)
|
||||
else:
|
||||
# Fallback to pkill
|
||||
subprocess.run(["pkill", "-f", "uvicorn"], capture_output=True)
|
||||
subprocess.run(["pkill", "-f", "next start"], capture_output=True)
|
||||
|
||||
# 2. Safety backup of current data
|
||||
safety_dir = os.path.join(data_dir, "backups_before_restore")
|
||||
os.makedirs(safety_dir, exist_ok=True)
|
||||
safety_backup = os.path.join(safety_dir, f"pre_restore_{int(time.time())}.tar.gz")
|
||||
|
||||
logger.info(f"Creating safety backup of current data: {safety_backup}")
|
||||
try:
|
||||
with tarfile.open(safety_backup, "w:gz") as tar:
|
||||
if os.path.exists(data_dir):
|
||||
tar.add(data_dir)
|
||||
if os.path.exists("config"):
|
||||
tar.add("config")
|
||||
except Exception as e:
|
||||
logger.warning(f"Safety backup failed (continuing anyway): {e}")
|
||||
|
||||
# 3. Extract backup
|
||||
logger.info(f"Extracting {args.backup_file}...")
|
||||
try:
|
||||
with tarfile.open(args.backup_file, "r:gz") as tar:
|
||||
tar.extractall(path=".")
|
||||
logger.info(f"{Colors.GREEN}✓ Extraction successful!{Colors.NC}")
|
||||
except Exception as e:
|
||||
logger.error(f"Extraction failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 4. Verification
|
||||
db_path = os.path.join(data_dir, "inventory.db")
|
||||
if not os.path.exists(db_path):
|
||||
logger.error(f"CRITICAL: Database not found at {db_path} after restore!")
|
||||
sys.exit(1)
|
||||
|
||||
# 5. Restart services
|
||||
if docker_mode:
|
||||
logger.info("Restarting Docker services...")
|
||||
subprocess.run(["docker-compose", "up", "-d"], check=True)
|
||||
logger.info(f"{Colors.GREEN}✓ Services started. Check 'docker-compose ps' for status.{Colors.NC}")
|
||||
else:
|
||||
logger.info(f"{Colors.YELLOW}Restore complete. Please start services manually using run_standalone.py{Colors.NC}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -344,6 +344,35 @@ def action_stop(backend_port: int, frontend_port: int, network_cfg: Dict[str, An
|
||||
|
||||
logger.info("Stopped.")
|
||||
|
||||
def generate_network_json(network_cfg: Dict[str, Any]):
|
||||
"""Sync network.yaml settings to frontend/public/network.json for runtime discovery."""
|
||||
server_ip = network_cfg.get("application", {}).get("server_ip", "localhost")
|
||||
ports = network_cfg.get("ports", {})
|
||||
|
||||
network_json = {
|
||||
"SERVER_IP": server_ip,
|
||||
"BACKEND_PORT": ports.get("backend_port", 8916),
|
||||
"BACKEND_SSL_PORT": ports.get("backend_ssl_port", 8918),
|
||||
"FRONTEND_PORT": ports.get("frontend_port", 8917),
|
||||
"FRONTEND_SSL_PORT": ports.get("frontend_ssl_port", 8919)
|
||||
}
|
||||
|
||||
# Try to find the frontend/public directory
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
public_dir = os.path.join(project_root, "frontend", "public")
|
||||
|
||||
if os.path.isdir(public_dir):
|
||||
target_path = os.path.join(public_dir, "network.json")
|
||||
try:
|
||||
import json
|
||||
with open(target_path, 'w') as f:
|
||||
json.dump(network_json, f, indent=2)
|
||||
logger.info(f"Generated {target_path} from network.yaml")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate network.json: {e}")
|
||||
else:
|
||||
logger.warning(f"Frontend public directory not found at {public_dir}. Skipping network.json generation.")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="TFM aInventory Standalone Launcher")
|
||||
parser.add_argument("action", nargs="?", choices=["start", "stop", "restart", "status", "run"], default="run",
|
||||
@@ -380,6 +409,9 @@ def main():
|
||||
if args.action in ["start", "run"]:
|
||||
background = (args.action == "start")
|
||||
|
||||
# Generate network.json for frontend discovery before starting anything
|
||||
generate_network_json(network_cfg)
|
||||
|
||||
if not args.frontend_only:
|
||||
if is_port_in_use(backend_port):
|
||||
logger.error(f"Port {backend_port} is already in use. Backend cannot start.")
|
||||
|
||||
Reference in New Issue
Block a user