Enterprise Oracle RMAN Backup Automation to Oracle Cloud Infrastructure

Database administrators face a critical challenge: ensuring reliable, cost-effective backup strategies that protect against both local hardware failures and provide efficient disaster recovery capabilities. This comprehensive guide presents an enterprise-grade solution that automates Oracle RMAN backups with intelligent optimization and seamless integration with Oracle Cloud Infrastructure (OCI) Object Storage.

Key Benefits of This Solution:

  • Automated full, incremental, and archive log backup strategies with parallel processing
  • Comprehensive error handling and validation for production reliability
  • Seamless cloud integration with automatic upload and verification
  • Built-in cleanup and maintenance procedures

The script addresses a common problem: traditional backup scripts often backup the same archive logs repeatedly, wasting storage space and network bandwidth. Our optimized solution ensures each archive log is backed up exactly once, dramatically improving efficiency.

Understanding the Backup Architecture

The Three-Tier Strategy

This solution implements a sophisticated backup approach that balances recovery objectives with operational efficiency:

Full Database Backups (Weekly Foundation)

Full backups create Level 0 incremental backups that serve as the foundation for all subsequent incremental backups. Using four parallel RMAN channels, these backups can reduce backup windows significantly. For example, a 500GB database that might take 4 hours to backup serially could complete in approximately 1.5 hours with parallel processing.

Incremental Backups (Daily Efficiency)

Level 1 incremental backups capture only changed data blocks since the last backup. In typical production environments where daily changes represent 5-10% of total database size, this approach can reduce backup storage by 90% compared to daily full backups.

Archive Log Backups (Continuous Protection with Smart Deduplication)

The revolutionary feature of this script is its intelligent archive log handling. Traditional approaches backup all available archive logs every execution, creating massive redundancy. Our solution uses RMAN's tag-based tracking to backup each archive log exactly once.

Execution Flow (Very Important)

START

main()

validate_environment

perform_rman_backup
├── fail → exit

upload_to_oci
├── fail → exit (backup kept locally)

cleanup_local

SUCCESS MESSAGE

END

The Complete Production Script

Here's the enterprise-ready script with dummy data for security:

#!/bin/bash
################################################################################
# Enterprise RMAN Backup to OCI - With Atomic Locking and 10-Hour Retention
# Location: /home/oracle/scripts/rman_oci_backup.sh
# Usage: ./rman_oci_backup.sh <FULL|INCREMENTAL|ARCHIVELOG>
# Database: ACMEPROD | Maintained by: Database Operations Team
################################################################################

# Source Oracle Environment FIRST (critical for consistent ORACLE_SID)
if [[ -f /home/oracle/.bash_profile ]]; then
    . /home/oracle/.bash_profile
else
    echo "ERROR: Cannot source Oracle environment from /home/oracle/.bash_profile"
    exit 1
fi

################################################################################
# MANDATORY PARAMETER VALIDATION
################################################################################

if [[ -z "$1" ]]; then
    echo "=========================================================="
    echo "ERROR: Backup type parameter is required!"
    echo "=========================================================="
    echo "Usage: $0 <FULL|INCREMENTAL|ARCHIVELOG>"
    echo ""
    echo "Examples:"
    echo "  $0 FULL        # Full database backup (Level 0)"
    echo "  $0 INCREMENTAL # Incremental backup (Level 1)"
    echo "  $0 ARCHIVELOG  # Archive log backup only"
    echo ""
    echo "Database: ${ORACLE_SID:-NOT SET}"
    echo "=========================================================="
    exit 1
fi

BACKUP_TYPE=$(echo "$1" | tr '[:lower:]' '[:upper:]')

# Validate backup type
case "$BACKUP_TYPE" in
    FULL|INCREMENTAL|ARCHIVELOG)
        # Valid backup type
        ;;
    *)
        echo "ERROR: Invalid backup type '$BACKUP_TYPE'"
        echo "Allowed types: FULL, INCREMENTAL, ARCHIVELOG"
        exit 1
        ;;
esac

################################################################################
# CONFIGURATION SECTION - UPDATE FOR YOUR ENVIRONMENT
################################################################################

# Directory Configuration
BACKUP_BASE="/u02/rman_staging"
LOG_DIR="/home/oracle/logs/backup_operations"

# OCI Configuration - UPDATE THESE VALUES
OCI_CONFIG_FILE="/home/oracle/.oci/config"
OCI_BUCKET="acme-prod-backups"
OCI_PREFIX="ACMEPROD_RMAN"

# Retention Configuration - UPDATED TO 10 HOURS
LOCAL_RETENTION_HOURS=10
LOG_RETENTION_DAYS=7

# System Variables
DATE_STAMP=$(date +%Y%m%d)
TIME_STAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="${BACKUP_BASE}/${DATE_STAMP}"
LOG_FILE="${LOG_DIR}/backup_${BACKUP_TYPE}_${TIME_STAMP}.log"

# Create required directories
mkdir -p "$LOG_DIR" "$BACKUP_DIR"

################################################################################
# ATOMIC LOCKING SYSTEM
################################################################################

# Validate ORACLE_SID before creating lock
if [[ -z "$ORACLE_SID" ]]; then
    echo "CRITICAL ERROR: ORACLE_SID is not set!"
    echo "This prevents proper lock file creation and could allow concurrent backups."
    echo "Please check your Oracle environment setup."
    exit 1
fi

# Lock file configuration - SINGLE GLOBAL LOCK PER DATABASE
LOCK_DIR="/var/lock/rman_oci"
LOCK_FILE="${LOCK_DIR}/rman_backup_${ORACLE_SID}_GLOBAL.lock"

# Create lock directory if it doesn't exist
if ! mkdir -p "$LOCK_DIR" 2>/dev/null; then
    # Fallback to tmp directory
    LOCK_DIR="/tmp/rman_oci_locks"
    LOCK_FILE="${LOCK_DIR}/rman_backup_${ORACLE_SID}_GLOBAL.lock"
    mkdir -p "$LOCK_DIR"
fi

# Open file descriptor 200 for locking
exec 200>"$LOCK_FILE"

################################################################################
# LOGGING FUNCTIONS
################################################################################

log_msg() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log_err() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" | tee -a "$LOG_FILE" >&2
}

log_ok() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" | tee -a "$LOG_FILE"
}

################################################################################
# COMPREHENSIVE LOCK MANAGEMENT
################################################################################

acquire_lock() {
    log_msg "=========================================================="
    log_msg "INITIALIZING ATOMIC BACKUP LOCK SYSTEM"
    log_msg "=========================================================="
    log_msg "Requested Backup Type: $BACKUP_TYPE"
    log_msg "Database SID: $ORACLE_SID"
    log_msg "Process ID: $$"
    log_msg "Lock File: $LOCK_FILE"
    log_msg "=========================================================="
    
    # Attempt to acquire exclusive lock (non-blocking)
    if ! flock -n 200; then
        log_err "=========================================================="
        log_err "BACKUP CONFLICT DETECTED - CANNOT PROCEED"
        log_err "=========================================================="
        
        # Try to get information about the conflicting process
        local lock_info=""
        if [[ -f "$LOCK_FILE" ]]; then
            lock_info=$(cat "$LOCK_FILE" 2>/dev/null)
        fi
        
        if [[ -n "$lock_info" ]]; then
            log_err "Another RMAN backup is currently running on database: $ORACLE_SID"
            log_err "Lock file contents:"
            echo "$lock_info" | while IFS= read -r line; do
                log_err "  $line"
            done
        else
            log_err "Another RMAN backup process has locked this database: $ORACLE_SID"
        fi
        
        # Show any running RMAN processes for additional context
        local rman_processes
        rman_processes=$(ps -ef | grep -E "(rman target|rman_oci_backup)" | grep -v grep | grep -v "$$")
        
        if [[ -n "$rman_processes" ]]; then
            log_err ""
            log_err "Currently running RMAN-related processes:"
            echo "$rman_processes" | while IFS= read -r line; do
                log_err "  $line"
            done
        fi
        
        log_err ""
        log_err "RESOLUTION OPTIONS:"
        log_err "  1. WAIT: Allow current backup to complete"
        log_err "  2. MONITOR: Check running processes with 'ps -ef | grep rman'"
        log_err "  3. EMERGENCY: Force unlock with 'rm -f $LOCK_FILE' (CAUTION!)"
        log_err "=========================================================="
        
        return 1
    fi
    
    # Lock acquired successfully - write process information
    cat > "$LOCK_FILE" <<LOCKINFO
PID: $$
BACKUP_TYPE: $BACKUP_TYPE
START_TIME: $(date '+%Y-%m-%d %H:%M:%S')
START_TIMESTAMP: $(date +%s)
HOSTNAME: $(hostname)
USER: $(whoami)
ORACLE_SID: $ORACLE_SID
SCRIPT_PATH: $0
LOCKINFO

    log_ok "=========================================================="
    log_ok "EXCLUSIVE BACKUP LOCK ACQUIRED SUCCESSFULLY"
    log_ok "=========================================================="
    log_ok "This process now has exclusive backup rights for: $ORACLE_SID"
    log_ok "No other backup operations can start until this completes"
    log_ok "=========================================================="
    
    return 0
}

release_lock() {
    # Lock is automatically released when file descriptor 200 closes
    # But we can also clean up the lock file for tidiness
    if [[ -f "$LOCK_FILE" ]]; then
        local lock_pid
        lock_pid=$(grep "^PID:" "$LOCK_FILE" 2>/dev/null | cut -d' ' -f2)
        
        # Only clean up if this process owns the lock
        if [[ "$lock_pid" == "$$" ]]; then
            rm -f "$LOCK_FILE" 2>/dev/null
            log_msg "Lock file cleaned up successfully"
        fi
    fi
    
    log_msg "=========================================================="
    log_ok "BACKUP LOCK RELEASED"
    log_msg "Other backup operations can now proceed"
    log_msg "=========================================================="
}

# Ensure lock is released on any exit condition
trap 'release_lock' EXIT

################################################################################
# ENVIRONMENT VALIDATION
################################################################################

validate_environment() {
    log_msg "Starting comprehensive environment validation..."
    
    # Check Oracle Environment Variables
    if [[ -z "$ORACLE_SID" || -z "$ORACLE_HOME" ]]; then
        log_err "Oracle environment not properly configured"
        log_err "ORACLE_SID: ${ORACLE_SID:-NOT SET}"
        log_err "ORACLE_HOME: ${ORACLE_HOME:-NOT SET}"
        log_err "Verify /home/oracle/.bash_profile contains correct settings"
        exit 1
    fi
    log_ok "Oracle environment validated: SID=$ORACLE_SID, HOME=$ORACLE_HOME"
    
    # Verify Database Status
    local db_status
    db_status=$(sqlplus -s / as sysdba <<EOF
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SELECT status FROM v\$instance;
EXIT;
EOF
)
    db_status=$(echo "$db_status" | tr -d '[:space:]')
    
    if [[ "$db_status" != "OPEN" ]]; then
        log_err "Database not in OPEN state. Current status: $db_status"
        exit 1
    fi
    log_ok "Database status verified: $db_status"
    
    # Verify ARCHIVELOG Mode
    local log_mode
    log_mode=$(sqlplus -s / as sysdba <<EOF
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SELECT log_mode FROM v\$database;
EXIT;
EOF
)
    log_mode=$(echo "$log_mode" | tr -d '[:space:]')
    
    if [[ "$log_mode" != "ARCHIVELOG" ]]; then
        log_err "Database not in ARCHIVELOG mode. Current: $log_mode"
        exit 1
    fi
    log_ok "Database ARCHIVELOG mode confirmed"
    
    # Check Disk Space (minimum 50GB required for 10-hour retention)
    local avail_space
    avail_space=$(df -BG "$BACKUP_BASE" | tail -1 | awk '{print $4}' | sed 's/G//')
    if (( avail_space < 50 )); then
        log_err "Insufficient disk space: ${avail_space}GB (minimum 50GB required for 10-hour retention)"
        log_err "Required space: Database Size + (2 × Daily Archive Logs) + 20% buffer"
        exit 1
    fi
    log_ok "Disk space validated: ${avail_space}GB available"
    
    # Verify OCI CLI and Connectivity
    if ! command -v oci >/dev/null; then
        log_err "OCI CLI not found in PATH"
        exit 1
    fi
    
    if ! oci os ns get --config-file "$OCI_CONFIG_FILE" >/dev/null 2>&1; then
        log_err "OCI authentication failed. Check config: $OCI_CONFIG_FILE"
        exit 1
    fi
    
    if ! oci os bucket get --bucket-name "$OCI_BUCKET" --config-file "$OCI_CONFIG_FILE" >/dev/null 2>&1; then
        log_err "Cannot access OCI bucket: $OCI_BUCKET"
        exit 1
    fi
    log_ok "OCI connectivity and bucket access verified"
}

################################################################################
# DISK SPACE MONITORING
################################################################################

check_disk_space() {
    local avail_space
    avail_space=$(df -BG "$BACKUP_BASE" | tail -1 | awk '{print $4}' | sed 's/G//')
    local used_percent
    used_percent=$(df -h "$BACKUP_BASE" | tail -1 | awk '{print $5}' | sed 's/%//')
    
    log_msg "Disk space status: ${avail_space}GB available (${used_percent}% used)"
    
    if (( used_percent > 85 )); then
        log_err "WARNING: Disk usage exceeds 85% threshold"
        log_err "Consider reducing LOCAL_RETENTION_HOURS or expanding storage"
    fi
}

################################################################################
# RMAN BACKUP EXECUTION WITH OPTIMIZATION
################################################################################

perform_rman_backup() {
    log_msg "Starting RMAN $BACKUP_TYPE backup operation"
    log_msg "Target database: $ORACLE_SID | Staging: $BACKUP_DIR"
    
    local rman_commands=""
    
    case "$BACKUP_TYPE" in
        FULL)
            local backup_tag="ACME_FULL_${DATE_STAMP}"
            log_msg "Executing FULL backup with 4 parallel channels"
            rman_commands="
            RUN {
                ALLOCATE CHANNEL ch1 DEVICE TYPE DISK FORMAT '$BACKUP_DIR/FULL_%d_%T_%U.bkp';
                ALLOCATE CHANNEL ch2 DEVICE TYPE DISK FORMAT '$BACKUP_DIR/FULL_%d_%T_%U.bkp';
                ALLOCATE CHANNEL ch3 DEVICE TYPE DISK FORMAT '$BACKUP_DIR/FULL_%d_%T_%U.bkp';
                ALLOCATE CHANNEL ch4 DEVICE TYPE DISK FORMAT '$BACKUP_DIR/FULL_%d_%T_%U.bkp';
                
                BACKUP AS COMPRESSED BACKUPSET 
                    INCREMENTAL LEVEL 0 
                    DATABASE 
                    TAG '$backup_tag'
                    PLUS ARCHIVELOG TAG '${backup_tag}_ARCH';
                
                BACKUP CURRENT CONTROLFILE FORMAT '$BACKUP_DIR/CTL_%d_%T_%U.ctl' TAG '$backup_tag';
                BACKUP SPFILE FORMAT '$BACKUP_DIR/SPF_%d_%T_%U.ora' TAG '$backup_tag';
                
                RELEASE CHANNEL ch1;
                RELEASE CHANNEL ch2;
                RELEASE CHANNEL ch3;
                RELEASE CHANNEL ch4;
            }"
            ;;
            
        INCREMENTAL)
            local backup_tag="ACME_INCR_${DATE_STAMP}"
            log_msg "Executing INCREMENTAL backup with 3 parallel channels"
            rman_commands="
            RUN {
                ALLOCATE CHANNEL ch1 DEVICE TYPE DISK FORMAT '$BACKUP_DIR/INCR_%d_%T_%U.bkp';
                ALLOCATE CHANNEL ch2 DEVICE TYPE DISK FORMAT '$BACKUP_DIR/INCR_%d_%T_%U.bkp';
                ALLOCATE CHANNEL ch3 DEVICE TYPE DISK FORMAT '$BACKUP_DIR/INCR_%d_%T_%U.bkp';
                
                BACKUP AS COMPRESSED BACKUPSET 
                    INCREMENTAL LEVEL 1 
                    DATABASE 
                    TAG '$backup_tag'
                    PLUS ARCHIVELOG TAG '${backup_tag}_ARCH';
                
                BACKUP CURRENT CONTROLFILE FORMAT '$BACKUP_DIR/CTL_%d_%T_%U.ctl' TAG '$backup_tag';
                
                RELEASE CHANNEL ch1;
                RELEASE CHANNEL ch2;
                RELEASE CHANNEL ch3;
            }"
            ;;
            
        ARCHIVELOG)
            log_msg "Executing OPTIMIZED archive log backup"
            log_msg "Strategy: Backup only NEW archive logs using intelligent deduplication"
            rman_commands="
            RUN {
                ALLOCATE CHANNEL ch1 DEVICE TYPE DISK FORMAT '$BACKUP_DIR/ARCH_%d_%T_%U.bkp';
                
                BACKUP AS COMPRESSED BACKUPSET 
                    ARCHIVELOG ALL 
                    NOT BACKED UP 1 TIMES 
                    TAG 'ACME_ARCH_SECONDARY';
                
                BACKUP CURRENT CONTROLFILE FORMAT '$BACKUP_DIR/CTL_%d_%T_%U.ctl' TAG 'ACME_ARCH_CTL';
                
                RELEASE CHANNEL ch1;
            }"
            ;;
    esac
    
    # Execute RMAN
    rman target / << RMANEOF >> "$LOG_FILE" 2>&1
$rman_commands
LIST BACKUP SUMMARY;
EXIT;
RMANEOF
    
    # Check RMAN Status
    if [[ $? -ne 0 ]] || grep -qi "RMAN-[0-9]*.*ERROR" "$LOG_FILE"; then
        log_err "RMAN backup failed. Check $LOG_FILE for details"
        return 1
    fi
    
    # Verify Files Created
    local file_count
    file_count=$(find "$BACKUP_DIR" -type f 2>/dev/null | wc -l)
    if [[ $file_count -eq 0 ]]; then
        log_msg "No new files to backup (optimization prevented unnecessary backup)"
        return 0
    fi
    
    local total_size
    total_size=$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1)
    log_ok "RMAN backup completed: $file_count files, $total_size"
    return 0
}

################################################################################
# OCI UPLOAD WITH VERIFICATION AND RETRY LOGIC
################################################################################

upload_to_oci() {
    log_msg "Starting OCI upload to $OCI_BUCKET/$OCI_PREFIX/$DATE_STAMP/"
    
    local file_count
    file_count=$(find "$BACKUP_DIR" -type f 2>/dev/null | wc -l)
    
    if [[ $file_count -eq 0 ]]; then
        log_msg "No files to upload (no new backups created)"
        return 0
    fi
    
    local total_size
    total_size=$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1)
    log_msg "Uploading $file_count files ($total_size) to OCI Object Storage"
    
    # Execute bulk upload to OCI Object Storage
    if ! oci os object bulk-upload \
        --config-file "$OCI_CONFIG_FILE" \
        --bucket-name "$OCI_BUCKET" \
        --src-dir "$BACKUP_DIR" \
        --prefix "$OCI_PREFIX/$DATE_STAMP/" \
        --overwrite >> "$LOG_FILE" 2>&1; then
        
        log_err "OCI upload failed"
        return 1
    fi
    
    # Verify Upload with retry logic
    local retry_count=0
    local max_retries=3
    local uploaded_count=0
    
    while [[ $retry_count -lt $max_retries ]]; do
        sleep 5
        uploaded_count=$(oci os object list \
            --config-file "$OCI_CONFIG_FILE" \
            --bucket-name "$OCI_BUCKET" \
            --prefix "$OCI_PREFIX/$DATE_STAMP/" \
            --all 2>/dev/null | grep -c '"name"' || echo "0")
        
        if (( uploaded_count >= file_count )); then
            log_ok "Upload verified: $uploaded_count files in OCI"
            
            # Create upload verification marker with timestamp
            touch "${BACKUP_DIR}/.uploaded_$(date +%s)"
            log_msg "Upload marker created for retention management"
            
            return 0
        fi
        
        ((retry_count++))
        log_msg "Verification attempt $retry_count: Found $uploaded_count of $file_count files"
    done
    
    log_err "Upload verification failed after $max_retries attempts"
    log_err "Expected $file_count files, found $uploaded_count"
    return 1
}

################################################################################
# BACKUP INVENTORY DISPLAY
################################################################################

display_backup_inventory() {
    local current_dirs
    current_dirs=$(find "$BACKUP_BASE" -maxdepth 1 -type d -name "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]" 2>/dev/null | wc -l)
    
    if [[ $current_dirs -gt 0 ]]; then
        local total_local_size
        total_local_size=$(du -sh "$BACKUP_BASE" 2>/dev/null | cut -f1)
        log_msg "=========================================="
        log_msg "Local Backup Inventory Summary"
        log_msg "=========================================="
        log_msg "Total directories: $current_dirs | Total size: $total_local_size"
        log_msg ""
        
        find "$BACKUP_BASE" -maxdepth 1 -type d -name "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]" 2>/dev/null | sort -r | while read -r dir_path; do
            local dir
            dir=$(basename "$dir_path")
            local dir_size
            dir_size=$(du -sh "$dir_path" 2>/dev/null | cut -f1)
            local dir_age_hours
            dir_age_hours=$(( ($(date +%s) - $(stat -c %Y "$dir_path" 2>/dev/null || echo "0")) / 3600 ))
            
            # SAFE glob handling for upload status
            local upload_status="Not uploaded"
            shopt -s nullglob
            local markers=("$dir_path"/.uploaded_*)
            shopt -u nullglob
            if (( ${#markers[@]} > 0 )); then
                upload_status="Uploaded to OCI"
                log_ok "  $dir: $dir_size, ${dir_age_hours}h old, $upload_status"
            else
                log_msg "  $dir: $dir_size, ${dir_age_hours}h old, $upload_status"
            fi
        done
        log_msg "=========================================="
    else
        log_msg "No local backup directories found"
    fi
}

################################################################################
# ENHANCED CLEANUP WITH 10-HOUR RETENTION (SHELL-COMPATIBLE)
################################################################################

cleanup_local() {
    log_msg "Starting cleanup with ${LOCAL_RETENTION_HOURS}-hour local retention policy..."
    log_msg "Current backup preserved: $BACKUP_DIR (retained for ${LOCAL_RETENTION_HOURS}h)"
    
    # Calculate retention threshold in minutes for precise control
    local retention_minutes=$((LOCAL_RETENTION_HOURS * 60))
    log_msg "Retention threshold: ${LOCAL_RETENTION_HOURS} hours (${retention_minutes} minutes)"
    
    # Find and process directories older than retention period
    local cleaned_count=0
    local cleaned_size_mb=0
    local preserved_count=0
    
    # Create secure temporary file for directory list (FIXES SYNTAX ERROR)
    local temp_dirs
    temp_dirs=$(mktemp) || {
        log_err "Failed to create temporary file for cleanup"
        return 1
    }
    
    # Find directories older than retention period
    find "$BACKUP_BASE" -maxdepth 1 -type d \
         -name "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]" \
         -mmin +${retention_minutes} 2>/dev/null > "$temp_dirs"
    
    # Process each old directory
    while IFS= read -r old_dir; do
        [[ -z "$old_dir" ]] && continue
        
        local dir_name
        dir_name=$(basename "$old_dir")
        
        # Verify it's a date-formatted directory (8 digits)
        if [[ ! "$dir_name" =~ ^[0-9]{8}$ ]]; then
            log_msg "Skipping non-date directory: $dir_name"
            continue
        fi
        
        # Skip current backup directory explicitly
        if [[ "$old_dir" == "$BACKUP_DIR" ]]; then
            log_msg "Preserving current backup: $dir_name"
            continue
        fi
        
        # SAFE glob handling for upload markers
        shopt -s nullglob
        local markers=("$old_dir"/.uploaded_*)
        shopt -u nullglob
        
        if (( ${#markers[@]} == 0 )); then
            log_msg "Preserving unverified backup: $dir_name (upload may have failed or in progress)"
            ((preserved_count++))
            continue
        fi
        
        # DOUBLE-CHECK: Verify backup actually exists in OCI
        local oci_object_count
        oci_object_count=$(oci os object list \
            --config-file "$OCI_CONFIG_FILE" \
            --bucket-name "$OCI_BUCKET" \
            --prefix "$OCI_PREFIX/$dir_name/" \
            --all 2>/dev/null | grep -c '"name"' || echo "0")
        
        if (( oci_object_count == 0 )); then
            log_msg "Preserving backup $dir_name: upload marker exists but no objects found in OCI"
            ((preserved_count++))
            continue
        fi
        
        # Safe to delete: old enough + marked + verified in OCI
        local dir_size_mb
        dir_size_mb=$(du -sm "$old_dir" 2>/dev/null | cut -f1)
        
        log_msg "Removing verified old backup: $dir_name (${dir_size_mb}MB, confirmed in OCI: $oci_object_count files)"
        
        if rm -rf "$old_dir" 2>/dev/null; then
            ((cleaned_count++))
            ((cleaned_size_mb+=dir_size_mb))
        else
            log_err "Failed to remove directory: $old_dir"
        fi
        
    done < "$temp_dirs"
    
    # Clean up temporary file
    rm -f "$temp_dirs"
    
    # Report cleanup results
    if [[ $cleaned_count -gt 0 ]]; then
        local cleaned_size_gb
        cleaned_size_gb=$(echo "scale=2; $cleaned_size_mb / 1024" | bc 2>/dev/null || echo "0")
        log_ok "Cleaned $cleaned_count old backup directories (${cleaned_size_gb}GB freed)"
    else
        log_msg "No directories older than ${LOCAL_RETENTION_HOURS} hours were eligible for cleanup"
    fi
    
    if [[ $preserved_count -gt 0 ]]; then
        log_msg "Preserved $preserved_count backup directories (within retention or unverified)"
    fi
    
    # Display current local backup inventory
    display_backup_inventory
    
    # Clean old log files
    local old_logs
    old_logs=$(find "$LOG_DIR" -name "*.log" -mtime +${LOG_RETENTION_DAYS} 2>/dev/null | wc -l)
    
    if [[ $old_logs -gt 0 ]]; then
        find "$LOG_DIR" -name "*.log" -mtime +${LOG_RETENTION_DAYS} -delete 2>/dev/null
        log_ok "Cleaned $old_logs old log files (>${LOG_RETENTION_DAYS} days)"
    fi
    
    log_ok "Cleanup completed"
}

################################################################################
# MAIN EXECUTION
################################################################################

main() {
    # CRITICAL: Acquire atomic lock FIRST
    if ! acquire_lock; then
        exit 1
    fi
    
    log_msg "=========================================================="
    log_msg "ENTERPRISE RMAN BACKUP TO OCI - ACME CORPORATION"
    log_msg "=========================================================="
    log_msg "Backup Type: $BACKUP_TYPE"
    log_msg "Database: $ORACLE_SID"
    log_msg "Local Retention: ${LOCAL_RETENTION_HOURS} hours"
    log_msg "OCI Bucket: $OCI_BUCKET"
    log_msg "=========================================================="
    
    # Pre-backup disk space check
    check_disk_space
    
    validate_environment
    
    if ! perform_rman_backup; then
        log_err "Backup failed. Process aborted."
        exit 1
    fi
    
    if ! upload_to_oci; then
        log_err "Upload failed. Local backup preserved at: $BACKUP_DIR"
        log_err "Will retry upload on next backup cycle"
        # Still run cleanup to manage old backups
        cleanup_local
        exit 1
    fi
    
    # Always run cleanup to remove old directories
    cleanup_local
    
    # Post-backup disk space check
    check_disk_space
    
    log_msg "=========================================================="
    log_ok "BACKUP COMPLETED SUCCESSFULLY"
    log_msg "=========================================================="
    log_msg "Local Copy: $BACKUP_DIR"
    log_msg "Retention: ${LOCAL_RETENTION_HOURS} hours from upload completion"
    log_msg "OCI Location: $OCI_BUCKET/$OCI_PREFIX/$DATE_STAMP/"
    log_msg "=========================================================="
}

main "$@"

The Archive Log Optimization Breakthrough

The Problem: Redundant Backups

Traditional archive log backup scripts suffer from a critical inefficiency. Consider this scenario without optimization:

  • 2:00 PM: Script backs up archive logs 1-100 (all available)
  • 2:30 PM: Script backs up archive logs 1-125 (includes duplicates 1-100)
  • 3:00 PM: Script backs up archive logs 1-150 (includes duplicates 1-125)

Over 24 hours with 48 executions, each archive log gets backed up an average of 24 times, wasting storage and bandwidth.

The Solution: Tag-Based Intelligence

Our script uses RMAN's sophisticated tracking mechanism:

BACKUP ARCHIVELOG ALL NOT BACKED UP 1 TIMES TAG 'ACME_ARCH_SECONDARY';

How It Works:

  • 2:00 PM: Backs up logs 1-100, tags them with 'ACME_ARCH_SECONDARY'
  • 2:30 PM: RMAN checks all logs, finds 1-100 already have the tag, backs up only 101-125
  • 3:00 PM: Only backs up new logs 126-150

Result: Each archive log is backed up exactly once, reducing storage consumption by up to 90%.

Why This Doesn't Interfere with Other Backups

The tag-based system operates independently of your primary backup strategy. Your automatic backup system uses different tags (or no tags), so this secondary system won't interfere with your existing processes.

Deployment Guide

Step 1: Environment Preparation

Create the necessary directory structure:

# Create backup staging area
sudo mkdir -p /u02/rman_staging
sudo chown oracle:oinstall /u02/rman_staging
sudo chmod 755 /u02/rman_staging

# Create log directory
mkdir -p /home/oracle/logs/backup_operations
chmod 755 /home/oracle/logs/backup_operations

Step 2: OCI Configuration

Set up OCI CLI configuration:

mkdir -p /home/oracle/.oci
chmod 700 /home/oracle/.oci

# Create configuration file
vi /home/oracle/.oci/config

Add your configuration (replace with actual values):

[DEFAULT]
user=ocid1.user.oc1..aaaaaaaexampleuserocid
fingerprint=aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
key_file=/home/oracle/.oci/oci_api_key.pem
tenancy=ocid1.tenancy.oc1..aaaaaaaexampletenancyocid
region=us-phoenix-1

Test connectivity:

oci os ns get --config-file /home/oracle/.oci/config

Step 3: Script Installation

Install the script:

vi /home/oracle/scripts/rman_oci_backup.sh
# Paste the script and update the CONFIGURATION section
chmod +x /home/oracle/scripts/rman_oci_backup.sh

# Verify syntax
bash -n /home/oracle/scripts/rman_oci_backup.sh
echo "Exit code: $?"

Step 4: Testing and Validation

Test each backup type:

# Test archive log backup (demonstrates optimization)
/home/oracle/scripts/rman_oci_backup.sh ARCHIVELOG

# Run again immediately to see optimization in action
/home/oracle/scripts/rman_oci_backup.sh ARCHIVELOG

# Test incremental backup
/home/oracle/scripts/rman_oci_backup.sh INCREMENTAL

# Verify OCI upload
TODAY=$(date +%Y%m%d)
oci os object list --bucket-name acme-prod-backups --prefix "ACMEPROD_RMAN/$TODAY/" --output table

Production Scheduling

Configure automated scheduling using cron:

crontab -e

Add this production schedule:

# ACME Corporation Database Backup Schedule
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""

# Full Backup: Saturday at 2:00 AM (creates Level 0 baseline)
0 2 * * 6 /home/oracle/scripts/rman_oci_backup.sh FULL >> /home/oracle/logs/backup_operations/cron_full_$(date +\%Y\%m\%d).log 2>&1

# Incremental Backup: Daily (Sunday-Friday) at 2:00 AM
0 2 * * 0-5 /home/oracle/scripts/rman_oci_backup.sh INCREMENTAL >> /home/oracle/logs/backup_operations/cron_incr_$(date +\%Y\%m\%d).log 2>&1

# Archive Log Backup: Every 30 minutes (optimized for efficiency)
*/30 * * * * /home/oracle/scripts/rman_oci_backup.sh ARCHIVELOG >> /home/oracle/logs/backup_operations/cron_arch_$(date +\%Y\%m\%d).log 2>&1

Monitoring and Maintenance

Daily Monitoring Script

Create a monitoring script to verify backup success:

#!/bin/bash
# Save as /home/oracle/scripts/check_backups.sh

TODAY=$(date +%Y%m%d)
echo "=== Daily Backup Status Report ==="
echo "Generated: $(date)"

# Check today's backups in OCI
oci os object list \
    --bucket-name acme-prod-backups \
    --prefix "ACMEPROD_RMAN/$TODAY/" \
    --config-file /home/oracle/.oci/config \
    --output table

# Check for recent errors
echo -e "\nRecent Errors:"
grep -i error /home/oracle/logs/backup_operations/*.log | tail -10

Space Requirements Calculation

Calculate required staging space using this formula:

For example, a 500GB database generating 50GB of daily archive logs:

Cost Optimization Strategies

Lifecycle Policies

Configure OCI lifecycle policies to manage costs:

# Create lifecycle policy to archive old backups
cat > /tmp/lifecycle-policy.json << 'EOF'
{
  "items": [
    {
      "name": "archive-monthly-backups",
      "action": "ARCHIVE",
      "timeAmount": 30,
      "timeUnit": "DAYS",
      "isEnabled": true,
      "target": "objects"
    },
    {
      "name": "delete-old-backups", 
      "action": "DELETE",
      "timeAmount": 180,
      "timeUnit": "DAYS",
      "isEnabled": true,
      "target": "objects"
    }
  ]
}
EOF

oci os object-lifecycle-policy put \
    --bucket-name acme-prod-backups \
    --lifecycle-policy file:///tmp/lifecycle-policy.json \
    --config-file /home/oracle/.oci/config

This policy automatically moves backups older than 30 days to Archive Storage (90% cost reduction) and deletes backups older than 180 days.

Troubleshooting Common Issues

Issue: Insufficient Disk Space

Solution: Verify space requirements and expand staging area:

df -h /u02
# If insufficient, either expand filesystem or reduce parallel channels

Issue: OCI Authentication Failures

Solution: Verify OCI configuration:

# Test authentication
oci os ns get --config-file /home/oracle/.oci/config

# Verify key permissions
ls -l /home/oracle/.oci/oci_api_key.pem  # Should be 600

Issue: Archive Log Backups Taking Too Long

This usually indicates the optimization isn't working. Check RMAN backup history:

rman target / << EOF
LIST BACKUP OF ARCHIVELOG ALL;
EXIT;
EOF

Look for consistent use of the 'ACME_ARCH_SECONDARY' tag.

Security Considerations

Access Control

Implement restrictive permissions:

chmod 700 /u02/rman_staging
chmod 700 /home/oracle/logs/backup_operations
chmod 600 /home/oracle/.oci/oci_api_key.pem

Encryption

OCI Object Storage provides encryption at rest by default. For additional security, configure customer-managed encryption keys through OCI Vault.

Key Benefits Achieved:

  • Cost Efficiency: Eliminates redundant archive log backups
  • Reliability: Comprehensive error handling and validation
  • Scalability: Parallel processing reduces backup windows
  • Security: Encrypted cloud storage with access controls
  • Automation: Complete hands-off operation with monitoring

Regular testing, monitoring, and maintenance of this backup system will provide the confidence that your critical database systems are protected against any disaster scenario while maintaining cost-effective cloud storage utilization.







Please do like and subscribe to my youtube channel: https://www.youtube.com/@foalabs If you like this post please follow,share and comment