Add a11ywatch and related configurations for Podman and Nginx

- Introduced a new module for a11ywatch with Podman support, creating a bridge network and defining backend and frontend containers.
- Configured Nginx to serve the a11ywatch application with SSL and ACME support.
- Added user and group configurations for a11ywatch.
- Created a systemd service to ensure the Podman network exists on boot.

Implement Firefox Container Controller extension and host

- Added a module for the Firefox Container Controller extension, allowing installation via Nix.
- Created a native messaging host for the extension to communicate with the container controller.
- Included CLI helpers to enqueue commands for showing and hiding containers.

Enable fingerprint authentication in PAM

- Configured fingerprint authentication for login, sudo, and swaylock services.

Setup Raspberry Pi OS image creation script

- Developed a script to create a read-only Raspberry Pi OS Lite image with Snapcast client.
- Included configuration for Wi-Fi, hostname, and Snapcast server.
- Implemented user and group setup for Snapcast client and ensured necessary services are enabled.

Document Raspberry Pi Zero W setup instructions

- Added detailed instructions for configuring Raspberry Pi OS on Zero W, including disabling unused services and setting up Snapcast client.

Create test configuration script for NixOS

- Implemented a script to perform dry-builds for NixOS configurations, allowing for easy validation of host configurations.
This commit is contained in:
2025-05-29 00:10:07 +02:00
parent 8e52274edd
commit 53d73142ae
32 changed files with 1280 additions and 104 deletions

4
raspberry-new/config.txt Normal file
View File

@@ -0,0 +1,4 @@
WIFI_SSID="Cloonar-Multimedia"
WIFI_PSK="K2MC28Zhk$4zsx6Y"
SNAPCAST_SERVER="snapcast.cloonar.com"
# You can add other configurations here if needed later

416
raspberry-new/create_rpi_image.sh Executable file
View File

@@ -0,0 +1,416 @@
#!/usr/bin/env bash
# Script to create a read-only Raspberry Pi OS Lite image with Snapcast client.
# Requires sudo privileges for many operations.
# Ensure you have all dependencies from shell.nix (e.g., qemu-arm-static, parted, etc.)
set -euo pipefail # Exit on error, undefined variable, or pipe failure
# --- Configuration & Defaults ---
RASPI_OS_LITE_URL="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2025-05-07/2025-05-06-raspios-bookworm-arm64-lite.img.xz"
# Check for the latest image URL from https://www.raspberrypi.com/software/operating-systems/
# The script assumes an .img.xz file. If it's .zip, adjust extraction.
WORK_DIR="rpi_build_temp"
# QEMU is no longer used in this script.
# --- Helper Functions ---
info() { echo -e "\033[0;32m[INFO]\033[0m $1"; }
warn() { echo -e "\033[0;33m[WARN]\033[0m $1"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $1" >&2; exit 1; }
cleanup_exit() {
warn "Cleaning up and exiting..."
local rootfs_mount_point="${PWD}/${WORK_DIR}/rootfs" # PWD should be the script's initial dir, WORK_DIR is relative
# Attempt to unmount in reverse order of mounting
if mount | grep -q "${rootfs_mount_point}/boot"; then
info "Cleanup: Unmounting ${rootfs_mount_point}/boot..."
sudo umount "${rootfs_mount_point}/boot" || sudo umount -l "${rootfs_mount_point}/boot" || warn "Failed to unmount ${rootfs_mount_point}/boot during cleanup."
fi
if mount | grep -q "${rootfs_mount_point}"; then # Check rootfs itself
info "Cleanup: Unmounting ${rootfs_mount_point}..."
sudo umount "${rootfs_mount_point}" || sudo umount -l "${rootfs_mount_point}" || warn "Failed to unmount ${rootfs_mount_point} during cleanup."
fi
if [ -n "${LOOP_DEV:-}" ] && losetup -a | grep -q "${LOOP_DEV}"; then
info "Cleanup: Detaching loop device ${LOOP_DEV}..."
sudo losetup -d "${LOOP_DEV}" || warn "Failed to detach ${LOOP_DEV} during cleanup."
fi
# sudo rm -rf "${WORK_DIR}" # Optional: clean work dir on error
exit 1
}
trap cleanup_exit ERR INT TERM
# --- Argument Parsing ---
DEVICE_TYPE=""
HOSTNAME_PI=""
CONFIG_FILE="./config.txt"
OUTPUT_IMAGE_FILE=""
usage() {
echo "Usage: $0 -d <device_type> -n <hostname> [-c <config_file>] [-o <output_image>]"
echo " -d: Device type (rpizero2w | rpi4)"
echo " -n: Desired hostname for the Raspberry Pi"
echo " -c: Path to config.txt (default: ./config.txt)"
echo " -o: Output image file name (default: snapcast-client-<device_type>-<hostname>.img)"
exit 1
}
while getopts "d:n:c:o:h" opt; do
case ${opt} in
d) DEVICE_TYPE="${OPTARG}";;
n) HOSTNAME_PI="${OPTARG}";;
c) CONFIG_FILE="${OPTARG}";;
o) OUTPUT_IMAGE_FILE="${OPTARG}";;
h) usage;;
*) usage;;
esac
done
if [ -z "${DEVICE_TYPE}" ]; then
error "Mandatory argument -d <device_type> is missing. Use -h for help."
fi
if [ -z "${HOSTNAME_PI}" ]; then
error "Mandatory argument -n <hostname> is missing. Use -h for help."
fi
if [ "${DEVICE_TYPE}" != "rpizero2w" ] && [ "${DEVICE_TYPE}" != "rpi4" ]; then
error "Invalid device type: '${DEVICE_TYPE}'. Must be 'rpizero2w' or 'rpi4'."
fi
if [ ! -f "${CONFIG_FILE}" ]; then
error "Config file not found: ${CONFIG_FILE}"
fi
if [ -z "${OUTPUT_IMAGE_FILE}" ]; then
OUTPUT_IMAGE_FILE="snapcast-client-${DEVICE_TYPE}-${HOSTNAME_PI}.img"
fi
info "Starting Raspberry Pi Image Builder..."
info "Device Type: ${DEVICE_TYPE}"
info "Hostname: ${HOSTNAME_PI}"
info "Config File: ${CONFIG_FILE}"
info "Output Image: ${OUTPUT_IMAGE_FILE}"
# --- Load Configuration ---
source "${CONFIG_FILE}"
if [ -z "${WIFI_SSID:-}" ] || [ -z "${WIFI_PSK:-}" ] || [ -z "${SNAPCAST_SERVER:-}" ]; then
error "WIFI_SSID, WIFI_PSK, or SNAPCAST_SERVER not set in config file."
fi
# --- Prepare Workspace ---
sudo rm -rf "${WORK_DIR}"
mkdir -p "${WORK_DIR}"
cd "${WORK_DIR}"
# --- 1. Base Image Acquisition ---
IMG_XZ_NAME=$(basename "${RASPI_OS_LITE_URL}")
IMG_NAME="${IMG_XZ_NAME%.xz}"
# Check for uncompressed image first
if [ -f "${IMG_NAME}" ]; then
info "Using existing uncompressed image: ${IMG_NAME}"
# Else, check for compressed image
elif [ -f "${IMG_XZ_NAME}" ]; then
info "Found existing compressed image: ${IMG_XZ_NAME}. Extracting..."
xz -d -k "${IMG_XZ_NAME}" # -k to keep the original .xz file
if [ ! -f "${IMG_NAME}" ]; then # Double check extraction
error "Failed to extract ${IMG_XZ_NAME} to ${IMG_NAME}"
fi
info "Extraction complete: ${IMG_NAME}"
# Else, download and extract
else
info "Downloading Raspberry Pi OS Lite image: ${IMG_XZ_NAME}..."
wget -q --show-progress -O "${IMG_XZ_NAME}" "${RASPI_OS_LITE_URL}"
info "Extracting image..."
xz -d -k "${IMG_XZ_NAME}" # -k to keep the original .xz file
if [ ! -f "${IMG_NAME}" ]; then # Double check extraction
error "Failed to extract ${IMG_XZ_NAME} to ${IMG_NAME}"
fi
info "Extraction complete: ${IMG_NAME}"
fi
# Always work on a copy for the output image
info "Copying ${IMG_NAME} to ${OUTPUT_IMAGE_FILE}..."
cp "${IMG_NAME}" "${OUTPUT_IMAGE_FILE}"
# --- 2. Mount Image Partitions ---
info "Setting up loop device for ${OUTPUT_IMAGE_FILE}..."
LOOP_DEV=$(sudo losetup -Pf --show "${OUTPUT_IMAGE_FILE}")
if [ -z "${LOOP_DEV}" ]; then error "Failed to setup loop device."; fi
info "Loop device: ${LOOP_DEV}"
# Wait for device nodes to be created
sleep 2
sudo partprobe "${LOOP_DEV}"
sleep 2
BOOT_PART="${LOOP_DEV}p1"
ROOT_PART="${LOOP_DEV}p2"
mkdir -p rootfs
info "Mounting root partition (${ROOT_PART}) to rootfs/..."
sudo mount "${ROOT_PART}" rootfs
info "Mounting boot partition (${BOOT_PART}) to rootfs/boot/..."
# Note: Newer RPi OS might use /boot/firmware. Adjust if needed.
# Check if /boot/firmware exists, if so, use it.
# For simplicity, this script assumes /boot. If issues, this is a place to check.
# A more robust check: BOOT_MOUNT_POINT="rootfs/boot"; if [ -d "rootfs/boot/firmware" ]; then BOOT_MOUNT_POINT="rootfs/boot/firmware"; fi
# sudo mount "${BOOT_PART}" "${BOOT_MOUNT_POINT}"
sudo mount "${BOOT_PART}" rootfs/boot
# --- 3. System Configuration (Directly on Mounted Rootfs) ---
# QEMU and chroot are no longer used. All operations are on the mounted rootfs.
if ! command -v dpkg-deb &> /dev/null; then
error "dpkg-deb command not found. Please ensure dpkg is installed and in your PATH (e.g., via Nix shell)."
fi
info "Setting hostname to ${HOSTNAME_PI} on rootfs..."
sudo sh -c "echo '${HOSTNAME_PI}' > rootfs/etc/hostname"
sudo sed -i "s/127.0.1.1.*raspberrypi/127.0.1.1\t${HOSTNAME_PI}/g" rootfs/etc/hosts
sudo sed -i "s/raspberrypi/${HOSTNAME_PI}/g" rootfs/etc/hosts # Also replace other occurrences
info "Configuring Wi-Fi (wpa_supplicant) on rootfs..."
sudo sh -c "cat > rootfs/boot/wpa_supplicant.conf <<WPA
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=AT # Set your country code
network={
ssid=\"${WIFI_SSID}\"
psk=\"${WIFI_PSK}\"
scan_ssid=1
}
WPA"
sudo chmod 600 rootfs/boot/wpa_supplicant.conf
info "Downloading and installing Snapcast client from .deb..."
SNAPCLIENT_DEB_URL="https://github.com/badaix/snapcast/releases/download/v0.31.0/snapclient_0.31.0-1_arm64_bookworm_with-pulse.deb"
SNAPCLIENT_DEB_NAME=$(basename "${SNAPCLIENT_DEB_URL}")
if [ ! -f "${SNAPCLIENT_DEB_NAME}" ]; then
wget -q --show-progress -O "${SNAPCLIENT_DEB_NAME}" "${SNAPCLIENT_DEB_URL}"
else
info "Using existing ${SNAPCLIENT_DEB_NAME}"
fi
mkdir -p snapclient_deb_extract
info "Extracting ${SNAPCLIENT_DEB_NAME}..."
dpkg-deb -x "${SNAPCLIENT_DEB_NAME}" snapclient_deb_extract
info "Copying extracted files to rootfs using rsync..."
# Use rsync to handle merging and symlinks like /lib -> /usr/lib correctly
# The trailing slash on snapclient_deb_extract/ is important for rsync
if ! command -v rsync &> /dev/null; then
error "rsync command not found. Please ensure rsync is installed and in your PATH (e.g., via Nix shell)."
fi
if ! command -v openssl &> /dev/null; then
error "openssl command not found. Please ensure openssl is installed and in your PATH (e.g., via Nix shell)."
fi
sudo rsync -aK --chown=root:root snapclient_deb_extract/ rootfs/
rm -rf snapclient_deb_extract "${SNAPCLIENT_DEB_NAME}"
info "Snapclient files installed."
info "Attempting to create 'snapclient' user and group..."
SNAPCLIENT_UID=987 # Choose an appropriate UID/GID
SNAPCLIENT_GID=987
SNAPCLIENT_USER="snapclient"
SNAPCLIENT_GROUP="snapclient"
SNAPCLIENT_HOME="/var/lib/snapclient" # A typical home for system users, though not strictly needed if nologin
SNAPCLIENT_SHELL="/usr/sbin/nologin"
# Create group if it doesn't exist
if ! sudo grep -q "^${SNAPCLIENT_GROUP}:" rootfs/etc/group; then
info "Creating group '${SNAPCLIENT_GROUP}' (${SNAPCLIENT_GID}) in rootfs/etc/group"
sudo sh -c "echo '${SNAPCLIENT_GROUP}:x:${SNAPCLIENT_GID}:' >> rootfs/etc/group"
else
info "Group '${SNAPCLIENT_GROUP}' already exists in rootfs/etc/group."
fi
# Create user if it doesn't exist
if ! sudo grep -q "^${SNAPCLIENT_USER}:" rootfs/etc/passwd; then
info "Creating user '${SNAPCLIENT_USER}' (${SNAPCLIENT_UID}) in rootfs/etc/passwd"
sudo sh -c "echo '${SNAPCLIENT_USER}:x:${SNAPCLIENT_UID}:${SNAPCLIENT_GID}:${SNAPCLIENT_USER} system user:${SNAPCLIENT_HOME}:${SNAPCLIENT_SHELL}' >> rootfs/etc/passwd"
info "Creating basic shadow entry for '${SNAPCLIENT_USER}' (account locked)"
# '!' in password field locks the account. '*' also works.
sudo sh -c "echo '${SNAPCLIENT_USER}:!:19700:0:99999:7:::' >> rootfs/etc/shadow"
# Create home directory if it doesn't exist and set permissions
sudo mkdir -p "rootfs${SNAPCLIENT_HOME}"
sudo chown "${SNAPCLIENT_UID}:${SNAPCLIENT_GID}" "rootfs${SNAPCLIENT_HOME}"
sudo chmod 700 "rootfs${SNAPCLIENT_HOME}"
else
info "User '${SNAPCLIENT_USER}' already exists in rootfs/etc/passwd."
fi
# Remove previous warnings
# warn "The snapclient user and group were NOT automatically created."
# warn "Ensure 'snapclient' user/group exist on target or adjust service file."
info "Creating Snapcast systemd service file on rootfs..."
# Note: SNAPCAST_SERVER_IP is from the config file via 'source "${CONFIG_FILE}"'
sudo sh -c "cat > rootfs/etc/systemd/system/snapclient.service <<SERVICE
[Unit]
Description=Snapcast client
After=network-online.target sound.target
Wants=network-online.target
[Service]
ExecStart=/usr/bin/snapclient -h ${SNAPCAST_SERVER} --player pulse # Or alsa, adjust player if needed
Restart=always
User=${SNAPCLIENT_USER}
Group=${SNAPCLIENT_GROUP}
# User and group should now be created by this script.
[Install]
WantedBy=multi-user.target
SERVICE"
info "Enabling Snapcast systemd service on rootfs..."
sudo mkdir -p rootfs/etc/systemd/system/multi-user.target.wants
sudo ln -sf ../../../../lib/systemd/system/snapclient.service rootfs/etc/systemd/system/multi-user.target.wants/snapclient.service
# Note: The above symlink path assumes snapclient.service from the .deb is installed to /lib/systemd/system/snapclient.service
# If dpkg-deb -x places it in /usr/lib/systemd/system, adjust the symlink source.
# A common location for package-installed units is /lib/systemd/system.
# If the .deb actually places it in /etc/systemd/system, then the symlink would be:
# sudo ln -sf ../snapclient.service rootfs/etc/systemd/system/multi-user.target.wants/snapclient.service
# Let's assume the .deb installs it to /usr/lib/systemd/system or /lib/systemd/system.
# The .deb extraction copies to rootfs/, so if the .deb has ./usr/lib/systemd/system/snapclient.service,
# it will be at rootfs/usr/lib/systemd/system/snapclient.service.
# The service file we created is at rootfs/etc/systemd/system/snapclient.service.
# Systemd prefers /etc/systemd/system over /lib/systemd/system.
# So, if we create it in /etc/systemd/system, that should be fine.
# The symlink should point to the file in /etc/systemd/system if we create it there.
info "Enabling dhcpcd systemd service on rootfs..."
# Ensure the multi-user.target.wants directory exists (already created for snapclient)
# Standard path for dhcpcd.service in base RPi OS images is /lib/systemd/system/dhcpcd.service or /usr/lib/systemd/system/dhcpcd.service
# The symlink needs to point from /etc/systemd/system/multi-user.target.wants/ to that location.
if [ -f "rootfs/lib/systemd/system/dhcpcd.service" ]; then
sudo ln -sf ../../../../lib/systemd/system/dhcpcd.service rootfs/etc/systemd/system/multi-user.target.wants/dhcpcd.service
info "dhcpcd.service enabled (symlink created from /lib)."
elif [ -f "rootfs/usr/lib/systemd/system/dhcpcd.service" ]; then
sudo ln -sf ../../../../usr/lib/systemd/system/dhcpcd.service rootfs/etc/systemd/system/multi-user.target.wants/dhcpcd.service
info "dhcpcd.service enabled (symlink created from /usr/lib)."
else
warn "dhcpcd.service file not found in rootfs/lib/systemd/system/ or rootfs/usr/lib/systemd/system/. Cannot enable dhcpcd."
fi
# Let's ensure the symlink points to the one we just created:
sudo rm -f rootfs/etc/systemd/system/multi-user.target.wants/snapclient.service # remove if exists
sudo ln -s ../snapclient.service rootfs/etc/systemd/system/multi-user.target.wants/snapclient.service
info "Snapclient service enabled (symlink created)."
if [ "${DEVICE_TYPE}" == "rpizero2w" ]; then
info "Applying HifiBerry DAC+ overlay for Raspberry Pi Zero 2 W on rootfs..."
sudo sh -c "echo 'dtoverlay=hifiberry-dacplus' >> rootfs/boot/config.txt"
fi
info "Configuring for read-only filesystem on rootfs..."
# 1. Modify /etc/fstab
sudo sed -i -E 's/(\s+\/\s+ext4\s+)(defaults,noatime)(\s+0\s+1)/\1ro,defaults,noatime\3/' rootfs/etc/fstab
sudo sed -i -E 's/(\s+\/boot\s+vfat\s+)(defaults)(\s+0\s+2)/\1ro,defaults,nofail\3/' rootfs/etc/fstab
# 2. Add fastboot and ro to /boot/cmdline.txt
BOOT_CMDLINE_FILE="rootfs/boot/cmdline.txt"
if [ -f "${BOOT_CMDLINE_FILE}" ]; then
if ! sudo grep -q "fastboot" "${BOOT_CMDLINE_FILE}"; then
sudo sed -i '1 s/$/ fastboot/' "${BOOT_CMDLINE_FILE}"
fi
if ! sudo grep -q " ro" "${BOOT_CMDLINE_FILE}"; then # space before ro is important
sudo sed -i '1 s/$/ ro/' "${BOOT_CMDLINE_FILE}" # Add ro if not present
fi
else
warn "${BOOT_CMDLINE_FILE} not found. Skipping cmdline modifications."
fi
info "Creating userconf.txt for predefined user setup..."
DEFAULT_USER="rpiuser"
DEFAULT_PASS="raspberry"
# Check if openssl is available (already done above, but good to be mindful here)
ENCRYPTED_PASS=$(echo "${DEFAULT_PASS}" | openssl passwd -6 -stdin)
if [ -z "${ENCRYPTED_PASS}" ]; then
error "Failed to encrypt password using openssl."
fi
sudo sh -c "echo '${DEFAULT_USER}:${ENCRYPTED_PASS}' > rootfs/boot/userconf.txt"
sudo chmod 600 rootfs/boot/userconf.txt # Set appropriate permissions
info "userconf.txt created with user '${DEFAULT_USER}'."
info "Attempting to disable unnecessary write-heavy services on rootfs..."
# This is a best-effort attempt by removing common symlinks.
# The exact paths might vary or services might not be installed.
DISABLED_SERVICES_COUNT=0
declare -a services_to_disable=(
"apt-daily.timer"
"apt-daily-upgrade.timer"
"man-db.timer"
"dphys-swapfile.service" # RPi OS specific swap service
"logrotate.timer"
"motd-news.timer"
)
declare -a common_wants_dirs=(
"multi-user.target.wants"
"timers.target.wants"
"sysinit.target.wants"
# Add other .wants directories if known
)
for service in "${services_to_disable[@]}"; do
for wants_dir in "${common_wants_dirs[@]}"; do
link_path="rootfs/etc/systemd/system/${wants_dir}/${service}"
if [ -L "${link_path}" ]; then
info "Disabling ${service} by removing symlink ${link_path}"
sudo rm -f "${link_path}"
DISABLED_SERVICES_COUNT=$((DISABLED_SERVICES_COUNT + 1))
fi
done
# Also check for service files directly in /lib/systemd/system and mask them if we want to be more aggressive
# For now, just removing symlinks from /etc/systemd/system is less intrusive.
done
info "Attempted to disable ${DISABLED_SERVICES_COUNT} services by removing symlinks."
warn "Service disabling is best-effort. Review target system services."
# No apt cache to clean as we didn't use apt
info "System configuration on rootfs complete."
# --- 5. Cleanup & Unmount ---
# Pseudo-filesystems and QEMU are no longer used, so their cleanup is removed.
info "Unmounting partitions..."
# Unmount boot first, then root
# BOOT_MOUNT_POINT="rootfs/boot"; if [ -d "rootfs/boot/firmware" ]; then BOOT_MOUNT_POINT="rootfs/boot/firmware"; fi
# sudo umount "${BOOT_MOUNT_POINT}"
sudo umount rootfs/boot
sudo umount rootfs
info "Detaching loop device ${LOOP_DEV}..."
sudo losetup -d "${LOOP_DEV}"
unset LOOP_DEV # Important for trap
rmdir rootfs
cd .. # Back to original directory
# --- 6. Shrink Image (Optional) ---
info "Image shrinking (optional step)..."
info "If you want to shrink the image, you can use a tool like PiShrink."
info "Example: sudo pishrink.sh ${WORK_DIR}/${OUTPUT_IMAGE_FILE}"
# Check if pishrink is available and executable
# if [ -x "./pishrink.sh" ]; then
# info "Running PiShrink..."
# sudo ./pishrink.sh "${WORK_DIR}/${OUTPUT_IMAGE_FILE}"
# else
# warn "PiShrink script (pishrink.sh) not found or not executable in current directory. Skipping shrink."
# fi
# --- 7. Final Output ---
FINAL_IMAGE_PATH="${WORK_DIR}/${OUTPUT_IMAGE_FILE}"
info "---------------------------------------------------------------------"
info "Raspberry Pi image created successfully!"
info "Output image: ${FINAL_IMAGE_PATH}"
info "Device Type: ${DEVICE_TYPE}"
info "Hostname: ${HOSTNAME_PI}"
info "Wi-Fi SSID: ${WIFI_SSID}"
info "Snapcast Server: ${SNAPCAST_SERVER}"
info ""
info "To write to an SD card (e.g., /dev/sdX - BE VERY CAREFUL):"
info " sudo dd bs=4M if=${FINAL_IMAGE_PATH} of=/dev/sdX status=progress conv=fsync"
info "---------------------------------------------------------------------"
exit 0

19
raspberry-new/shell.nix Normal file
View File

@@ -0,0 +1,19 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShellNoCC {
packages = with pkgs; [
gnumake
ncurses
pkg-config
flex
bison
openssl
bc
which
file
];
shellHook = ''
export KCONFIG_CONFIG=.config
'';
}