Repository navigation
Expand file tree
/
Copy pathsetup-btrfs.sh
More file actions
executable file
·1462 lines (1352 loc) · 55 KB
/
Copy pathsetup-btrfs.sh
File metadata and controls
executable file
·1462 lines (1352 loc) · 55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Verwendung: setup-btrfs.sh [Optionen]
(ohne Option) Erstmigration (Root nach @) oder, wenn / schon von @ läuft,
inkrementelles Ergänzen fehlender Subvolumes.
--finish-migration Halb migrierte Systeme abschließen: / läuft noch von einem anderen
Subvolume (z. B. @rootfs), @ wird neu befüllt, GRUB im neuen Root
neu erzeugt. Danach Reboot nötig.
--cleanup-old-root Nach erfolgreichem Reboot von @: Altdaten des alten Roots entfernen
(@rootfs bzw. die Root-Dateien im Top-Level). Löscht Daten.
--fix-boot Läuft / von @ und bootet GRUB trotzdem eine veraltete /boot-Kopie im
Top-Level (Kernel wird nie aktualisiert)? Schreibt GRUB neu und setzt das
Default-Subvolume auf den Top-Level zurück. Danach Reboot.
--fix-swap Reparatur einer Swap-Datei auf Btrfs, die nicht aktiv ist ("swapon failed:
Invalid argument", typisch nach einer Migration, die die Datei als normale
CoW-Datei kopiert hat): legt das Subvolume @swap (ohne CoW) unter /swap an,
erzeugt dort /swap/swapfile in der bisherigen Größe, passt /etc/fstab an und
aktiviert den Swap sofort. Kein Reboot nötig.
--swap-size GRÖSSE Größe der neuen Swap-Datei (z. B. 4G); Standard: Größe der bisherigen Datei.
--subvols LISTE Kommagetrennte Subvolume-Namen (z. B. @root,@home,@microk8s) statt
Auswahldialog bzw. Standardauswahl.
--map PFAD:@NAME[:ALGO]
Zusätzliches Subvolume für einen beliebigen Pfad (mehrfach nutzbar),
z. B. --map /srv/system/backups/borg:@borg:no. ALGO ist die
Btrfs-Kompression des Subvolumes: no, zstd (Standard), lzo oder zlib.
Die Stufe (zstd:3) lässt sich pro Subvolume nicht setzen, sie kommt
aus den Mountoptionen. Die Einträge werden automatisch ausgewählt;
Dienste, die den Pfad nutzen, vorher selbst beenden.
--yes, -y Rückfrage "Backup vorhanden?" automatisch bejahen.
-h, --help Diese Hilfe.
USAGE
}
# Aus "/dev/vda2[/@rootfs]" den Subvolume-Pfad ("/@rootfs") ermitteln; leer, wenn Top-Level.
root_subvol_of() {
local src="$1"
[[ "$src" == *"["* ]] || { echo ""; return 0; }
src="${src#*[}"
echo "${src%]}"
}
# detect_mode ROOT_SRC FINISH(0|1) -> initial | incremental | finish | done
detect_mode() {
local sub
sub=$(root_subvol_of "$1")
if [[ -z "$sub" ]]; then
echo initial
elif [[ "$2" == 1 ]]; then
if [[ "$sub" == "/@" ]]; then echo "done"; else echo "finish"; fi
else
echo incremental
fi
}
# validate_map_spec "PFAD:@NAME[:ALGO]" -> gibt "PFAD @NAME ALGO" aus, sonst Meldung auf
# stderr und Exit 1. ALGO: no (auch none), zstd, lzo, zlib; Standard zstd.
validate_map_spec() {
local spec="$1" path name algo rest
IFS=: read -r path name algo rest <<< "$spec"
if [[ -n "$rest" ]]; then
echo "FEHLER: --map '$spec': zu viele ':'-Felder (erwartet PFAD:@NAME[:ALGO])." >&2
return 1
fi
if [[ ! "$path" =~ ^/[A-Za-z0-9._+/-]+$ || "$path" == */ || "$path" =~ (^|/)\.\.(/|$) ]]; then
echo "FEHLER: --map '$spec': Pfad muss absolut sein, darf nicht auf '/' enden, kein '..' enthalten und nur [A-Za-z0-9._+/-] verwenden." >&2
return 1
fi
if [[ ! "$name" =~ ^@[A-Za-z0-9._-]+$ ]]; then
echo "FEHLER: --map '$spec': Subvolume-Name muss mit '@' beginnen und nur [A-Za-z0-9._-] enthalten." >&2
return 1
fi
algo="${algo:-zstd}"
case "$algo" in
no|none) algo=no ;;
zstd|lzo|zlib) ;;
*) echo "FEHLER: --map '$spec': ALGO muss no, zstd, lzo oder zlib sein (ist: $algo)." >&2; return 1 ;;
esac
echo "$path $name $algo"
}
# Zum Testen der Funktionen oben per "source" ohne Nebenwirkungen laden.
if [[ "${SETUP_BTRFS_SOURCE_ONLY:-}" == 1 ]]; then
return 0 2>/dev/null || exit 0
fi
MODE_FINISH=0
MODE_CLEANUP=0
MODE_FIXBOOT=0
MODE_FIXSWAP=0
SWAP_SIZE=""
ASSUME_YES=0
SUBVOLS_ARG=""
declare -a EXTRA_MAP_SPECS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--finish-migration) MODE_FINISH=1 ;;
--cleanup-old-root) MODE_CLEANUP=1 ;;
--fix-boot) MODE_FIXBOOT=1 ;;
--fix-swap) MODE_FIXSWAP=1 ;;
--swap-size) [[ $# -ge 2 ]] || { echo "--swap-size braucht eine Größe (z. B. 4G)." >&2; exit 2; }; SWAP_SIZE="$2"; shift ;;
--subvols) [[ $# -ge 2 ]] || { echo "--subvols braucht eine Liste." >&2; exit 2; }; SUBVOLS_ARG="$2"; shift ;;
--map) [[ $# -ge 2 ]] || { echo "--map braucht PFAD:@NAME[:ALGO]." >&2; exit 2; }; EXTRA_MAP_SPECS+=("$2"); shift ;;
--yes|-y) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unbekannte Option: $1" >&2; usage >&2; exit 2 ;;
esac
shift
done
if (( MODE_FINISH + MODE_CLEANUP + MODE_FIXBOOT + MODE_FIXSWAP > 1 )); then
echo "--finish-migration, --cleanup-old-root, --fix-boot und --fix-swap schließen sich gegenseitig aus." >&2
exit 2
fi
if [[ -n "$SWAP_SIZE" ]]; then
if [[ ! "$SWAP_SIZE" =~ ^[0-9]+[KMGkmg]$ ]]; then
echo "FEHLER: --swap-size muss Zahl plus K, M oder G sein (z. B. 4G), ist: $SWAP_SIZE" >&2
exit 2
fi
if [[ $MODE_FIXSWAP -eq 0 ]]; then
echo "--swap-size gehört zu --fix-swap." >&2
exit 2
fi
fi
declare -A seen_map_paths=() seen_map_names=()
for spec in "${EXTRA_MAP_SPECS[@]}"; do
map_path=""
map_name=""
read -r map_path map_name _ < <(validate_map_spec "$spec") || true
[[ -n "$map_path" ]] || { validate_map_spec "$spec" >/dev/null || exit 2; }
if [[ -n "${seen_map_paths[$map_path]:-}" || -n "${seen_map_names[$map_name]:-}" ]]; then
echo "FEHLER: --map '$spec': Pfad oder Name kommt in mehreren --map-Angaben vor." >&2
exit 2
fi
seen_map_paths[$map_path]=1
seen_map_names[$map_name]=1
done
if (( ${#EXTRA_MAP_SPECS[@]} > 0 && (MODE_CLEANUP + MODE_FIXBOOT + MODE_FIXSWAP) > 0 )); then
echo "--map passt nur zur Erstmigration, zum inkrementellen Lauf und zu --finish-migration." >&2
exit 2
fi
echo ">>> Btrfs-Setup: Root auf @ + alle Subvolumes/Mounts (final)"
if [[ $EUID -ne 0 ]]; then
echo "Bitte als root ausführen." >&2
exit 1
fi
# --- Abhängigkeiten sicherstellen (Debian/apt) ---
need_pkg() {
local cmd="$1" pkg="$2"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo ">>> Installiere benötigtes Paket: $pkg"
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg"
else
echo ">>> Abhängigkeit $pkg ($cmd) ist bereits vorhanden."
fi
}
need_pkg rsync rsync
need_pkg btrfs btrfs-progs
# --- Root-Quelle ermitteln, z.B. /dev/vda2[/@rootfs] ---
ROOT_SRC=$(findmnt -no SOURCE / || true)
if [[ -z "$ROOT_SRC" ]]; then
echo "Konnte Root-Quelle nicht ermitteln." >&2
exit 1
fi
ROOT_DEV=${ROOT_SRC%%[*}
FSTYPE=$(findmnt -no FSTYPE / || true)
if [[ "$FSTYPE" != "btrfs" ]]; then
echo "/ ist kein Btrfs-Dateisystem (FSTYPE=$FSTYPE). Abbruch." >&2
exit 1
fi
UUID=$(blkid -s UUID -o value "$ROOT_DEV" || true)
if [[ -z "$UUID" ]]; then
echo "Konnte UUID von $ROOT_DEV nicht ermitteln. Abbruch." >&2
exit 1
fi
# --- Modus erkennen: erstmaliger Umstieg oder nachtraegliches Ergaenzen? ---
# Wenn / schon von einem benannten Subvolume laeuft, wurde dieses Skript (oder
# eine gleichwertige Migration) bereits erfolgreich durchgefuehrt. Root, GRUB
# und das Default-Subvolume bleiben dann unangetastet; es werden nur noch
# fehlende Subvolumes fuer noch nicht separat gemountete Pfade ergaenzt - ohne
# Neustart, da kein Root-Wechsel mehr noetig ist.
# --- Altdaten nach erfolgreichem Wechsel auf @ entfernen (--cleanup-old-root) ---
cleanup_old_root() {
local mnt=/mnt/btrfs-root x name size confirm
local -a subs=() cands=() nested=()
local old_root_names=(bin boot dev etc home lib lib32 lib64 libx32 lost+found media mnt opt proc root run
sbin srv sys tmp usr var snap swap.img bin.usr-is-merged lib.usr-is-merged sbin.usr-is-merged)
if [[ "$(root_subvol_of "$ROOT_SRC")" != "/@" ]]; then
echo "FEHLER: / läuft nicht von @ (${ROOT_SRC}). Aufräumen abgelehnt, solange der alte Root noch gebraucht wird." >&2
exit 1
fi
if mount | grep -q " on $mnt "; then
echo "$mnt ist bereits gemountet, bitte zuerst aushängen." >&2
exit 1
fi
mkdir -p "$mnt"
mount -o subvolid=5 "$ROOT_DEV" "$mnt"
trap 'umount "$mnt" 2>/dev/null || true' RETURN
if [[ ! -d "$mnt/@/etc" || ! -d "$mnt/@/usr" ]]; then
echo "FEHLER: @ enthält kein vollständiges System (etc/usr fehlen). Abbruch." >&2
return 1
fi
mapfile -t subs < <(btrfs subvolume list "$mnt" | awk '{print $NF}')
is_subvol() { local y; for y in "${subs[@]}"; do [[ "$y" == "$1" ]] && return 0; done; return 1; }
is_subvol "@rootfs" && cands+=("@rootfs")
for name in "${old_root_names[@]}"; do
[[ -e "$mnt/$name" || -L "$mnt/$name" ]] || continue
is_subvol "$name" && continue
cands+=("$name")
done
if [[ ${#cands[@]} -eq 0 ]]; then
echo ">>> Keine Altdaten des alten Roots gefunden - nichts zu tun."
return 0
fi
echo ">>> Folgende Altdaten im Top-Level werden GELÖSCHT (@, @home, timeshift-btrfs usw. bleiben):"
for name in "${cands[@]}"; do
size=$(du -shx "$mnt/$name" 2>/dev/null | awk '{print $1}')
echo " $name (${size:-?})"
done
if [[ $ASSUME_YES -eq 1 ]]; then
echo ">>> --yes angegeben - Bestätigung übersprungen."
elif [[ -t 0 ]]; then
read -r -p "Wirklich löschen? Zum Fortfahren exakt 'ja' eingeben: " confirm
[[ "$confirm" == "ja" ]] || { echo "Abgebrochen." >&2; return 1; }
else
echo "FEHLER: Kein Terminal und kein --yes - Löschen abgelehnt." >&2
return 1
fi
for name in "${cands[@]}"; do
# verschachtelte Subvolumes (z. B. Docker-btrfs-Driver) zuerst, tiefste zuerst
mapfile -t nested < <(printf '%s\n' "${subs[@]}" | grep -E "^${name//./\\.}/" | sort -r || true)
for x in "${nested[@]}"; do
[[ -n "$x" ]] || continue
echo ">>> Lösche Subvolume $x"
btrfs subvolume delete "$mnt/$x" >/dev/null
done
if is_subvol "$name"; then
echo ">>> Lösche Subvolume $name"
btrfs subvolume delete "$mnt/$name" >/dev/null
else
echo ">>> Lösche $name"
rm -rf --one-file-system "${mnt:?}/$name"
fi
done
echo ">>> Aufräumen abgeschlossen."
df -h / | tail -1
}
# --- Bootkonfiguration (GRUB) -------------------------------------------------------------
# GRUB loest Pfade auf Btrfs relativ zum TOP-LEVEL (subvolid=5) auf, NICHT relativ zum mit
# "btrfs subvolume set-default" gesetzten Default-Subvolume (im QEMU-Test geprueft: in der
# GRUB-Shell zeigt "ls (hd0,gpt2)/" @, @home, ...). Daraus folgt:
# - Das Default-Subvolume muss der Top-Level bleiben (ID 5). Nur dann erzeugen
# grub-mkconfig und grub-install Pfade der Form /@/boot/... und "rootflags=subvol=@".
# Aeltere Versionen dieses Skripts setzten @ als Default; GRUB las danach weiter die
# veraltete /boot-Kopie im Top-Level und startete neue Kernel nie (node6: Kernel 7.0.0-27
# trotz installiertem 7.0.0-34).
# - GRUB selbst (Image/grub.cfg auf der ESP bzw. core.img im MBR-Gap) muss per
# grub-install neu geschrieben werden, damit der Praefix /@/boot/grub lautet.
# - grub.cfg muss im neuen Root (@) erzeugt werden.
BOOT_ROOT="" # leer = laufendes System, sonst chroot-Verzeichnis
DEFAULT_CHANGED=0
OLD_DEFAULT_ID=""
boot_run() {
if [[ -n "$BOOT_ROOT" ]]; then chroot "$BOOT_ROOT" "$@"; else "$@"; fi
}
ensure_toplevel_default() {
OLD_DEFAULT_ID=$(btrfs subvolume get-default "$MNT" 2>/dev/null | awk '{print $2}')
if [[ -n "$OLD_DEFAULT_ID" && "$OLD_DEFAULT_ID" != "5" ]]; then
echo ">>> Default-Subvolume ist ID ${OLD_DEFAULT_ID}, nicht der Top-Level (5). Wird zurückgesetzt, weil GRUB"
echo ">>> Pfade relativ zum Top-Level auflöst (siehe Kommentar im Skript)."
btrfs subvolume set-default 5 "$MNT"
DEFAULT_CHANGED=1
fi
}
restore_default_subvolume() {
if [[ $DEFAULT_CHANGED -eq 1 && -n "$OLD_DEFAULT_ID" ]]; then
echo ">>> Setze Default-Subvolume zurück auf ID $OLD_DEFAULT_ID." >&2
btrfs subvolume set-default "$OLD_DEFAULT_ID" "$MNT" || true
DEFAULT_CHANGED=0
fi
}
# Fallback-Loader (EFI/BOOT) aktualisieren, falls vorhanden: Signierte Setups (shim) bekommen
# die Dateien aus dem Distributionsverzeichnis, Standalone-Images werden kopiert.
refresh_fallback_loader() {
local esp="$1" arch up id d name
case "$(uname -m)" in
x86_64) arch=x64 ;;
aarch64) arch=aa64 ;;
*) return 0 ;;
esac
up="${arch^^}"
[[ -f "$esp/EFI/BOOT/BOOT${up}.EFI" ]] || return 0
# Standard-Fallback von Ubuntu/Debian (shim + fbx64.efi): Der Fallback-Loader startet ueber
# BOOT*.CSV den Eintrag aus EFI/<id>/ und enthaelt selbst kein GRUB - nichts zu aktualisieren.
[[ -f "$esp/EFI/BOOT/fb${arch}.efi" ]] && return 0
id=""
for d in "$esp"/EFI/*/; do
name=$(basename "$d")
[[ "$name" == BOOT ]] && continue
[[ -f "$d/grub${arch}.efi" ]] || continue
id="$name"
break
done
if [[ -z "$id" ]]; then
echo "WARNUNG: Kein GRUB-Verzeichnis auf der ESP gefunden - Fallback-Loader EFI/BOOT nicht aktualisiert." >&2
return 0
fi
echo ">>> Aktualisiere Fallback-Loader EFI/BOOT aus EFI/$id"
if [[ -f "$esp/EFI/$id/shim${arch}.efi" ]]; then
cp "$esp/EFI/$id/shim${arch}.efi" "$esp/EFI/BOOT/BOOT${up}.EFI"
cp "$esp/EFI/$id/grub${arch}.efi" "$esp/EFI/BOOT/"
[[ -f "$esp/EFI/$id/mm${arch}.efi" ]] && cp "$esp/EFI/$id/mm${arch}.efi" "$esp/EFI/BOOT/"
else
cp "$esp/EFI/$id/grub${arch}.efi" "$esp/EFI/BOOT/BOOT${up}.EFI"
fi
}
write_boot_files() {
local esp="${BOOT_ROOT}/boot/efi" disk
if [[ -d /sys/firmware/efi ]]; then
if mountpoint -q "$esp"; then
echo ">>> grub-install (EFI) im Zielsystem"
boot_run grub-install --no-nvram || return 1
refresh_fallback_loader "$esp"
else
echo "FEHLER: /boot/efi ist nicht gemountet - GRUB (EFI) kann nicht aktualisiert werden." >&2
return 1
fi
else
disk=$(lsblk -no PKNAME "$ROOT_DEV" 2>/dev/null | head -1)
if [[ -z "$disk" ]]; then
echo "FEHLER: Konnte die Platte von $ROOT_DEV für grub-install (BIOS) nicht ermitteln." >&2
return 1
fi
echo ">>> grub-install (BIOS) auf /dev/$disk"
boot_run grub-install "/dev/$disk" || return 1
fi
echo ">>> GRUB-Konfiguration erzeugen"
boot_run /bin/sh -c '
if command -v update-grub >/dev/null 2>&1; then update-grub
elif command -v grub-mkconfig >/dev/null 2>&1; then grub-mkconfig -o /boot/grub/grub.cfg
else echo "Weder update-grub noch grub-mkconfig gefunden" >&2; exit 3; fi'
}
verify_boot_config() {
local cfg="${BOOT_ROOT}/boot/grub/grub.cfg" esp="${BOOT_ROOT}/boot/efi" f ok=1
if [[ ! -s "$cfg" ]] || ! grep -Eq '^[[:space:]]*linux[[:space:]]' "$cfg"; then
echo "FEHLER: Erzeugte grub.cfg enthält keine Kernel-Einträge ($cfg)." >&2
return 1
fi
if ! grep -Eq 'rootflags=subvol=@([[:space:]]|$)' "$cfg"; then
echo "FEHLER: grub.cfg enthält kein rootflags=subvol=@ - das System würde nicht von @ starten." >&2
ok=0
fi
if [[ -n "${ROOT_SUBVOL:-}" && "$ROOT_SUBVOL" != "/@" ]] && grep -Fq "${ROOT_SUBVOL#/}" "$cfg"; then
echo "FEHLER: grub.cfg verweist noch auf das alte Root-Subvolume ${ROOT_SUBVOL}." >&2
ok=0
fi
for f in "$esp"/EFI/*/grub.cfg; do
[[ -f "$f" ]] || continue
if grep -q 'set prefix=' "$f" && ! grep -Eq "set prefix=.*/@/boot/grub" "$f"; then
echo "FEHLER: $f verweist nicht auf /@/boot/grub (GRUB würde die alte Konfiguration lesen)." >&2
ok=0
fi
done
[[ $ok -eq 1 ]]
}
# Erstmigration/--finish-migration: Bootkonfiguration im frisch befuellten @ erzeugen.
regenerate_boot_in_new_root() {
local newroot=/mnt/btrfs-newroot d ok=1
ensure_toplevel_default
echo ">>> Bootkonfiguration im neuen Root (@) erzeugen"
mkdir -p "$newroot"
mount -o subvol=@ "$ROOT_DEV" "$newroot"
for d in dev proc sys run; do
mount --rbind "/$d" "$newroot/$d"
mount --make-rslave "$newroot/$d"
done
if mountpoint -q /boot; then mount --bind /boot "$newroot/boot"; fi
if mountpoint -q /boot/efi; then
mkdir -p "$newroot/boot/efi"
mount --bind /boot/efi "$newroot/boot/efi"
fi
if [[ -f "$newroot/etc/default/grub" ]]; then
sed -i 's/@rootfs/@/g' "$newroot/etc/default/grub"
fi
BOOT_ROOT="$newroot"
write_boot_files || ok=0
if [[ $ok -eq 1 ]]; then verify_boot_config || ok=0; fi
BOOT_ROOT=""
umount -R -l "$newroot" || true
if [[ $ok -ne 1 ]]; then
echo "FEHLER: Bootkonfiguration im neuen Root fehlgeschlagen." >&2
restore_default_subvolume
restart_stopped_services
umount "$MNT" || true
exit 1
fi
}
# --fix-boot: Bootkonfiguration des laufenden @-Systems reparieren (z. B. node6 nach der
# alten Skriptversion: Default-Subvolume @, GRUB liest veraltete /boot-Kopie im Top-Level).
fix_boot() {
local confirm
ROOT_SUBVOL=$(root_subvol_of "$ROOT_SRC")
if [[ "$ROOT_SUBVOL" != "/@" ]]; then
echo "FEHLER: --fix-boot erwartet, dass / von @ läuft (ist: ${ROOT_SRC})." >&2
exit 1
fi
echo ">>> --fix-boot: GRUB wird neu geschrieben (grub-install, update-grub) und das Default-Subvolume"
echo ">>> bei Bedarf auf den Top-Level zurückgesetzt. Ein Fehler kann das System unbootbar machen;"
echo ">>> die Konfiguration wird vorher in /root/btrfs-layout-boot-backup-<Zeit> gesichert."
if [[ $ASSUME_YES -eq 1 ]]; then
echo ">>> --yes angegeben - Bestätigung übersprungen."
elif [[ -t 0 ]]; then
read -r -p "Fortfahren? Exakt 'ja' eingeben: " confirm
[[ "$confirm" == "ja" ]] || { echo "Abgebrochen." >&2; exit 1; }
fi
local bk
bk="/root/btrfs-layout-boot-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$bk"
cp -a /boot/grub "$bk/" 2>/dev/null || true
cp -a /boot/efi/EFI "$bk/EFI" 2>/dev/null || true
btrfs subvolume get-default / > "$bk/default-subvolume.txt" 2>/dev/null || true
MNT=/mnt/btrfs-root
mkdir -p "$MNT"
mount -o subvolid=5 "$ROOT_DEV" "$MNT"
# shellcheck disable=SC2064
trap "umount '$MNT' 2>/dev/null || true" EXIT
ensure_toplevel_default
BOOT_ROOT=""
if ! write_boot_files || ! verify_boot_config; then
echo "FEHLER: Bootkonfiguration konnte nicht erzeugt werden. Sicherung: $bk" >&2
restore_default_subvolume
umount "$MNT" || true
exit 1
fi
umount "$MNT" || true
echo ">>> Bootkonfiguration repariert. Sicherung: $bk"
echo ">>> Jetzt neu starten und prüfen: /proc/cmdline soll BOOT_IMAGE=/@/boot/... und rootflags=subvol=@ zeigen."
}
if [[ $MODE_CLEANUP -eq 1 ]]; then
cleanup_old_root
exit $?
fi
# --fix-swap: Swap-Datei auf Btrfs reparieren. Eine per rsync oder cp nach @ kopierte Swap-Datei ist
# CoW; swapon scheitert dann mit "Invalid argument". Neu angelegt wird sie in einem eigenen NOCOW-
# Subvolume @swap (Mountpunkt /swap), das auch Snapshots von @ nicht blockiert.
fix_swap() {
local fstab=/etc/fstab old_path size new=/swap/swapfile mnt=/mnt/btrfs-root bk confirm tmp
old_path=$(awk '$0 !~ /^[[:space:]]*#/ && $3 == "swap" && $1 ~ /^\// && $1 !~ /^\/dev\// { print $1; exit }' "$fstab")
if [[ "$(findmnt -no FSTYPE /)" != btrfs ]]; then
echo "FEHLER: / ist kein Btrfs-Dateisystem." >&2
exit 1
fi
if [[ -n "$old_path" ]] && swapon --noheadings --raw --show=NAME 2>/dev/null | grep -qx "$old_path"; then
echo ">>> Swap-Datei $old_path ist aktiv - nichts zu tun."
return 0
fi
if [[ -n "$SWAP_SIZE" ]]; then
size="$SWAP_SIZE"
elif [[ -n "$old_path" && -f "$old_path" ]]; then
size=$(stat -c %s "$old_path")
else
echo "FEHLER: Keine bisherige Swap-Datei gefunden. Größe mit --swap-size angeben (z. B. --swap-size 4G)." >&2
exit 1
fi
echo ">>> --fix-swap: ${old_path:-keine bisherige Swap-Datei} ist nicht aktiv."
echo ">>> Es wird das Subvolume @swap (ohne CoW) unter /swap angelegt, dort ${new} (${size}) erzeugt,"
echo ">>> /etc/fstab angepasst und der Swap sofort aktiviert. Die alte Datei wird danach gelöscht."
if [[ $ASSUME_YES -eq 1 ]]; then
echo ">>> --yes angegeben - Bestätigung übersprungen."
elif [[ -t 0 ]]; then
read -r -p "Fortfahren? Exakt 'ja' eingeben: " confirm
[[ "$confirm" == "ja" ]] || { echo "Abgebrochen." >&2; exit 1; }
fi
bk="/root/btrfs-layout-swap-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$bk"
cp -a "$fstab" "$bk/fstab"
swap_fail() {
echo "FEHLER: $1" >&2
echo ">>> Stelle /etc/fstab aus $bk wieder her." >&2
cp -a "$bk/fstab" "$fstab"
systemctl daemon-reload 2>/dev/null || true
exit 1
}
# 1. Subvolume @swap im Top-Level anlegen (falls noch nicht vorhanden), NOCOW fuer neue Dateien
mkdir -p "$mnt"
mount -o subvolid=5 "$ROOT_DEV" "$mnt" || swap_fail "Top-Level konnte nicht gemountet werden."
# shellcheck disable=SC2064
trap "umount '$mnt' 2>/dev/null || true" EXIT
if ! btrfs subvolume list "$mnt" | awk '{print $NF}' | grep -qx '@swap'; then
btrfs subvolume create "$mnt/@swap" >/dev/null || swap_fail "@swap konnte nicht angelegt werden."
fi
chattr +C "$mnt/@swap" || swap_fail "NOCOW konnte auf @swap nicht gesetzt werden."
umount "$mnt"
trap - EXIT
# 2. /swap einhaengen (fstab-Eintrag nur, wenn es noch keinen gibt)
mkdir -p /swap
if ! grep -Eq '^[^#[:space:]]+[[:space:]]+/swap[[:space:]]+btrfs' "$fstab"; then
echo "UUID=${UUID} /swap btrfs noatime,subvol=@swap 0 0" >> "$fstab"
fi
systemctl daemon-reload
mountpoint -q /swap || mount /swap || swap_fail "/swap konnte nicht eingehängt werden."
# 3. Neue Swap-Datei (mkswapfile setzt NOCOW und legt sie ohne Luecken an)
rm -f "$new"
if btrfs filesystem mkswapfile --help >/dev/null 2>&1; then
btrfs filesystem mkswapfile --size "$size" "$new" >/dev/null || swap_fail "Swap-Datei konnte nicht erzeugt werden."
else
truncate -s 0 "$new"
chattr +C "$new"
fallocate -l "$size" "$new" || swap_fail "Swap-Datei konnte nicht erzeugt werden."
chmod 600 "$new"
mkswap "$new" >/dev/null || swap_fail "mkswap ist fehlgeschlagen."
fi
# 4. fstab: alte Zeile auskommentieren, neue eintragen
if [[ -n "$old_path" && "$old_path" != "$new" ]]; then
tmp="${fstab}.new"
awk -v old="$old_path" '
$0 !~ /^[[:space:]]*#/ && $1 == old && $3 == "swap" { print "#OLD-SWAP " $0; next }
{ print }
' "$fstab" > "$tmp"
mv "$tmp" "$fstab"
fi
if ! grep -Eq '^/swap/swapfile[[:space:]]+none[[:space:]]+swap' "$fstab"; then
echo "/swap/swapfile none swap defaults 0 0" >> "$fstab"
fi
systemctl daemon-reload
# 5. Aktivieren und pruefen
swapon "$new" || swap_fail "swapon ${new} ist fehlgeschlagen."
if ! swapon --noheadings --raw --show=NAME | grep -qx "$new"; then
swap_fail "$new ist nach swapon nicht in der Swap-Liste."
fi
if [[ -n "$old_path" && "$old_path" != "$new" ]]; then
rm -f "$old_path"
systemctl reset-failed "$(systemd-escape -p --suffix=swap "$old_path")" 2>/dev/null || true
fi
echo ">>> Swap aktiv:"
swapon --show
echo ">>> Sicherung der alten fstab: $bk/fstab"
}
if [[ $MODE_FIXBOOT -eq 1 ]]; then
fix_boot
exit $?
fi
if [[ $MODE_FIXSWAP -eq 1 ]]; then
fix_swap
exit $?
fi
ROOT_SUBVOL=$(root_subvol_of "$ROOT_SRC")
MODE=$(detect_mode "$ROOT_SRC" "$MODE_FINISH")
INCREMENTAL=0
case "$MODE" in
done)
echo ">>> / läuft bereits von @ - die Migration ist abgeschlossen, nichts zu tun."
echo ">>> Alte Root-Daten entfernen: setup-btrfs.sh --cleanup-old-root"
exit 0
;;
finish)
echo ">>> / läuft von ${ROOT_SUBVOL}, nicht von @. Migration wird abgeschlossen (--finish-migration):"
echo ">>> @ wird aus dem laufenden Root neu befüllt, GRUB im neuen Root neu erzeugt."
;;
incremental)
INCREMENTAL=1
echo ">>> / läuft bereits von einem benannten Subvolume (${ROOT_SRC})."
echo ">>> Inkrementeller Modus: nur fehlende Subvolumes werden ergänzt."
echo ">>> Root, GRUB und Default-Subvolume bleiben unangetastet, kein Neustart nötig."
if [[ "$ROOT_SUBVOL" != "/@" ]]; then
echo "WARNUNG: / läuft von ${ROOT_SUBVOL} und nicht von @. Ist die Migration unvollständig?" >&2
echo " Dann mit --finish-migration abschließen." >&2
fi
;;
esac
# --- Ausdrückliche Bestätigung, bevor irgendetwas verändert wird ---
echo
echo "!!! ACHTUNG !!!"
if [[ $INCREMENTAL -eq 1 ]]; then
echo "Dieses Skript legt zusätzliche Subvolumes für noch nicht separat"
echo "gemountete Pfade an und kopiert deren aktuelle Daten hinein."
echo "Root, /etc/default/grub und das Default-Subvolume werden NICHT verändert;"
echo "ein Neustart ist nicht nötig."
else
echo "Dieses Skript modifiziert /etc/fstab und /etc/default/grub und kopiert das"
echo "komplette Root-Dateisystem in neue Subvolumes. Der eigentliche Root-Wechsel"
echo "wird erst mit einem Neustart wirksam. Ein Fehlschlag kann das System"
echo "unbootbar machen; ein Rollback ist dann nur manuell über eine Rescue-Konsole"
echo "möglich (die alte fstab wird zwar gesichert, aber nicht automatisch"
echo "zurückgespielt)."
fi
echo
if [[ $ASSUME_YES -eq 1 ]]; then
echo ">>> --yes angegeben - Bestätigung übersprungen."
elif [[ -t 0 ]]; then
read -r -p "Backup vorhanden? Zum Fortfahren exakt 'ja' eingeben: " CONFIRM
if [[ "$CONFIRM" != "ja" ]]; then
echo "Abgebrochen." >&2
exit 1
fi
else
echo ">>> Kein interaktives Terminal erkannt – Bestätigung übersprungen (automatisierter Lauf)."
fi
MNT=/mnt/btrfs-root
if mount | grep -q " on $MNT "; then
echo "$MNT ist bereits gemountet, bitte zuerst aushängen." >&2
exit 1
fi
mkdir -p "$MNT"
echo ">>> Mount Top-Level (subvolid=5) von $ROOT_DEV nach $MNT"
mount -o subvolid=5 "$ROOT_DEV" "$MNT"
# Bei jedem vorzeitigen Ende (Fehler, exit) den Top-Level wieder aushaengen, sonst scheitert der
# naechste Lauf mit "bereits gemountet". Normale Pfade haengen selbst aus; ein zweites umount ist harmlos.
trap 'umount "$MNT" 2>/dev/null || true' EXIT
echo ">>> Vorhandene Subvolumes:"
btrfs subvolume list "$MNT" || true
create_subvol() {
local name="$1"
if btrfs subvolume list "$MNT" | awk '{print $NF}' | grep -qx "$name"; then
echo "Subvolume $name existiert bereits – ok."
else
echo "Erzeuge Subvolume $name"
btrfs subvolume create "$MNT/$name"
fi
}
apt_package_available() {
local pkg="$1"
apt-cache show "$pkg" >/dev/null 2>&1
}
offer_optional_btrfs_tools() {
[[ -t 0 && -t 1 ]] || return 0
local -a tools=(
"timeshift:Einfache System-Restore-Snapshots"
"snapper:Server/CLI-Snapshotverwaltung für Btrfs"
"btrbk:Btrfs-Backups und Replikation per SSH"
"btrfsmaintenance:Scrub, Balance, Trim und Defrag planen"
"duperemove:Deduplizierung gleicher Btrfs-Extents"
"grub-btrfs:Btrfs-Snapshots im GRUB-Bootmenü bootbar machen"
)
local -a checklist_args=()
local -a selected_tools=()
local entry pkg desc selected
for entry in "${tools[@]}"; do
pkg="${entry%%:*}"
desc="${entry#*:}"
if apt_package_available "$pkg"; then
checklist_args+=("$pkg" "$desc" "OFF")
else
echo ">>> Optionales APT-Paket $pkg ist nicht verfügbar, überspringe."
fi
done
[[ ${#checklist_args[@]} -gt 0 ]] || return 0
need_pkg whiptail whiptail
if ! selected=$(whiptail --title "Optionale Btrfs-/Snapshot-Tools" \
--checklist "Diese Auswahl installiert nur normale APT-Pakete, keine Snap- oder Flatpak-Pakete. Alle Tools bleiben unkonfiguriert; Timeshift, Snapper, btrbk und Wartungsjobs müssen danach manuell eingerichtet werden.\nLeertaste = an-/abwählen, Enter = bestätigen." \
22 92 10 \
"${checklist_args[@]}" \
3>&1 1>&2 2>&3); then
echo ">>> Optionale Tool-Installation übersprungen."
return 0
fi
eval "selected_tools=($selected)"
[[ ${#selected_tools[@]} -gt 0 ]] || { echo ">>> Keine optionalen Tools ausgewählt."; return 0; }
echo ">>> Installiere optionale APT-Pakete: ${selected_tools[*]}"
if apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y "${selected_tools[@]}"; then
echo ">>> Optionale Tools installiert. Bitte Timeshift/Snapper/btrbk/btrfsmaintenance bei Bedarf manuell konfigurieren."
echo ">>> Hinweis grub-btrfs: für die vollständige Timeshift-Integration danach setup-timeshift.sh ausführen; für Snapper setup-snapper.sh verwenden."
else
echo "WARNUNG: Optionale Tool-Installation ist fehlgeschlagen. Das Btrfs-Setup wird fortgesetzt." >&2
fi
}
# --- Mapping Quelle -> Subvolume (für Daten); einzige Quelle der Wahrheit für
# Subvolume-Namen, Rsync-Ausschlüsse, Mountpoint-Vorbereitung, -Erzeugung und
# fstab-Einträge (siehe SUBVOL_OPTS weiter unten) ---
declare -a ALL_MAPS=(
# Allgemeine Systempfade: sinnvoll auf so gut wie jedem Server, unabhaengig
# vom installierten Software-Stack. Stehen deshalb im Auswahldialog oben.
"/root:@root"
"/home:@home"
"/var/spool:@spool"
"/var/log:@log"
"/var/cache:@cache"
"/var/tmp:@tmp_var"
"/srv:@srv"
"/tmp:@tmp"
"/opt:@opt"
"/var/www:@www"
# Zentrale Journal-Sammelstelle (systemd-journal-remote). Liegt innerhalb von @log;
# eigenes Subvolume erlaubt gezielte Snapshots und Rechte, getrennt vom lokalen Journal.
"/var/log/journal/remote:@journal-remote"
# Container-Engines und ihre benannten Volumes. Volume-Subvolumes stehen
# direkt hinter ihrem Eltern-Subvolume (Zugehoerigkeit im Dialog erkennbar)
# und sind getrennt von @docker/@containers, damit Volume-Daten gezielt von
# Kompression ausgenommen werden koennen, waehrend Image-Layer/Metadaten
# weiterhin von compress=zstd profitieren.
"/var/lib/containers:@containers"
"/var/lib/containers/storage/volumes:@containers-volumes"
"/var/lib/docker:@docker"
"/var/lib/docker/volumes:@docker-volumes"
# Kubernetes (MicroK8s-Snap): Laufzeitdaten samt Datastore und lokale PersistentVolumes.
"/var/snap/microk8s/common:@microk8s"
"/var/lib/k8s-storage:@k8s-storage"
# Weitere optionale Pfade: Borg-Repositories (schon komprimiert und verschluesselt),
# Snap-Images (SquashFS) und der Docker-Host-Containerd.
"/var/lib/borg:@borg"
"/var/lib/snapd:@snapd"
"/var/lib/containerd:@containerd"
# Datenbank-/Datastore-Pfade: stark vom konkreten Software-Stack abhaengig,
# deshalb im Auswahldialog ganz unten.
"/var/lib/mongodb:@mongodb"
"/var/lib/mysql:@mysql"
"/var/lib/postgresql:@postgresql"
"/var/lib/chroma:@chroma"
"/var/lib/clamav:@clamav"
"/var/lib/stalwart:@stalwart"
"/var/lib/elasticsearch:@elasticsearch"
"/var/lib/opensearch:@opensearch"
"/var/lib/clickhouse:@clickhouse"
"/var/lib/cassandra:@cassandra"
"/var/lib/couchdb:@couchdb"
"/var/lib/neo4j:@neo4j"
"/var/lib/rabbitmq:@rabbitmq"
)
# fstab-Mount-Optionen je Subvolume. Btrfs-Mountoptionen wie compress,
# nodatacow und autodefrag sind nicht verlaesslich pro Subvolume steuerbar;
# Datenbank-/Volume-Subvolumes bekommen deshalb unten per Inode-Property
# "compression no" statt eigener fstab-Kompressionsoptionen.
declare -A SUBVOL_OPTS=(
[@root]="noatime,compress=zstd,space_cache=v2"
[@home]="noatime,compress=zstd,space_cache=v2,autodefrag"
[@spool]="noatime,compress=zstd,space_cache=v2,autodefrag"
[@log]="noatime,compress=zstd,space_cache=v2,autodefrag"
[@cache]="noatime,compress=zstd,space_cache=v2"
[@tmp_var]="noatime,compress=zstd,space_cache=v2"
[@srv]="noatime,compress=zstd,space_cache=v2"
[@tmp]="noatime,compress=zstd,space_cache=v2"
[@opt]="noatime,compress=zstd,space_cache=v2"
[@containers]="noatime,compress=zstd,space_cache=v2"
[@docker]="noatime,compress=zstd,space_cache=v2"
[@www]="noatime,compress=zstd,space_cache=v2"
[@mongodb]="noatime,compress=zstd,space_cache=v2"
[@mysql]="noatime,compress=zstd,space_cache=v2"
[@postgresql]="noatime,compress=zstd,space_cache=v2"
[@chroma]="noatime,compress=zstd,space_cache=v2"
[@clamav]="noatime,compress=zstd,space_cache=v2"
[@stalwart]="noatime,compress=zstd,space_cache=v2"
[@elasticsearch]="noatime,compress=zstd,space_cache=v2"
[@opensearch]="noatime,compress=zstd,space_cache=v2"
[@clickhouse]="noatime,compress=zstd,space_cache=v2"
[@cassandra]="noatime,compress=zstd,space_cache=v2"
[@couchdb]="noatime,compress=zstd,space_cache=v2"
[@neo4j]="noatime,compress=zstd,space_cache=v2"
[@rabbitmq]="noatime,compress=zstd,space_cache=v2"
[@docker-volumes]="noatime,compress=zstd,space_cache=v2"
[@containers-volumes]="noatime,compress=zstd,space_cache=v2"
[@journal-remote]="noatime,compress=zstd,space_cache=v2"
[@microk8s]="noatime,compress=zstd,space_cache=v2"
[@k8s-storage]="noatime,compress=zstd,space_cache=v2"
[@borg]="noatime,compress=zstd,space_cache=v2"
[@snapd]="noatime,compress=zstd,space_cache=v2"
[@containerd]="noatime,compress=zstd,space_cache=v2"
)
# Diese Subvolumes behalten CoW und Checksums, werden aber per
# btrfs-property von der Kompression ausgenommen. Die Property muss vor dem
# Kopieren der Daten gesetzt werden, damit neue Dateien darunter sie erben.
declare -A NO_COMPRESSION_SUBVOLS=(
[@mongodb]=1
[@mysql]=1
[@postgresql]=1
[@chroma]=1
[@clamav]=1
[@stalwart]=1
[@elasticsearch]=1
[@opensearch]=1
[@clickhouse]=1
[@cassandra]=1
[@couchdb]=1
[@neo4j]=1
[@rabbitmq]=1
[@docker-volumes]=1
[@containers-volumes]=1
[@journal-remote]=1
[@microk8s]=1
[@k8s-storage]=1
[@borg]=1
[@snapd]=1
)
# Explizite Kompressionsalgorithmen (zstd, lzo, zlib), die per btrfs-property gesetzt werden.
# Nur der Algorithmus laesst sich pro Subvolume festlegen, nicht die Stufe; diese kommt aus
# den Mountoptionen (compress=zstd:N) und gilt fuer das ganze Dateisystem. Subvolumes ohne
# Eintrag und ohne NO_COMPRESSION_SUBVOLS erben die Mountoption.
declare -A SUBVOL_COMPRESSION=()
# Volume-Subvolumes liegen INNERHALB ihres Eltern-Subvolumes und brauchen es
# als eigenes Subvolume (sonst legt prepare_mp verwaiste Verzeichnisse unter
# einem nie erzeugten Eltern-Subvolume an).
declare -A VOLUME_PARENT=(
[@docker-volumes]=@docker
[@containers-volumes]=@containers
)
# --- --map-Eintraege registrieren (nach der festen Liste, vor der Klassifizierung) ---
declare -a EXTRA_MAP_NAMES=()
for spec in "${EXTRA_MAP_SPECS[@]}"; do
read -r map_path map_name map_algo < <(validate_map_spec "$spec")
for entry in "${ALL_MAPS[@]}"; do
if [[ "${entry%%:*}" == "$map_path" || "${entry##*:}" == "$map_name" ]]; then
echo "FEHLER: --map '$spec' kollidiert mit dem vorhandenen Eintrag '$entry'." >&2
exit 2
fi
done
ALL_MAPS+=("$map_path:$map_name")
SUBVOL_OPTS[$map_name]="noatime,compress=zstd,space_cache=v2"
if [[ "$map_algo" == "no" ]]; then
NO_COMPRESSION_SUBVOLS[$map_name]=1
else
SUBVOL_COMPRESSION[$map_name]="$map_algo"
fi
EXTRA_MAP_NAMES+=("$map_name")
done
# --- Bereits erledigte bzw. anderweitig belegte Zielpfade aussortieren ---
# Drei Kategorien statt eines Alles-oder-nichts-Abbruchs:
# - schon korrekt eingerichtet (genau das erwartete Subvolume gemountet)
# -> wird uebersprungen, taucht im Auswahldialog gar nicht erst auf.
# - anderweitig belegt (gemountet, aber nicht vom erwarteten Subvolume)
# -> wird uebersprungen und gewarnt, statt blind drueberzuschreiben.
# - noch offen (kein eigener Mount) -> Kandidat fuer die Auswahl.
declare -a MAPS=()
declare -a ALREADY_DONE=()
declare -a CONFLICTS=()
for entry in "${ALL_MAPS[@]}"; do
src="${entry%%:*}"
sub="${entry##*:}"
this_src=$(findmnt -no SOURCE "$src" 2>/dev/null || true)
if [[ -z "$this_src" ]]; then
MAPS+=("$entry")
elif [[ "$this_src" == *"[/${sub}]"* ]]; then
ALREADY_DONE+=("$entry")
else
CONFLICTS+=("$entry ($this_src)")
fi
done
if [[ ${#ALREADY_DONE[@]} -gt 0 ]]; then
echo ">>> Bereits eingerichtet (übersprungen):"
for entry in "${ALREADY_DONE[@]}"; do
echo " ${entry%%:*} -> ${entry##*:}"
done
fi
if [[ ${#CONFLICTS[@]} -gt 0 ]]; then
echo ">>> WARNUNG: anderweitig belegt, wird übersprungen (bitte manuell prüfen):" >&2
for c in "${CONFLICTS[@]}"; do
echo " $c" >&2
done
fi
if [[ ${#MAPS[@]} -eq 0 && $INCREMENTAL -eq 1 ]]; then
echo ">>> Nichts zu tun - alle Subvolumes sind bereits eingerichtet oder anderweitig belegt."
umount "$MNT"
exit 0
fi
# --- Interaktive Auswahl: welche der noch offenen Subvolumes anlegen? ---
# Universell sinnvolle Subvolumes sind vorausgewählt (klein/leer auf so gut
# wie jedem Debian-Server, so gut wie nie schaedlich). Alles, was von der
# konkreten Serverrolle oder dem installierten Software-Stack abhaengt
# (Mail-Spool, Webserver-Docroot, Container-Engines, Datenbanken), startet
# abgewaehlt, bleibt aber frei anwaehlbar. Abgewaehlte Pfade bekommen kein
# eigenes Subvolume und bleiben einfach Teil von @ (Root) - das Skript
# braucht dafuer keine Sonderbehandlung, da alles Weitere aus MAPS
# abgeleitet wird.
declare -A DEFAULT_ON=(
[@root]=1
[@home]=1
[@log]=1
[@cache]=1
[@tmp_var]=1
[@tmp]=1
)
# --map-Eintraege hat der Aufrufer ausdruecklich verlangt: automatisch ausgewaehlt.
for name in "${EXTRA_MAP_NAMES[@]}"; do
DEFAULT_ON[$name]=1
done
checklist_desc_for() {
local src="$1" sub="$2" category compression
if [[ -n "${DEFAULT_ON[$sub]:-}" ]]; then
category="default"
else
category="optional"
fi
if [[ -n "${NO_COMPRESSION_SUBVOLS[$sub]:-}" ]]; then
compression="no-compress"
elif [[ -n "${SUBVOL_COMPRESSION[$sub]:-}" ]]; then
compression="${SUBVOL_COMPRESSION[$sub]}"
else
compression="compress"
fi
echo "${category}, ${compression}: $src"
}
echo ">>> Noch offene Subvolumes:"
for entry in "${MAPS[@]}"; do
src="${entry%%:*}"
sub="${entry##*:}"
echo " $sub ($(checklist_desc_for "$src" "$sub"))"
done
# Wendet SELECTED_ARR auf MAPS an (inkl. automatischer Eltern-Subvolumes).
apply_selection() {
# Volume-Subvolumes brauchen ihr Eltern-Subvolume (siehe VOLUME_PARENT
# oben); wird nur das Volume gewaehlt, muss das Elternteil automatisch
# ergaenzt werden - sonst legt prepare_mp spaeter verwaiste Verzeichnisse
# unter einem nie erzeugten Eltern-Subvolume an.
for sel in "${SELECTED_ARR[@]}"; do
parent="${VOLUME_PARENT[$sel]:-}"
[[ -n "$parent" ]] || continue
already=0
for s in "${SELECTED_ARR[@]}"; do
[[ "$s" == "$parent" ]] && { already=1; break; }
done
[[ $already -eq 1 ]] && continue
candidate=0
for entry in "${MAPS[@]}"; do
[[ "${entry##*:}" == "$parent" ]] && { candidate=1; break; }
done
if [[ $candidate -eq 1 ]]; then
echo ">>> $sel gewählt -> $parent automatisch mitausgewählt (Volume liegt darin)."
SELECTED_ARR+=("$parent")
fi
done
FILTERED_MAPS=()
for entry in "${MAPS[@]}"; do
sub="${entry##*:}"
for sel in "${SELECTED_ARR[@]}"; do
if [[ "$sub" == "$sel" ]]; then
FILTERED_MAPS+=("$entry")
break
fi
done
done