added rpi-lvm-imager

This commit is contained in:
2026-01-02 12:45:12 +01:00
parent 9990f05cf5
commit 71369509dd
2 changed files with 453 additions and 3 deletions

View File

@@ -6,9 +6,9 @@ are of any benefit to other enthusiasts. Use them freely and please let me know
in case you encounter any issues or require changes. in case you encounter any issues or require changes.
The latest versions, documentation and bugtracker available on my The latest versions, documentation and bugtracker available on my
[GitLab instance](https://gitlab.lindenaar.net/scripts/raspberrypi) [Gitea instance](https://gitea.lindenaar.net/scripts/raspberrypi)
Copyright (c) 2019 Frederik Lindenaar. free for distribution under Copyright (c) 2019 - 2025 Frederik Lindenaar. free for distribution under
the GNU General Public License, see [below](#license) the GNU General Public License, see [below](#license)
Contents Contents
@@ -24,6 +24,8 @@ This repository contains the following scripts:
is a systemd service to disable on-board WiFi on Raspberry Pi at boot is a systemd service to disable on-board WiFi on Raspberry Pi at boot
* [rpi_poweroff_button.service](#services) * [rpi_poweroff_button.service](#services)
is a systemd service to support a power-off button using [gpio_trigger.py](#gpio_trigger) is a systemd service to support a power-off button using [gpio_trigger.py](#gpio_trigger)
* [rpi-lvm-imager.sh](#rpi-lvm-imager)
initialize an NVME drive with Raspberry Pi OS (Lite) on LVM
<a name=gpio_trigger>gpio_trigger.py</a> <a name=gpio_trigger>gpio_trigger.py</a>
@@ -94,6 +96,37 @@ systemctl disable service <<filename without .service>>
~~~ ~~~
<a name=rpi-lvm-imager>rpi-lvm-imager.sh</a>
--------------------------------------------
This script will wipe and partition a (large) storage device with a small boot
partition, setup LVM for the rest of the disk, and then writes the latest
version of Raspbian Pi OS (Lite) or another OS image availabile via the
Raspberry Pi Imager to LVM managed partition(s) and makes the device bootable.
The reason I worte this script was that the Raspberry Pi Imager can only write
an image to a full disk and does not allow one to partition it nor perform the
installation on an LVM managed disk. This was needed for a Raspberry Pi 5 with
NVME SSD as the device had way more diskspace than would ever be needed/useful
for the system partition.
This script was inspired by the write-up found [here](https://raspberrypi.stackexchange.com/questions/7159/can-the-raspberry-boot-to-an-lvm-root-partition).
That was apparently superseded by [this approach](https://raspberrypi.stackexchange.com/questions/85958/easy-backups-and-snapshots-of-a-running-system-with-lvm),
hoever, I like the initial approach more. This script will write the OS to a
device after setting up LVM but before booting it so it is just quicker than
writing the OS and then moving it to an LVM managed partition.
To write the latest version of Raspberry Pi OS (64bit Lite) to an NVME SSD
device simply run:
~~~
sudo rpi-lvm-imager.sh --lvm-group nvme /dev/nvme0n1
~~~
Please note that this will unmound and wipe the specified device so be careful!
to see all aailable options, run: `./rpi-lvm-imager.sh -h`
<a name="license">License</a> <a name="license">License</a>
----------------------------- -----------------------------
These scripts, documentation & configration examples are free software: you can These scripts, documentation & configration examples are free software: you can
@@ -107,4 +140,4 @@ warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details. General Public License for more details.
You should have received a copy of the GNU General Public License along with You should have received a copy of the GNU General Public License along with
this program. If not, download it from <http://www.gnu.org/licenses/>. this program. If not, download it from <http://www.gnu.org/licenses/>.

417
rpi-lvm-imager.sh Executable file
View 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!