autoNAS / scripts / autonas-core.sh
1 contributor
1170 lines | 39.536kb
#!/bin/bash

# AutoNAS Core Library
# Shared business logic for all AutoNAS components
# This library contains all core functions to eliminate duplication

# Standard install paths
AUTONAS_ORG_ID="${AUTONAS_ORG_ID:-xdev}"
AUTONAS_PROJECT_ID="${AUTONAS_PROJECT_ID:-autonas}"
AUTONAS_RUNTIME_DIR="${AUTONAS_RUNTIME_DIR:-/usr/local/lib/${AUTONAS_ORG_ID}/${AUTONAS_PROJECT_ID}}"
AUTONAS_COMMAND="${AUTONAS_COMMAND:-/usr/local/sbin/autonas}"
AUTONAS_DEFAULTS_FILE="${AUTONAS_DEFAULTS_FILE:-/etc/default/${AUTONAS_ORG_ID}-${AUTONAS_PROJECT_ID}}"
AUTONAS_DOC_DIR="${AUTONAS_DOC_DIR:-/usr/local/share/doc/${AUTONAS_ORG_ID}/${AUTONAS_PROJECT_ID}}"
AUTONAS_MEDIA_IMPORTER="${AUTONAS_MEDIA_IMPORTER:-${AUTONAS_RUNTIME_DIR}/autonas-media-importer.sh}"
AUTONAS_DISK_HANDLER="${AUTONAS_DISK_HANDLER:-${AUTONAS_RUNTIME_DIR}/autonas-disk-handler.sh}"

# Load default configuration
if [ -f "${AUTONAS_DEFAULTS_FILE}" ]; then
    source "${AUTONAS_DEFAULTS_FILE}"
fi

# Set defaults if not loaded from config
AUTONAS_DEBUG="${AUTONAS_DEBUG:-false}"
AUTONAS_LOG_LEVEL="${AUTONAS_LOG_LEVEL:-info}"

# Global configuration
CONFIG_FILE="/etc/pve/autonas/disks.conf"
MOUNT_BASE="/mnt/autonas"

# ============================================================================
# LOGGING FUNCTIONS
# ============================================================================

# Function to log messages with debug support
log_message() {
    local message="$1"
    local priority="${2:-info}"
    local context="${3:-${LOG_TAG:-autonas}}"
    
    # Skip debug messages unless debug mode is enabled
    if [ "$priority" = "debug" ] && [ "$AUTONAS_DEBUG" != "true" ]; then
        return 0
    fi
    
    # Log to syslog with facility local0 and specified priority
    logger -p "local0.$priority" -t "$context" "$message"
    
    # Also log to stderr if debug mode is enabled
    if [ "$AUTONAS_DEBUG" = "true" ]; then
        echo "[$context] [$priority] $message" >&2
    fi
    
    # Also echo to stdout/stderr for interactive use
    if [ -t 1 ]; then
        echo "$(date '+%Y-%m-%d %H:%M:%S') - $message"
    fi
}

# Function for debug logging (convenience function)
debug_log() {
    log_message "$1" "debug" "${2:-${LOG_TAG:-autonas}}"
}

# ============================================================================
# CONFIGURATION FUNCTIONS
# ============================================================================

# Function to get disk configuration by UUID
get_disk_config() {
    local uuid="$1"
    grep "^${uuid}:" "$CONFIG_FILE" 2>/dev/null
}

# Function to parse configuration line
parse_config() {
    local config="$1"
    IFS=':' read -r uuid name ip interface mount_point nfs_options <<< "$config"
    echo "$uuid" "$name" "$ip" "$interface" "$mount_point" "$nfs_options"
}

# Function to get configuration for a specific UUID (handles systemd escaping)
get_config() {
    local uuid="$1"
    
    if [ -z "$uuid" ]; then
        return 1
    fi

    # Un-escape systemd-escaped UUID (e.g., 8765\x2d4321 -> 8765-4321)
    local unescaped_uuid
    unescaped_uuid=$(echo "$uuid" | sed 's/\\x2d/-/g')

    # Find configuration line for UUID (try both escaped and unescaped)
    grep "^${unescaped_uuid}:" "$CONFIG_FILE" 2>/dev/null || grep "^${uuid}:" "$CONFIG_FILE" 2>/dev/null
}

# Function to get configuration for a specific name
get_config_by_name() {
    local name="$1"
    
    if [ -z "$name" ]; then
        return 1
    fi

    # Find configuration line by name (second field)
    grep ":${name}:" "$CONFIG_FILE" 2>/dev/null
}

# Function to get configuration for a specific mount point
get_config_by_mount_point() {
    local mount_point="$1"

    if [ -z "$mount_point" ] || [ ! -f "$CONFIG_FILE" ]; then
        return 1
    fi

    awk -F':' -v target="$mount_point" '
        $1 !~ /^[[:space:]]*#/ && $5 == target { print; exit }
    ' "$CONFIG_FILE" 2>/dev/null
}

# Resolve an entry identifier as UUID, name, or mount point
resolve_config_identifier() {
    local identifier="$1"

    if [ -z "$identifier" ]; then
        return 1
    fi

    # Try UUID first (supports escaped UUIDs from systemd), then name, then mount point.
    get_config "$identifier" \
        || get_config_by_name "$identifier" \
        || get_config_by_mount_point "$identifier"
}

# ============================================================================
# VALIDATION FUNCTIONS
# ============================================================================

# Function to validate UUID format
validate_uuid() {
    local uuid="$1"
    # UUID can be standard format (8-4-4-4-12) or shorter formats used by some filesystems
    if [[ "$uuid" =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]] || \
       [[ "$uuid" =~ ^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$ ]] || \
       [[ "$uuid" =~ ^[0-9A-Fa-f]{8}$ ]]; then
        return 0
    fi
    return 1
}

# Function to validate disk name
validate_disk_name() {
    local name="$1"
    
    # Check length (max 50 characters)
    if [[ ${#name} -gt 50 ]]; then
        return 1
    fi
    
    # Check if it starts with letter or number
    if [[ ! "$name" =~ ^[a-zA-Z0-9] ]]; then
        return 1
    fi
    
    # Check allowed characters: letters, numbers, hyphens, underscores, at symbol
    if [[ ! "$name" =~ ^[a-zA-Z0-9_@-]+$ ]]; then
        return 1
    fi
    
    # Check for reserved names
    local reserved_names=("root" "home" "tmp" "var" "usr" "etc" "proc" "sys" "dev" "boot" "mnt" "media" "opt" "srv")
    for reserved in "${reserved_names[@]}"; do
        if [[ "$name" == "$reserved" ]]; then
            return 1
        fi
    done
    
    return 0
}

# Function to check if UUID exists in configuration
check_uuid_exists_in_config() {
    local uuid="$1"
    grep -q "^${uuid}:" "$CONFIG_FILE" 2>/dev/null
}

# Function to list existing disk names (helper for duplicate detection)
list_existing_disk_names() {
    if [[ -f "$CONFIG_FILE" ]]; then
        echo "Existing disk names:"
        while IFS=':' read -r uuid name rest; do
            if [[ -n "$uuid" && -n "$name" ]]; then
                echo "  • $name (UUID: ${uuid:0:8}...)"
            fi
        done < "$CONFIG_FILE"
    fi
}

# Function to check if disk name already exists in configuration
check_disk_name_exists() {
    local name="$1"
    if [[ -f "$CONFIG_FILE" ]]; then
        # Check if name exists as second field in any configuration line
        grep -q ":${name}:" "$CONFIG_FILE" 2>/dev/null
    fi
}

# Function to validate disk name (includes duplicate check)
validate_disk_name_complete() {
    local name="$1"
    local uuid="$2"  # Optional: exclude current UUID from duplicate check
    
    # Run basic validation first
    if ! validate_disk_name "$name"; then
        return 1
    fi
    
    # Check for duplicate names
    if check_disk_name_exists "$name"; then
        # If UUID provided, check if this name belongs to the same UUID (allow updates)
        if [[ -n "$uuid" ]] && [[ -f "$CONFIG_FILE" ]]; then
            local existing_uuid=$(grep ":${name}:" "$CONFIG_FILE" | cut -d':' -f1)
            if [[ "$existing_uuid" == "$uuid" ]]; then
                # Same UUID, same name - this is an update, allow it
                return 0
            fi
        fi
        # Different UUID or no UUID provided - this is a duplicate
        return 2  # Special return code for duplicate names
    fi
    
    return 0
}

# Function to check if UUID exists in system
check_uuid_exists() {
    local uuid="$1"
    blkid | grep -q "$uuid"
}

# Function to check if disk is already mounted at AutoNAS location
is_disk_mounted() {
    local uuid="$1"
    local mount_point="$2"
    
    # Check if already mounted at the correct location
    if mount | grep -q "UUID=$uuid.*$mount_point"; then
        return 0  # Already mounted correctly
    fi
    
    # Check if mounted elsewhere
    if mount | grep -q "UUID=$uuid"; then
        return 1  # Mounted elsewhere
    fi
    
    return 2  # Not mounted
}

# ============================================================================
# DEVICE FUNCTIONS
# ============================================================================

# Function to get device information
get_device_info() {
    local uuid="$1"
    
    # Find device by UUID using blkid
    local device_line
    device_line=$(blkid | grep "UUID=\"$uuid\"")
    
    if [[ -z "$device_line" ]]; then
        return 1
    fi
    
    local device=$(echo "$device_line" | cut -d: -f1)
    local blkid_info=$(echo "$device_line" | cut -d: -f2-)
    
    echo "device:$device"
    echo "$blkid_info"
    return 0
}

# ============================================================================
# NETWORK FUNCTIONS
# ============================================================================

# Function to check if network interface exists
interface_exists() {
    local interface="$1"
    ip link show "$interface" >/dev/null 2>&1
}

# Function to check if interface is configured for AutoNAS
interface_has_autonas_config() {
    local interface="$1"
    grep -q ":${interface}:" "$CONFIG_FILE" 2>/dev/null
}

# Function to check if IP is configured on interface
is_ip_configured() {
    local ip="$1"
    local interface="$2"
    ip addr show "$interface" | grep -q "inet $ip/"
}

# Function to count disks using specific IP
count_disks_using_ip() {
    local target_ip="$1"
    local count=0
    
    while IFS=':' read -r uuid name ip interface mount_point nfs_options; do
        # Skip empty lines and comments
        [[ -z "$uuid" || "$uuid" =~ ^# ]] && continue
        
        if [[ "$ip" == "$target_ip" ]]; then
            # Check if this disk is currently attached (mounted)
            if mountpoint -q "$mount_point" 2>/dev/null; then
                count=$((count + 1))
            fi
        fi
    done < <(grep -v '^#' "$CONFIG_FILE" 2>/dev/null | grep -v '^$')
    
    echo $count
}

# Function to activate IP on interface with smart retry
activate_ip() {
    local ip="$1"
    local interface="$2"
    local max_retries=3
    local retry_count=0
    
    # Skip activation for special cases
    if [[ "$ip" == "IMPORT" || "$ip" == "LOCAL" ]]; then
        log_message "Skipping IP activation for special configuration: $ip" "debug"
        return 0
    fi
    
    log_message "Activating IP $ip on interface $interface" "info"
    
    # Check if interface exists
    if ! interface_exists "$interface"; then
        log_message "Interface $interface does not exist - waiting for it to appear" "warning"
        
        # Wait for interface to appear (common with USB/Thunderbolt)
        local wait_count=0
        while [ $wait_count -lt 30 ] && ! interface_exists "$interface"; do
            sleep 2
            wait_count=$((wait_count + 1))
            debug_log "Waiting for interface $interface... (${wait_count}/30)"
        done
        
        if ! interface_exists "$interface"; then
            log_message "Error: Interface $interface does not exist" "err"
            return 1
        fi
        
        log_message "Interface $interface is now available" "info"
    fi
    
    # Check if IP is already configured
    if is_ip_configured "$ip" "$interface"; then
        log_message "IP $ip already configured on interface $interface"
        return 0
    fi
    
    # Check if interface is up
    if ! ip link show "$interface" | grep -q "state UP"; then
        log_message "Bringing up interface $interface"
        if ! ip link set "$interface" up; then
            log_message "Error: Failed to bring up interface $interface" "err"
            return 1
        fi
        # Wait a moment for interface to come up
        sleep 1
    fi
    
    # Try to configure IP with retries
    while [ $retry_count -lt $max_retries ]; do
        if ip addr add "$ip/24" dev "$interface" 2>/dev/null; then
            log_message "Successfully activated IP $ip on interface $interface"
            return 0
        else
            retry_count=$((retry_count + 1))
            if [ $retry_count -lt $max_retries ]; then
                log_message "Failed to add IP $ip to interface $interface, retrying ($retry_count/$max_retries)" "warning"
                sleep 2
            fi
        fi
    done
    
    log_message "Error: Failed to activate IP $ip on interface $interface" "err"
    return 1
}

# Function to restore IPs for interface with comprehensive checking
restore_interface_ips() {
    local interface="$1"
    local restored=0
    local total_configs=0
    
    log_message "Checking IP restoration needs for interface: $interface" "info"
    
    # Check if interface exists and is up
    if ! interface_exists "$interface"; then
        log_message "Interface $interface does not exist, skipping restoration" "warning"
        return 1
    fi
    
    # Check if interface is up
    if ! ip link show "$interface" | grep -q "state UP"; then
        log_message "Interface $interface is down, waiting for it to come up..." "info"
        local wait_count=0
        while [ $wait_count -lt 20 ] && ! ip link show "$interface" | grep -q "state UP"; do
            sleep 1
            wait_count=$((wait_count + 1))
        done
        
        if ! ip link show "$interface" | grep -q "state UP"; then
            log_message "Interface $interface did not come up within 20 seconds" "warning"
            return 1
        fi
    fi
    
    log_message "Interface $interface is up, checking for required IP configurations" "info"
    
    while IFS= read -r line; do
        # Skip comments and empty lines
        [[ "$line" =~ ^[[:space:]]*# ]] && continue
        [[ -z "$line" ]] && continue
        
        # Parse configuration
        IFS=':' read -r uuid name ip config_interface mount_point nfs_options <<< "$line"
        
        # Check if this line is for our interface
        if [ "$config_interface" = "$interface" ]; then
            ((total_configs++))
            debug_log "Found configuration: $name ($uuid) requires IP $ip on $interface"
            
            # Check if the disk is currently mounted
            if mountpoint -q "$mount_point" 2>/dev/null; then
                log_message "Disk $name is mounted, checking IP configuration" "info"
                
                # Check if IP is already configured
                if ! is_ip_configured "$ip" "$interface"; then
                    log_message "IP $ip missing on interface $interface for mounted disk: $name" "warning"
                    
                    # Use the core activate_ip function
                    if activate_ip "$ip" "$interface"; then
                        log_message "Successfully restored IP $ip on interface $interface for disk $name" "info"
                        ((restored++))
                    else
                        log_message "Failed to restore IP $ip on interface $interface for disk $name" "error"
                    fi
                else
                    debug_log "IP $ip already configured on interface $interface for disk $name"
                fi
            else
                debug_log "Disk $name is not mounted, skipping IP configuration"
            fi
        fi
    done < "$CONFIG_FILE"
    
    if [ $total_configs -eq 0 ]; then
        debug_log "No AutoNAS configurations found for interface $interface"
    elif [ $restored -gt 0 ]; then
        log_message "Restored $restored/$total_configs IP addresses on interface $interface" "info"
        
        # Trigger NFS export refresh if we restored any IPs
        log_message "Refreshing NFS exports after IP restoration" "info"
        exportfs -ra 2>/dev/null
        systemctl reload nfs-kernel-server 2>/dev/null
    else
        log_message "All required IPs already configured on interface $interface" "info"
    fi
    
    return 0
}

# Function to deactivate IP on interface with smart checking
deactivate_ip() {
    local ip="$1"
    local interface="$2"
    
    # Skip deactivation for special cases
    if [[ "$ip" == "IMPORT" || "$ip" == "LOCAL" ]]; then
        log_message "Skipping IP deactivation for special configuration: $ip" "debug"
        return 0
    fi
    
    # Check how many other disks are using this IP
    local usage_count
    usage_count=$(count_disks_using_ip "$ip")
    
    if [[ $usage_count -gt 1 ]]; then
        log_message "IP $ip still used by $((usage_count - 1)) other disks, keeping it active"
        return 0
    fi
    
    log_message "Deactivating IP $ip on interface $interface"
    
    # Check if IP is configured
    if ! is_ip_configured "$ip" "$interface"; then
        log_message "IP $ip not configured on interface $interface"
        return 0
    fi
    
    # Remove IP from interface
    if ip addr del "$ip/24" dev "$interface" 2>/dev/null; then
        log_message "Successfully deactivated IP $ip on interface $interface"
        return 0
    else
        log_message "Error: Failed to deactivate IP $ip on interface $interface" "err"
        return 1
    fi
}

# ============================================================================
# NFS FUNCTIONS
# ============================================================================

# Function to add NFS export with AutoNAS identification
add_nfs_export() {
    local mount_point="$1"
    local nfs_options="$2"
    local uuid="${3:-}"
    local name="${4:-}"
    
    # Skip NFS export for special cases
    if [[ "$nfs_options" == "LOCAL" ]] || [[ "$mount_point" =~ ^.*@Camera.*$ ]]; then
        log_message "Skipping NFS export for local/camera configuration" "debug"
        return 0
    fi
    
    log_message "Adding NFS export for $mount_point"
    
    # Create NFS export entry
    local export_entry="$mount_point $nfs_options"
    
    # Check if export already exists
    if grep -Fxq "$export_entry" /etc/exports 2>/dev/null; then
        log_message "NFS export already exists: $export_entry"
        return 0
    fi
    
    # Add identifying comment if UUID/name provided
    if [[ -n "$uuid" && -n "$name" ]]; then
        echo "# AutoNAS Export - UUID:$uuid NAME:$name" >> /etc/exports
    fi
    
    # Add export to /etc/exports
    echo "$export_entry" >> /etc/exports
    if [[ $? -eq 0 ]]; then
        log_message "Successfully added NFS export: $export_entry"
        
        # Reload exports
        if exportfs -ra; then
            log_message "NFS exports reloaded successfully"
            return 0
        else
            log_message "Warning: Failed to reload NFS exports" "warning"
            return 1
        fi
    else
        log_message "Error: Failed to add NFS export" "err"
        return 1
    fi
}

# Function to remove NFS export
remove_nfs_export() {
    local mount_point="$1"
    local uuid="${2:-}"
    local name="${3:-}"
    local nfs_options="${4:-}"
    
    # Skip NFS export removal for special cases
    if [[ "$mount_point" =~ ^.*@Camera.*$ ]] || [[ "$nfs_options" == "LOCAL" ]]; then
        log_message "Skipping NFS export removal for camera/local configuration" "debug"
        return 0
    fi
    
    log_message "Removing NFS export for $mount_point"
    
    # Create temporary file without the export
    local temp_exports=$(mktemp)
    if [[ ! -f /etc/exports ]]; then
        log_message "No /etc/exports file found"
        rm -f "$temp_exports"
        return 0
    fi
    
    # If UUID provided, use AutoNAS marker method
    if [[ -n "$uuid" ]]; then
        local skip_next=false
        local found_export=false
        
        # Process /etc/exports line by line
        while IFS= read -r line; do
            if [[ "$line" =~ ^#\ AutoNAS\ Export\ -\ UUID:${uuid}\ NAME: ]]; then
                # Found our marker comment, skip this line and the next one
                skip_next=true
                found_export=true
                debug_log "Found AutoNAS export marker for $name"
                continue
            elif [ "$skip_next" = true ]; then
                # Skip the export line that follows our marker
                skip_next=false
                debug_log "Skipping export line for $name"
                continue
            else
                # Keep all other lines
                echo "$line" >> "$temp_exports"
            fi
        done < /etc/exports
        
        if [ "$found_export" = true ]; then
            # Replace /etc/exports with cleaned version
            mv "$temp_exports" /etc/exports
            log_message "NFS export removed using AutoNAS marker for $name"
        else
            rm -f "$temp_exports"
            # Fallback to generic method
            grep -v "^${mount_point} " /etc/exports > "$temp_exports"
            mv "$temp_exports" /etc/exports
            log_message "NFS export removed using generic method for $mount_point"
        fi
    else
        # Generic removal method
        grep -v "^${mount_point} " /etc/exports > "$temp_exports"
        mv "$temp_exports" /etc/exports
        log_message "NFS export removed for $mount_point"
    fi
    
    # Reload exports
    if exportfs -ra; then
        log_message "NFS exports reloaded successfully"
        return 0
    else
        log_message "Warning: Failed to reload NFS exports" "warning"
        return 1
    fi
}

# ============================================================================
# STORAGE FUNCTIONS
# ============================================================================

# Function to mount disk with comprehensive filesystem support
mount_disk() {
    local uuid="$1"
    local mount_point="$2"
    local filesystem_type="${3:-}"
    
    log_message "Mounting disk with UUID $uuid to $mount_point"
    
    # Wait for device to be ready (udev sometimes triggers too early)
    local device_path="/dev/disk/by-uuid/$uuid"
    local wait_count=0
    while [ ! -e "$device_path" ] && [ $wait_count -lt 10 ]; do
        debug_log "Waiting for device $device_path to appear..."
        sleep 1
        wait_count=$((wait_count + 1))
    done
    
    if [ ! -e "$device_path" ]; then
        # Try alternative method - find device by UUID using blkid
        local device
        device=$(blkid | grep "UUID=\"$uuid\"" | cut -d: -f1)
        
        if [[ -z "$device" ]]; then
            log_message "Error: Device with UUID $uuid not found" "err"
            return 1
        fi
        
        device_path="$device"
    fi
    
    # Get the actual device name
    local actual_device=$(readlink -f "$device_path" 2>/dev/null || echo "$device_path")
    log_message "Device UUID $uuid maps to $actual_device"
    
    # Check if device is already mounted
    if mountpoint -q "$mount_point" 2>/dev/null; then
        # Check if it's mounted to the correct device
        local mounted_device
        mounted_device=$(findmnt -n -o SOURCE "$mount_point")
        if [[ "$mounted_device" == "$actual_device" ]]; then
            log_message "Disk already mounted correctly at $mount_point"
            return 0
        else
            log_message "Warning: Different device mounted at $mount_point, unmounting first" "warning"
            umount "$mount_point" 2>/dev/null
        fi
    fi
    
    # Create mount point if it doesn't exist
    if [[ ! -d "$mount_point" ]]; then
        if mkdir -p "$mount_point"; then
            log_message "Created mount point directory: $mount_point"
        else
            log_message "Error: Failed to create mount point directory: $mount_point" "err"
            return 1
        fi
    fi
    
    # Detect filesystem if not provided
    if [[ -z "$filesystem_type" ]]; then
        filesystem_type=$(blkid -o value -s TYPE "$actual_device" 2>/dev/null)
        debug_log "Detected filesystem type: $filesystem_type"
    fi
    
    # Mount based on filesystem type
    local mount_options
    case "$filesystem_type" in
        "ntfs")
            mount_options="-t ntfs-3g -o rw,uid=0,gid=0,umask=000"
            log_message "Mounting NTFS filesystem"
            ;;
        "exfat")
            mount_options="-t exfat -o rw,uid=0,gid=0,umask=000"
            log_message "Mounting exFAT filesystem"
            ;;
        "vfat"|"fat32")
            mount_options="-t vfat -o rw,uid=0,gid=0,umask=000"
            log_message "Mounting FAT32 filesystem"
            ;;
        "ext2"|"ext3"|"ext4")
            mount_options="-t $filesystem_type"
            log_message "Mounting Linux filesystem ($filesystem_type)"
            ;;
        "btrfs")
            mount_options="-t btrfs -o defaults,compress=zstd"
            log_message "Mounting BTRFS filesystem with compression"
            ;;
        "xfs")
            mount_options="-t xfs"
            log_message "Mounting XFS filesystem"
            ;;
        *)
            mount_options=""
            log_message "Unknown or no filesystem type, attempting default mount"
            ;;
    esac
    
    # Perform the mount
    local mount_cmd="mount $mount_options \"$actual_device\" \"$mount_point\""
    debug_log "Executing: $mount_cmd"
    
    if eval $mount_cmd; then
        # Verify mount was successful
        if mountpoint -q "$mount_point"; then
            log_message "Disk mounted successfully at $mount_point"
            
            # Set appropriate permissions for the mount point
            chmod 755 "$mount_point" 2>/dev/null
            
            return 0
        else
            log_message "Error: Mount command succeeded but mountpoint verification failed" "err"
            return 1
        fi
    else
        # Try fallback mount by UUID for better compatibility
        log_message "Direct mount failed, attempting UUID fallback" "warning"
        if mount UUID="$uuid" "$mount_point" 2>/dev/null; then
            log_message "Disk mounted successfully using UUID fallback at $mount_point"
            return 0
        else
            log_message "Error: Failed to mount $actual_device to $mount_point" "err"
            return 1
        fi
    fi
}

# Function to unmount disk
unmount_disk() {
    local mount_point="$1"
    
    log_message "Unmounting disk from $mount_point"
    
    # Check if actually mounted
    if ! mountpoint -q "$mount_point" 2>/dev/null; then
        log_message "Path $mount_point is not a mount point"
        return 0
    fi
    
    # Attempt unmount with timeout
    if timeout 30 umount "$mount_point"; then
        log_message "Successfully unmounted $mount_point"
        
        # Remove mount point if empty
        if rmdir "$mount_point" 2>/dev/null; then
            debug_log "Removed empty mount point directory: $mount_point"
        else
            debug_log "Mount point directory not empty or removal failed: $mount_point"
        fi
        return 0
    else
        log_message "Error: Failed to unmount $mount_point" "err"
        return 1
    fi
}

# ============================================================================
# CAMERA IMPORT FUNCTIONS
# ============================================================================

# Function to handle camera import
handle_camera_import() {
    local uuid="$1"
    local mount_point="$2"
    local import_config="$3"
    
    # Parse import configuration (format: destination_path only)
    destination_path="$import_config"
    
    if [[ -z "$destination_path" ]]; then
        log_message "Error: No destination path specified for camera import" "err"
        return 1
    fi
    
    # Always use the integrated media importer (non-configurable)
    script_path="${AUTONAS_MEDIA_IMPORTER}"
    
    # Check if import script exists
    if [[ ! -x "$script_path" ]]; then
        log_message "Error: Camera import script not found or not executable: $script_path" "err"
        return 1
    fi
    
    log_message "Starting camera import process for UUID: $uuid"
    log_message "Import destination: $destination_path"

    if systemctl is-active --quiet "autonas-import-$uuid.service" 2>/dev/null; then
        log_message "Import already running for UUID: $uuid; skipping duplicate start" "warning"
        return 0
    fi
    
    # Clean up any previous transient unit before starting a new one
    systemctl stop autonas-import-$uuid.service 2>/dev/null || true
    systemctl reset-failed autonas-import-$uuid.service 2>/dev/null || true
    systemctl disable autonas-import-$uuid.service 2>/dev/null || true
    rm -f /run/systemd/transient/autonas-import-$uuid.service 2>/dev/null || true

    # Run import in background using systemd-run for better process management
    systemd-run --no-block --unit="autonas-import-$uuid" \
                "${AUTONAS_DISK_HANDLER}" import "$uuid" "$mount_point" "$destination_path" "$script_path" || {
        log_message "Error: Failed to start background import process" "err"
        return 1
    }
    
    log_message "Camera import process started successfully"
    return 0
}

# Function to run background import
run_background_import() {
    local uuid="$1"
    local mount_point="$2"
    local destination="$3"
    local script_path="${4:-${AUTONAS_MEDIA_IMPORTER}}"
    
    log_message "Running background import for UUID: $uuid" "info" "autonas-import"
    log_message "Source: $mount_point, Destination: $destination" "info" "autonas-import"

    local lock_dir="/run/lock/autonas"
    local lock_file="$lock_dir/import-$uuid.lock"
    local import_lock_fd

    mkdir -p "$lock_dir"
    exec {import_lock_fd}>"$lock_file" || {
        log_message "Error: Cannot open import lock for UUID: $uuid" "err" "autonas-import"
        return 1
    }

    if ! flock -n "$import_lock_fd"; then
        log_message "Import already running for UUID: $uuid; duplicate background run skipped" "warning" "autonas-import"
        return 0
    fi
    
    # Change to a safe directory
    cd /tmp || cd /
    
    # Create destination directory if it doesn't exist
    mkdir -p "$destination"
    
    # Ensure destination exists
    if [[ ! -d "$destination" ]]; then
        log_message "Error: Cannot access destination directory: $destination" "err" "autonas-import"
        return 1
    fi
    
    # Run the import script
    log_message "Executing import script: $script_path" "info" "autonas-import"
    if "$script_path" "$mount_point" "$destination" --unattended --verbose; then
        log_message "Import completed successfully for UUID: $uuid" "info" "autonas-import"
        
        # Cleanup: unmount and remove mount point
        cleanup_and_unmount "$uuid" "$mount_point"
        return 0
    else
        log_message "Error: Import failed for UUID: $uuid" "err" "autonas-import"
        cleanup_and_unmount "$uuid" "$mount_point"
        return 1
    fi
}

# Function to cleanup and unmount after camera import
cleanup_and_unmount() {
    local uuid="$1"
    local mount_point="$2"
    
    log_message "Cleaning up after import for UUID: $uuid" "info" "autonas-import"
    
    # Unmount the device
    if mountpoint -q "$mount_point" 2>/dev/null; then
        if umount "$mount_point"; then
            log_message "Successfully unmounted $mount_point" "info" "autonas-import"
        else
            log_message "Warning: Failed to unmount $mount_point" "warning" "autonas-import"
        fi
    fi
    
    # Remove mount point directory if empty
    if [[ -d "$mount_point" ]] && rmdir "$mount_point" 2>/dev/null; then
        log_message "Removed empty mount point directory: $mount_point" "info" "autonas-import"
    fi
    
    log_message "Cleanup completed for UUID: $uuid" "info" "autonas-import"
}

# ============================================================================
# MAIN WORKFLOW FUNCTIONS
# ============================================================================

# Function to handle disk attachment
handle_attach() {
    local identifier="$1"

    log_message "AutoNAS attach operation started for identifier: $identifier"

    # Get configuration by UUID, name, or mount point
    local config
    config=$(resolve_config_identifier "$identifier")

    if [[ -z "$config" ]]; then
        log_message "No configuration found for identifier: $identifier - disk will be ignored"
        log_message "To configure this disk, run: autonas add"
        return 1
    fi

    # Parse configuration
    local parsed
    parsed=($(parse_config "$config"))
    local cfg_uuid="${parsed[0]}"
    local name="${parsed[1]}"
    local ip="${parsed[2]}"
    local interface="${parsed[3]}"
    local mount_point="${parsed[4]}"
    local nfs_options="${parsed[5]}"

    log_message "Handling disk attachment for UUID: $cfg_uuid"
    log_message "Processing disk: $name (IP: $ip, Mount: $mount_point)"

    # Handle different disk types
    if [[ "$ip" == "IMPORT" && "$interface" == "IMPORT" ]]; then
        # Camera import workflow: mount temporarily, import, then unmount
        log_message "Detected camera import configuration for UUID: $cfg_uuid"

        # Mount disk for import
        if mount_disk "$cfg_uuid" "$mount_point"; then
            # Start import process
            handle_camera_import "$cfg_uuid" "$mount_point" "$nfs_options"
            return $?
        else
            log_message "Error: Failed to mount camera device" "err"
            return 1
        fi
    elif [[ "$ip" == "LOCAL" ]]; then
        # Local mount only (no network sharing)
        if mount_disk "$cfg_uuid" "$mount_point"; then
            log_message "Local disk mounted successfully at $mount_point"
            return 0
        else
            return 1
        fi
    else
        # Regular NFS workflow: activate IP, mount, export via NFS
        if activate_ip "$ip" "$interface"; then
            if mount_disk "$cfg_uuid" "$mount_point"; then
                if add_nfs_export "$mount_point" "$nfs_options" "$cfg_uuid" "$name"; then
                    log_message "Disk attached successfully: $name"
                    return 0
                else
                    log_message "Warning: Disk mounted but NFS export failed" "warning"
                    return 1
                fi
            else
                deactivate_ip "$ip" "$interface"
                return 1
            fi
        else
            return 1
        fi
    fi
}

# Function to handle disk detachment
handle_detach() {
    local identifier="$1"

    log_message "AutoNAS detach operation started for identifier: $identifier"

    # Get configuration by UUID, name, or mount point
    local config
    config=$(resolve_config_identifier "$identifier")

    if [[ -z "$config" ]]; then
        log_message "No configuration found for identifier: $identifier"
        return 1
    fi

    # Parse configuration
    local parsed
    parsed=($(parse_config "$config"))
    local cfg_uuid="${parsed[0]}"
    local name="${parsed[1]}"
    local ip="${parsed[2]}"
    local interface="${parsed[3]}"
    local mount_point="${parsed[4]}"
    local nfs_options="${parsed[5]}"

    log_message "Handling disk detachment for UUID: $cfg_uuid"
    log_message "Processing disk: $name (Mount: $mount_point)"
    
    # Remove NFS export (if applicable)
    if [[ "$ip" != "LOCAL" && "$ip" != "IMPORT" ]]; then
        remove_nfs_export "$mount_point" "$cfg_uuid" "$name" "$nfs_options"
    fi
    
    # Unmount disk
    if unmount_disk "$mount_point"; then
        # Deactivate IP (if applicable)
        if [[ "$ip" != "LOCAL" && "$ip" != "IMPORT" ]]; then
            deactivate_ip "$ip" "$interface"
        fi
        
        log_message "Disk detached successfully: $name"
        return 0
    else
        log_message "Error: Failed to detach disk: $name" "err"
        return 1
    fi
}

# Function to reload configuration
handle_reload() {
    log_message "Reloading AutoNAS configuration"
    
    log_message "Reloading udev rules"
    udevadm control --reload-rules
    udevadm trigger --subsystem-match=block
    
    log_message "Reloading NFS exports"
    exportfs -ra || log_message "Warning: Failed to reload NFS exports" "warning"
    
    log_message "Reloading systemd daemon"
    systemctl daemon-reload || log_message "Warning: Failed to reload systemd daemon" "warning"
    
    log_message "AutoNAS configuration reload completed"
    return 0
}

# ============================================================================
# DISPLAY FUNCTIONS
# ============================================================================

# Function to show available disks
show_available_disks() {
    echo "=== Available Storage Devices ==="
    echo
    
    local found_devices=0
    local blkid_output
    
    # Get blkid output and process it
    if ! blkid_output=$(blkid 2>/dev/null); then
        echo "Error: Unable to scan for block devices (are you running as root?)"
        return 1
    fi
    
    while IFS= read -r blkid_line; do
        [[ -z "$blkid_line" ]] && continue
        
        local device=$(echo "$blkid_line" | cut -d: -f1)
        
        # Skip if device doesn't exist (shouldn't happen with blkid, but just in case)
        [[ ! -b "$device" ]] && continue
        
        # Skip loop devices, ram disks, and other virtual devices
        case "$device" in
            /dev/loop*|/dev/ram*|/dev/dm-*) continue ;;
        esac
        
        # Extract device information
        local uuid=$(echo "$blkid_line" | grep -o 'UUID="[^"]*"' | cut -d'"' -f2)
        local label=$(echo "$blkid_line" | grep -o 'LABEL="[^"]*"' | cut -d'"' -f2)
        local fstype=$(echo "$blkid_line" | grep -o 'TYPE="[^"]*"' | cut -d'"' -f2)
        local partuuid=$(echo "$blkid_line" | grep -o 'PARTUUID="[^"]*"' | cut -d'"' -f2)
        
        # Skip if no UUID (we need UUID for AutoNAS)
        [[ -z "$uuid" ]] && continue
        
        found_devices=$((found_devices + 1))
        
        # Get device size
        local size=""
        if [[ -b "$device" ]]; then
            size=$(lsblk -b -d -o SIZE "$device" 2>/dev/null | tail -n1)
            if [[ -n "$size" ]] && [[ "$size" =~ ^[0-9]+$ ]]; then
                # Convert to human readable
                size=$(numfmt --to=iec --suffix=B "$size" 2>/dev/null || echo "${size}B")
            fi
        fi
        
        # Display device information
        echo "Device: $device"
        [[ -n "$size" ]] && echo "  Size: $size"
        echo "  UUID: $uuid"
        [[ -n "$label" ]] && echo "  Label: $label"
        [[ -n "$fstype" ]] && echo "  Filesystem: $fstype"
        
        # Check if already configured
        if grep -q "^${uuid}:" "$CONFIG_FILE" 2>/dev/null; then
            local config_line=$(grep "^${uuid}:" "$CONFIG_FILE" 2>/dev/null)
            local config_name=$(echo "$config_line" | cut -d: -f2)
            echo "  Status: ✅ Configured as '$config_name'"
        else
            echo "  Status: ⚠️  Not configured"
        fi
        
        # Check if currently mounted
        local mount_info
        mount_info=$(findmnt -n -o TARGET "$device" 2>/dev/null)
        if [[ -n "$mount_info" ]]; then
            echo "  Mount: 🟢 $mount_info"
        else
            echo "  Mount: ⚪ Not mounted"
        fi
        
        echo
    done <<< "$blkid_output"
    
    if [[ $found_devices -eq 0 ]]; then
        echo "No storage devices with UUIDs found."
        echo "Make sure devices are connected and you're running as root."
    else
        echo "Found $found_devices storage device(s)"
        echo
        echo "To configure a device: autonas add <UUID>"
        echo "To list configured devices: autonas list"
    fi
    
    return 0
}

# Mark that core library has been loaded
AUTONAS_CORE_LOADED=true