autoNAS / scripts / autonas-core.sh
Newer Older
1170 lines | 39.536kb
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

            
Bogdan Timofte authored 2 weeks ago
7
# Standard install paths
8
AUTONAS_ORG_ID="${AUTONAS_ORG_ID:-xdev}"
9
AUTONAS_PROJECT_ID="${AUTONAS_PROJECT_ID:-autonas}"
10
AUTONAS_RUNTIME_DIR="${AUTONAS_RUNTIME_DIR:-/usr/local/lib/${AUTONAS_ORG_ID}/${AUTONAS_PROJECT_ID}}"
11
AUTONAS_COMMAND="${AUTONAS_COMMAND:-/usr/local/sbin/autonas}"
12
AUTONAS_DEFAULTS_FILE="${AUTONAS_DEFAULTS_FILE:-/etc/default/${AUTONAS_ORG_ID}-${AUTONAS_PROJECT_ID}}"
13
AUTONAS_DOC_DIR="${AUTONAS_DOC_DIR:-/usr/local/share/doc/${AUTONAS_ORG_ID}/${AUTONAS_PROJECT_ID}}"
14
AUTONAS_MEDIA_IMPORTER="${AUTONAS_MEDIA_IMPORTER:-${AUTONAS_RUNTIME_DIR}/autonas-media-importer.sh}"
15
AUTONAS_DISK_HANDLER="${AUTONAS_DISK_HANDLER:-${AUTONAS_RUNTIME_DIR}/autonas-disk-handler.sh}"
16

            
Bogdan Timofte authored 3 months ago
17
# Load default configuration
Bogdan Timofte authored 2 weeks ago
18
if [ -f "${AUTONAS_DEFAULTS_FILE}" ]; then
19
    source "${AUTONAS_DEFAULTS_FILE}"
Bogdan Timofte authored 3 months ago
20
fi
21

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

            
26
# Global configuration
27
CONFIG_FILE="/etc/pve/autonas/disks.conf"
28
MOUNT_BASE="/mnt/autonas"
29

            
30
# ============================================================================
31
# LOGGING FUNCTIONS
32
# ============================================================================
33

            
34
# Function to log messages with debug support
35
log_message() {
36
    local message="$1"
37
    local priority="${2:-info}"
38
    local context="${3:-${LOG_TAG:-autonas}}"
39

            
40
    # Skip debug messages unless debug mode is enabled
41
    if [ "$priority" = "debug" ] && [ "$AUTONAS_DEBUG" != "true" ]; then
42
        return 0
43
    fi
44

            
45
    # Log to syslog with facility local0 and specified priority
46
    logger -p "local0.$priority" -t "$context" "$message"
47

            
48
    # Also log to stderr if debug mode is enabled
49
    if [ "$AUTONAS_DEBUG" = "true" ]; then
50
        echo "[$context] [$priority] $message" >&2
51
    fi
52

            
53
    # Also echo to stdout/stderr for interactive use
54
    if [ -t 1 ]; then
55
        echo "$(date '+%Y-%m-%d %H:%M:%S') - $message"
56
    fi
57
}
58

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

            
64
# ============================================================================
65
# CONFIGURATION FUNCTIONS
66
# ============================================================================
67

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

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

            
81
# Function to get configuration for a specific UUID (handles systemd escaping)
82
get_config() {
83
    local uuid="$1"
84

            
85
    if [ -z "$uuid" ]; then
86
        return 1
87
    fi
88

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

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

            
97
# Function to get configuration for a specific name
98
get_config_by_name() {
99
    local name="$1"
100

            
101
    if [ -z "$name" ]; then
102
        return 1
103
    fi
104

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

            
Bogdan Timofte authored 3 months ago
109
# Function to get configuration for a specific mount point
110
get_config_by_mount_point() {
111
    local mount_point="$1"
112

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

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

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

            
126
    if [ -z "$identifier" ]; then
127
        return 1
128
    fi
129

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

            
Bogdan Timofte authored 3 months ago
136
# ============================================================================
137
# VALIDATION FUNCTIONS
138
# ============================================================================
139

            
140
# Function to validate UUID format
141
validate_uuid() {
142
    local uuid="$1"
143
    # UUID can be standard format (8-4-4-4-12) or shorter formats used by some filesystems
144
    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}$ ]] || \
145
       [[ "$uuid" =~ ^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$ ]] || \
146
       [[ "$uuid" =~ ^[0-9A-Fa-f]{8}$ ]]; then
147
        return 0
148
    fi
149
    return 1
150
}
151

            
152
# Function to validate disk name
153
validate_disk_name() {
154
    local name="$1"
155

            
156
    # Check length (max 50 characters)
157
    if [[ ${#name} -gt 50 ]]; then
158
        return 1
159
    fi
160

            
161
    # Check if it starts with letter or number
162
    if [[ ! "$name" =~ ^[a-zA-Z0-9] ]]; then
163
        return 1
164
    fi
165

            
166
    # Check allowed characters: letters, numbers, hyphens, underscores, at symbol
167
    if [[ ! "$name" =~ ^[a-zA-Z0-9_@-]+$ ]]; then
168
        return 1
169
    fi
170

            
171
    # Check for reserved names
172
    local reserved_names=("root" "home" "tmp" "var" "usr" "etc" "proc" "sys" "dev" "boot" "mnt" "media" "opt" "srv")
173
    for reserved in "${reserved_names[@]}"; do
174
        if [[ "$name" == "$reserved" ]]; then
175
            return 1
176
        fi
177
    done
178

            
179
    return 0
180
}
181

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

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

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

            
209
# Function to validate disk name (includes duplicate check)
210
validate_disk_name_complete() {
211
    local name="$1"
212
    local uuid="$2"  # Optional: exclude current UUID from duplicate check
213

            
214
    # Run basic validation first
215
    if ! validate_disk_name "$name"; then
216
        return 1
217
    fi
218

            
219
    # Check for duplicate names
220
    if check_disk_name_exists "$name"; then
221
        # If UUID provided, check if this name belongs to the same UUID (allow updates)
222
        if [[ -n "$uuid" ]] && [[ -f "$CONFIG_FILE" ]]; then
223
            local existing_uuid=$(grep ":${name}:" "$CONFIG_FILE" | cut -d':' -f1)
224
            if [[ "$existing_uuid" == "$uuid" ]]; then
225
                # Same UUID, same name - this is an update, allow it
226
                return 0
227
            fi
228
        fi
229
        # Different UUID or no UUID provided - this is a duplicate
230
        return 2  # Special return code for duplicate names
231
    fi
232

            
233
    return 0
234
}
235

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

            
242
# Function to check if disk is already mounted at AutoNAS location
243
is_disk_mounted() {
244
    local uuid="$1"
245
    local mount_point="$2"
246

            
247
    # Check if already mounted at the correct location
248
    if mount | grep -q "UUID=$uuid.*$mount_point"; then
249
        return 0  # Already mounted correctly
250
    fi
251

            
252
    # Check if mounted elsewhere
253
    if mount | grep -q "UUID=$uuid"; then
254
        return 1  # Mounted elsewhere
255
    fi
256

            
257
    return 2  # Not mounted
258
}
259

            
260
# ============================================================================
261
# DEVICE FUNCTIONS
262
# ============================================================================
263

            
264
# Function to get device information
265
get_device_info() {
266
    local uuid="$1"
267

            
268
    # Find device by UUID using blkid
269
    local device_line
270
    device_line=$(blkid | grep "UUID=\"$uuid\"")
271

            
272
    if [[ -z "$device_line" ]]; then
273
        return 1
274
    fi
275

            
276
    local device=$(echo "$device_line" | cut -d: -f1)
277
    local blkid_info=$(echo "$device_line" | cut -d: -f2-)
278

            
279
    echo "device:$device"
280
    echo "$blkid_info"
281
    return 0
282
}
283

            
284
# ============================================================================
285
# NETWORK FUNCTIONS
286
# ============================================================================
287

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

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

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

            
307
# Function to count disks using specific IP
308
count_disks_using_ip() {
309
    local target_ip="$1"
310
    local count=0
311

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

            
316
        if [[ "$ip" == "$target_ip" ]]; then
317
            # Check if this disk is currently attached (mounted)
318
            if mountpoint -q "$mount_point" 2>/dev/null; then
319
                count=$((count + 1))
320
            fi
321
        fi
322
    done < <(grep -v '^#' "$CONFIG_FILE" 2>/dev/null | grep -v '^$')
323

            
324
    echo $count
325
}
326

            
327
# Function to activate IP on interface with smart retry
328
activate_ip() {
329
    local ip="$1"
330
    local interface="$2"
331
    local max_retries=3
332
    local retry_count=0
333

            
334
    # Skip activation for special cases
335
    if [[ "$ip" == "IMPORT" || "$ip" == "LOCAL" ]]; then
336
        log_message "Skipping IP activation for special configuration: $ip" "debug"
337
        return 0
338
    fi
339

            
340
    log_message "Activating IP $ip on interface $interface" "info"
341

            
342
    # Check if interface exists
343
    if ! interface_exists "$interface"; then
344
        log_message "Interface $interface does not exist - waiting for it to appear" "warning"
345

            
346
        # Wait for interface to appear (common with USB/Thunderbolt)
347
        local wait_count=0
348
        while [ $wait_count -lt 30 ] && ! interface_exists "$interface"; do
349
            sleep 2
350
            wait_count=$((wait_count + 1))
351
            debug_log "Waiting for interface $interface... (${wait_count}/30)"
352
        done
353

            
354
        if ! interface_exists "$interface"; then
355
            log_message "Error: Interface $interface does not exist" "err"
356
            return 1
357
        fi
358

            
359
        log_message "Interface $interface is now available" "info"
360
    fi
361

            
362
    # Check if IP is already configured
363
    if is_ip_configured "$ip" "$interface"; then
364
        log_message "IP $ip already configured on interface $interface"
365
        return 0
366
    fi
367

            
368
    # Check if interface is up
369
    if ! ip link show "$interface" | grep -q "state UP"; then
370
        log_message "Bringing up interface $interface"
371
        if ! ip link set "$interface" up; then
372
            log_message "Error: Failed to bring up interface $interface" "err"
373
            return 1
374
        fi
375
        # Wait a moment for interface to come up
376
        sleep 1
377
    fi
378

            
379
    # Try to configure IP with retries
380
    while [ $retry_count -lt $max_retries ]; do
381
        if ip addr add "$ip/24" dev "$interface" 2>/dev/null; then
382
            log_message "Successfully activated IP $ip on interface $interface"
383
            return 0
384
        else
385
            retry_count=$((retry_count + 1))
386
            if [ $retry_count -lt $max_retries ]; then
387
                log_message "Failed to add IP $ip to interface $interface, retrying ($retry_count/$max_retries)" "warning"
388
                sleep 2
389
            fi
390
        fi
391
    done
392

            
393
    log_message "Error: Failed to activate IP $ip on interface $interface" "err"
394
    return 1
395
}
396

            
397
# Function to restore IPs for interface with comprehensive checking
398
restore_interface_ips() {
399
    local interface="$1"
400
    local restored=0
401
    local total_configs=0
402

            
403
    log_message "Checking IP restoration needs for interface: $interface" "info"
404

            
405
    # Check if interface exists and is up
406
    if ! interface_exists "$interface"; then
407
        log_message "Interface $interface does not exist, skipping restoration" "warning"
408
        return 1
409
    fi
410

            
411
    # Check if interface is up
412
    if ! ip link show "$interface" | grep -q "state UP"; then
413
        log_message "Interface $interface is down, waiting for it to come up..." "info"
414
        local wait_count=0
415
        while [ $wait_count -lt 20 ] && ! ip link show "$interface" | grep -q "state UP"; do
416
            sleep 1
417
            wait_count=$((wait_count + 1))
418
        done
419

            
420
        if ! ip link show "$interface" | grep -q "state UP"; then
421
            log_message "Interface $interface did not come up within 20 seconds" "warning"
422
            return 1
423
        fi
424
    fi
425

            
426
    log_message "Interface $interface is up, checking for required IP configurations" "info"
427

            
428
    while IFS= read -r line; do
429
        # Skip comments and empty lines
430
        [[ "$line" =~ ^[[:space:]]*# ]] && continue
431
        [[ -z "$line" ]] && continue
432

            
433
        # Parse configuration
434
        IFS=':' read -r uuid name ip config_interface mount_point nfs_options <<< "$line"
435

            
436
        # Check if this line is for our interface
437
        if [ "$config_interface" = "$interface" ]; then
438
            ((total_configs++))
439
            debug_log "Found configuration: $name ($uuid) requires IP $ip on $interface"
440

            
441
            # Check if the disk is currently mounted
442
            if mountpoint -q "$mount_point" 2>/dev/null; then
443
                log_message "Disk $name is mounted, checking IP configuration" "info"
444

            
445
                # Check if IP is already configured
446
                if ! is_ip_configured "$ip" "$interface"; then
447
                    log_message "IP $ip missing on interface $interface for mounted disk: $name" "warning"
448

            
449
                    # Use the core activate_ip function
450
                    if activate_ip "$ip" "$interface"; then
451
                        log_message "Successfully restored IP $ip on interface $interface for disk $name" "info"
452
                        ((restored++))
453
                    else
454
                        log_message "Failed to restore IP $ip on interface $interface for disk $name" "error"
455
                    fi
456
                else
457
                    debug_log "IP $ip already configured on interface $interface for disk $name"
458
                fi
459
            else
460
                debug_log "Disk $name is not mounted, skipping IP configuration"
461
            fi
462
        fi
463
    done < "$CONFIG_FILE"
464

            
465
    if [ $total_configs -eq 0 ]; then
466
        debug_log "No AutoNAS configurations found for interface $interface"
467
    elif [ $restored -gt 0 ]; then
468
        log_message "Restored $restored/$total_configs IP addresses on interface $interface" "info"
469

            
470
        # Trigger NFS export refresh if we restored any IPs
471
        log_message "Refreshing NFS exports after IP restoration" "info"
472
        exportfs -ra 2>/dev/null
473
        systemctl reload nfs-kernel-server 2>/dev/null
474
    else
475
        log_message "All required IPs already configured on interface $interface" "info"
476
    fi
477

            
478
    return 0
479
}
480

            
481
# Function to deactivate IP on interface with smart checking
482
deactivate_ip() {
483
    local ip="$1"
484
    local interface="$2"
485

            
486
    # Skip deactivation for special cases
487
    if [[ "$ip" == "IMPORT" || "$ip" == "LOCAL" ]]; then
488
        log_message "Skipping IP deactivation for special configuration: $ip" "debug"
489
        return 0
490
    fi
491

            
492
    # Check how many other disks are using this IP
493
    local usage_count
494
    usage_count=$(count_disks_using_ip "$ip")
495

            
496
    if [[ $usage_count -gt 1 ]]; then
497
        log_message "IP $ip still used by $((usage_count - 1)) other disks, keeping it active"
498
        return 0
499
    fi
500

            
501
    log_message "Deactivating IP $ip on interface $interface"
502

            
503
    # Check if IP is configured
504
    if ! is_ip_configured "$ip" "$interface"; then
505
        log_message "IP $ip not configured on interface $interface"
506
        return 0
507
    fi
508

            
509
    # Remove IP from interface
510
    if ip addr del "$ip/24" dev "$interface" 2>/dev/null; then
511
        log_message "Successfully deactivated IP $ip on interface $interface"
512
        return 0
513
    else
514
        log_message "Error: Failed to deactivate IP $ip on interface $interface" "err"
515
        return 1
516
    fi
517
}
518

            
519
# ============================================================================
520
# NFS FUNCTIONS
521
# ============================================================================
522

            
523
# Function to add NFS export with AutoNAS identification
524
add_nfs_export() {
525
    local mount_point="$1"
526
    local nfs_options="$2"
527
    local uuid="${3:-}"
528
    local name="${4:-}"
529

            
530
    # Skip NFS export for special cases
531
    if [[ "$nfs_options" == "LOCAL" ]] || [[ "$mount_point" =~ ^.*@Camera.*$ ]]; then
532
        log_message "Skipping NFS export for local/camera configuration" "debug"
533
        return 0
534
    fi
535

            
536
    log_message "Adding NFS export for $mount_point"
537

            
538
    # Create NFS export entry
539
    local export_entry="$mount_point $nfs_options"
540

            
541
    # Check if export already exists
542
    if grep -Fxq "$export_entry" /etc/exports 2>/dev/null; then
543
        log_message "NFS export already exists: $export_entry"
544
        return 0
545
    fi
546

            
547
    # Add identifying comment if UUID/name provided
548
    if [[ -n "$uuid" && -n "$name" ]]; then
549
        echo "# AutoNAS Export - UUID:$uuid NAME:$name" >> /etc/exports
550
    fi
551

            
552
    # Add export to /etc/exports
553
    echo "$export_entry" >> /etc/exports
554
    if [[ $? -eq 0 ]]; then
555
        log_message "Successfully added NFS export: $export_entry"
556

            
557
        # Reload exports
558
        if exportfs -ra; then
559
            log_message "NFS exports reloaded successfully"
560
            return 0
561
        else
562
            log_message "Warning: Failed to reload NFS exports" "warning"
563
            return 1
564
        fi
565
    else
566
        log_message "Error: Failed to add NFS export" "err"
567
        return 1
568
    fi
569
}
570

            
571
# Function to remove NFS export
572
remove_nfs_export() {
573
    local mount_point="$1"
574
    local uuid="${2:-}"
575
    local name="${3:-}"
576
    local nfs_options="${4:-}"
577

            
578
    # Skip NFS export removal for special cases
579
    if [[ "$mount_point" =~ ^.*@Camera.*$ ]] || [[ "$nfs_options" == "LOCAL" ]]; then
580
        log_message "Skipping NFS export removal for camera/local configuration" "debug"
581
        return 0
582
    fi
583

            
584
    log_message "Removing NFS export for $mount_point"
585

            
586
    # Create temporary file without the export
587
    local temp_exports=$(mktemp)
588
    if [[ ! -f /etc/exports ]]; then
589
        log_message "No /etc/exports file found"
590
        rm -f "$temp_exports"
591
        return 0
592
    fi
593

            
594
    # If UUID provided, use AutoNAS marker method
595
    if [[ -n "$uuid" ]]; then
596
        local skip_next=false
597
        local found_export=false
598

            
599
        # Process /etc/exports line by line
600
        while IFS= read -r line; do
601
            if [[ "$line" =~ ^#\ AutoNAS\ Export\ -\ UUID:${uuid}\ NAME: ]]; then
602
                # Found our marker comment, skip this line and the next one
603
                skip_next=true
604
                found_export=true
605
                debug_log "Found AutoNAS export marker for $name"
606
                continue
607
            elif [ "$skip_next" = true ]; then
608
                # Skip the export line that follows our marker
609
                skip_next=false
610
                debug_log "Skipping export line for $name"
611
                continue
612
            else
613
                # Keep all other lines
614
                echo "$line" >> "$temp_exports"
615
            fi
616
        done < /etc/exports
617

            
618
        if [ "$found_export" = true ]; then
619
            # Replace /etc/exports with cleaned version
620
            mv "$temp_exports" /etc/exports
621
            log_message "NFS export removed using AutoNAS marker for $name"
622
        else
623
            rm -f "$temp_exports"
624
            # Fallback to generic method
625
            grep -v "^${mount_point} " /etc/exports > "$temp_exports"
626
            mv "$temp_exports" /etc/exports
627
            log_message "NFS export removed using generic method for $mount_point"
628
        fi
629
    else
630
        # Generic removal method
631
        grep -v "^${mount_point} " /etc/exports > "$temp_exports"
632
        mv "$temp_exports" /etc/exports
633
        log_message "NFS export removed for $mount_point"
634
    fi
635

            
636
    # Reload exports
637
    if exportfs -ra; then
638
        log_message "NFS exports reloaded successfully"
639
        return 0
640
    else
641
        log_message "Warning: Failed to reload NFS exports" "warning"
642
        return 1
643
    fi
644
}
645

            
646
# ============================================================================
647
# STORAGE FUNCTIONS
648
# ============================================================================
649

            
650
# Function to mount disk with comprehensive filesystem support
651
mount_disk() {
652
    local uuid="$1"
653
    local mount_point="$2"
654
    local filesystem_type="${3:-}"
655

            
656
    log_message "Mounting disk with UUID $uuid to $mount_point"
657

            
658
    # Wait for device to be ready (udev sometimes triggers too early)
659
    local device_path="/dev/disk/by-uuid/$uuid"
660
    local wait_count=0
661
    while [ ! -e "$device_path" ] && [ $wait_count -lt 10 ]; do
662
        debug_log "Waiting for device $device_path to appear..."
663
        sleep 1
664
        wait_count=$((wait_count + 1))
665
    done
666

            
667
    if [ ! -e "$device_path" ]; then
668
        # Try alternative method - find device by UUID using blkid
669
        local device
670
        device=$(blkid | grep "UUID=\"$uuid\"" | cut -d: -f1)
671

            
672
        if [[ -z "$device" ]]; then
673
            log_message "Error: Device with UUID $uuid not found" "err"
674
            return 1
675
        fi
676

            
677
        device_path="$device"
678
    fi
679

            
680
    # Get the actual device name
681
    local actual_device=$(readlink -f "$device_path" 2>/dev/null || echo "$device_path")
682
    log_message "Device UUID $uuid maps to $actual_device"
683

            
684
    # Check if device is already mounted
685
    if mountpoint -q "$mount_point" 2>/dev/null; then
686
        # Check if it's mounted to the correct device
687
        local mounted_device
688
        mounted_device=$(findmnt -n -o SOURCE "$mount_point")
689
        if [[ "$mounted_device" == "$actual_device" ]]; then
690
            log_message "Disk already mounted correctly at $mount_point"
691
            return 0
692
        else
693
            log_message "Warning: Different device mounted at $mount_point, unmounting first" "warning"
694
            umount "$mount_point" 2>/dev/null
695
        fi
696
    fi
697

            
698
    # Create mount point if it doesn't exist
699
    if [[ ! -d "$mount_point" ]]; then
700
        if mkdir -p "$mount_point"; then
701
            log_message "Created mount point directory: $mount_point"
702
        else
703
            log_message "Error: Failed to create mount point directory: $mount_point" "err"
704
            return 1
705
        fi
706
    fi
707

            
708
    # Detect filesystem if not provided
709
    if [[ -z "$filesystem_type" ]]; then
710
        filesystem_type=$(blkid -o value -s TYPE "$actual_device" 2>/dev/null)
711
        debug_log "Detected filesystem type: $filesystem_type"
712
    fi
713

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

            
747
    # Perform the mount
748
    local mount_cmd="mount $mount_options \"$actual_device\" \"$mount_point\""
749
    debug_log "Executing: $mount_cmd"
750

            
751
    if eval $mount_cmd; then
752
        # Verify mount was successful
753
        if mountpoint -q "$mount_point"; then
754
            log_message "Disk mounted successfully at $mount_point"
755

            
756
            # Set appropriate permissions for the mount point
757
            chmod 755 "$mount_point" 2>/dev/null
758

            
759
            return 0
760
        else
761
            log_message "Error: Mount command succeeded but mountpoint verification failed" "err"
762
            return 1
763
        fi
764
    else
765
        # Try fallback mount by UUID for better compatibility
766
        log_message "Direct mount failed, attempting UUID fallback" "warning"
767
        if mount UUID="$uuid" "$mount_point" 2>/dev/null; then
768
            log_message "Disk mounted successfully using UUID fallback at $mount_point"
769
            return 0
770
        else
771
            log_message "Error: Failed to mount $actual_device to $mount_point" "err"
772
            return 1
773
        fi
774
    fi
775
}
776

            
777
# Function to unmount disk
778
unmount_disk() {
779
    local mount_point="$1"
780

            
781
    log_message "Unmounting disk from $mount_point"
782

            
783
    # Check if actually mounted
784
    if ! mountpoint -q "$mount_point" 2>/dev/null; then
785
        log_message "Path $mount_point is not a mount point"
786
        return 0
787
    fi
788

            
789
    # Attempt unmount with timeout
790
    if timeout 30 umount "$mount_point"; then
791
        log_message "Successfully unmounted $mount_point"
792

            
793
        # Remove mount point if empty
794
        if rmdir "$mount_point" 2>/dev/null; then
795
            debug_log "Removed empty mount point directory: $mount_point"
796
        else
797
            debug_log "Mount point directory not empty or removal failed: $mount_point"
798
        fi
799
        return 0
800
    else
801
        log_message "Error: Failed to unmount $mount_point" "err"
802
        return 1
803
    fi
804
}
805

            
806
# ============================================================================
807
# CAMERA IMPORT FUNCTIONS
808
# ============================================================================
809

            
810
# Function to handle camera import
811
handle_camera_import() {
812
    local uuid="$1"
813
    local mount_point="$2"
814
    local import_config="$3"
815

            
816
    # Parse import configuration (format: destination_path only)
817
    destination_path="$import_config"
818

            
819
    if [[ -z "$destination_path" ]]; then
820
        log_message "Error: No destination path specified for camera import" "err"
821
        return 1
822
    fi
823

            
824
    # Always use the integrated media importer (non-configurable)
Bogdan Timofte authored 2 weeks ago
825
    script_path="${AUTONAS_MEDIA_IMPORTER}"
Bogdan Timofte authored 3 months ago
826

            
827
    # Check if import script exists
828
    if [[ ! -x "$script_path" ]]; then
829
        log_message "Error: Camera import script not found or not executable: $script_path" "err"
830
        return 1
831
    fi
832

            
833
    log_message "Starting camera import process for UUID: $uuid"
834
    log_message "Import destination: $destination_path"
Bogdan Timofte authored 2 weeks ago
835

            
836
    if systemctl is-active --quiet "autonas-import-$uuid.service" 2>/dev/null; then
837
        log_message "Import already running for UUID: $uuid; skipping duplicate start" "warning"
838
        return 0
839
    fi
Bogdan Timofte authored 3 months ago
840

            
841
    # Clean up any previous transient unit before starting a new one
842
    systemctl stop autonas-import-$uuid.service 2>/dev/null || true
843
    systemctl reset-failed autonas-import-$uuid.service 2>/dev/null || true
844
    systemctl disable autonas-import-$uuid.service 2>/dev/null || true
845
    rm -f /run/systemd/transient/autonas-import-$uuid.service 2>/dev/null || true
846

            
847
    # Run import in background using systemd-run for better process management
848
    systemd-run --no-block --unit="autonas-import-$uuid" \
Bogdan Timofte authored 2 weeks ago
849
                "${AUTONAS_DISK_HANDLER}" import "$uuid" "$mount_point" "$destination_path" "$script_path" || {
Bogdan Timofte authored 3 months ago
850
        log_message "Error: Failed to start background import process" "err"
851
        return 1
852
    }
853

            
854
    log_message "Camera import process started successfully"
855
    return 0
856
}
857

            
858
# Function to run background import
859
run_background_import() {
860
    local uuid="$1"
861
    local mount_point="$2"
862
    local destination="$3"
Bogdan Timofte authored 2 weeks ago
863
    local script_path="${4:-${AUTONAS_MEDIA_IMPORTER}}"
Bogdan Timofte authored 3 months ago
864

            
865
    log_message "Running background import for UUID: $uuid" "info" "autonas-import"
866
    log_message "Source: $mount_point, Destination: $destination" "info" "autonas-import"
Bogdan Timofte authored 2 weeks ago
867

            
868
    local lock_dir="/run/lock/autonas"
869
    local lock_file="$lock_dir/import-$uuid.lock"
870
    local import_lock_fd
871

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

            
878
    if ! flock -n "$import_lock_fd"; then
879
        log_message "Import already running for UUID: $uuid; duplicate background run skipped" "warning" "autonas-import"
880
        return 0
881
    fi
Bogdan Timofte authored 3 months ago
882

            
883
    # Change to a safe directory
884
    cd /tmp || cd /
885

            
886
    # Create destination directory if it doesn't exist
887
    mkdir -p "$destination"
888

            
889
    # Ensure destination exists
890
    if [[ ! -d "$destination" ]]; then
891
        log_message "Error: Cannot access destination directory: $destination" "err" "autonas-import"
892
        return 1
893
    fi
894

            
895
    # Run the import script
896
    log_message "Executing import script: $script_path" "info" "autonas-import"
Bogdan Timofte authored 2 weeks ago
897
    if "$script_path" "$mount_point" "$destination" --unattended --verbose; then
Bogdan Timofte authored 3 months ago
898
        log_message "Import completed successfully for UUID: $uuid" "info" "autonas-import"
899

            
900
        # Cleanup: unmount and remove mount point
901
        cleanup_and_unmount "$uuid" "$mount_point"
902
        return 0
903
    else
904
        log_message "Error: Import failed for UUID: $uuid" "err" "autonas-import"
905
        cleanup_and_unmount "$uuid" "$mount_point"
906
        return 1
907
    fi
908
}
909

            
910
# Function to cleanup and unmount after camera import
911
cleanup_and_unmount() {
912
    local uuid="$1"
913
    local mount_point="$2"
914

            
915
    log_message "Cleaning up after import for UUID: $uuid" "info" "autonas-import"
916

            
917
    # Unmount the device
918
    if mountpoint -q "$mount_point" 2>/dev/null; then
919
        if umount "$mount_point"; then
920
            log_message "Successfully unmounted $mount_point" "info" "autonas-import"
921
        else
922
            log_message "Warning: Failed to unmount $mount_point" "warning" "autonas-import"
923
        fi
924
    fi
925

            
926
    # Remove mount point directory if empty
927
    if [[ -d "$mount_point" ]] && rmdir "$mount_point" 2>/dev/null; then
928
        log_message "Removed empty mount point directory: $mount_point" "info" "autonas-import"
929
    fi
930

            
931
    log_message "Cleanup completed for UUID: $uuid" "info" "autonas-import"
932
}
933

            
934
# ============================================================================
935
# MAIN WORKFLOW FUNCTIONS
936
# ============================================================================
937

            
938
# Function to handle disk attachment
939
handle_attach() {
Bogdan Timofte authored 3 months ago
940
    local identifier="$1"
941

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

            
944
    # Get configuration by UUID, name, or mount point
Bogdan Timofte authored 3 months ago
945
    local config
Bogdan Timofte authored 3 months ago
946
    config=$(resolve_config_identifier "$identifier")
947

            
Bogdan Timofte authored 3 months ago
948
    if [[ -z "$config" ]]; then
Bogdan Timofte authored 3 months ago
949
        log_message "No configuration found for identifier: $identifier - disk will be ignored"
Bogdan Timofte authored 3 months ago
950
        log_message "To configure this disk, run: autonas add"
951
        return 1
952
    fi
Bogdan Timofte authored 3 months ago
953

            
Bogdan Timofte authored 3 months ago
954
    # Parse configuration
955
    local parsed
956
    parsed=($(parse_config "$config"))
957
    local cfg_uuid="${parsed[0]}"
958
    local name="${parsed[1]}"
959
    local ip="${parsed[2]}"
960
    local interface="${parsed[3]}"
961
    local mount_point="${parsed[4]}"
962
    local nfs_options="${parsed[5]}"
Bogdan Timofte authored 3 months ago
963

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

            
Bogdan Timofte authored 3 months ago
967
    # Handle different disk types
968
    if [[ "$ip" == "IMPORT" && "$interface" == "IMPORT" ]]; then
969
        # Camera import workflow: mount temporarily, import, then unmount
Bogdan Timofte authored 3 months ago
970
        log_message "Detected camera import configuration for UUID: $cfg_uuid"
971

            
Bogdan Timofte authored 3 months ago
972
        # Mount disk for import
Bogdan Timofte authored 3 months ago
973
        if mount_disk "$cfg_uuid" "$mount_point"; then
Bogdan Timofte authored 3 months ago
974
            # Start import process
Bogdan Timofte authored 3 months ago
975
            handle_camera_import "$cfg_uuid" "$mount_point" "$nfs_options"
Bogdan Timofte authored 3 months ago
976
            return $?
977
        else
978
            log_message "Error: Failed to mount camera device" "err"
979
            return 1
980
        fi
981
    elif [[ "$ip" == "LOCAL" ]]; then
982
        # Local mount only (no network sharing)
Bogdan Timofte authored 3 months ago
983
        if mount_disk "$cfg_uuid" "$mount_point"; then
Bogdan Timofte authored 3 months ago
984
            log_message "Local disk mounted successfully at $mount_point"
985
            return 0
986
        else
987
            return 1
988
        fi
989
    else
990
        # Regular NFS workflow: activate IP, mount, export via NFS
991
        if activate_ip "$ip" "$interface"; then
Bogdan Timofte authored 3 months ago
992
            if mount_disk "$cfg_uuid" "$mount_point"; then
993
                if add_nfs_export "$mount_point" "$nfs_options" "$cfg_uuid" "$name"; then
Bogdan Timofte authored 3 months ago
994
                    log_message "Disk attached successfully: $name"
995
                    return 0
996
                else
997
                    log_message "Warning: Disk mounted but NFS export failed" "warning"
998
                    return 1
999
                fi
1000
            else
1001
                deactivate_ip "$ip" "$interface"
1002
                return 1
1003
            fi
1004
        else
1005
            return 1
1006
        fi
1007
    fi
1008
}
1009

            
1010
# Function to handle disk detachment
1011
handle_detach() {
Bogdan Timofte authored 3 months ago
1012
    local identifier="$1"
1013

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

            
1016
    # Get configuration by UUID, name, or mount point
Bogdan Timofte authored 3 months ago
1017
    local config
Bogdan Timofte authored 3 months ago
1018
    config=$(resolve_config_identifier "$identifier")
1019

            
Bogdan Timofte authored 3 months ago
1020
    if [[ -z "$config" ]]; then
Bogdan Timofte authored 3 months ago
1021
        log_message "No configuration found for identifier: $identifier"
Bogdan Timofte authored 3 months ago
1022
        return 1
1023
    fi
Bogdan Timofte authored 3 months ago
1024

            
Bogdan Timofte authored 3 months ago
1025
    # Parse configuration
1026
    local parsed
1027
    parsed=($(parse_config "$config"))
1028
    local cfg_uuid="${parsed[0]}"
1029
    local name="${parsed[1]}"
1030
    local ip="${parsed[2]}"
1031
    local interface="${parsed[3]}"
1032
    local mount_point="${parsed[4]}"
1033
    local nfs_options="${parsed[5]}"
Bogdan Timofte authored 3 months ago
1034

            
1035
    log_message "Handling disk detachment for UUID: $cfg_uuid"
Bogdan Timofte authored 3 months ago
1036
    log_message "Processing disk: $name (Mount: $mount_point)"
1037

            
1038
    # Remove NFS export (if applicable)
1039
    if [[ "$ip" != "LOCAL" && "$ip" != "IMPORT" ]]; then
Bogdan Timofte authored 3 months ago
1040
        remove_nfs_export "$mount_point" "$cfg_uuid" "$name" "$nfs_options"
Bogdan Timofte authored 3 months ago
1041
    fi
1042

            
1043
    # Unmount disk
1044
    if unmount_disk "$mount_point"; then
1045
        # Deactivate IP (if applicable)
1046
        if [[ "$ip" != "LOCAL" && "$ip" != "IMPORT" ]]; then
1047
            deactivate_ip "$ip" "$interface"
1048
        fi
1049

            
1050
        log_message "Disk detached successfully: $name"
1051
        return 0
1052
    else
1053
        log_message "Error: Failed to detach disk: $name" "err"
1054
        return 1
1055
    fi
1056
}
1057

            
1058
# Function to reload configuration
1059
handle_reload() {
1060
    log_message "Reloading AutoNAS configuration"
1061

            
1062
    log_message "Reloading udev rules"
1063
    udevadm control --reload-rules
1064
    udevadm trigger --subsystem-match=block
1065

            
1066
    log_message "Reloading NFS exports"
1067
    exportfs -ra || log_message "Warning: Failed to reload NFS exports" "warning"
1068

            
1069
    log_message "Reloading systemd daemon"
1070
    systemctl daemon-reload || log_message "Warning: Failed to reload systemd daemon" "warning"
1071

            
1072
    log_message "AutoNAS configuration reload completed"
1073
    return 0
1074
}
1075

            
1076
# ============================================================================
1077
# DISPLAY FUNCTIONS
1078
# ============================================================================
1079

            
1080
# Function to show available disks
1081
show_available_disks() {
1082
    echo "=== Available Storage Devices ==="
1083
    echo
1084

            
1085
    local found_devices=0
1086
    local blkid_output
1087

            
1088
    # Get blkid output and process it
1089
    if ! blkid_output=$(blkid 2>/dev/null); then
1090
        echo "Error: Unable to scan for block devices (are you running as root?)"
1091
        return 1
1092
    fi
1093

            
1094
    while IFS= read -r blkid_line; do
1095
        [[ -z "$blkid_line" ]] && continue
1096

            
1097
        local device=$(echo "$blkid_line" | cut -d: -f1)
1098

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

            
1102
        # Skip loop devices, ram disks, and other virtual devices
1103
        case "$device" in
1104
            /dev/loop*|/dev/ram*|/dev/dm-*) continue ;;
1105
        esac
1106

            
1107
        # Extract device information
1108
        local uuid=$(echo "$blkid_line" | grep -o 'UUID="[^"]*"' | cut -d'"' -f2)
1109
        local label=$(echo "$blkid_line" | grep -o 'LABEL="[^"]*"' | cut -d'"' -f2)
1110
        local fstype=$(echo "$blkid_line" | grep -o 'TYPE="[^"]*"' | cut -d'"' -f2)
1111
        local partuuid=$(echo "$blkid_line" | grep -o 'PARTUUID="[^"]*"' | cut -d'"' -f2)
1112

            
1113
        # Skip if no UUID (we need UUID for AutoNAS)
1114
        [[ -z "$uuid" ]] && continue
1115

            
1116
        found_devices=$((found_devices + 1))
1117

            
1118
        # Get device size
1119
        local size=""
1120
        if [[ -b "$device" ]]; then
1121
            size=$(lsblk -b -d -o SIZE "$device" 2>/dev/null | tail -n1)
1122
            if [[ -n "$size" ]] && [[ "$size" =~ ^[0-9]+$ ]]; then
1123
                # Convert to human readable
1124
                size=$(numfmt --to=iec --suffix=B "$size" 2>/dev/null || echo "${size}B")
1125
            fi
1126
        fi
1127

            
1128
        # Display device information
1129
        echo "Device: $device"
1130
        [[ -n "$size" ]] && echo "  Size: $size"
1131
        echo "  UUID: $uuid"
1132
        [[ -n "$label" ]] && echo "  Label: $label"
1133
        [[ -n "$fstype" ]] && echo "  Filesystem: $fstype"
1134

            
1135
        # Check if already configured
1136
        if grep -q "^${uuid}:" "$CONFIG_FILE" 2>/dev/null; then
1137
            local config_line=$(grep "^${uuid}:" "$CONFIG_FILE" 2>/dev/null)
1138
            local config_name=$(echo "$config_line" | cut -d: -f2)
1139
            echo "  Status: ✅ Configured as '$config_name'"
1140
        else
1141
            echo "  Status: ⚠️  Not configured"
1142
        fi
1143

            
1144
        # Check if currently mounted
1145
        local mount_info
1146
        mount_info=$(findmnt -n -o TARGET "$device" 2>/dev/null)
1147
        if [[ -n "$mount_info" ]]; then
1148
            echo "  Mount: 🟢 $mount_info"
1149
        else
1150
            echo "  Mount: ⚪ Not mounted"
1151
        fi
1152

            
1153
        echo
1154
    done <<< "$blkid_output"
1155

            
1156
    if [[ $found_devices -eq 0 ]]; then
1157
        echo "No storage devices with UUIDs found."
1158
        echo "Make sure devices are connected and you're running as root."
1159
    else
1160
        echo "Found $found_devices storage device(s)"
1161
        echo
1162
        echo "To configure a device: autonas add <UUID>"
1163
        echo "To list configured devices: autonas list"
1164
    fi
1165

            
1166
    return 0
1167
}
1168

            
1169
# Mark that core library has been loaded
1170
AUTONAS_CORE_LOADED=true