-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathcryptpilot-convert.sh
More file actions
executable file
·1643 lines (1421 loc) · 63.7 KB
/
cryptpilot-convert.sh
File metadata and controls
executable file
·1643 lines (1421 loc) · 63.7 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
#!/bin/bash
set -e # Exit on error
set -u # Exit on undefined variable
shopt -s nullglob
# Ensure consistent locale for parsing.
export LC_ALL=C
# ANSI color codes
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly PURPLE='\033[0;35m'
readonly CYAN='\033[0;36m'
readonly NC='\033[0m' # No Color
# Default boot partition size (can be overridden via --boot_part_size)
BOOT_PART_SIZE="512M"
# Partition alignment in sectors (aligned to 1 MiB boundary)
readonly PARTITION_SECTOR_ALIGNMENT=2048
# Set up logging: redirect stdout/stderr to both terminal and log file,
# and enable shell tracing into the same log.
log::setup_log_file() {
# https://stackoverflow.com/a/40939603/15011229
#
log_file=/tmp/.cryptpilot-convert.log
exec 3>${log_file}
# redirect stdout/stderr to a file but also keep them on terminal
exec 1> >(tee >(cat >&3)) 2>&1
# https://serverfault.com/a/579078
#
# Tell bash to send the trace to log file
BASH_XTRACEFD=3
# turn on trace
set -x
}
# Colored logging functions
log::info() {
# https://stackoverflow.com/a/7287873/15011229
#
# note: printf is used instead of echo to avoid backslash
# processing and to properly handle values that begin with a '-'.
printf "${CYAN}ℹ️ %s${NC}\n" "$*" >&2
}
log::success() {
printf "${GREEN}✅ %s${NC}\n" "$*" >&2
}
log::warn() {
printf "${YELLOW}⚠️ %s${NC}\n" "$*" >&2
}
log::error() {
printf "${RED}❌ ERROR: %s${NC}\n" "$*" >&2
}
log::step() {
printf "${GREEN}▶️ %s${NC}\n" "$*" >&2
}
log::highlight() {
printf "${PURPLE}📌 %s${NC}\n" "$*" >&2
}
proc::fatal() {
log::error "$@"
exit 1
}
# Internal: run before exiting due to error
proc::_trap_cmd_pre() {
local exit_status=$?
set +e
if [[ ${exit_status} -ne 0 ]]; then
echo
log::error "Command failed with exit status ${exit_status}. Collecting diagnostic info..."
(
echo "===== Diagnostic Info (begin) ====="
lsblk
mount
lsof /dev/nbd* /dev/mapper/* 2>/dev/null || true
echo "===== Diagnostic Info (end) ====="
) >&3
log::warn "Full logs saved to: ${log_file}"
fi
}
# Append a command to one or more traps (e.g., EXIT, INT)
# Usage: proc::_trap_add "command" TRAP_NAME...
proc::_trap_add() {
local trap_add_cmd=$1
shift || proc::fatal "${FUNCNAME[0]} usage error"
# get the num of args
if [[ $# -eq 0 ]]; then
proc::fatal "trap name not specitied"
fi
for trap_add_name in "$@"; do
trap -- "$(
# print the new trap command
printf 'proc::_trap_cmd_pre\n%s\n' "${trap_add_cmd}"
# helper fn to get existing trap command from output
# of trap -p
# shellcheck disable=SC2329
proc::_extract_trap_cmd() { printf '%s\n' "${3:-:;}" | sed '/proc::_trap_cmd_pre/d'; }
# print existing trap command with newline
eval "proc::_extract_trap_cmd $(trap -p "${trap_add_name}") "
)" "${trap_add_name}" || proc::fatal "Failed to add command to trap: ${trap_add_name}"
done
}
declare -f -t proc::_trap_add # Required for modifying DEBUG/RETURN traps
# Register cleanup commands on script exit
# Usage: proc::hook_exit "cleanup_command"
proc::hook_exit() {
set +x
if [[ $BASH_SUBSHELL -ne 0 ]]; then
proc::fatal "proc::hook_exit must not be called from subshell"
fi
proc::_trap_add "$1" EXIT INT QUIT TERM
set -x
}
declare -f -t proc::hook_exit
disk::assert_disk_not_busy() {
# Check if lvm is using the disk
if [[ $(lsblk --list -o TYPE "$1" | awk 'NR>1 {print $1}' | grep -c -v -E '(part|disk)') -gt 0 ]]; then
proc::fatal "The disk is in use, please stop it first."
fi
if [[ $(lsblk -l -o MOUNTPOINT "$1" | awk 'NR>1 {print $1}') != "" ]]; then
proc::fatal "The disk is some where mounted, please unmount it first."
fi
}
disk::dm_remove_all() {
local device="$1"
for dm_name in $(cat <(lsblk "$device" --list | awk 'NR>1 {print $1}') <(dmsetup ls | awk '{print $1}') | sort | uniq -d); do
disk::dm_remove_wait_busy "$dm_name"
done
}
disk::align_start_sector() {
local start_sector=$1
if ((start_sector % PARTITION_SECTOR_ALIGNMENT != 0)); then
start_sector=$((((start_sector - 1) / PARTITION_SECTOR_ALIGNMENT + 1) * PARTITION_SECTOR_ALIGNMENT))
fi
echo "$start_sector"
}
# https://unix.stackexchange.com/a/312273
disk::nbd_available() {
[[ $(blockdev --getsize64 "$1") == 0 ]]
}
disk::get_available_nbd() {
{ lsmod | grep nbd >/dev/null; } || modprobe nbd max_part=8
# If run in container, use following instead
#
# mknod /dev/nbd0 b 43 0
local a
for a in /dev/nbd[0-9] /dev/nbd[1-9][0-9]; do
disk::nbd_available "$a" || continue
echo "$a"
return 0
done
return 1
}
disk::umount_wait_busy() {
while true; do
if ! mountpoint -q "$1"; then
return 0
fi
if umount --recursive "$1"; then
return 0
fi
log::warn "Waiting for $1 to be unmounted..."
sleep 1
done
}
disk::dm_remove_wait_busy() {
while true; do
if ! [ -e /dev/mapper/"$1" ]; then
return 0
fi
if dmsetup remove "$1"; then
return 0
fi
log::warn "Waiting for device mapper $1 to be removed..."
sleep 1
done
}
# Print usage help and exit
proc::print_help_and_exit() {
echo "Usage:"
echo " $0 --in <input_file> --out <output_file> --config-dir <cryptpilot_config_dir> --rootfs-passphrase <rootfs_encrypt_passphrase> [--package <rpm_package>...]"
echo " $0 --in <input_file> --out <output_file> --config-dir <cryptpilot_config_dir> --rootfs-no-encryption [--package <rpm_package>...]"
echo " $0 --device <device> --config-dir <cryptpilot_config_dir> --rootfs-passphrase <rootfs_encrypt_passphrase> [--package <rpm_package>...]"
echo ""
echo "Options:"
echo " -d, --device <device> The device to operate on."
echo " --in <input_file> The input OS image file (vhd or qcow2)."
echo " --out <output_file> The output OS image file (vhd or qcow2)."
echo " -c, --config-dir <cryptpilot_config_dir> The directory containing cryptpilot configuration files."
echo " --rootfs-passphrase <rootfs_encrypt_passphrase> The passphrase for rootfs encryption."
echo " --rootfs-no-encryption Skip rootfs encryption, but keep the rootfs measuring feature enabled."
echo " --rootfs-part-num <rootfs_part_num> The partition number of the rootfs partition on the original disk. By default the tool will"
echo " search for the rootfs partition by label='root' and fail if not found. You can override this"
echo " behavior by specifying the partition number."
echo " --package <rpm_package> Specify an RPM package name or path to the RPM file to install in to the disk before"
echo " -b, --boot_part_size <size> Instead of using the default partition size(512MB), specify the size of the boot partition"
echo " converting. This can be specified multiple times."
echo " --wipe-freed-space Wipe the freed space with zero, so that the qemu-img convert would generate smaller image"
echo " --uki Generate a Unified Kernel Image image and boot from it instead of boot with GRUB"
echo " --uki-append-cmdline <cmdline> Append custom command line parameters when generating a UKI image. By default, only essential"
echo " parameters are included. This option allows you to extend the kernel command line. The default"
echo " value is 'console=tty0 console=ttyS0,115200n8'."
echo " -h, --help Show this help message and exit."
exit "$1"
}
# Execute command in subshell with all file descriptors closed except std*
proc::exec_subshell_flose_fds() {
(
set +x
eval exec {3..255}">&-"
exec "$@"
)
}
# Detect rootfs partition by label 'root', or fallback to largest ext*/xfs partition
disk::find_rootfs_partition() {
local device=$1
local specified_part_num=$2 # optional specified partition number
local part_num=1
if [[ -n "${specified_part_num}" ]]; then
local part_path=${device}p${specified_part_num}
if [ ! -b "$part_path" ]; then
log::error "Specified rootfs partition $part_path does not exist"
return 1
fi
rootfs_orig_part_num="$specified_part_num"
rootfs_orig_part_exist=true
return
fi
while true; do
local part_path=${device}p${part_num}
[ -b "$part_path" ] || break
local label
label=$(blkid -o value -s LABEL "$part_path")
if [ "$label" = "root" ]; then
rootfs_orig_part_num="$part_num"
rootfs_orig_part_exist=true
return
fi
part_num=$((part_num + 1))
done
# Collect all partition names + sizes, sort by size descending
mapfile -t parts < <(
lsblk -lnpo NAME,TYPE,SIZE "$device" |
awk '$2=="part" {print $1, $3}' |
sort -k2,2nr
)
for entry in "${parts[@]}"; do
read -r part _ <<<"$entry"
# Try mounting without specifying fstype
local rootfs_mount_point=${workdir}/rootfs
mkdir -p "${rootfs_mount_point}"
proc::hook_exit "mountpoint -q ${rootfs_mount_point} && disk::umount_wait_busy ${rootfs_mount_point}"
if mount -o ro "$part" "$rootfs_mount_point" 2>/dev/null; then
if [[ -d "$rootfs_mount_point/etc" && -d "$rootfs_mount_point/bin" && -d "$rootfs_mount_point/usr" ]]; then
disk::umount_wait_busy "$rootfs_mount_point"
rootfs_orig_part_num="${part##*p}"
rootfs_orig_part_exist=true
return
fi
disk::umount_wait_busy "$rootfs_mount_point"
fi
done
}
# find_efi_partition: locate EFI System partition number by multiple heuristics
disk::find_efi_partition() {
local device=$1
# Iterate all partition nodes under device
while IFS= read -r part; do
[[ "$part" =~ [0-9]+$ ]] || continue
# 1) PARTLABEL starts with "EFI"
local label
label=$(blkid -o value -s PARTLABEL "$part" 2>/dev/null)
if [[ "$label" == EFI* ]]; then
efi_part_num="${part##*p}"
efi_part_exist=true
return
fi
# 2) GPT PARTTYPE GUID matches EFI System GUID
local ptype
ptype=$(blkid -o value -s PARTTYPE "$part" 2>/dev/null)
if [[ "${ptype,,}" == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" ]]; then
efi_part_num="${part##*p}"
efi_part_exist=true
return
fi
# 3) vfat filesystem with msdos sec_type
local sec_type fstype
sec_type=$(blkid -o value -s SEC_TYPE "$part" 2>/dev/null)
fstype=$(blkid -o value -s TYPE "$part" 2>/dev/null)
if [[ "${sec_type,,}" == "msdos" && "${fstype,,}" == "vfat" ]]; then
efi_part_num="${part##*p}"
efi_part_exist=true
return
fi
# 4) mount and inspect: must have EFI/ and no vmlinuz-* files
local efi_mount_point=${workdir}/efi
mkdir -p "${efi_mount_point}"
proc::hook_exit "mountpoint -q ${efi_mount_point} && disk::umount_wait_busy ${efi_mount_point}"
if mount -o ro "$part" "$efi_mount_point" 2>/dev/null; then
# Check for the existence of the EFI directory
if [ -d "$efi_mount_point/EFI" ]; then
# Check that there are no vmlinuz-* files under the root
vms=("$efi_mount_point"/vmlinuz-*)
if [ "${#vms[@]}" -eq 0 ]; then
disk::umount_wait_busy "$efi_mount_point"
efi_part_num="${part##*p}"
efi_part_exist=true
return
fi
fi
disk::umount_wait_busy "$efi_mount_point"
fi
done < <(lsblk -lnpo NAME "$device")
}
disk::find_boot_partition() {
local device=$1
while IFS= read -r part; do
[[ "$part" =~ [0-9]+$ ]] || continue
local boot_mount_point=${workdir}/boot
mkdir -p "${boot_mount_point}"
proc::hook_exit "mountpoint -q ${boot_mount_point} && disk::umount_wait_busy ${boot_mount_point}"
# Mount Partition (read-only)
if mount -o ro "$part" "$boot_mount_point" 2>/dev/null; then
# Check for common boot content directly under mount point
# Collect all matches
vms=("$boot_mount_point"/vmlinuz-*)
if [ "${#vms[@]}" -gt 0 ]; then
# At least one vmlinuz-* actually exists
disk::umount_wait_busy "$boot_mount_point"
boot_part_num="${part##*p}"
boot_part_exist=true
return
fi
disk::umount_wait_busy "$boot_mount_point"
fi
done < <(lsblk -lnpo NAME "$device")
}
step::setup_workdir() {
# init a tmp workdir with mktemp
workdir=$(mktemp -d "/tmp/.cryptpilot-convert-XXXXXXXX")
mkdir -p "${workdir}"
proc::hook_exit "rm -rf ${workdir}"
}
step::extract_boot_part_from_rootfs() {
local rootfs_orig_part=$1
boot_file_path="${workdir}/boot.img"
log::info "Creating boot partition image of size $BOOT_PART_SIZE"
fallocate -l "$BOOT_PART_SIZE" "$boot_file_path"
VERSION=$(mke2fs -V 2>&1 | head -n1 | awk '{print $2}')
if printf '%s\n' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
# Use sort -V for version comparison
if printf '%s\n' "1.47.0" "$VERSION" | sort -V | head -n1 | grep -q '1.47.0'; then
echo "e2fsprogs version $VERSION >= 1.47.0, proceeding..."
yes | mkfs.ext4 -F -O ^orphan_file,^metadata_csum_seed "$boot_file_path"
else
echo "e2fsprogs version $VERSION < 1.47.0, skipping advanced features."
# Fallback to a standard format command
yes | mkfs.ext4 "$boot_file_path"
fi
else
echo "Could not determine e2fsprogs version."
exit 1
fi
local boot_mount_point=${workdir}/boot
mkdir -p "$boot_mount_point"
proc::hook_exit "mountpoint -q ${boot_mount_point} && disk::umount_wait_busy ${boot_mount_point}"
mount "$boot_file_path" "$boot_mount_point"
# mount the rootfs
local rootfs_mount_point=${workdir}/rootfs
mkdir -p "${rootfs_mount_point}"
proc::hook_exit "mountpoint -q ${rootfs_mount_point} && disk::umount_wait_busy ${rootfs_mount_point}"
mount "${rootfs_orig_part}" "${rootfs_mount_point}"
# extract the /boot content to a boot.img
log::info "Extracting /boot content to boot partition image"
cp -a "${rootfs_mount_point}/boot/." "${boot_mount_point}"
find "${rootfs_mount_point}/boot/" -mindepth 1 -delete
# When booting alinux3 image with legecy BIOS support in UEFI ECS instance, the real grub.cfg is located at /boot/grub2/grub.cfg, and will be searched by matching path.
# i.e.:
# search --no-floppy --set prefix --file /boot/grub2/grub.cfg
#
# Here we create a symlink to the boot directory so that grub can find it's grub.cfg.
ln -s -f . "${boot_mount_point}"/boot
disk::umount_wait_busy "${boot_mount_point}"
disk::umount_wait_busy "${rootfs_mount_point}"
}
# Install zram kernel modules if needed (for Ubuntu systems)
install_zram_module_if_needed() {
local rootfs_mount_point="$1"
# Check if we're in an Ubuntu-like system by looking for the presence of apt
if [ -x "${rootfs_mount_point}/usr/bin/apt-get" ] && [ -x "${rootfs_mount_point}/usr/bin/dpkg" ]; then
log::info "Detecting Ubuntu-like system, attempting to install zram kernel modules"
# Find the kernel version from the currently installed kernel image
local kernel_version
kernel_version=$(chroot "${rootfs_mount_point}" bash -c "dpkg -l | grep -oP 'linux-image-\K[0-9.-]+-generic' | head -n1")
if [ -z "$kernel_version" ]; then
log::error "Could not determine kernel version, zram module installation failed"
return 1
fi
log::info "Detected kernel version: $kernel_version"
# Install the modules package with minimal dependencies
if ! chroot "${rootfs_mount_point}" /usr/bin/env ${http_proxy:+http_proxy=$http_proxy} \
${https_proxy:+https_proxy=$https_proxy} \
${ftp_proxy:+ftp_proxy=$ftp_proxy} \
${rsync_proxy:+rsync_proxy=$rsync_proxy} \
${all_proxy:+all_proxy=$all_proxy} \
${no_proxy:+no_proxy=$no_proxy} \
bash -c "apt-get update && apt-get install -y --no-install-recommends --no-install-suggests linux-modules-extra-$kernel_version"; then
log::error "Could not install zram modules, possibly due to disk space constraints or missing package"
return 1
fi
fi
}
disk::install_rpm_on_rootfs() {
local rootfs_mount_point="$1"
shift
local packages=("$@")
local copied_rpms=() # Will store the local paths inside chroot to the copied .rpm files
local user_packages=() # User provided packages to install
# Step 1: Install user-provided packages first
for package in "${packages[@]}"; do
if [[ -f "$package" && "$package" == *.rpm ]]; then
# This is a valid .rpm file on the host
base_name=$(basename "$package")
cp "$package" "${rootfs_mount_point}/tmp/" # Copy into rootfs /tmp/
copied_rpms+=("/tmp/$base_name") # Record path inside rootfs
user_packages+=("/tmp/$base_name") # Add to installation list
else
# Assume this is a regular package name (to be installed via yum)
user_packages+=("$package")
fi
done
# Install user-provided packages
if [ ${#user_packages[@]} -gt 0 ]; then
chroot "${rootfs_mount_point}" rpmdb --rebuilddb --dbpath /var/lib/rpm
chroot "${rootfs_mount_point}" /usr/bin/env ${http_proxy:+http_proxy=$http_proxy} \
${https_proxy:+https_proxy=$https_proxy} \
${ftp_proxy:+ftp_proxy=$ftp_proxy} \
${rsync_proxy:+rsync_proxy=$rsync_proxy} \
${all_proxy:+all_proxy=$all_proxy} \
${no_proxy:+no_proxy=$no_proxy} \
yum install -y "${user_packages[@]}"
fi
# Step 2: Build essential packages list
local cryptpilot_fde_version=""
# Try to query the version of cryptpilot-fde from the current system
if command -v rpm >/dev/null 2>&1; then
cryptpilot_fde_version=$(rpm -q cryptpilot-fde --qf '%{VERSION}-%{RELEASE}' 2>/dev/null || true)
elif command -v dpkg-query >/dev/null 2>&1; then
cryptpilot_fde_version=$(dpkg-query -W -f='${Version}' cryptpilot-fde 2>/dev/null || true)
fi
local essential_packages_with_version=()
local essential_package_names=()
if [ -n "${cryptpilot_fde_version}" ]; then
log::info "Detected cryptpilot-fde version: ${cryptpilot_fde_version}"
essential_packages_with_version+=("cryptpilot-fde-${cryptpilot_fde_version}")
else
log::warn "Failed to detect cryptpilot-fde version, installing latest version"
essential_packages_with_version+=("cryptpilot-fde")
fi
essential_package_names+=("cryptpilot-fde")
essential_packages_with_version+=("yum-plugin-versionlock")
essential_package_names+=("yum-plugin-versionlock")
# Step 3: Check and install missing essential packages
local packages_to_install=()
for i in "${!essential_packages_with_version[@]}"; do
local pkg_with_version="${essential_packages_with_version[$i]}"
local pkg_name="${essential_package_names[$i]}"
# Check if package is already installed in chroot
if chroot "${rootfs_mount_point}" rpm -q "$pkg_name" >/dev/null 2>&1; then
log::info "Package $pkg_name is already installed, skipping"
else
log::info "Package $pkg_name is not installed, will install: $pkg_with_version"
packages_to_install+=("$pkg_with_version")
fi
done
# Install missing essential packages
if [ ${#packages_to_install[@]} -gt 0 ]; then
chroot "${rootfs_mount_point}" /usr/bin/env ${http_proxy:+http_proxy=$http_proxy} \
${https_proxy:+https_proxy=$https_proxy} \
${ftp_proxy:+ftp_proxy=$ftp_proxy} \
${rsync_proxy:+rsync_proxy=$rsync_proxy} \
${all_proxy:+all_proxy=$all_proxy} \
${no_proxy:+no_proxy=$no_proxy} \
yum install -y "${packages_to_install[@]}"
fi
# Step 4: Lock version for all essential packages (using base package name)
chroot "${rootfs_mount_point}" yum --cacheonly versionlock "${essential_package_names[@]}"
chroot "${rootfs_mount_point}" yum clean all
# Remove the copied .rpm files from the chroot after installation
for rpm in "${copied_rpms[@]}"; do
rm -f "${rootfs_mount_point}${rpm}"
done
}
disk::install_deb_on_rootfs() {
local rootfs_mount_point="$1"
shift
local packages=("$@")
local copied_debs=() # Will store the local paths inside chroot to the copied .deb files
local user_packages=() # User provided packages to install
# Step 1: Install user-provided packages first
for package in "${packages[@]}"; do
if [[ -f "$package" && "$package" == *.deb ]]; then
# This is a valid .deb file on the host
base_name=$(basename "$package")
cp "$package" "${rootfs_mount_point}/tmp/" # Copy into rootfs /tmp/
copied_debs+=("/tmp/$base_name") # Record path inside rootfs
user_packages+=("/tmp/$base_name") # Add to installation list
else
# Assume this is a regular package name (to be installed via apt)
user_packages+=("$package")
fi
done
# Install user-provided packages
if [ ${#user_packages[@]} -gt 0 ]; then
chroot "${rootfs_mount_point}" bash -c "dpkg --configure -a || true"
chroot "${rootfs_mount_point}" bash -c "dpkg -i $(printf '%s ' "${user_packages[@]}"| sed 's/ $//')" || true
# Fix dependencies
chroot "${rootfs_mount_point}" /usr/bin/env ${http_proxy:+http_proxy=$http_proxy} \
${https_proxy:+https_proxy=$https_proxy} \
${ftp_proxy:+ftp_proxy=$ftp_proxy} \
${rsync_proxy:+rsync_proxy=$rsync_proxy} \
${all_proxy:+all_proxy=$all_proxy} \
${no_proxy:+no_proxy=$no_proxy} \
apt-get update || true
# Fix dependencies
chroot "${rootfs_mount_point}" /usr/bin/env ${http_proxy:+http_proxy=$http_proxy} \
${https_proxy:+https_proxy=$https_proxy} \
${ftp_proxy:+ftp_proxy=$ftp_proxy} \
${rsync_proxy:+rsync_proxy=$rsync_proxy} \
${all_proxy:+all_proxy=$all_proxy} \
${no_proxy:+no_proxy=$no_proxy} \
apt-get -y -f install || true
fi
# Step 2: Build essential packages list
local cryptpilot_fde_version=""
# Try to query the version of cryptpilot-fde from the current system
if command -v rpm >/dev/null 2>&1; then
cryptpilot_fde_version=$(rpm -q cryptpilot-fde --qf '%{VERSION}-%{RELEASE}' 2>/dev/null || true)
elif command -v dpkg-query >/dev/null 2>&1; then
# Extract version from dpkg-query output, removing epoch if present
cryptpilot_fde_version=$(dpkg-query -W -f='${Version}' cryptpilot-fde 2>/dev/null || true)
fi
local essential_packages_with_version=()
local essential_package_names=()
if [ -n "${cryptpilot_fde_version}" ]; then
log::info "Detected cryptpilot-fde version: ${cryptpilot_fde_version}"
essential_packages_with_version+=("cryptpilot-fde=${cryptpilot_fde_version}")
else
log::warn "Failed to detect cryptpilot-fde version, installing latest version"
essential_packages_with_version+=("cryptpilot-fde")
fi
essential_package_names+=("cryptpilot-fde")
# Also include apt-utils for better apt handling
if ! chroot "${rootfs_mount_point}" dpkg -l apt-utils >/dev/null 2>&1; then
essential_packages_with_version+=("apt-utils")
essential_package_names+=("apt-utils")
fi
# Step 3: Check and install missing essential packages
local packages_to_install=()
for i in "${!essential_packages_with_version[@]}"; do
local pkg_with_version="${essential_packages_with_version[$i]}"
local pkg_name="${essential_package_names[$i]}"
# Check if package is already installed in chroot
if chroot "${rootfs_mount_point}" dpkg -l "$pkg_name" 2>/dev/null | grep -q "^ii"; then
log::info "Package $pkg_name is already installed, skipping"
else
log::info "Package $pkg_name is not installed, will install: $pkg_with_version"
packages_to_install+=("$pkg_with_version")
fi
done
# Install missing essential packages
if [ ${#packages_to_install[@]} -gt 0 ]; then
chroot "${rootfs_mount_point}" /usr/bin/env ${http_proxy:+http_proxy=$http_proxy} \
${https_proxy:+https_proxy=$https_proxy} \
${ftp_proxy:+ftp_proxy=$ftp_proxy} \
${rsync_proxy:+rsync_proxy=$rsync_proxy} \
${all_proxy:+all_proxy=$all_proxy} \
${no_proxy:+no_proxy=$no_proxy} \
apt-get -y install "${packages_to_install[@]}"
fi
# Step 4: Install zram kernel module for Ubuntu
install_zram_module_if_needed "${rootfs_mount_point}"
# Step 5: Lock version for all essential packages (using base package name)
chroot "${rootfs_mount_point}" apt-mark hold "${essential_package_names[@]}"
chroot "${rootfs_mount_point}" apt-get clean
# Remove the copied .deb files from the chroot after installation
for deb in "${copied_debs[@]}"; do
rm -f "${rootfs_mount_point}${deb}"
done
}
# Sets up a chroot environment by mounting essential filesystems and configurations.
# This includes virtual filesystems (dev, proc, sys, run, pts), boot/efi partitions if applicable,
# and bind-mounts host's resolv.conf and hosts for network access inside chroot.
#
# Note: This function assumes the following global variables are set:
# - boot_part_exist: "true" if /boot is on a separate partition; "false" otherwise
# - boot_part: device path of /boot partition (used when boot_part_exist="true")
# - efi_part_exist: "true" if EFI system partition exists
#
# Arguments:
# $1 - Root filesystem mount point (e.g., /mnt/rootfs or ${workdir}/rootfs)
# $2 - Root filesystem device or image file to mount (e.g., /dev/sda2 or ./root.img)
# $3 - EFI partition device path (optional; e.g., /dev/sda1) — only used if efi_part_exist=true
# $4 - Boot file/device path (e.g., /dev/sda2 or ./boot.img) — used when boot_part_exist=false
#
setup_chroot_mounts() {
local rootfs="$1"
local rootfs_file_or_part="$2"
local efi_part="$3"
local boot_file_path="$4"
log::info "Preparing chroot environment at $rootfs"
# Ensure the rootfs directory exists
mkdir -p "$rootfs"
# Register cleanup hook to safely unmount rootfs on script exit
proc::hook_exit "mountpoint -q '$rootfs' && disk::umount_wait_busy '$rootfs'"
# Mount the root filesystem (could be block device or loop-mounted image)
mount "$rootfs_file_or_part" "$rootfs"
# Mount required pseudo-filesystems: dev, proc, sys, run, tmp, and devpts
for dir in dev dev/pts proc run sys tmp; do
local target="$rootfs/$dir"
mkdir -p "$target"
# Register unmount hook for each mount point
proc::hook_exit "mountpoint -q '$target' && disk::umount_wait_busy '$target'"
case "$dir" in
dev) mount -t devtmpfs devtmpfs "$target" ;;
dev/pts) mount -t devpts devpts "$target" ;;
proc) mount -t proc proc "$target" ;;
run) mount -t tmpfs tmpfs "$target" ;;
sys) mount -t sysfs sysfs "$target" ;;
tmp) mount -t tmpfs tmpfs "$target" ;;
esac
done
# Mount /boot — either from dedicated partition, from a file/image, or skip in UKI mode if no boot partition
local boot_target="$rootfs/boot"
mkdir -p "$boot_target"
proc::hook_exit "mountpoint -q '$boot_target' && disk::umount_wait_busy '$boot_target'"
if [ "$boot_part_exist" = "false" ]; then
if [ -n "$boot_file_path" ]; then
# /boot is part of root or stored as a file (e.g., in embedded systems)
mount "$boot_file_path" "$boot_target"
fi
else
# /boot has its own partition
mount "$boot_part" "$boot_target"
fi
# Conditionally mount EFI system partition under /boot/efi
if [ "$efi_part_exist" = "true" ] && [ -n "$efi_part" ]; then
local efi_target="$rootfs/boot/efi"
mkdir -p "$efi_target"
proc::hook_exit "mountpoint -q '$efi_target' && disk::umount_wait_busy '$efi_target'"
mount "$efi_part" "$efi_target"
fi
# Bind-mount critical network config files from host into chroot (read-only)
for file in resolv.conf hosts; do
local src="/etc/$file"
local dst="$rootfs/etc/$file"
local backup="$dst.cryptpilot" # Backup original file before bind-mounting
# Backup existing file in chroot (if any)
mv "$dst" "$backup" 2>/dev/null || true
touch "$dst" # Ensure destination exists before bind-mounting
# Bind-mount host's version as read-only
proc::hook_exit "mountpoint -q '$dst' && disk::umount_wait_busy '$dst'"
mount -o bind,ro "$(realpath "$src")" "$dst"
done
}
# Cleans up all mounted filesystems and restores original configuration files
# after chroot operations are complete. Ensures no mounts remain active.
#
# Arguments:
# $1 - Root filesystem mount point (same as passed to setup_chroot_mounts)
#
cleanup_chroot_mounts() {
local rootfs="$1"
log::info "Cleaning up chroot environment: unmounting all filesystems"
# Unmount in reverse order (from innermost to outermost)
for dir in etc/hosts etc/resolv.conf boot/efi boot sys run proc dev/pts dev; do
disk::umount_wait_busy "$rootfs/$dir" 2>/dev/null || true
done
# Restore original resolv.conf and hosts files from backup
for file in resolv.conf hosts; do
local dst="$rootfs/etc/$file"
local backup="$dst.cryptpilot"
if [ -f "$backup" ]; then
rm -f "$dst" # Remove bind-mounted or empty file
mv "$backup" "$dst" # Restore original content
fi
done
# Finally, unmount the main root filesystem
disk::umount_wait_busy "$rootfs"
}
# Executes a user-defined function within a fully prepared chroot mount environment.
# Automatically sets up mounts, runs the specified function, then cleans up.
#
# The target function must be defined in the current scope and accept:
# $1 - Root filesystem mount point
# $2+ - Any additional arguments passed after the function name
#
# Arguments:
# $1 - Device or file for root filesystem (e.g., /dev/sda2 or root.img)
# $2 - EFI partition (e.g., /dev/sda1), optional; pass "" if not used
# $3 - Boot file/device path (used when boot_part_exist=false)
# $4 - Name of the function to execute inside the environment
# $5+ - Optional arguments to pass to the target function
#
# Note:
# - The root mount point is automatically set to ${workdir}/rootfs
# - Example usage:
# run_in_chroot_mounts "/dev/sda2" "/dev/sda1" "./boot.img" "install_grub" "--force"
#
run_in_chroot_mounts() {
local rootfs_file_or_part="$1" # Root device/file
local efi_part="$2" # EFI partition
local boot_file_path="$3" # Boot file or device
local func_name="$4" # Function to call
shift 4 # Shift out first four args; rest go to target function
local rootfs_mount_point="${workdir}/rootfs"
# Setup full chroot mount environment
setup_chroot_mounts "$rootfs_mount_point" "$rootfs_file_or_part" "$efi_part" "$boot_file_path"
# Execute the provided function with mount point + extra args
log::info "Executing function '$func_name' inside mounted chroot environment"
"$func_name" "$rootfs_mount_point" "$@"
# Clean up all mounts regardless of success/failure
cleanup_chroot_mounts "$rootfs_mount_point"
}
step:update_rootfs() {
local efi_part=$1
local boot_file_path=$2
local uki=$3
# Expand the rootfs partition to utilize available disk space before update
# This ensures sufficient space for package installations that may trigger scripts
log::info "Expanding rootfs partition to utilize available disk space before update"
# Get current rootfs partition size information
local original_size_in_bytes
original_size_in_bytes=$(blockdev --getsize64 "${rootfs_orig_part}")
local original_size_mb=$((original_size_in_bytes / 1024 / 1024))
log::info "Original rootfs partition size: ${original_size_mb}MB (${original_size_in_bytes} bytes)"
# Use growpart to expand the partition to maximum available space
# This is more reliable than manual calculations
if command -v growpart >/dev/null 2>&1; then
log::info "Using growpart to expand partition ${rootfs_orig_part_num} on device $device"
if growpart "$device" "$rootfs_orig_part_num"; then
# Resize the filesystem to fill the new partition size
log::info "Resizing filesystem to fill new partition size..."
e2fsck -f "${rootfs_orig_part}" -p || true # Run e2fsck first to ensure filesystem integrity
resize2fs "${rootfs_orig_part}"
# Verify the new size
local new_size_in_bytes
new_size_in_bytes=$(blockdev --getsize64 "${rootfs_orig_part}")
local new_size_mb=$((new_size_in_bytes / 1024 / 1024))
log::info "New rootfs partition size: ${new_size_mb}MB (${new_size_in_bytes} bytes)"
log::info "Rootfs partition and filesystem resized successfully"
else
log::warn "growpart failed, proceeding with original partition size"
fi
else
log::warn "growpart command not found, proceeding with original partition size"
fi
update_rootfs_inner() {
local rootfs_mount_point=$1
local uki=$2
log::info "Installing packages into target rootfs"
# Detect package manager inside chroot and choose appropriate installer
if [ -x "${rootfs_mount_point}/usr/bin/apt-get" ] || [ -x "${rootfs_mount_point}/usr/bin/dpkg" ]; then
log::info "Detected Debian/Ubuntu rootfs; using DEB installer"
disk::install_deb_on_rootfs "$rootfs_mount_point" "${packages[@]}"
else
log::info "Detected RPM-based rootfs; using RPM installer"
disk::install_rpm_on_rootfs "$rootfs_mount_point" "${packages[@]}"
fi
log::info "Updating /etc/fstab"
# Prevent duplicate mounting of efi partitions
sed -i '/[[:space:]]\/boot\/efi[[:space:]]/ s/defaults,/defaults,auto,nofail,/' "${rootfs_mount_point}/etc/fstab"
if [ "$boot_part_exist" = "false" ] && [ "$uki" = false ]; then
log::info "Update /etc/fstab for adding /boot mountpoint"
# update /etc/fstab
local root_mount_line_number
root_mount_line_number=$(grep -n -E '^[[:space:]]*[^#][^[:space:]]+[[:space:]]+/[[:space:]]+.*$' "${rootfs_mount_point}/etc/fstab" | head -n 1 | cut -d: -f1)
if [ -z "${root_mount_line_number}" ]; then
proc::fatal "Cannot find mount for / in /etc/fstab"
fi
## insert boot mount line
local boot_uuid
boot_uuid=$(blkid -o value -s UUID "$boot_file_path") # get uuid of the boot image
local boot_mount_line="UUID=${boot_uuid} /boot ext4 defaults,auto,nofail 0 2"
local boot_mount_insert_line_number
boot_mount_insert_line_number=$((root_mount_line_number + 1))
sed -i "${boot_mount_insert_line_number}i${boot_mount_line}" "${rootfs_mount_point}/etc/fstab"
fi
chroot "${rootfs_mount_point}" bash -c "uki='${uki}' ; $(
cat <<'EOF'
set -e
set -u
BASH_XTRACEFD=3
set -x
if [ "${uki:-false}" = false ]; then
# Ensure kernel cmdline includes rd.neednet=1 ip=dhcp for Ubuntu only (prefer cloudimg cfg if present)
if command -v apt-get >/dev/null 2>&1; then
GRUB_TARGET="/etc/default/grub.d/50-cloudimg-settings.cfg"
if [ ! -f "$GRUB_TARGET" ]; then
GRUB_TARGET="/etc/default/grub"
fi
[ -f "$GRUB_TARGET" ] || touch "$GRUB_TARGET"
grub_add_args() {
local var="$1"
local line current
line=$(grep "^${var}=" "$GRUB_TARGET" | head -n1)
if [ -n "$line" ]; then
# Extract value: remove var name and = sign, then remove outer quotes if present
current="${line#${var}=}"
current="${current#\"}"
current="${current%\"}"
else
current=""
fi
case " $current " in
*" rd.neednet=1 "*) : ;;
*) current="$current rd.neednet=1" ;;
esac
case " $current " in
*" ip=dhcp "*) : ;;
*) current="$current ip=dhcp" ;;
esac
# normalize whitespace
set -- $current
current="$*"
local tmp
tmp=$(mktemp)
grep -v "^${var}=" "$GRUB_TARGET" > "$tmp" || true
echo "${var}=\"${current}\"" >> "$tmp"
mv "$tmp" "$GRUB_TARGET"
}
grub_add_args "GRUB_CMDLINE_LINUX_DEFAULT"
fi
echo "Updating grub2.cfg"
grub2_cfg=""
if [ -e /etc/grub2.cfg ] ; then
# alinux3 iso with lagecy BIOS support. The real grub2.cfg is in /boot/grub2/grub.cfg
grub2_cfg=/etc/grub2.cfg
elif [ -e /etc/grub2-efi.cfg ] ; then
# alinux3 iso for UEFI only. The real grub2.cfg is in /boot/efi/EFI/alinux/grub.cfg
grub2_cfg=/etc/grub2-efi.cfg
elif [ -e /boot/grub2/grub.cfg ] ; then
# fallback for other distros
grub2_cfg=/boot/grub2/grub.cfg
elif [ -e /boot/grub/grub.cfg ] ; then
grub2_cfg=/boot/grub/grub.cfg
else
echo "Cannot find grub config file, will attempt to run update-grub if available"
fi
if [ -n "$grub2_cfg" ]; then
if command -v grub2-mkconfig >/dev/null 2>&1; then
echo "Generating grub config with grub2-mkconfig -> $grub2_cfg"
grub2-mkconfig -o "$grub2_cfg" || true
elif command -v grub-mkconfig >/dev/null 2>&1; then
echo "Generating grub config with grub-mkconfig -> $grub2_cfg"
grub-mkconfig -o "$grub2_cfg" || true
else
echo "No grub-mkconfig found, will try update-grub if available"
fi
else
if command -v update-grub >/dev/null 2>&1; then
echo "Running update-grub"
update-grub || true
fi
fi
echo "Cleaning up package manager cache..."
if command -v yum >/dev/null 2>&1; then