added rpi-lvm-imager
This commit is contained in:
417
rpi-lvm-imager.sh
Executable file
417
rpi-lvm-imager.sh
Executable file
@@ -0,0 +1,417 @@
|
||||
#! /bin/bash -e
|
||||
|
||||
#
|
||||
# rpi-lvm-imager.sh - initialize NVME with Raspberry Pi OS (Lite) on LVM
|
||||
#
|
||||
# Version 1.0, latest version, documentation and bugtracker available at:
|
||||
# https://gitea.lindenaar.net/scripts/raspberrypi
|
||||
#
|
||||
# Copyright (c) 2025 Frederik Lindenaar
|
||||
#
|
||||
# This script is free software: you can redistribute and/or modify it under the
|
||||
# terms of version 3 of the GNU General Public License as published by the Free
|
||||
# Software Foundation, or (at your option) any later version of the license.
|
||||
#
|
||||
# This script is distributed in the hope that it will be useful but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, visit <http://www.gnu.org/licenses/> to download it.
|
||||
#
|
||||
|
||||
# Inspired by: https://raspberrypi.stackexchange.com/questions/7159/can-the-raspberry-boot-to-an-lvm-root-partition
|
||||
|
||||
# Fetch the list of Raspberry Pi OS images, if required, and flatten it
|
||||
# next return the url to download the specified image or list of images
|
||||
PIOS_LIST_DOWNLOAD_URL=https://downloads.raspberrypi.com/os_list_imagingutility_v4.json
|
||||
get_raspios() {
|
||||
# Download the Raspberry Pi Imager catalog (if a newer version exists)
|
||||
wget --quiet --timestamping --directory-prefix "${WORKDIR}" "${PIOS_LIST_DOWNLOAD_URL}"
|
||||
RPI_IMAGER_CONFIG="${WORKDIR}/$(basename "${PIOS_LIST_DOWNLOAD_URL}" )"
|
||||
|
||||
# Extract and flatten the list of entries, if required
|
||||
PIOS_LIST_FILE="${WORKDIR}/os_list.json"
|
||||
if [ "${RPI_IMAGER_CONFIG}" -nt "${PIOS_LIST_FILE}" ]; then
|
||||
jq '.os_list
|
||||
| map(select( .url ))
|
||||
+ map(select( .subitems ).subitems
|
||||
| map(select( .url ))
|
||||
+ map(select( .subitems ).subitems
|
||||
| map(select( .url ))
|
||||
+ map(select( .subitems ).subitems
|
||||
| map(select( .url ))
|
||||
+ map(select( .subitems ).subitems)
|
||||
| flatten)
|
||||
| flatten)
|
||||
| flatten)
|
||||
| flatten' "${RPI_IMAGER_CONFIG}" > "${PIOS_LIST_FILE}"
|
||||
if fgrep -q '"subitems"' "${PIOS_LIST_FILE}"; then
|
||||
echo >&2 Warning: structure of the Raspberry Pi Imager catalog has changed
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$1" -o "$1" = list ]; then
|
||||
jq --raw-output "map(.name ) | .[]" "${PIOS_LIST_FILE}" | sort -u
|
||||
else
|
||||
echo $(jq --raw-output "map(select( .name == \"${1}\"))[0] | .url,.extract_size,.extract_sha256" "${PIOS_LIST_FILE}")
|
||||
fi
|
||||
}
|
||||
|
||||
# defaults for command line options
|
||||
opt_raspios_lite=true
|
||||
opt_raspios_bits=64
|
||||
opt_raspios_image=
|
||||
opt_raspios_checksum=
|
||||
opt_check_image_checksum=true
|
||||
opt_raspios_checksum=
|
||||
opt_lvm_group_name=
|
||||
opt_lvm_size=+
|
||||
opt_boot_size=
|
||||
opt_root_size=16G
|
||||
opt_lvm_partitions=
|
||||
opt_nvme_pcie_gen3=false
|
||||
|
||||
# parse command line parameters
|
||||
while [ $# -gt 0 ]
|
||||
do
|
||||
case "$1" in
|
||||
-h|--help) cat << EOT
|
||||
Raspberry Pi LVM Imager - write Raspberry Pi OS image to an LVM-enabled device
|
||||
|
||||
Usage: $(baesename $0) [options] device
|
||||
|
||||
Where <<options>> can be one or more of:
|
||||
|
||||
-h, --help show this help and exit
|
||||
|
||||
-l, --list-images output the list of available Raspberry Pi images and exit
|
||||
-L, --lite install Raspberry Pi OS Lite (default)
|
||||
-R, --regular install the regular Raspberry Pi OS (with destktop)
|
||||
-6, --64bit install a 64-bit version of Raspberry Pi OS
|
||||
-3, --32bit install a 32-bit version of Raspberry Pi OS
|
||||
-i, --image-name name of the image to install
|
||||
-U, --url provide the download URL for Raspberry Pi OS
|
||||
see https://www.raspberrypi.com/software/operating-systems
|
||||
-C, --checksum provide sha256 checksum of image
|
||||
-n, --no-checksum disable checking the sha256 checksum of the downloaded image
|
||||
|
||||
-g, --lvm-group name of the LVM volume group to create (required and must not exist!)
|
||||
-s, --lvm-size size of the LVM volume group to create (defaults to the rest of the disk)
|
||||
-b, --boot-size size of the boot filesystem (defaults to the size in the image)
|
||||
-r, --root-size size of the root filesystem (defaults to 16G)
|
||||
-p, --lvm-partition comma separated /path:size list to move to separate LVMs
|
||||
|
||||
-P, --nvme-pcie-gen3 enable PCIe Gen 3 mode for the NVMe
|
||||
|
||||
-w, --workdir (persistent) work directory to store files, not cleaned up
|
||||
EOT
|
||||
exit
|
||||
;;
|
||||
-l|--list-images) opt_raspios_image=list
|
||||
;;
|
||||
-i|--image-name) opt_raspios_image="$2"
|
||||
shift
|
||||
;;
|
||||
-L|--lite) opt_raspios_lite=true
|
||||
;;
|
||||
-R|--regular) opt_raspios_lite=false
|
||||
;;
|
||||
-3|--32-bit) opt_raspios_bits=32
|
||||
;;
|
||||
-6|--64-bit) opt_raspios_bits=64
|
||||
;;
|
||||
-U|--url) IMAGE_DOWNLOAD_URL="$2"
|
||||
shift
|
||||
;;
|
||||
-C|--checksum) opt_raspios_checksum="$2"
|
||||
shift
|
||||
;;
|
||||
-n|--no-checksum) opt_check_image_checksum=false
|
||||
;;
|
||||
-g|--lvm-group) opt_lvm_group_name="$2"
|
||||
shift
|
||||
;;
|
||||
-s|--lvm-size) opt_lvm_size="$2"
|
||||
shift
|
||||
;;
|
||||
-b|--boot-size) opt_boot_size="$2"
|
||||
shift
|
||||
;;
|
||||
-r|--root-size) opt_root_size="$2"
|
||||
shift
|
||||
;;
|
||||
-p|--lvm-partition) opt_lvm_partitions="$2"
|
||||
shift
|
||||
;;
|
||||
-P|--nvme-pcie-gen3) opt_nvme_pcie_gen3=true
|
||||
;;
|
||||
-w|--workdir) WORKDIR="$2"
|
||||
shift
|
||||
;;
|
||||
-*) echo Error: unknown option $1, run $0 -h for supported options
|
||||
exit 2
|
||||
;;
|
||||
*) if [ -z "$opt_target_device" ]; then
|
||||
opt_target_device=$1
|
||||
else
|
||||
echo Error: only one device is supported, run $0 -h for usage
|
||||
exit 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Ensure the script is run as root
|
||||
if [ $(id -u) -gt 0 ]; then
|
||||
echo "Error: $(basename $0) must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure cleanup is done when we exist, irrespective how
|
||||
cleanup() {
|
||||
echo cleaning up
|
||||
# Unmount the partitions under the rootfs in reverse order
|
||||
if [ -n "${TARGET_ROOT_MOUNTPOINT}" ]; then
|
||||
findmnt --raw --noheadings --output target --submounts "${TARGET_ROOT_MOUNTPOINT}" | sort -r | while read mountpoint
|
||||
do
|
||||
umount "${mountpoint}"
|
||||
done
|
||||
fi
|
||||
|
||||
# Unmount the image from the loopback when it was mounted
|
||||
if [ -n "$RASPIOS_DEV" ]; then
|
||||
losetup --detach ${RASPIOS_DEV}
|
||||
fi
|
||||
|
||||
# Remove the temporary working directory (if created)
|
||||
if [ -n "$TEMPDIR" ]; then
|
||||
rm -rf $TEMPDIR
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create a temporary working directory, if required
|
||||
if [ -z "$WORKDIR" ]; then
|
||||
TEMPDIR=$(mktemp --directory --tmpdir $(basename $0 .sh).XXXXXXXX)
|
||||
WORKDIR=${TEMPDIR}
|
||||
elif [ ! -d "${WORKDIR}" ]; then
|
||||
mkdir "${WORKDIR}"
|
||||
fi
|
||||
|
||||
# If we need to print the list of available images, print it and exit
|
||||
if [ "${opt_raspios_image}" = "list" ]; then
|
||||
get_raspios list
|
||||
exit
|
||||
fi
|
||||
|
||||
# Ensure the target is a valid disk
|
||||
if [ -z "${opt_target_device}" ]; then
|
||||
echo Error: target device is required, run $0 -h for usage
|
||||
exit 2
|
||||
elif [ "$(lsblk --nodeps --noheadings ${opt_target_device} -o type)" != "disk" ]; then
|
||||
echo Error: target device ${opt_target_device} is not a disk
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Ensure no partitions of the target device are still mounted
|
||||
if [ -n "$(findmnt -D| fgrep "${opt_target_device}")" ]; then
|
||||
echo Error: partitions of target device ${opt_target_device} are still mounted:
|
||||
findmnt -D | egrep "^SOURCE|${opt_target_device}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure the LVM group (if one is to be setup) is provided and does not exist yet
|
||||
if [ -z "${opt_lvm_group_name}" ]; then
|
||||
echo Error: target LVM volume group name is required, run $0 -h for usage
|
||||
exit 2
|
||||
elif [ -n "${opt_lvm_group_name}" -a -n "$(pvdisplay --columns --options pv_name,vg_name --noheading | egrep "${opt_lvm_group_name}|${opt_target_device}")" ]; then
|
||||
echo Existing LVM configuration found, cleaning up conflicting setup
|
||||
TTY=$(tty)
|
||||
pvdisplay --columns --options pv_name,vg_name --noheading | while read dev vg
|
||||
do
|
||||
if [ "${vg}" = "${opt_lvm_group_name}" ]; then
|
||||
wipefs --quiet --all /dev/${vg}/*
|
||||
vgremove --quiet ${vg} < ${TTY}
|
||||
fi
|
||||
if [[ "${dev}" == ${opt_target_device}* ]]; then
|
||||
if ! [ "${vg}" != "${opt_lvm_group_name}" ] || vgreduce --quiet ${vg} ${dev} < ${TTY}; then
|
||||
pvremove --quiet ${dev} < ${TTY}
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
lvm_dev=
|
||||
|
||||
# Ensure the partition table of the taget device is empty
|
||||
if [ "$(sudo sfdisk --dump "${opt_target_device}" 2>/dev/null )" ]; then
|
||||
echo Warning: target device ${opt_target_device} may contain data:
|
||||
sfdisk --quiet --list "${opt_target_device}"
|
||||
|
||||
echo
|
||||
read -p "erase everything and image ${opt_target_device}? (yes/no) [no]"
|
||||
if [ "${REPLY}" != "yes" ]; then
|
||||
echo Aborted.
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo Erasing data on "${opt_target_device}"
|
||||
wipefs --quiet --all ${opt_target_device}*
|
||||
sfdisk --quiet --wipe always --wipe-partitions always --delete "${opt_target_device}" && echo OK || echo NOK
|
||||
# dd if=/dev/zero "of=${opt_target_device}" bs=512 count=1 2> /dev/null
|
||||
fi
|
||||
|
||||
echo partitioning target disk
|
||||
echo 'label: dos' | sfdisk --quiet "${opt_target_device}"
|
||||
|
||||
# Ensure a Raspberry Pi OS download URL and local image name are set
|
||||
if [ -z "${IMAGE_DOWNLOAD_URL}" ]; then
|
||||
if [ -z "${opt_raspios_image}" ]; then
|
||||
if ${opt_raspios_lite}; then
|
||||
opt_raspios_image="Raspberry Pi OS Lite (${opt_raspios_bits}-bit)"
|
||||
else
|
||||
opt_raspios_image="Raspberry Pi OS (${opt_raspios_bits}-bit)"
|
||||
fi
|
||||
fi
|
||||
|
||||
read IMAGE_DOWNLOAD_URL IMAGE_EXTRACT_SIZE IMAGE_EXTRACT_SHA256 <<< $(get_raspios "${opt_raspios_image}")
|
||||
if [ -z "${IMAGE_DOWNLOAD_URL}" -o "${IMAGE_DOWNLOAD_URL}" = "null" ]; then
|
||||
echo "Error: no URL found for image name '${opt_raspios_image}'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
RASPIOS_IMG="${WORKDIR}/$(basename "${IMAGE_DOWNLOAD_URL}" .xz)"
|
||||
|
||||
# Download the image, if required, and/or verify its checksum (if possible)
|
||||
if [ ! -f "${RASPIOS_IMG}" -o -n "${IMAGE_EXTRACT_SIZE}" -a "$(du -b "${RASPIOS_IMG}" 2>/dev/null | sed "s/[^0-9].*//")" != "${IMAGE_EXTRACT_SIZE}" ]; then
|
||||
if $opt_check_image_checksum && [ -n "${opt_raspios_checksum:-$IMAGE_EXTRACT_SHA256}" ]; then
|
||||
echo "${opt_raspios_checksum:-$IMAGE_EXTRACT_SHA256} -" > "${RASPIOS_IMG}.$$.sha256sum"
|
||||
if ! wget --quiet --show-progress --output-document - "${IMAGE_DOWNLOAD_URL}" | xz --decompress | tee ${RASPIOS_IMG} | sha256sum --check --strict --quiet "${RASPIOS_IMG}.$$.sha256sum"; then
|
||||
echo Error: downloading, extracting and verifying the image failed
|
||||
exit 2
|
||||
fi
|
||||
rm ${RASPIOS_IMG}.*.sha256sum
|
||||
else
|
||||
if ! wget --quiet --show-progress --output-document - "${IMAGE_DOWNLOAD_URL}" | xz --decompress > ${RASPIOS_IMG}; then
|
||||
echo Error: downloading the image failed
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
elif $opt_check_image_checksum && [ -n "${opt_raspios_checksum:-$IMAGE_EXTRACT_SHA256}" ]; then
|
||||
if ! echo "${opt_raspios_checksum:-$IMAGE_EXTRACT_SHA256} ${RASPIOS_IMG}" | sha256sum --check --strict --quiet; then
|
||||
echo Error: image checksum does not match with the Raspberry Pi catalog
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Mount the image as a loopback device and get it's device name
|
||||
RASPIOS_DEV=$(losetup --find --show --partscan --read-only ${RASPIOS_IMG} )
|
||||
sleep 1
|
||||
|
||||
# Get the partition sizes of the image
|
||||
TTY=$(tty)
|
||||
sfdisk --dump ${RASPIOS_DEV} | egrep "^/" | sed -E "s/[:,][^=]+=/ /g; s/ +/ /g" | while read part start size type
|
||||
do
|
||||
label=$(lsblk "${part}" --noheadings --output label)
|
||||
echo Found $label partition $part with $start $size and type $type
|
||||
|
||||
if [ "${type}" = 'c' ]; then
|
||||
echo ",${size},${type}" | sfdisk --quiet --append --wipe always --wipe-partition always "${opt_target_device}"
|
||||
target_part=$(sfdisk --dump "${opt_target_device}" | tail -1 | cut -f1 -d\ )
|
||||
elif [ "${type}" = '83' ]; then
|
||||
if [ -z "${lvm_dev}" ]; then
|
||||
echo ",${opt_lvm_size},8e" | sfdisk --quiet --append --wipe always "${opt_target_device}" 2>/dev/null
|
||||
lvm_dev="$(sfdisk --dump "${opt_target_device}" | tail -1 | cut -f1 -d\ )"
|
||||
pvcreate --quiet "${lvm_dev}"
|
||||
vgcreate --quiet "${opt_lvm_group_name}" "${lvm_dev}"
|
||||
fi
|
||||
|
||||
lvcreate --name "${label}" --size "${opt_root_size}" --wipesignatures y "${opt_lvm_group_name}" <$TTY
|
||||
target_part="/dev/${opt_lvm_group_name}/${label}"
|
||||
|
||||
else
|
||||
echo Error: image contains unsupported partition ${part} with type 0x$type
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo Copying $label to ${target_part}
|
||||
dd if=${part} of=$target_part 2>/dev/null
|
||||
done
|
||||
|
||||
echo Setting up the root filesystem for its initial boot
|
||||
|
||||
TARGET_BOOTFS_DEV=$(lsblk --raw --noheadings --output path,label "${opt_target_device}" | fgrep bootfs | cut -d\ -f1)
|
||||
TARGET_ROOTFS_DEV=$(lsblk --raw --noheadings --output path,label "${opt_target_device}" | fgrep rootfs | cut -d\ -f1)
|
||||
|
||||
# Mount the new root filesystem for post-processing
|
||||
TARGET_ROOT_MOUNTPOINT="${WORKDIR}/rootfs"
|
||||
echo mount "${filesystems[rootfs]}" "${TARGET_ROOT_MOUNTPOINT}"
|
||||
mount -o noatime "${TARGET_ROOTFS_DEV}" "${TARGET_ROOT_MOUNTPOINT}"
|
||||
mount "${TARGET_BOOTFS_DEV}" "${TARGET_ROOT_MOUNTPOINT}/boot/firmware"
|
||||
mount -t proc /proc "${TARGET_ROOT_MOUNTPOINT}/proc"
|
||||
mount -o bind /dev "${TARGET_ROOT_MOUNTPOINT}/dev"
|
||||
mount -o bind /dev/pts "${TARGET_ROOT_MOUNTPOINT}/dev/pts"
|
||||
mount -t sysfs /sys "${TARGET_ROOT_MOUNTPOINT}/sys"
|
||||
|
||||
# patch /boot/firmware/config.txt so tat it contains the correct root filesystem
|
||||
sed -Ei "s|root=[^ \t]+|root=${TARGET_ROOTFS_DEV}|" "${TARGET_ROOT_MOUNTPOINT}/boot/firmware/cmdline.txt"
|
||||
|
||||
# patch /etc/fstab so that it contains the right devices
|
||||
sed -Ei "s|^[^# \t]+([ \t]+/boot/firmware[ \t])|${TARGET_BOOTFS_DEV}\1|" "${TARGET_ROOT_MOUNTPOINT}/etc/fstab"
|
||||
sed -Ei "s|^[^# \t]+([ \t]+/[ \t])|${TARGET_ROOTFS_DEV}\1|" "${TARGET_ROOT_MOUNTPOINT}/etc/fstab"
|
||||
|
||||
# move filesystems to separate LVMs and ensure these are automaticaly mounted
|
||||
for p in ${opt_lvm_partitions//,/ }
|
||||
do
|
||||
IFS=: read path size <<< "${p}"
|
||||
lvname=${path//*\//}fs
|
||||
echo moving ${path} to a LVM ${lvmname} of ${size}
|
||||
|
||||
lvcreate --name "${lvname}" --size "${size}" --wipesignatures y "${opt_lvm_group_name}" <$TTY
|
||||
lvpath=$(lsblk --raw --noheadings --output path,label "${opt_target_device}" | fgrep "${lvname}" | cut -d\ -f1)
|
||||
mke2fs -L "${lvname}" "${lvpath}"
|
||||
|
||||
if ! [ -d "${TARGET_ROOT_MOUNTPOINT}${path}" ]; then
|
||||
echo Error: direcory ${vpath} does not exist in the target environment
|
||||
exit 1
|
||||
# mkdir "${TARGET_ROOT_MOUNTPOINT}/${path}"
|
||||
else
|
||||
mount "${lvpath}" "${TARGET_ROOT_MOUNTPOINT}/mnt"
|
||||
chroot "${TARGET_ROOT_MOUNTPOINT}" rsync -aAXH "${path}/" "/mnt/"
|
||||
umount "${TARGET_ROOT_MOUNTPOINT}/mnt"
|
||||
chroot "${TARGET_ROOT_MOUNTPOINT}" bash -c "rm -rf ${path}/* ${path}/.*"
|
||||
fi
|
||||
|
||||
if egrep -Eq "^[^# \t]+[ \t]+${path}[ \t]" "${TARGET_ROOT_MOUNTPOINT}/etc/fstab" ; then
|
||||
sed -Ei "s|^[^# \t]+([ \t]+${path}[ \t])|${lvpath}\1|" "${TARGET_ROOT_MOUNTPOINT}/etc/fstab"
|
||||
else
|
||||
echo "${lvpath} ${path} ext4 defaults,noatime 0 2" >> "${TARGET_ROOT_MOUNTPOINT}/etc/fstab"
|
||||
fi
|
||||
|
||||
mount "${lvpath}" "${TARGET_ROOT_MOUNTPOINT}${path}"
|
||||
done
|
||||
|
||||
cat "${TARGET_ROOT_MOUNTPOINT}/etc/fstab"
|
||||
|
||||
# install required packages for things to work
|
||||
chroot "${TARGET_ROOT_MOUNTPOINT}" apt install lvm2
|
||||
|
||||
# generate password
|
||||
# openssl passwd -6
|
||||
|
||||
# setup initial (defaultt) user
|
||||
echo 'jfl:$6$EyEFwTRqBv16ws7j$/5qABGo7Bm3p44mml1/LmFp9b/VdkCURRHNsGxl9YOC3IdzZ7N5LdYFOF5nZjGFQONWGgwjBBIn.JGhGNdnP70' > "${TARGET_ROOT_MOUNTPOINT}/boot/firmware/userconf"
|
||||
|
||||
# enable SSH
|
||||
touch "${TARGET_ROOT_MOUNTPOINT}/boot/firmware/ssh"
|
||||
|
||||
# Enable PCIe Gen 3 for NVMe, if requested
|
||||
if ${opt_nvme_pcie_gen3}; then
|
||||
if ! egrep -q "^\s*dtparam\s*=\s*pciex1_gen\s*=" "${TARGET_ROOT_MOUNTPOINT}/boot/firmware/config.txt"; then
|
||||
echo dtparam=pciex1_gen=3 >> "${TARGET_ROOT_MOUNTPOINT}/boot/firmware/config.txt"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo Done!
|
||||
Reference in New Issue
Block a user