Dev ops prep
This commit is contained in:
193
scripts/ops/backup-db.sh
Normal file
193
scripts/ops/backup-db.sh
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Database Backup Script
|
||||
# =============================================================================
|
||||
# Usage: ./backup-db.sh [daily|weekly|monthly|pre-deploy]
|
||||
# Creates compressed database backup with automatic retention
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
BACKUP_TYPE="${1:-daily}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
DATE_FOLDER=$(date +%Y%m%d)
|
||||
|
||||
# Directories
|
||||
BACKUP_ROOT="/data/backups"
|
||||
DAILY_DIR="${BACKUP_ROOT}/daily"
|
||||
WEEKLY_DIR="${BACKUP_ROOT}/weekly"
|
||||
MONTHLY_DIR="${BACKUP_ROOT}/monthly"
|
||||
PREDEPLOY_DIR="${BACKUP_ROOT}/pre-deploy"
|
||||
|
||||
# Database settings (from docker environment)
|
||||
DB_CONTAINER="postgres"
|
||||
DB_NAME="igny8_db"
|
||||
DB_USER="igny8"
|
||||
|
||||
# Retention settings
|
||||
DAILY_RETENTION_DAYS=7
|
||||
WEEKLY_RETENTION_WEEKS=4
|
||||
MONTHLY_RETENTION_MONTHS=12
|
||||
PREDEPLOY_RETENTION_COUNT=5
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log() {
|
||||
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
log "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
log "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
log "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Create directories if they don't exist
|
||||
create_dirs() {
|
||||
mkdir -p "${DAILY_DIR}"
|
||||
mkdir -p "${WEEKLY_DIR}"
|
||||
mkdir -p "${MONTHLY_DIR}"
|
||||
mkdir -p "${PREDEPLOY_DIR}"
|
||||
}
|
||||
|
||||
# Determine backup destination based on type
|
||||
get_backup_path() {
|
||||
case $BACKUP_TYPE in
|
||||
daily)
|
||||
echo "${DAILY_DIR}/${DATE_FOLDER}"
|
||||
;;
|
||||
weekly)
|
||||
echo "${WEEKLY_DIR}/week_$(date +%Y_%W)"
|
||||
;;
|
||||
monthly)
|
||||
echo "${MONTHLY_DIR}/$(date +%Y_%m)"
|
||||
;;
|
||||
pre-deploy)
|
||||
echo "${PREDEPLOY_DIR}/${TIMESTAMP}"
|
||||
;;
|
||||
*)
|
||||
log_error "Invalid backup type: $BACKUP_TYPE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Perform database backup
|
||||
backup_database() {
|
||||
local backup_dir="$1"
|
||||
local backup_file="${backup_dir}/db_${DB_NAME}_${TIMESTAMP}.sql.gz"
|
||||
|
||||
mkdir -p "${backup_dir}"
|
||||
|
||||
log "Starting database backup: ${DB_NAME}"
|
||||
log "Destination: ${backup_file}"
|
||||
|
||||
# Check if postgres container is running
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then
|
||||
log_error "PostgreSQL container '${DB_CONTAINER}' is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Perform backup with compression
|
||||
if docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${DB_NAME}" | gzip > "${backup_file}"; then
|
||||
local size=$(du -h "${backup_file}" | cut -f1)
|
||||
log_success "Database backup complete: ${backup_file} (${size})"
|
||||
|
||||
# Create latest symlink
|
||||
ln -sf "${backup_file}" "${BACKUP_ROOT}/latest_db.sql.gz"
|
||||
|
||||
return 0
|
||||
else
|
||||
log_error "Database backup failed"
|
||||
rm -f "${backup_file}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Clean up old backups based on retention policy
|
||||
cleanup_old_backups() {
|
||||
log "Cleaning up old backups..."
|
||||
|
||||
# Daily backups - keep last N days
|
||||
if [[ -d "${DAILY_DIR}" ]]; then
|
||||
find "${DAILY_DIR}" -type d -mtime +${DAILY_RETENTION_DAYS} -exec rm -rf {} \; 2>/dev/null || true
|
||||
local daily_count=$(find "${DAILY_DIR}" -type f -name "*.sql.gz" | wc -l)
|
||||
log "Daily backups: ${daily_count} files"
|
||||
fi
|
||||
|
||||
# Weekly backups - keep last N weeks
|
||||
if [[ -d "${WEEKLY_DIR}" ]]; then
|
||||
find "${WEEKLY_DIR}" -type d -mtime +$((WEEKLY_RETENTION_WEEKS * 7)) -exec rm -rf {} \; 2>/dev/null || true
|
||||
local weekly_count=$(find "${WEEKLY_DIR}" -type f -name "*.sql.gz" | wc -l)
|
||||
log "Weekly backups: ${weekly_count} files"
|
||||
fi
|
||||
|
||||
# Monthly backups - keep last N months
|
||||
if [[ -d "${MONTHLY_DIR}" ]]; then
|
||||
find "${MONTHLY_DIR}" -type d -mtime +$((MONTHLY_RETENTION_MONTHS * 30)) -exec rm -rf {} \; 2>/dev/null || true
|
||||
local monthly_count=$(find "${MONTHLY_DIR}" -type f -name "*.sql.gz" | wc -l)
|
||||
log "Monthly backups: ${monthly_count} files"
|
||||
fi
|
||||
|
||||
# Pre-deploy backups - keep last N
|
||||
if [[ -d "${PREDEPLOY_DIR}" ]]; then
|
||||
local predeploy_dirs=$(ls -dt "${PREDEPLOY_DIR}"/*/ 2>/dev/null | tail -n +$((PREDEPLOY_RETENTION_COUNT + 1)))
|
||||
if [[ -n "$predeploy_dirs" ]]; then
|
||||
echo "$predeploy_dirs" | xargs rm -rf 2>/dev/null || true
|
||||
fi
|
||||
local predeploy_count=$(find "${PREDEPLOY_DIR}" -maxdepth 1 -type d | wc -l)
|
||||
log "Pre-deploy backups: $((predeploy_count - 1)) snapshots"
|
||||
fi
|
||||
|
||||
log_success "Cleanup complete"
|
||||
}
|
||||
|
||||
# Display backup summary
|
||||
show_summary() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "BACKUP SUMMARY"
|
||||
echo "=========================================="
|
||||
echo "Type: ${BACKUP_TYPE}"
|
||||
echo "Database: ${DB_NAME}"
|
||||
echo "Timestamp: ${TIMESTAMP}"
|
||||
echo ""
|
||||
echo "Disk Usage:"
|
||||
du -sh "${BACKUP_ROOT}"/* 2>/dev/null || echo "No backups yet"
|
||||
echo ""
|
||||
echo "Latest backup: $(readlink -f ${BACKUP_ROOT}/latest_db.sql.gz 2>/dev/null || echo 'None')"
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
echo "=========================================="
|
||||
echo "IGNY8 Database Backup"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
create_dirs
|
||||
|
||||
local backup_path=$(get_backup_path)
|
||||
|
||||
if backup_database "${backup_path}"; then
|
||||
cleanup_old_backups
|
||||
show_summary
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
150
scripts/ops/backup-full.sh
Normal file
150
scripts/ops/backup-full.sh
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Full Backup Script
|
||||
# =============================================================================
|
||||
# Creates complete backup: database + configuration + code snapshot
|
||||
# Usage: ./backup-full.sh [weekly|monthly]
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
BACKUP_TYPE="${1:-weekly}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
APP_DIR="/data/app/igny8"
|
||||
BACKUP_ROOT="/data/backups"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
log "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
log "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Determine backup directory
|
||||
case $BACKUP_TYPE in
|
||||
weekly)
|
||||
BACKUP_DIR="${BACKUP_ROOT}/weekly/week_$(date +%Y_%W)"
|
||||
;;
|
||||
monthly)
|
||||
BACKUP_DIR="${BACKUP_ROOT}/monthly/$(date +%Y_%m)"
|
||||
;;
|
||||
*)
|
||||
log_error "Invalid backup type. Use: weekly or monthly"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
echo "=========================================="
|
||||
echo "IGNY8 Full Backup"
|
||||
echo "=========================================="
|
||||
echo "Type: ${BACKUP_TYPE}"
|
||||
echo "Destination: ${BACKUP_DIR}"
|
||||
echo ""
|
||||
|
||||
# Step 1: Database backup
|
||||
log "Step 1/4: Backing up database..."
|
||||
/data/app/igny8/scripts/ops/backup-db.sh "${BACKUP_TYPE}" || {
|
||||
log_error "Database backup failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Step 2: Configuration backup
|
||||
log "Step 2/4: Backing up configuration files..."
|
||||
CONFIG_BACKUP="${BACKUP_DIR}/config_${TIMESTAMP}.tar.gz"
|
||||
tar -czf "${CONFIG_BACKUP}" \
|
||||
-C "${APP_DIR}" \
|
||||
.env \
|
||||
.env.staging 2>/dev/null \
|
||||
docker-compose.app.yml \
|
||||
docker-compose.staging.yml 2>/dev/null \
|
||||
--ignore-failed-read || true
|
||||
|
||||
if [[ -f "${CONFIG_BACKUP}" ]]; then
|
||||
log_success "Config backup: ${CONFIG_BACKUP}"
|
||||
else
|
||||
log_error "Config backup failed"
|
||||
fi
|
||||
|
||||
# Step 3: Code snapshot (without node_modules, __pycache__, etc.)
|
||||
log "Step 3/4: Creating code snapshot..."
|
||||
CODE_BACKUP="${BACKUP_DIR}/code_${TIMESTAMP}.tar.gz"
|
||||
tar -czf "${CODE_BACKUP}" \
|
||||
-C "/data/app" \
|
||||
--exclude='igny8/node_modules' \
|
||||
--exclude='igny8/frontend/node_modules' \
|
||||
--exclude='igny8/__pycache__' \
|
||||
--exclude='igny8/**/__pycache__' \
|
||||
--exclude='igny8/*.pyc' \
|
||||
--exclude='igny8/**/*.pyc' \
|
||||
--exclude='igny8/.git' \
|
||||
--exclude='igny8/backend/staticfiles' \
|
||||
--exclude='igny8/frontend/dist' \
|
||||
--exclude='igny8/backend/media' \
|
||||
--exclude='igny8/logs' \
|
||||
igny8/ 2>/dev/null || true
|
||||
|
||||
if [[ -f "${CODE_BACKUP}" ]]; then
|
||||
local size=$(du -h "${CODE_BACKUP}" | cut -f1)
|
||||
log_success "Code backup: ${CODE_BACKUP} (${size})"
|
||||
else
|
||||
log_error "Code backup failed"
|
||||
fi
|
||||
|
||||
# Step 4: Media files (if they exist)
|
||||
log "Step 4/4: Backing up media files..."
|
||||
MEDIA_DIR="${APP_DIR}/backend/media"
|
||||
if [[ -d "${MEDIA_DIR}" ]] && [[ "$(ls -A ${MEDIA_DIR})" ]]; then
|
||||
MEDIA_BACKUP="${BACKUP_DIR}/media_${TIMESTAMP}.tar.gz"
|
||||
tar -czf "${MEDIA_BACKUP}" -C "${APP_DIR}/backend" media/ 2>/dev/null || true
|
||||
if [[ -f "${MEDIA_BACKUP}" ]]; then
|
||||
local media_size=$(du -h "${MEDIA_BACKUP}" | cut -f1)
|
||||
log_success "Media backup: ${MEDIA_BACKUP} (${media_size})"
|
||||
fi
|
||||
else
|
||||
log "No media files to backup"
|
||||
fi
|
||||
|
||||
# Create manifest
|
||||
MANIFEST="${BACKUP_DIR}/manifest.txt"
|
||||
cat > "${MANIFEST}" << EOF
|
||||
IGNY8 Full Backup Manifest
|
||||
===========================
|
||||
Timestamp: ${TIMESTAMP}
|
||||
Type: ${BACKUP_TYPE}
|
||||
Server: $(hostname)
|
||||
|
||||
Files:
|
||||
$(ls -la "${BACKUP_DIR}")
|
||||
|
||||
Sizes:
|
||||
$(du -sh "${BACKUP_DIR}"/*)
|
||||
|
||||
Git Info:
|
||||
$(cd "${APP_DIR}" && git log -1 --format='Commit: %H%nDate: %ci%nMessage: %s' 2>/dev/null || echo 'Not a git repo')
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "BACKUP COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Location: ${BACKUP_DIR}"
|
||||
echo ""
|
||||
echo "Contents:"
|
||||
ls -lh "${BACKUP_DIR}"
|
||||
echo ""
|
||||
echo "Total size: $(du -sh ${BACKUP_DIR} | cut -f1)"
|
||||
echo "=========================================="
|
||||
145
scripts/ops/deploy-production.sh
Normal file
145
scripts/ops/deploy-production.sh
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Deploy to Production
|
||||
# =============================================================================
|
||||
# Safely deploys code to production with backup and rollback capability
|
||||
# Usage: ./deploy-production.sh
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
APP_DIR="/data/app/igny8"
|
||||
COMPOSE_FILE="docker-compose.app.yml"
|
||||
PROJECT_NAME="igny8-app"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "IGNY8 PRODUCTION DEPLOYMENT"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Safety confirmation
|
||||
log_warn "This will deploy to PRODUCTION!"
|
||||
log_warn "Make sure you have tested on staging first."
|
||||
echo ""
|
||||
read -p "Type 'deploy' to continue: " confirm
|
||||
if [[ "${confirm}" != "deploy" ]]; then
|
||||
log "Deployment cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "${APP_DIR}"
|
||||
|
||||
# Verify on main branch
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
if [[ "${CURRENT_BRANCH}" != "main" ]]; then
|
||||
log_error "Not on main branch (currently on: ${CURRENT_BRANCH})"
|
||||
log "Switch to main: git checkout main && git pull"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Create pre-deploy backup
|
||||
log "Step 1/8: Creating pre-deploy backup..."
|
||||
/data/app/igny8/scripts/ops/backup-db.sh pre-deploy
|
||||
log_success "Backup created"
|
||||
|
||||
# Step 2: Tag current images for rollback
|
||||
log "Step 2/8: Tagging current images for rollback..."
|
||||
docker tag igny8-backend:latest igny8-backend:rollback 2>/dev/null || true
|
||||
docker tag igny8-frontend-dev:latest igny8-frontend-dev:rollback 2>/dev/null || true
|
||||
docker tag igny8-marketing-dev:latest igny8-marketing-dev:rollback 2>/dev/null || true
|
||||
log_success "Rollback images tagged"
|
||||
|
||||
# Step 3: Pull latest code
|
||||
log "Step 3/8: Pulling latest code..."
|
||||
git pull origin main
|
||||
log_success "Code updated"
|
||||
|
||||
# Step 4: Build new images
|
||||
log "Step 4/8: Building new images..."
|
||||
docker build -t igny8-backend:latest -f backend/Dockerfile backend/
|
||||
docker build -t igny8-frontend-dev:latest -f frontend/Dockerfile.dev frontend/
|
||||
docker build -t igny8-marketing-dev:latest -f frontend/Dockerfile.marketing.dev frontend/
|
||||
log_success "Images built"
|
||||
|
||||
# Step 5: Check for pending migrations
|
||||
log "Step 5/8: Checking for pending migrations..."
|
||||
PENDING=$(docker exec igny8_backend python manage.py showmigrations --plan 2>/dev/null | grep "\[ \]" || true)
|
||||
if [[ -n "${PENDING}" ]]; then
|
||||
log_warn "Pending migrations found:"
|
||||
echo "${PENDING}"
|
||||
read -p "Apply migrations? (yes/no): " apply_migrations
|
||||
if [[ "${apply_migrations}" != "yes" ]]; then
|
||||
log "Skipping migrations"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 6: Restart containers
|
||||
log "Step 6/8: Restarting containers..."
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d
|
||||
|
||||
# Step 7: Wait for backend to be healthy
|
||||
log "Step 7/8: Waiting for backend to be healthy..."
|
||||
for i in {1..30}; do
|
||||
if docker exec igny8_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then
|
||||
log_success "Backend is healthy"
|
||||
break
|
||||
fi
|
||||
if [[ $i -eq 30 ]]; then
|
||||
log_error "Backend failed to become healthy"
|
||||
log_error "ROLLING BACK..."
|
||||
/data/app/igny8/scripts/ops/rollback.sh --auto
|
||||
exit 1
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Step 8: Apply migrations and collect static
|
||||
log "Step 8/8: Applying migrations and collecting static..."
|
||||
if [[ "${apply_migrations}" == "yes" ]] || [[ -z "${PENDING}" ]]; then
|
||||
docker exec igny8_backend python manage.py migrate --noinput
|
||||
fi
|
||||
docker exec igny8_backend python manage.py collectstatic --noinput
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
log_success "PRODUCTION DEPLOYMENT COMPLETE"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Production is live!"
|
||||
echo " App: https://app.igny8.com"
|
||||
echo " API: https://api.igny8.com"
|
||||
echo ""
|
||||
echo "Monitor logs for 10 minutes:"
|
||||
echo " docker compose -f ${COMPOSE_FILE} -p ${PROJECT_NAME} logs -f"
|
||||
echo ""
|
||||
echo "If issues occur, rollback:"
|
||||
echo " /data/app/igny8/scripts/ops/rollback.sh"
|
||||
echo ""
|
||||
echo "Container status:"
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps
|
||||
echo "=========================================="
|
||||
125
scripts/ops/deploy-staging.sh
Normal file
125
scripts/ops/deploy-staging.sh
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Deploy to Staging
|
||||
# =============================================================================
|
||||
# Safely deploys code to staging environment
|
||||
# Usage: ./deploy-staging.sh
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
APP_DIR="/data/app/igny8"
|
||||
COMPOSE_FILE="docker-compose.staging.yml"
|
||||
PROJECT_NAME="igny8-staging"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "IGNY8 STAGING DEPLOYMENT"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
cd "${APP_DIR}"
|
||||
|
||||
# Check if staging compose file exists
|
||||
if [[ ! -f "${COMPOSE_FILE}" ]]; then
|
||||
log_error "Staging compose file not found: ${COMPOSE_FILE}"
|
||||
log "Please create it from the documentation template"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if .env.staging exists
|
||||
if [[ ! -f ".env.staging" ]]; then
|
||||
log_error "Staging environment file not found: .env.staging"
|
||||
log "Please create it from .env.example"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Pull latest code
|
||||
log "Step 1/6: Pulling latest code..."
|
||||
git fetch origin
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
log "Current branch: ${CURRENT_BRANCH}"
|
||||
|
||||
if [[ "${CURRENT_BRANCH}" == "staging" ]]; then
|
||||
git pull origin staging
|
||||
elif [[ "${CURRENT_BRANCH}" == "main" ]]; then
|
||||
log_warn "On main branch, switching to staging..."
|
||||
git checkout staging
|
||||
git pull origin staging
|
||||
else
|
||||
log_warn "On feature branch '${CURRENT_BRANCH}', not pulling..."
|
||||
fi
|
||||
|
||||
# Step 2: Build staging images
|
||||
log "Step 2/6: Building staging images..."
|
||||
docker build -t igny8-backend:staging -f backend/Dockerfile backend/
|
||||
docker build -t igny8-frontend-dev:staging -f frontend/Dockerfile.dev frontend/
|
||||
docker build -t igny8-marketing-dev:staging -f frontend/Dockerfile.marketing.dev frontend/
|
||||
log_success "Images built"
|
||||
|
||||
# Step 3: Stop existing staging containers
|
||||
log "Step 3/6: Stopping existing staging containers..."
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down 2>/dev/null || true
|
||||
|
||||
# Step 4: Start staging containers
|
||||
log "Step 4/6: Starting staging containers..."
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d
|
||||
|
||||
# Step 5: Wait for backend to be healthy
|
||||
log "Step 5/6: Waiting for backend to be healthy..."
|
||||
for i in {1..30}; do
|
||||
if docker exec igny8_staging_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then
|
||||
log_success "Backend is healthy"
|
||||
break
|
||||
fi
|
||||
if [[ $i -eq 30 ]]; then
|
||||
log_error "Backend failed to become healthy"
|
||||
docker logs igny8_staging_backend --tail 50
|
||||
exit 1
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Step 6: Run migrations
|
||||
log "Step 6/6: Running database migrations..."
|
||||
docker exec igny8_staging_backend python manage.py migrate --noinput
|
||||
docker exec igny8_staging_backend python manage.py collectstatic --noinput
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
log_success "STAGING DEPLOYMENT COMPLETE"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Staging environment is ready:"
|
||||
echo " App: https://staging.igny8.com"
|
||||
echo " API: https://staging-api.igny8.com"
|
||||
echo ""
|
||||
echo "View logs:"
|
||||
echo " docker compose -f ${COMPOSE_FILE} -p ${PROJECT_NAME} logs -f"
|
||||
echo ""
|
||||
echo "Container status:"
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps
|
||||
echo "=========================================="
|
||||
199
scripts/ops/health-check.sh
Normal file
199
scripts/ops/health-check.sh
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Health Check Script
|
||||
# =============================================================================
|
||||
# Checks health of all services and reports status
|
||||
# Usage: ./health-check.sh [--quiet]
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
QUIET="${1}"
|
||||
EXIT_CODE=0
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Output functions
|
||||
output() {
|
||||
if [[ "${QUIET}" != "--quiet" ]]; then
|
||||
echo -e "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
check_passed() {
|
||||
output "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
check_failed() {
|
||||
output "${RED}❌ $1${NC}"
|
||||
EXIT_CODE=1
|
||||
}
|
||||
|
||||
check_warn() {
|
||||
output "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
# Check HTTP endpoint
|
||||
check_endpoint() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local timeout="${3:-10}"
|
||||
|
||||
local response
|
||||
local http_code
|
||||
local time_total
|
||||
|
||||
response=$(curl -s -o /dev/null -w "%{http_code}|%{time_total}" --max-time "${timeout}" "${url}" 2>/dev/null || echo "000|0")
|
||||
http_code=$(echo "${response}" | cut -d'|' -f1)
|
||||
time_total=$(echo "${response}" | cut -d'|' -f2)
|
||||
|
||||
if [[ "${http_code}" == "200" ]]; then
|
||||
check_passed "${name}: OK (${time_total}s)"
|
||||
return 0
|
||||
else
|
||||
check_failed "${name}: FAILED (HTTP ${http_code})"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check container status
|
||||
check_container() {
|
||||
local name="$1"
|
||||
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${name}$"; then
|
||||
local status=$(docker inspect --format='{{.State.Health.Status}}' "${name}" 2>/dev/null || echo "none")
|
||||
if [[ "${status}" == "healthy" ]]; then
|
||||
check_passed "Container ${name}: Running (healthy)"
|
||||
elif [[ "${status}" == "none" ]]; then
|
||||
check_passed "Container ${name}: Running (no healthcheck)"
|
||||
else
|
||||
check_warn "Container ${name}: Running (${status})"
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
check_failed "Container ${name}: NOT RUNNING"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check disk space
|
||||
check_disk() {
|
||||
local path="$1"
|
||||
local threshold="${2:-80}"
|
||||
|
||||
local usage=$(df "${path}" | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||
|
||||
if [[ ${usage} -lt ${threshold} ]]; then
|
||||
check_passed "Disk ${path}: ${usage}% used"
|
||||
elif [[ ${usage} -lt 90 ]]; then
|
||||
check_warn "Disk ${path}: ${usage}% used (warning)"
|
||||
else
|
||||
check_failed "Disk ${path}: ${usage}% used (critical)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check Redis connection
|
||||
check_redis() {
|
||||
if docker exec redis redis-cli ping 2>/dev/null | grep -q "PONG"; then
|
||||
check_passed "Redis: Connected"
|
||||
return 0
|
||||
else
|
||||
check_failed "Redis: NOT RESPONDING"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check PostgreSQL connection
|
||||
check_postgres() {
|
||||
if docker exec postgres pg_isready -U igny8 2>/dev/null | grep -q "accepting"; then
|
||||
check_passed "PostgreSQL: Accepting connections"
|
||||
return 0
|
||||
else
|
||||
check_failed "PostgreSQL: NOT READY"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check Celery queue
|
||||
check_celery_queue() {
|
||||
local queue_length=$(docker exec redis redis-cli llen celery 2>/dev/null || echo "error")
|
||||
|
||||
if [[ "${queue_length}" == "error" ]]; then
|
||||
check_warn "Celery queue: Cannot check"
|
||||
elif [[ ${queue_length} -lt 50 ]]; then
|
||||
check_passed "Celery queue: ${queue_length} tasks"
|
||||
elif [[ ${queue_length} -lt 200 ]]; then
|
||||
check_warn "Celery queue: ${queue_length} tasks (high)"
|
||||
else
|
||||
check_failed "Celery queue: ${queue_length} tasks (critical)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main health check
|
||||
main() {
|
||||
if [[ "${QUIET}" != "--quiet" ]]; then
|
||||
echo "=========================================="
|
||||
echo "IGNY8 Health Check"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Infrastructure
|
||||
output "INFRASTRUCTURE:"
|
||||
check_postgres
|
||||
check_redis
|
||||
check_disk "/data"
|
||||
echo ""
|
||||
|
||||
# Production containers
|
||||
output "PRODUCTION CONTAINERS:"
|
||||
check_container "igny8_backend"
|
||||
check_container "igny8_frontend"
|
||||
check_container "igny8_celery_worker"
|
||||
check_container "igny8_celery_beat"
|
||||
echo ""
|
||||
|
||||
# Production endpoints
|
||||
output "PRODUCTION ENDPOINTS:"
|
||||
check_endpoint "API Status" "http://localhost:8011/api/v1/system/status/" 5
|
||||
check_endpoint "Frontend" "http://localhost:8021" 5
|
||||
echo ""
|
||||
|
||||
# Celery
|
||||
output "BACKGROUND TASKS:"
|
||||
check_celery_queue
|
||||
echo ""
|
||||
|
||||
# Staging (if running)
|
||||
if docker ps --format '{{.Names}}' | grep -q "igny8_staging_backend"; then
|
||||
output "STAGING CONTAINERS:"
|
||||
check_container "igny8_staging_backend"
|
||||
check_container "igny8_staging_frontend"
|
||||
echo ""
|
||||
|
||||
output "STAGING ENDPOINTS:"
|
||||
check_endpoint "Staging API" "http://localhost:8012/api/v1/system/status/" 5
|
||||
check_endpoint "Staging Frontend" "http://localhost:8024" 5
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Summary
|
||||
if [[ "${QUIET}" != "--quiet" ]]; then
|
||||
echo "=========================================="
|
||||
if [[ ${EXIT_CODE} -eq 0 ]]; then
|
||||
echo -e "${GREEN}All checks passed${NC}"
|
||||
else
|
||||
echo -e "${RED}Some checks failed${NC}"
|
||||
fi
|
||||
echo "=========================================="
|
||||
fi
|
||||
|
||||
exit ${EXIT_CODE}
|
||||
}
|
||||
|
||||
main "$@"
|
||||
49
scripts/ops/igny8-cron
Normal file
49
scripts/ops/igny8-cron
Normal file
@@ -0,0 +1,49 @@
|
||||
# =============================================================================
|
||||
# IGNY8 Automated Tasks (Cron Configuration)
|
||||
# =============================================================================
|
||||
# Install: sudo cp igny8-cron /etc/cron.d/igny8
|
||||
# Verify: sudo crontab -l -u root
|
||||
# =============================================================================
|
||||
|
||||
# Environment
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||
|
||||
# =============================================================================
|
||||
# BACKUP JOBS
|
||||
# =============================================================================
|
||||
|
||||
# Daily database backup at 1:00 AM
|
||||
0 1 * * * root /data/app/igny8/scripts/ops/backup-db.sh daily >> /data/logs/backup.log 2>&1
|
||||
|
||||
# Weekly full backup on Sunday at 2:00 AM
|
||||
0 2 * * 0 root /data/app/igny8/scripts/ops/backup-full.sh weekly >> /data/logs/backup.log 2>&1
|
||||
|
||||
# Monthly full backup on 1st of month at 3:00 AM
|
||||
0 3 1 * * root /data/app/igny8/scripts/ops/backup-full.sh monthly >> /data/logs/backup.log 2>&1
|
||||
|
||||
# =============================================================================
|
||||
# HEALTH & MONITORING
|
||||
# =============================================================================
|
||||
|
||||
# Health check every 5 minutes (logs only failures)
|
||||
*/5 * * * * root /data/app/igny8/scripts/ops/health-check.sh --quiet || echo "[$(date)] Health check failed" >> /data/logs/health.log 2>&1
|
||||
|
||||
# =============================================================================
|
||||
# MAINTENANCE
|
||||
# =============================================================================
|
||||
|
||||
# Log rotation daily at midnight
|
||||
0 0 * * * root /data/app/igny8/scripts/ops/log-rotate.sh >> /data/logs/maintenance.log 2>&1
|
||||
|
||||
# Docker cleanup weekly on Saturday at 4:00 AM
|
||||
0 4 * * 6 root docker system prune -f >> /data/logs/maintenance.log 2>&1
|
||||
|
||||
# =============================================================================
|
||||
# OPTIONAL: External backup sync (uncomment if using remote backup)
|
||||
# =============================================================================
|
||||
|
||||
# Sync backups to remote storage daily at 5:00 AM
|
||||
# 0 5 * * * root rsync -avz /data/backups/ user@backup-server:/backups/igny8/ >> /data/logs/backup.log 2>&1
|
||||
|
||||
# =============================================================================
|
||||
108
scripts/ops/log-rotate.sh
Normal file
108
scripts/ops/log-rotate.sh
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Log Rotation Script
|
||||
# =============================================================================
|
||||
# Rotates and compresses old log files
|
||||
# Usage: ./log-rotate.sh
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
LOG_DIR="/data/logs"
|
||||
ARCHIVE_DIR="${LOG_DIR}/archive"
|
||||
RETENTION_DAYS=30
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "IGNY8 Log Rotation"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Create archive directory
|
||||
mkdir -p "${ARCHIVE_DIR}"
|
||||
|
||||
# Rotate function
|
||||
rotate_log() {
|
||||
local log_file="$1"
|
||||
local max_size="${2:-50M}"
|
||||
|
||||
if [[ ! -f "${log_file}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local size=$(stat -f%z "${log_file}" 2>/dev/null || stat -c%s "${log_file}" 2>/dev/null || echo 0)
|
||||
local max_bytes=$((50 * 1024 * 1024)) # 50MB
|
||||
|
||||
if [[ ${size} -gt ${max_bytes} ]]; then
|
||||
local basename=$(basename "${log_file}")
|
||||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local archive_file="${ARCHIVE_DIR}/${basename}.${timestamp}.gz"
|
||||
|
||||
log "Rotating ${log_file} ($(numfmt --to=iec ${size}))"
|
||||
gzip -c "${log_file}" > "${archive_file}"
|
||||
> "${log_file}" # Truncate original
|
||||
log_success "Archived to ${archive_file}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Rotate production logs
|
||||
log "Checking production logs..."
|
||||
for log_file in "${LOG_DIR}/production"/*.log; do
|
||||
[[ -f "${log_file}" ]] && rotate_log "${log_file}"
|
||||
done
|
||||
|
||||
# Rotate staging logs
|
||||
log "Checking staging logs..."
|
||||
for log_file in "${LOG_DIR}/staging"/*.log; do
|
||||
[[ -f "${log_file}" ]] && rotate_log "${log_file}"
|
||||
done
|
||||
|
||||
# Rotate Caddy logs
|
||||
log "Checking Caddy logs..."
|
||||
for log_file in "${LOG_DIR}/caddy"/*.log; do
|
||||
[[ -f "${log_file}" ]] && rotate_log "${log_file}"
|
||||
done
|
||||
|
||||
# Clean up old archives
|
||||
log "Cleaning old archives (> ${RETENTION_DAYS} days)..."
|
||||
DELETED=$(find "${ARCHIVE_DIR}" -name "*.gz" -mtime +${RETENTION_DAYS} -delete -print | wc -l)
|
||||
if [[ ${DELETED} -gt 0 ]]; then
|
||||
log "Deleted ${DELETED} old archive files"
|
||||
fi
|
||||
|
||||
# Docker container logs
|
||||
log "Pruning Docker container logs..."
|
||||
for container in igny8_backend igny8_frontend igny8_celery_worker igny8_celery_beat; do
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then
|
||||
LOG_FILE=$(docker inspect --format='{{.LogPath}}' "${container}" 2>/dev/null || true)
|
||||
if [[ -n "${LOG_FILE}" ]] && [[ -f "${LOG_FILE}" ]]; then
|
||||
local size=$(stat -c%s "${LOG_FILE}" 2>/dev/null || echo 0)
|
||||
if [[ ${size} -gt $((100 * 1024 * 1024)) ]]; then # 100MB
|
||||
log "Truncating Docker log for ${container}"
|
||||
truncate -s 0 "${LOG_FILE}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Report disk usage
|
||||
echo ""
|
||||
echo "Log disk usage:"
|
||||
du -sh "${LOG_DIR}"/* 2>/dev/null || echo "No logs found"
|
||||
|
||||
echo ""
|
||||
log_success "Log rotation complete"
|
||||
echo "=========================================="
|
||||
142
scripts/ops/restore-db.sh
Normal file
142
scripts/ops/restore-db.sh
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Database Restore Script
|
||||
# =============================================================================
|
||||
# Usage: ./restore-db.sh <backup_file> [target_db]
|
||||
# Restores database from a backup file (.sql or .sql.gz)
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
BACKUP_FILE="${1}"
|
||||
TARGET_DB="${2:-igny8_db}"
|
||||
DB_CONTAINER="postgres"
|
||||
DB_USER="igny8"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
log "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
log "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
log "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Validate input
|
||||
if [[ -z "${BACKUP_FILE}" ]]; then
|
||||
echo "Usage: ./restore-db.sh <backup_file> [target_db]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " backup_file Path to backup file (.sql or .sql.gz)"
|
||||
echo " target_db Target database (default: igny8_db)"
|
||||
echo ""
|
||||
echo "Available backups:"
|
||||
ls -la /data/backups/latest_db.sql.gz 2>/dev/null || echo " No latest backup found"
|
||||
echo ""
|
||||
find /data/backups -name "*.sql.gz" -type f 2>/dev/null | head -10
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${BACKUP_FILE}" ]]; then
|
||||
log_error "Backup file not found: ${BACKUP_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "IGNY8 Database Restore"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Backup file: ${BACKUP_FILE}"
|
||||
echo "Target DB: ${TARGET_DB}"
|
||||
echo ""
|
||||
|
||||
# Safety confirmation
|
||||
log_warn "This will REPLACE the database '${TARGET_DB}' with backup data!"
|
||||
read -p "Are you sure? Type 'yes' to continue: " confirm
|
||||
if [[ "${confirm}" != "yes" ]]; then
|
||||
log "Restore cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if postgres container is running
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then
|
||||
log_error "PostgreSQL container '${DB_CONTAINER}' is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create pre-restore backup
|
||||
log "Creating pre-restore backup of current database..."
|
||||
PRE_RESTORE_BACKUP="/data/backups/pre-restore_${TARGET_DB}_$(date +%Y%m%d_%H%M%S).sql.gz"
|
||||
if docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${TARGET_DB}" 2>/dev/null | gzip > "${PRE_RESTORE_BACKUP}"; then
|
||||
log_success "Pre-restore backup: ${PRE_RESTORE_BACKUP}"
|
||||
else
|
||||
log_warn "Could not create pre-restore backup (database might not exist)"
|
||||
fi
|
||||
|
||||
# Stop application containers to prevent connections
|
||||
log "Stopping application containers..."
|
||||
docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app stop igny8_backend igny8_celery_worker igny8_celery_beat 2>/dev/null || true
|
||||
|
||||
# Drop and recreate database
|
||||
log "Dropping existing database..."
|
||||
docker exec "${DB_CONTAINER}" psql -U postgres -c "DROP DATABASE IF EXISTS ${TARGET_DB};" || true
|
||||
|
||||
log "Creating fresh database..."
|
||||
docker exec "${DB_CONTAINER}" psql -U postgres -c "CREATE DATABASE ${TARGET_DB} OWNER ${DB_USER};"
|
||||
|
||||
# Restore from backup
|
||||
log "Restoring database from backup..."
|
||||
if [[ "${BACKUP_FILE}" == *.gz ]]; then
|
||||
# Compressed backup
|
||||
gunzip -c "${BACKUP_FILE}" | docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}"
|
||||
else
|
||||
# Uncompressed backup
|
||||
docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}" < "${BACKUP_FILE}"
|
||||
fi
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
log_success "Database restore complete"
|
||||
else
|
||||
log_error "Database restore failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Restart application containers
|
||||
log "Restarting application containers..."
|
||||
docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app start igny8_backend igny8_celery_worker igny8_celery_beat 2>/dev/null || true
|
||||
|
||||
# Wait for backend to be healthy
|
||||
log "Waiting for backend to be healthy..."
|
||||
sleep 10
|
||||
|
||||
# Verify restoration
|
||||
log "Verifying database..."
|
||||
TABLE_COUNT=$(docker exec "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';")
|
||||
log_success "Database restored with ${TABLE_COUNT} tables"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "RESTORE COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Database '${TARGET_DB}' has been restored"
|
||||
echo ""
|
||||
echo "Pre-restore backup saved to:"
|
||||
echo " ${PRE_RESTORE_BACKUP}"
|
||||
echo ""
|
||||
echo "To rollback this restore, run:"
|
||||
echo " ./restore-db.sh ${PRE_RESTORE_BACKUP} ${TARGET_DB}"
|
||||
echo "=========================================="
|
||||
108
scripts/ops/rollback.sh
Normal file
108
scripts/ops/rollback.sh
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Rollback Script
|
||||
# =============================================================================
|
||||
# Rolls back production to previous version using tagged images
|
||||
# Usage: ./rollback.sh [--auto]
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
APP_DIR="/data/app/igny8"
|
||||
COMPOSE_FILE="docker-compose.app.yml"
|
||||
PROJECT_NAME="igny8-app"
|
||||
AUTO_MODE="${1}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "IGNY8 PRODUCTION ROLLBACK"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check for rollback images
|
||||
if ! docker image inspect igny8-backend:rollback &>/dev/null; then
|
||||
log_error "No rollback image found!"
|
||||
log "Rollback images are created during deployment."
|
||||
log "To restore from backup, use:"
|
||||
log " /data/app/igny8/scripts/ops/restore-db.sh /data/backups/latest_db.sql.gz"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Confirmation (unless auto mode)
|
||||
if [[ "${AUTO_MODE}" != "--auto" ]]; then
|
||||
log_warn "This will rollback production to the previous version!"
|
||||
read -p "Type 'rollback' to continue: " confirm
|
||||
if [[ "${confirm}" != "rollback" ]]; then
|
||||
log "Rollback cancelled"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
cd "${APP_DIR}"
|
||||
|
||||
# Step 1: Stop current containers
|
||||
log "Step 1/4: Stopping current containers..."
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down
|
||||
|
||||
# Step 2: Restore rollback images
|
||||
log "Step 2/4: Restoring previous images..."
|
||||
docker tag igny8-backend:rollback igny8-backend:latest
|
||||
docker tag igny8-frontend-dev:rollback igny8-frontend-dev:latest
|
||||
docker tag igny8-marketing-dev:rollback igny8-marketing-dev:latest 2>/dev/null || true
|
||||
log_success "Images restored"
|
||||
|
||||
# Step 3: Start containers
|
||||
log "Step 3/4: Starting containers with previous version..."
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d
|
||||
|
||||
# Step 4: Wait for backend
|
||||
log "Step 4/4: Waiting for backend to be healthy..."
|
||||
for i in {1..30}; do
|
||||
if docker exec igny8_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then
|
||||
log_success "Backend is healthy"
|
||||
break
|
||||
fi
|
||||
if [[ $i -eq 30 ]]; then
|
||||
log_error "Backend failed to start after rollback"
|
||||
log "Manual intervention required!"
|
||||
exit 1
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
log_success "ROLLBACK COMPLETE"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Production is running on previous version."
|
||||
echo ""
|
||||
echo "If database rollback is also needed:"
|
||||
echo " /data/app/igny8/scripts/ops/restore-db.sh /data/backups/pre-deploy/LATEST/db_igny8_*.sql.gz"
|
||||
echo ""
|
||||
echo "Container status:"
|
||||
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps
|
||||
echo "=========================================="
|
||||
127
scripts/ops/sync-prod-to-staging.sh
Normal file
127
scripts/ops/sync-prod-to-staging.sh
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# IGNY8 Sync Production Data to Staging
|
||||
# =============================================================================
|
||||
# Syncs production database to staging with data sanitization
|
||||
# Usage: ./sync-prod-to-staging.sh
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
DB_CONTAINER="postgres"
|
||||
PROD_DB="igny8_db"
|
||||
STAGING_DB="igny8_staging_db"
|
||||
DB_USER="igny8"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "SYNC PRODUCTION → STAGING"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
log_warn "This will REPLACE staging database with production data!"
|
||||
log_warn "Data will be sanitized (emails, passwords anonymized)"
|
||||
echo ""
|
||||
read -p "Type 'sync' to continue: " confirm
|
||||
if [[ "${confirm}" != "sync" ]]; then
|
||||
log "Sync cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if staging containers are running
|
||||
STAGING_RUNNING=$(docker ps --format '{{.Names}}' | grep -c "igny8_staging" || true)
|
||||
if [[ ${STAGING_RUNNING} -gt 0 ]]; then
|
||||
log "Stopping staging containers..."
|
||||
docker compose -f /data/app/igny8/docker-compose.staging.yml -p igny8-staging stop igny8_staging_backend igny8_staging_celery_worker igny8_staging_celery_beat 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Step 1: Dump production
|
||||
log "Step 1/5: Dumping production database..."
|
||||
TEMP_DUMP="/tmp/prod_sync_$(date +%Y%m%d_%H%M%S).sql"
|
||||
docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${PROD_DB}" > "${TEMP_DUMP}"
|
||||
log_success "Production dump created: ${TEMP_DUMP}"
|
||||
|
||||
# Step 2: Drop staging database
|
||||
log "Step 2/5: Dropping staging database..."
|
||||
docker exec "${DB_CONTAINER}" psql -U postgres -c "DROP DATABASE IF EXISTS ${STAGING_DB};"
|
||||
docker exec "${DB_CONTAINER}" psql -U postgres -c "CREATE DATABASE ${STAGING_DB} OWNER ${DB_USER};"
|
||||
log_success "Staging database recreated"
|
||||
|
||||
# Step 3: Restore to staging
|
||||
log "Step 3/5: Restoring to staging database..."
|
||||
docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${STAGING_DB}" < "${TEMP_DUMP}"
|
||||
log_success "Data restored to staging"
|
||||
|
||||
# Step 4: Sanitize data
|
||||
log "Step 4/5: Sanitizing staging data..."
|
||||
docker exec "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${STAGING_DB}" << 'EOF'
|
||||
-- Sanitize user emails (keep superusers intact)
|
||||
UPDATE auth_user
|
||||
SET email = CONCAT('staging_', id, '@igny8.test')
|
||||
WHERE is_superuser = FALSE;
|
||||
|
||||
-- Invalidate all sessions
|
||||
DELETE FROM django_session;
|
||||
|
||||
-- Update notification endpoints to staging
|
||||
UPDATE notification_settings
|
||||
SET webhook_url = REPLACE(webhook_url, 'app.igny8.com', 'staging.igny8.com')
|
||||
WHERE webhook_url IS NOT NULL;
|
||||
|
||||
-- Log sanitization
|
||||
SELECT 'Sanitized ' || COUNT(*) || ' user emails' FROM auth_user WHERE email LIKE '%@igny8.test';
|
||||
EOF
|
||||
log_success "Data sanitized"
|
||||
|
||||
# Step 5: Run migrations (in case staging has newer schema)
|
||||
log "Step 5/5: Checking migrations..."
|
||||
if docker ps --format '{{.Names}}' | grep -q "igny8_staging_backend"; then
|
||||
docker exec igny8_staging_backend python manage.py migrate --noinput 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "${TEMP_DUMP}"
|
||||
|
||||
# Restart staging
|
||||
if [[ ${STAGING_RUNNING} -gt 0 ]]; then
|
||||
log "Restarting staging containers..."
|
||||
docker compose -f /data/app/igny8/docker-compose.staging.yml -p igny8-staging start igny8_staging_backend igny8_staging_celery_worker igny8_staging_celery_beat 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
log_success "SYNC COMPLETE"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Staging database now contains sanitized production data."
|
||||
echo ""
|
||||
echo "Changes made:"
|
||||
echo " - User emails changed to staging_*@igny8.test"
|
||||
echo " - All sessions invalidated"
|
||||
echo " - Webhook URLs updated to staging domain"
|
||||
echo ""
|
||||
echo "To test with a specific user:"
|
||||
echo " docker exec -it igny8_staging_backend python manage.py changepassword <username>"
|
||||
echo "=========================================="
|
||||
Reference in New Issue
Block a user