autoNAS / scripts / autonas-core.sh
Newer Older
d426b0e 3 months ago History
1140 lines | 38.183kb
Bogdan Timofte authored 3 months ago
1
#!/bin/bash
2

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

            
7
# Load default configuration
8
if [ -f "/etc/default/autonas" ]; then
9
    source "/etc/default/autonas"
10
fi
11

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

            
16
# Global configuration
17
CONFIG_FILE="/etc/pve/autonas/disks.conf"
18
MOUNT_BASE="/mnt/autonas"
19

            
20
# ============================================================================
21
# LOGGING FUNCTIONS
22
# ============================================================================
23

            
24
# Function to log messages with debug support
25
log_message() {
26
    local message="$1"
27
    local priority="${2:-info}"
28
    local context="${3:-${LOG_TAG:-autonas}}"
29

            
30
    # Skip debug messages unless debug mode is enabled
31
    if [ "$priority" = "debug" ] && [ "$AUTONAS_DEBUG" != "true" ]; then
32
        return 0
33
    fi
34

            
35
    # Log to syslog with facility local0 and specified priority
36
    logger -p "local0.$priority" -t "$context" "$message"
37

            
38
    # Also log to stderr if debug mode is enabled
39
    if [ "$AUTONAS_DEBUG" = "true" ]; then
40
        echo "[$context] [$priority] $message" >&2
41
    fi
42

            
43
    # Also echo to stdout/stderr for interactive use
44
    if [ -t 1 ]; then
45
        echo "$(date '+%Y-%m-%d %H:%M:%S') - $message"
46
    fi
47
}
48

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

            
54
# ============================================================================
55
# CONFIGURATION FUNCTIONS
56
# ============================================================================
57

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

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

            
71
# Function to get configuration for a specific UUID (handles systemd escaping)
72
get_config() {
73
    local uuid="$1"
74

            
75
    if [ -z "$uuid" ]; then
76
        return 1
77
    fi
78

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

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

            
87
# Function to get configuration for a specific name
88
get_config_by_name() {
89
    local name="$1"
90

            
91
    if [ -z "$name" ]; then
92
        return 1
93
    fi
94

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

            
Bogdan Timofte authored 3 months ago
99
# Function to get configuration for a specific mount point
100
get_config_by_mount_point() {
101
    local mount_point="$1"
102

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

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

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

            
116
    if [ -z "$identifier" ]; then
117
        return 1
118
    fi
119

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

            
Bogdan Timofte authored 3 months ago
126
# ============================================================================
127
# VALIDATION FUNCTIONS
128
# ============================================================================
129

            
130
# Function to validate UUID format
131
validate_uuid() {
132
    local uuid="$1"
133
    # UUID can be standard format (8-4-4-4-12) or shorter formats used by some filesystems
134
    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}$ ]] || \
135
       [[ "$uuid" =~ ^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$ ]] || \
136
       [[ "$uuid" =~ ^[0-9A-Fa-f]{8}$ ]]; then
137
        return 0
138
    fi
139
    return 1
140
}
141

            
142
# Function to validate disk name
143
validate_disk_name() {
144
    local name="$1"
145

            
146
    # Check length (max 50 characters)
147
    if [[ ${#name} -gt 50 ]]; then
148
        return 1
149
    fi
150

            
151
    # Check if it starts with letter or number
152
    if [[ ! "$name" =~ ^[a-zA-Z0-9] ]]; then
153
        return 1
154
    fi
155

            
156
    # Check allowed characters: letters, numbers, hyphens, underscores, at symbol
157
    if [[ ! "$name" =~ ^[a-zA-Z0-9_@-]+$ ]]; then
158
        return 1
159
    fi
160

            
161
    # Check for reserved names
162
    local reserved_names=("root" "home" "tmp" "var" "usr" "etc" "proc" "sys" "dev" "boot" "mnt" "media" "opt" "srv")
163
    for reserved in "${reserved_names[@]}"; do
164
        if [[ "$name" == "$reserved" ]]; then
165
            return 1
166
        fi
167
    done
168

            
169
    return 0
170
}
171

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

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

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

            
199
# Function to validate disk name (includes duplicate check)
200
validate_disk_name_complete() {
201
    local name="$1"
202
    local uuid="$2"  # Optional: exclude current UUID from duplicate check
203

            
204
    # Run basic validation first
205
    if ! validate_disk_name "$name"; then
206
        return 1
207
    fi
208

            
209
    # Check for duplicate names
210
    if check_disk_name_exists "$name"; then
211
        # If UUID provided, check if this name belongs to the same UUID (allow updates)
212
        if [[ -n "$uuid" ]] && [[ -f "$CONFIG_FILE" ]]; then
213
            local existing_uuid=$(grep ":${name}:" "$CONFIG_FILE" | cut -d':' -f1)
214
            if [[ "$existing_uuid" == "$uuid" ]]; then
215
                # Same UUID, same name - this is an update, allow it
216
                return 0
217
            fi
218
        fi
219
        # Different UUID or no UUID provided - this is a duplicate
220
        return 2  # Special return code for duplicate names
221
    fi
222

            
223
    return 0
224
}
225

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

            
232
# Function to check if disk is already mounted at AutoNAS location
233
is_disk_mounted() {
234
    local uuid="$1"
235
    local mount_point="$2"
236

            
237
    # Check if already mounted at the correct location
238
    if mount | grep -q "UUID=$uuid.*$mount_point"; then
239
        return 0  # Already mounted correctly
240
    fi
241

            
242
    # Check if mounted elsewhere
243
    if mount | grep -q "UUID=$uuid"; then
244
        return 1  # Mounted elsewhere
245
    fi
246

            
247
    return 2  # Not mounted
248
}
249

            
250
# ============================================================================
251
# DEVICE FUNCTIONS
252
# ============================================================================
253

            
254
# Function to get device information
255
get_device_info() {
256
    local uuid="$1"
257

            
258
    # Find device by UUID using blkid
259
    local device_line
260
    device_line=$(blkid | grep "UUID=\"$uuid\"")
261

            
262
    if [[ -z "$device_line" ]]; then
263
        return 1
264
    fi
265

            
266
    local device=$(echo "$device_line" | cut -d: -f1)
267
    local blkid_info=$(echo "$device_line" | cut -d: -f2-)
268

            
269
    echo "device:$device"
270
    echo "$blkid_info"
271
    return 0
272
}
273

            
274
# ============================================================================
275
# NETWORK FUNCTIONS
276
# ============================================================================
277

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

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

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

            
297
# Function to count disks using specific IP
298
count_disks_using_ip() {
299
    local target_ip="$1"
300
    local count=0
301

            
302
    while IFS=':' read -r uuid name ip interface mount_point nfs_options; do
303
        # Skip empty lines and comments
304
        [[ -z "$uuid" || "$uuid" =~ ^# ]] && continue
305

            
306
        if [[ "$ip" == "$target_ip" ]]; then
307
            # Check if this disk is currently attached (mounted)
308
            if mountpoint -q "$mount_point" 2>/dev/null; then
309
                count=$((count + 1))
310
            fi
311
        fi
312
    done < <(grep -v '^#' "$CONFIG_FILE" 2>/dev/null | grep -v '^$')
313

            
314
    echo $count
315
}
316

            
317
# Function to activate IP on interface with smart retry
318
activate_ip() {
319
    local ip="$1"
320
    local interface="$2"
321
    local max_retries=3
322
    local retry_count=0
323

            
324
    # Skip activation for special cases
325
    if [[ "$ip" == "IMPORT" || "$ip" == "LOCAL" ]]; then
326
        log_message "Skipping IP activation for special configuration: $ip" "debug"
327
        return 0
328
    fi
329

            
330
    log_message "Activating IP $ip on interface $interface" "info"
331

            
332
    # Check if interface exists
333
    if ! interface_exists "$interface"; then
334
        log_message "Interface $interface does not exist - waiting for it to appear" "warning"
335

            
336
        # Wait for interface to appear (common with USB/Thunderbolt)
337
        local wait_count=0
338
        while [ $wait_count -lt 30 ] && ! interface_exists "$interface"; do
339
            sleep 2
340
            wait_count=$((wait_count + 1))
341
            debug_log "Waiting for interface $interface... (${wait_count}/30)"
342
        done
343

            
344
        if ! interface_exists "$interface"; then
345
            log_message "Error: Interface $interface does not exist" "err"
346
            return 1
347
        fi
348

            
349
        log_message "Interface $interface is now available" "info"
350
    fi
351

            
352
    # Check if IP is already configured
353
    if is_ip_configured "$ip" "$interface"; then
354
        log_message "IP $ip already configured on interface $interface"
355
        return 0
356
    fi
357

            
358
    # Check if interface is up
359
    if ! ip link show "$interface" | grep -q "state UP"; then
360
        log_message "Bringing up interface $interface"
361
        if ! ip link set "$interface" up; then
362
            log_message "Error: Failed to bring up interface $interface" "err"
363
            return 1
364
        fi
365
        # Wait a moment for interface to come up
366
        sleep 1
367
    fi
368

            
369
    # Try to configure IP with retries
370
    while [ $retry_count -lt $max_retries ]; do
371
        if ip addr add "$ip/24" dev "$interface" 2>/dev/null; then
372
            log_message "Successfully activated IP $ip on interface $interface"
373
            return 0
374
        else
375
            retry_count=$((retry_count + 1))
376
            if [ $retry_count -lt $max_retries ]; then
377
                log_message "Failed to add IP $ip to interface $interface, retrying ($retry_count/$max_retries)" "warning"
378
                sleep 2
379
            fi
380
        fi
381
    done
382

            
383
    log_message "Error: Failed to activate IP $ip on interface $interface" "err"
384
    return 1
385
}
386

            
387
# Function to restore IPs for interface with comprehensive checking
388
restore_interface_ips() {
389
    local interface="$1"
390
    local restored=0
391
    local total_configs=0
392

            
393
    log_message "Checking IP restoration needs for interface: $interface" "info"
394

            
395
    # Check if interface exists and is up
396
    if ! interface_exists "$interface"; then
397
        log_message "Interface $interface does not exist, skipping restoration" "warning"
398
        return 1
399
    fi
400

            
401
    # Check if interface is up
402
    if ! ip link show "$interface" | grep -q "state UP"; then
403
        log_message "Interface $interface is down, waiting for it to come up..." "info"
404
        local wait_count=0
405
        while [ $wait_count -lt 20 ] && ! ip link show "$interface" | grep -q "state UP"; do
406
            sleep 1
407
            wait_count=$((wait_count + 1))
408
        done
409

            
410
        if ! ip link show "$interface" | grep -q "state UP"; then
411
            log_message "Interface $interface did not come up within 20 seconds" "warning"
412
            return 1
413
        fi
414
    fi
415

            
416
    log_message "Interface $interface is up, checking for required IP configurations" "info"
417

            
418
    while IFS= read -r line; do
419
        # Skip comments and empty lines
420
        [[ "$line" =~ ^[[:space:]]*# ]] && continue
421
        [[ -z "$line" ]] && continue
422

            
423
        # Parse configuration
424
        IFS=':' read -r uuid name ip config_interface mount_point nfs_options <<< "$line"
425

            
426
        # Check if this line is for our interface
427
        if [ "$config_interface" = "$interface" ]; then
428
            ((total_configs++))
429
            debug_log "Found configuration: $name ($uuid) requires IP $ip on $interface"
430

            
431
            # Check if the disk is currently mounted
432
            if mountpoint -q "$mount_point" 2>/dev/null; then
433
                log_message "Disk $name is mounted, checking IP configuration" "info"
434

            
435
                # Check if IP is already configured
436
                if ! is_ip_configured "$ip" "$interface"; then
437
                    log_message "IP $ip missing on interface $interface for mounted disk: $name" "warning"
438

            
439
                    # Use the core activate_ip function
440
                    if activate_ip "$ip" "$interface"; then
441
                        log_message "Successfully restored IP $ip on interface $interface for disk $name" "info"
442
                        ((restored++))
443
                    else
444
                        log_message "Failed to restore IP $ip on interface $interface for disk $name" "error"
445
                    fi
446
                else
447
                    debug_log "IP $ip already configured on interface $interface for disk $name"
448
                fi
449
            else
450
                debug_log "Disk $name is not mounted, skipping IP configuration"
451
            fi
452
        fi
453
    done < "$CONFIG_FILE"
454

            
455
    if [ $total_configs -eq 0 ]; then
456
        debug_log "No AutoNAS configurations found for interface $interface"
457
    elif [ $restored -gt 0 ]; then
458
        log_message "Restored $restored/$total_configs IP addresses on interface $interface" "info"
459

            
460
        # Trigger NFS export refresh if we restored any IPs
461
        log_message "Refreshing NFS exports after IP restoration" "info"
462
        exportfs -ra 2>/dev/null
463
        systemctl reload nfs-kernel-server 2>/dev/null
464
    else
465
        log_message "All required IPs already configured on interface $interface" "info"
466
    fi
467

            
468
    return 0
469
}
470

            
471
# Function to deactivate IP on interface with smart checking
472
deactivate_ip() {
473
    local ip="$1"
474
    local interface="$2"
475

            
476
    # Skip deactivation for special cases
477
    if [[ "$ip" == "IMPORT" || "$ip" == "LOCAL" ]]; then
478
        log_message "Skipping IP deactivation for special configuration: $ip" "debug"
479
        return 0
480
    fi
481

            
482
    # Check how many other disks are using this IP
483
    local usage_count
484
    usage_count=$(count_disks_using_ip "$ip")
485

            
486
    if [[ $usage_count -gt 1 ]]; then
487
        log_message "IP $ip still used by $((usage_count - 1)) other disks, keeping it active"
488
        return 0
489
    fi
490

            
491
    log_message "Deactivating IP $ip on interface $interface"
492

            
493
    # Check if IP is configured
494
    if ! is_ip_configured "$ip" "$interface"; then
495
        log_message "IP $ip not configured on interface $interface"
496
        return 0
497
    fi
498

            
499
    # Remove IP from interface
500
    if ip addr del "$ip/24" dev "$interface" 2>/dev/null; then
501
        log_message "Successfully deactivated IP $ip on interface $interface"
502
        return 0
503
    else
504
        log_message "Error: Failed to deactivate IP $ip on interface $interface" "err"
505
        return 1
506
    fi
507
}
508

            
509
# ============================================================================
510
# NFS FUNCTIONS
511
# ============================================================================
512

            
513
# Function to add NFS export with AutoNAS identification
514
add_nfs_export() {
515
    local mount_point="$1"
516
    local nfs_options="$2"
517
    local uuid="${3:-}"
518
    local name="${4:-}"
519

            
520
    # Skip NFS export for special cases
521
    if [[ "$nfs_options" == "LOCAL" ]] || [[ "$mount_point" =~ ^.*@Camera.*$ ]]; then
522
        log_message "Skipping NFS export for local/camera configuration" "debug"
523
        return 0
524
    fi
525

            
526
    log_message "Adding NFS export for $mount_point"
527

            
528
    # Create NFS export entry
529
    local export_entry="$mount_point $nfs_options"
530

            
531
    # Check if export already exists
532
    if grep -Fxq "$export_entry" /etc/exports 2>/dev/null; then
533
        log_message "NFS export already exists: $export_entry"
534
        return 0
535
    fi
536

            
537
    # Add identifying comment if UUID/name provided
538
    if [[ -n "$uuid" && -n "$name" ]]; then
539
        echo "# AutoNAS Export - UUID:$uuid NAME:$name" >> /etc/exports
540
    fi
541

            
542
    # Add export to /etc/exports
543
    echo "$export_entry" >> /etc/exports
544
    if [[ $? -eq 0 ]]; then
545
        log_message "Successfully added NFS export: $export_entry"
546

            
547
        # Reload exports
548
        if exportfs -ra; then
549
            log_message "NFS exports reloaded successfully"
550
            return 0
551
        else
552
            log_message "Warning: Failed to reload NFS exports" "warning"
553
            return 1
554
        fi
555
    else
556
        log_message "Error: Failed to add NFS export" "err"
557
        return 1
558
    fi
559
}
560

            
561
# Function to remove NFS export
562
remove_nfs_export() {
563
    local mount_point="$1"
564
    local uuid="${2:-}"
565
    local name="${3:-}"
566
    local nfs_options="${4:-}"
567

            
568
    # Skip NFS export removal for special cases
569
    if [[ "$mount_point" =~ ^.*@Camera.*$ ]] || [[ "$nfs_options" == "LOCAL" ]]; then
570
        log_message "Skipping NFS export removal for camera/local configuration" "debug"
571
        return 0
572
    fi
573

            
574
    log_message "Removing NFS export for $mount_point"
575

            
576
    # Create temporary file without the export
577
    local temp_exports=$(mktemp)
578
    if [[ ! -f /etc/exports ]]; then
579
        log_message "No /etc/exports file found"
580
        rm -f "$temp_exports"
581
        return 0
582
    fi
583

            
584
    # If UUID provided, use AutoNAS marker method
585
    if [[ -n "$uuid" ]]; then
586
        local skip_next=false
587
        local found_export=false
588

            
589
        # Process /etc/exports line by line
590
        while IFS= read -r line; do
591
            if [[ "$line" =~ ^#\ AutoNAS\ Export\ -\ UUID:${uuid}\ NAME: ]]; then
592
                # Found our marker comment, skip this line and the next one
593
                skip_next=true
594
                found_export=true
595
                debug_log "Found AutoNAS export marker for $name"
596
                continue
597
            elif [ "$skip_next" = true ]; then
598
                # Skip the export line that follows our marker
599
                skip_next=false
600
                debug_log "Skipping export line for $name"
601
                continue
602
            else
603
                # Keep all other lines
604
                echo "$line" >> "$temp_exports"
605
            fi
606
        done < /etc/exports
607

            
608
        if [ "$found_export" = true ]; then
609
            # Replace /etc/exports with cleaned version
610
            mv "$temp_exports" /etc/exports
611
            log_message "NFS export removed using AutoNAS marker for $name"
612
        else
613
            rm -f "$temp_exports"
614
            # Fallback to generic method
615
            grep -v "^${mount_point} " /etc/exports > "$temp_exports"
616
            mv "$temp_exports" /etc/exports
617
            log_message "NFS export removed using generic method for $mount_point"
618
        fi
619
    else
620
        # Generic removal method
621
        grep -v "^${mount_point} " /etc/exports > "$temp_exports"
622
        mv "$temp_exports" /etc/exports
623
        log_message "NFS export removed for $mount_point"
624
    fi
625

            
626
    # Reload exports
627
    if exportfs -ra; then
628
        log_message "NFS exports reloaded successfully"
629
        return 0
630
    else
631
        log_message "Warning: Failed to reload NFS exports" "warning"
632
        return 1
633
    fi
634
}
635

            
636
# ============================================================================
637
# STORAGE FUNCTIONS
638
# ============================================================================
639

            
640
# Function to mount disk with comprehensive filesystem support
641
mount_disk() {
642
    local uuid="$1"
643
    local mount_point="$2"
644
    local filesystem_type="${3:-}"
645

            
646
    log_message "Mounting disk with UUID $uuid to $mount_point"
647

            
648
    # Wait for device to be ready (udev sometimes triggers too early)
649
    local device_path="/dev/disk/by-uuid/$uuid"
650
    local wait_count=0
651
    while [ ! -e "$device_path" ] && [ $wait_count -lt 10 ]; do
652
        debug_log "Waiting for device $device_path to appear..."
653
        sleep 1
654
        wait_count=$((wait_count + 1))
655
    done
656

            
657
    if [ ! -e "$device_path" ]; then
658
        # Try alternative method - find device by UUID using blkid
659
        local device
660
        device=$(blkid | grep "UUID=\"$uuid\"" | cut -d: -f1)
661

            
662
        if [[ -z "$device" ]]; then
663
            log_message "Error: Device with UUID $uuid not found" "err"
664
            return 1
665
        fi
666

            
667
        device_path="$device"
668
    fi
669

            
670
    # Get the actual device name
671
    local actual_device=$(readlink -f "$device_path" 2>/dev/null || echo "$device_path")
672
    log_message "Device UUID $uuid maps to $actual_device"
673

            
674
    # Check if device is already mounted
675
    if mountpoint -q "$mount_point" 2>/dev/null; then
676
        # Check if it's mounted to the correct device
677
        local mounted_device
678
        mounted_device=$(findmnt -n -o SOURCE "$mount_point")
679
        if [[ "$mounted_device" == "$actual_device" ]]; then
680
            log_message "Disk already mounted correctly at $mount_point"
681
            return 0
682
        else
683
            log_message "Warning: Different device mounted at $mount_point, unmounting first" "warning"
684
            umount "$mount_point" 2>/dev/null
685
        fi
686
    fi
687

            
688
    # Create mount point if it doesn't exist
689
    if [[ ! -d "$mount_point" ]]; then
690
        if mkdir -p "$mount_point"; then
691
            log_message "Created mount point directory: $mount_point"
692
        else
693
            log_message "Error: Failed to create mount point directory: $mount_point" "err"
694
            return 1
695
        fi
696
    fi
697

            
698
    # Detect filesystem if not provided
699
    if [[ -z "$filesystem_type" ]]; then
700
        filesystem_type=$(blkid -o value -s TYPE "$actual_device" 2>/dev/null)
701
        debug_log "Detected filesystem type: $filesystem_type"
702
    fi
703

            
704
    # Mount based on filesystem type
705
    local mount_options
706
    case "$filesystem_type" in
707
        "ntfs")
708
            mount_options="-t ntfs-3g -o rw,uid=0,gid=0,umask=000"
709
            log_message "Mounting NTFS filesystem"
710
            ;;
711
        "exfat")
712
            mount_options="-t exfat -o rw,uid=0,gid=0,umask=000"
713
            log_message "Mounting exFAT filesystem"
714
            ;;
715
        "vfat"|"fat32")
716
            mount_options="-t vfat -o rw,uid=0,gid=0,umask=000"
717
            log_message "Mounting FAT32 filesystem"
718
            ;;
719
        "ext2"|"ext3"|"ext4")
720
            mount_options="-t $filesystem_type"
721
            log_message "Mounting Linux filesystem ($filesystem_type)"
722
            ;;
723
        "btrfs")
724
            mount_options="-t btrfs -o defaults,compress=zstd"
725
            log_message "Mounting BTRFS filesystem with compression"
726
            ;;
727
        "xfs")
728
            mount_options="-t xfs"
729
            log_message "Mounting XFS filesystem"
730
            ;;
731
        *)
732
            mount_options=""
733
            log_message "Unknown or no filesystem type, attempting default mount"
734
            ;;
735
    esac
736

            
737
    # Perform the mount
738
    local mount_cmd="mount $mount_options \"$actual_device\" \"$mount_point\""
739
    debug_log "Executing: $mount_cmd"
740

            
741
    if eval $mount_cmd; then
742
        # Verify mount was successful
743
        if mountpoint -q "$mount_point"; then
744
            log_message "Disk mounted successfully at $mount_point"
745

            
746
            # Set appropriate permissions for the mount point
747
            chmod 755 "$mount_point" 2>/dev/null
748

            
749
            return 0
750
        else
751
            log_message "Error: Mount command succeeded but mountpoint verification failed" "err"
752
            return 1
753
        fi
754
    else
755
        # Try fallback mount by UUID for better compatibility
756
        log_message "Direct mount failed, attempting UUID fallback" "warning"
757
        if mount UUID="$uuid" "$mount_point" 2>/dev/null; then
758
            log_message "Disk mounted successfully using UUID fallback at $mount_point"
759
            return 0
760
        else
761
            log_message "Error: Failed to mount $actual_device to $mount_point" "err"
762
            return 1
763
        fi
764
    fi
765
}
766

            
767
# Function to unmount disk
768
unmount_disk() {
769
    local mount_point="$1"
770

            
771
    log_message "Unmounting disk from $mount_point"
772

            
773
    # Check if actually mounted
774
    if ! mountpoint -q "$mount_point" 2>/dev/null; then
775
        log_message "Path $mount_point is not a mount point"
776
        return 0
777
    fi
778

            
779
    # Attempt unmount with timeout
780
    if timeout 30 umount "$mount_point"; then
781
        log_message "Successfully unmounted $mount_point"
782

            
783
        # Remove mount point if empty
784
        if rmdir "$mount_point" 2>/dev/null; then
785
            debug_log "Removed empty mount point directory: $mount_point"
786
        else
787
            debug_log "Mount point directory not empty or removal failed: $mount_point"
788
        fi
789
        return 0
790
    else
791
        log_message "Error: Failed to unmount $mount_point" "err"
792
        return 1
793
    fi
794
}
795

            
796
# ============================================================================
797
# CAMERA IMPORT FUNCTIONS
798
# ============================================================================
799

            
800
# Function to handle camera import
801
handle_camera_import() {
802
    local uuid="$1"
803
    local mount_point="$2"
804
    local import_config="$3"
805

            
806
    # Parse import configuration (format: destination_path only)
807
    destination_path="$import_config"
808

            
809
    if [[ -z "$destination_path" ]]; then
810
        log_message "Error: No destination path specified for camera import" "err"
811
        return 1
812
    fi
813

            
814
    # Always use the integrated media importer (non-configurable)
815
    script_path="/usr/local/bin/autonas-media-importer.sh"
816

            
817
    # Check if import script exists
818
    if [[ ! -x "$script_path" ]]; then
819
        log_message "Error: Camera import script not found or not executable: $script_path" "err"
820
        return 1
821
    fi
822

            
823
    log_message "Starting camera import process for UUID: $uuid"
824
    log_message "Import destination: $destination_path"
825

            
826
    # Clean up any previous transient unit before starting a new one
827
    systemctl stop autonas-import-$uuid.service 2>/dev/null || true
828
    systemctl reset-failed autonas-import-$uuid.service 2>/dev/null || true
829
    systemctl disable autonas-import-$uuid.service 2>/dev/null || true
830
    rm -f /run/systemd/transient/autonas-import-$uuid.service 2>/dev/null || true
831

            
832
    # Run import in background using systemd-run for better process management
833
    systemd-run --no-block --unit="autonas-import-$uuid" \
834
                /usr/local/bin/autonas-disk-handler.sh import "$uuid" "$mount_point" "$destination_path" "$script_path" || {
835
        log_message "Error: Failed to start background import process" "err"
836
        return 1
837
    }
838

            
839
    log_message "Camera import process started successfully"
840
    return 0
841
}
842

            
843
# Function to run background import
844
run_background_import() {
845
    local uuid="$1"
846
    local mount_point="$2"
847
    local destination="$3"
848
    local script_path="${4:-/usr/local/bin/autonas-media-importer.sh}"
849

            
850
    log_message "Running background import for UUID: $uuid" "info" "autonas-import"
851
    log_message "Source: $mount_point, Destination: $destination" "info" "autonas-import"
852

            
853
    # Change to a safe directory
854
    cd /tmp || cd /
855

            
856
    # Create destination directory if it doesn't exist
857
    mkdir -p "$destination"
858

            
859
    # Ensure destination exists
860
    if [[ ! -d "$destination" ]]; then
861
        log_message "Error: Cannot access destination directory: $destination" "err" "autonas-import"
862
        return 1
863
    fi
864

            
865
    # Run the import script
866
    log_message "Executing import script: $script_path" "info" "autonas-import"
867
    if "$script_path" "$mount_point" "$destination" --verbose; then
868
        log_message "Import completed successfully for UUID: $uuid" "info" "autonas-import"
869

            
870
        # Cleanup: unmount and remove mount point
871
        cleanup_and_unmount "$uuid" "$mount_point"
872
        return 0
873
    else
874
        log_message "Error: Import failed for UUID: $uuid" "err" "autonas-import"
875
        cleanup_and_unmount "$uuid" "$mount_point"
876
        return 1
877
    fi
878
}
879

            
880
# Function to cleanup and unmount after camera import
881
cleanup_and_unmount() {
882
    local uuid="$1"
883
    local mount_point="$2"
884

            
885
    log_message "Cleaning up after import for UUID: $uuid" "info" "autonas-import"
886

            
887
    # Unmount the device
888
    if mountpoint -q "$mount_point" 2>/dev/null; then
889
        if umount "$mount_point"; then
890
            log_message "Successfully unmounted $mount_point" "info" "autonas-import"
891
        else
892
            log_message "Warning: Failed to unmount $mount_point" "warning" "autonas-import"
893
        fi
894
    fi
895

            
896
    # Remove mount point directory if empty
897
    if [[ -d "$mount_point" ]] && rmdir "$mount_point" 2>/dev/null; then
898
        log_message "Removed empty mount point directory: $mount_point" "info" "autonas-import"
899
    fi
900

            
901
    log_message "Cleanup completed for UUID: $uuid" "info" "autonas-import"
902
}
903

            
904
# ============================================================================
905
# MAIN WORKFLOW FUNCTIONS
906
# ============================================================================
907

            
908
# Function to handle disk attachment
909
handle_attach() {
Bogdan Timofte authored 3 months ago
910
    local identifier="$1"
911

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

            
914
    # Get configuration by UUID, name, or mount point
Bogdan Timofte authored 3 months ago
915
    local config
Bogdan Timofte authored 3 months ago
916
    config=$(resolve_config_identifier "$identifier")
917

            
Bogdan Timofte authored 3 months ago
918
    if [[ -z "$config" ]]; then
Bogdan Timofte authored 3 months ago
919
        log_message "No configuration found for identifier: $identifier - disk will be ignored"
Bogdan Timofte authored 3 months ago
920
        log_message "To configure this disk, run: autonas add"
921
        return 1
922
    fi
Bogdan Timofte authored 3 months ago
923

            
Bogdan Timofte authored 3 months ago
924
    # Parse configuration
925
    local parsed
926
    parsed=($(parse_config "$config"))
927
    local cfg_uuid="${parsed[0]}"
928
    local name="${parsed[1]}"
929
    local ip="${parsed[2]}"
930
    local interface="${parsed[3]}"
931
    local mount_point="${parsed[4]}"
932
    local nfs_options="${parsed[5]}"
Bogdan Timofte authored 3 months ago
933

            
934
    log_message "Handling disk attachment for UUID: $cfg_uuid"
Bogdan Timofte authored 3 months ago
935
    log_message "Processing disk: $name (IP: $ip, Mount: $mount_point)"
Bogdan Timofte authored 3 months ago
936

            
Bogdan Timofte authored 3 months ago
937
    # Handle different disk types
938
    if [[ "$ip" == "IMPORT" && "$interface" == "IMPORT" ]]; then
939
        # Camera import workflow: mount temporarily, import, then unmount
Bogdan Timofte authored 3 months ago
940
        log_message "Detected camera import configuration for UUID: $cfg_uuid"
941

            
Bogdan Timofte authored 3 months ago
942
        # Mount disk for import
Bogdan Timofte authored 3 months ago
943
        if mount_disk "$cfg_uuid" "$mount_point"; then
Bogdan Timofte authored 3 months ago
944
            # Start import process
Bogdan Timofte authored 3 months ago
945
            handle_camera_import "$cfg_uuid" "$mount_point" "$nfs_options"
Bogdan Timofte authored 3 months ago
946
            return $?
947
        else
948
            log_message "Error: Failed to mount camera device" "err"
949
            return 1
950
        fi
951
    elif [[ "$ip" == "LOCAL" ]]; then
952
        # Local mount only (no network sharing)
Bogdan Timofte authored 3 months ago
953
        if mount_disk "$cfg_uuid" "$mount_point"; then
Bogdan Timofte authored 3 months ago
954
            log_message "Local disk mounted successfully at $mount_point"
955
            return 0
956
        else
957
            return 1
958
        fi
959
    else
960
        # Regular NFS workflow: activate IP, mount, export via NFS
961
        if activate_ip "$ip" "$interface"; then
Bogdan Timofte authored 3 months ago
962
            if mount_disk "$cfg_uuid" "$mount_point"; then
963
                if add_nfs_export "$mount_point" "$nfs_options" "$cfg_uuid" "$name"; then
Bogdan Timofte authored 3 months ago
964
                    log_message "Disk attached successfully: $name"
965
                    return 0
966
                else
967
                    log_message "Warning: Disk mounted but NFS export failed" "warning"
968
                    return 1
969
                fi
970
            else
971
                deactivate_ip "$ip" "$interface"
972
                return 1
973
            fi
974
        else
975
            return 1
976
        fi
977
    fi
978
}
979

            
980
# Function to handle disk detachment
981
handle_detach() {
Bogdan Timofte authored 3 months ago
982
    local identifier="$1"
983

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

            
986
    # Get configuration by UUID, name, or mount point
Bogdan Timofte authored 3 months ago
987
    local config
Bogdan Timofte authored 3 months ago
988
    config=$(resolve_config_identifier "$identifier")
989

            
Bogdan Timofte authored 3 months ago
990
    if [[ -z "$config" ]]; then
Bogdan Timofte authored 3 months ago
991
        log_message "No configuration found for identifier: $identifier"
Bogdan Timofte authored 3 months ago
992
        return 1
993
    fi
Bogdan Timofte authored 3 months ago
994

            
Bogdan Timofte authored 3 months ago
995
    # Parse configuration
996
    local parsed
997
    parsed=($(parse_config "$config"))
998
    local cfg_uuid="${parsed[0]}"
999
    local name="${parsed[1]}"
1000
    local ip="${parsed[2]}"
1001
    local interface="${parsed[3]}"
1002
    local mount_point="${parsed[4]}"
1003
    local nfs_options="${parsed[5]}"
Bogdan Timofte authored 3 months ago
1004

            
1005
    log_message "Handling disk detachment for UUID: $cfg_uuid"
Bogdan Timofte authored 3 months ago
1006
    log_message "Processing disk: $name (Mount: $mount_point)"
1007

            
1008
    # Remove NFS export (if applicable)
1009
    if [[ "$ip" != "LOCAL" && "$ip" != "IMPORT" ]]; then
Bogdan Timofte authored 3 months ago
1010
        remove_nfs_export "$mount_point" "$cfg_uuid" "$name" "$nfs_options"
Bogdan Timofte authored 3 months ago
1011
    fi
1012

            
1013
    # Unmount disk
1014
    if unmount_disk "$mount_point"; then
1015
        # Deactivate IP (if applicable)
1016
        if [[ "$ip" != "LOCAL" && "$ip" != "IMPORT" ]]; then
1017
            deactivate_ip "$ip" "$interface"
1018
        fi
1019

            
1020
        log_message "Disk detached successfully: $name"
1021
        return 0
1022
    else
1023
        log_message "Error: Failed to detach disk: $name" "err"
1024
        return 1
1025
    fi
1026
}
1027

            
1028
# Function to reload configuration
1029
handle_reload() {
1030
    log_message "Reloading AutoNAS configuration"
1031

            
1032
    log_message "Reloading udev rules"
1033
    udevadm control --reload-rules
1034
    udevadm trigger --subsystem-match=block
1035

            
1036
    log_message "Reloading NFS exports"
1037
    exportfs -ra || log_message "Warning: Failed to reload NFS exports" "warning"
1038

            
1039
    log_message "Reloading systemd daemon"
1040
    systemctl daemon-reload || log_message "Warning: Failed to reload systemd daemon" "warning"
1041

            
1042
    log_message "AutoNAS configuration reload completed"
1043
    return 0
1044
}
1045

            
1046
# ============================================================================
1047
# DISPLAY FUNCTIONS
1048
# ============================================================================
1049

            
1050
# Function to show available disks
1051
show_available_disks() {
1052
    echo "=== Available Storage Devices ==="
1053
    echo
1054

            
1055
    local found_devices=0
1056
    local blkid_output
1057

            
1058
    # Get blkid output and process it
1059
    if ! blkid_output=$(blkid 2>/dev/null); then
1060
        echo "Error: Unable to scan for block devices (are you running as root?)"
1061
        return 1
1062
    fi
1063

            
1064
    while IFS= read -r blkid_line; do
1065
        [[ -z "$blkid_line" ]] && continue
1066

            
1067
        local device=$(echo "$blkid_line" | cut -d: -f1)
1068

            
1069
        # Skip if device doesn't exist (shouldn't happen with blkid, but just in case)
1070
        [[ ! -b "$device" ]] && continue
1071

            
1072
        # Skip loop devices, ram disks, and other virtual devices
1073
        case "$device" in
1074
            /dev/loop*|/dev/ram*|/dev/dm-*) continue ;;
1075
        esac
1076

            
1077
        # Extract device information
1078
        local uuid=$(echo "$blkid_line" | grep -o 'UUID="[^"]*"' | cut -d'"' -f2)
1079
        local label=$(echo "$blkid_line" | grep -o 'LABEL="[^"]*"' | cut -d'"' -f2)
1080
        local fstype=$(echo "$blkid_line" | grep -o 'TYPE="[^"]*"' | cut -d'"' -f2)
1081
        local partuuid=$(echo "$blkid_line" | grep -o 'PARTUUID="[^"]*"' | cut -d'"' -f2)
1082

            
1083
        # Skip if no UUID (we need UUID for AutoNAS)
1084
        [[ -z "$uuid" ]] && continue
1085

            
1086
        found_devices=$((found_devices + 1))
1087

            
1088
        # Get device size
1089
        local size=""
1090
        if [[ -b "$device" ]]; then
1091
            size=$(lsblk -b -d -o SIZE "$device" 2>/dev/null | tail -n1)
1092
            if [[ -n "$size" ]] && [[ "$size" =~ ^[0-9]+$ ]]; then
1093
                # Convert to human readable
1094
                size=$(numfmt --to=iec --suffix=B "$size" 2>/dev/null || echo "${size}B")
1095
            fi
1096
        fi
1097

            
1098
        # Display device information
1099
        echo "Device: $device"
1100
        [[ -n "$size" ]] && echo "  Size: $size"
1101
        echo "  UUID: $uuid"
1102
        [[ -n "$label" ]] && echo "  Label: $label"
1103
        [[ -n "$fstype" ]] && echo "  Filesystem: $fstype"
1104

            
1105
        # Check if already configured
1106
        if grep -q "^${uuid}:" "$CONFIG_FILE" 2>/dev/null; then
1107
            local config_line=$(grep "^${uuid}:" "$CONFIG_FILE" 2>/dev/null)
1108
            local config_name=$(echo "$config_line" | cut -d: -f2)
1109
            echo "  Status: ✅ Configured as '$config_name'"
1110
        else
1111
            echo "  Status: ⚠️  Not configured"
1112
        fi
1113

            
1114
        # Check if currently mounted
1115
        local mount_info
1116
        mount_info=$(findmnt -n -o TARGET "$device" 2>/dev/null)
1117
        if [[ -n "$mount_info" ]]; then
1118
            echo "  Mount: 🟢 $mount_info"
1119
        else
1120
            echo "  Mount: ⚪ Not mounted"
1121
        fi
1122

            
1123
        echo
1124
    done <<< "$blkid_output"
1125

            
1126
    if [[ $found_devices -eq 0 ]]; then
1127
        echo "No storage devices with UUIDs found."
1128
        echo "Make sure devices are connected and you're running as root."
1129
    else
1130
        echo "Found $found_devices storage device(s)"
1131
        echo
1132
        echo "To configure a device: autonas add <UUID>"
1133
        echo "To list configured devices: autonas list"
1134
    fi
1135

            
1136
    return 0
1137
}
1138

            
1139
# Mark that core library has been loaded
1140
AUTONAS_CORE_LOADED=true