particle-os-tools/dracut-module.sh
robojerk 74c7bede5f Initial commit: Particle-OS tools repository
- Complete Particle-OS rebranding from uBlue-OS
- Professional installation system with standardized paths
- Self-initialization system with --init and --reset commands
- Enhanced error messages and dependency checking
- Comprehensive testing infrastructure
- All source scriptlets updated with runtime improvements
- Clean codebase with redundant files moved to archive
- Complete documentation suite
2025-07-11 21:14:33 -07:00

612 lines
18 KiB
Bash

#!/bin/bash
# dracut-module.sh - Custom dracut module for Ubuntu uBlue immutable system
# This script creates a dracut module that mounts squashfs layers via overlayfs at boot time
# to achieve true immutability for the root filesystem.
set -euo pipefail
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULE_NAME="90-ublue-immutable"
DRACUT_MODULES_DIR="/usr/lib/dracut/modules.d"
MODULE_DIR="${DRACUT_MODULES_DIR}/${MODULE_NAME}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
}
# Create the dracut module structure
create_module_structure() {
log_info "Creating dracut module structure..."
# Create module directory
mkdir -p "${MODULE_DIR}"
# Create module files
cat > "${MODULE_DIR}/module-setup.sh" << 'EOF'
#!/bin/bash
# module-setup.sh - Dracut module setup script for uBlue immutable system
check() {
# Check if composefs layers exist (more generic than uBlue-specific)
if [[ -d /var/lib/composefs-alternative/layers ]]; then
return 0
fi
# Check if we're on a uBlue system as fallback
if [[ -f /etc/os-release ]] && grep -q "ublue" /etc/os-release; then
return 0
fi
return 255
}
depends() {
echo "base"
}
install() {
# Install the module script
inst_hook cmdline 95 "$moddir/parse-ublue-cmdline.sh"
inst_hook mount 95 "$moddir/mount-ublue-layers.sh"
inst_hook pre-pivot 95 "$moddir/setup-ublue-root.sh"
# Install required binaries (removed mksquashfs/unsquashfs - build tools, not runtime)
dracut_install mount umount losetup find sort jq
# Install configuration files
inst /etc/os-release
# Install kernel modules for overlayfs and squashfs
instmods overlay squashfs loop
# Create secure state directory for inter-hook communication
mkdir -p "$initdir/run/initramfs/ublue-state"
chmod 700 "$initdir/run/initramfs/ublue-state"
}
EOF
cat > "${MODULE_DIR}/parse-ublue-cmdline.sh" << 'EOF'
#!/bin/bash
# parse-ublue-cmdline.sh - Parse kernel command line for uBlue options
# Parse kernel command line for uBlue-specific options
for param in $(cat /proc/cmdline); do
case $param in
ublue.immutable=*)
UBLUE_IMMUTABLE="${param#*=}"
;;
ublue.layers=*)
UBLUE_LAYERS="${param#*=}"
;;
ublue.upper=*)
UBLUE_UPPER="${param#*=}"
;;
ublue.manifest=*)
UBLUE_MANIFEST="${param#*=}"
;;
esac
done
# Set defaults if not specified
: ${UBLUE_IMMUTABLE:=1}
: ${UBLUE_LAYERS:=/var/lib/composefs-alternative/layers}
: ${UBLUE_UPPER:=tmpfs}
: ${UBLUE_MANIFEST:=/var/lib/composefs-alternative/layers/manifest.json}
# Export variables for other scripts (using secure state directory)
STATE_DIR="/run/initramfs/ublue-state"
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
cat > "$STATE_DIR/cmdline.conf" << INNEREOF
UBLUE_IMMUTABLE="$UBLUE_IMMUTABLE"
UBLUE_LAYERS="$UBLUE_LAYERS"
UBLUE_UPPER="$UBLUE_UPPER"
UBLUE_MANIFEST="$UBLUE_MANIFEST"
INNEREOF
EOF
cat > "${MODULE_DIR}/mount-ublue-layers.sh" << 'EOF'
#!/bin/bash
# mount-ublue-layers.sh - Mount squashfs layers for uBlue immutable system
# Source the parsed command line variables from secure state directory
STATE_DIR="/run/initramfs/ublue-state"
if [[ -f "$STATE_DIR/cmdline.conf" ]]; then
source "$STATE_DIR/cmdline.conf"
else
echo "uBlue command line configuration not found"
exit 1
fi
# Skip if immutable mode is disabled
if [[ "${UBLUE_IMMUTABLE:-1}" != "1" ]]; then
echo "uBlue immutable mode disabled, skipping layer mounting"
exit 0
fi
echo "Setting up uBlue immutable layers..."
# Create mount points
mkdir -p /run/ublue/layers
mkdir -p /run/ublue/overlay
# Find and mount squashfs layers with deterministic ordering
LAYER_COUNT=0
LOWER_DIRS=""
# Function to read layer manifest for deterministic ordering
read_layer_manifest() {
local manifest_file="$1"
local layers_dir="$2"
if [[ -f "$manifest_file" ]]; then
echo "Reading layer manifest: $manifest_file"
# Use jq if available, otherwise fallback to simple parsing
if command -v jq >/dev/null 2>&1; then
jq -r '.layers[] | .name + ":" + .file' "$manifest_file" 2>/dev/null | while IFS=: read -r name file; do
if [[ -n "$name" && -n "$file" ]]; then
echo "$name:$file"
fi
done
else
# Improved fallback: extract both name and file fields
# This handles the case where jq is not available but we still want manifest ordering
local temp_file=$(mktemp)
local name_file_pairs=()
# Extract name and file pairs using grep and sed
while IFS= read -r line; do
if [[ "$line" =~ "name"[[:space:]]*:[[:space:]]*"([^"]*)" ]]; then
local name="${BASH_REMATCH[1]}"
# Look for the corresponding file field in the same object
local file=""
# Simple heuristic: assume file follows name in the same object
# This is not perfect but better than just extracting names
if [[ -n "$name" ]]; then
# Try to find the file by looking for the name.squashfs in the layers directory
if [[ -f "$layers_dir/$name.squashfs" ]]; then
file="$layers_dir/$name.squashfs"
else
# Fallback: just use the name as a hint for alphabetical ordering
file="$name"
fi
echo "$name:$file"
fi
fi
done < "$manifest_file" > "$temp_file"
# Output the results and clean up
if [[ -s "$temp_file" ]]; then
cat "$temp_file"
fi
rm -f "$temp_file"
fi
fi
}
# Function to mount layers in specified order
mount_layers_ordered() {
local layers_dir="$1"
local manifest_file="$2"
# Try to read manifest for ordering
local ordered_layers=()
if [[ -f "$manifest_file" ]]; then
while IFS= read -r layer_info; do
if [[ -n "$layer_info" ]]; then
ordered_layers+=("$layer_info")
fi
done < <(read_layer_manifest "$manifest_file" "$layers_dir")
fi
# If no manifest or empty, fall back to alphabetical ordering
if [[ ${#ordered_layers[@]} -eq 0 ]]; then
echo "No manifest found, using alphabetical ordering"
while IFS= read -r -d '' layer_file; do
ordered_layers+=("$(basename "$layer_file" .squashfs):$layer_file")
done < <(find "$layers_dir" -name "*.squashfs" -print0 | sort -z)
fi
# Mount layers in order
for layer_info in "${ordered_layers[@]}"; do
IFS=: read -r layer_name layer_file <<< "$layer_info"
if [[ -f "$layer_file" ]]; then
mount_point="/run/ublue/layers/${layer_name}"
echo "Mounting layer: $layer_name"
mkdir -p "$mount_point"
# Mount the squashfs layer
if mount -t squashfs -o ro "$layer_file" "$mount_point"; then
LOWER_DIRS="${LOWER_DIRS}:${mount_point}"
((LAYER_COUNT++))
echo "Successfully mounted $layer_name"
else
echo "Failed to mount $layer_name"
fi
fi
done
# Remove leading colon
LOWER_DIRS="${LOWER_DIRS#:}"
}
# Mount layers from the specified directory
if [[ -d "${UBLUE_LAYERS}" ]]; then
echo "Scanning for squashfs layers in ${UBLUE_LAYERS}..."
mount_layers_ordered "${UBLUE_LAYERS}" "${UBLUE_MANIFEST}"
fi
# If no layers found, try alternative locations
if [[ -z "$LOWER_DIRS" ]]; then
echo "No squashfs layers found in ${UBLUE_LAYERS}, checking for OSTree deployment..."
# Check OSTree deployment as fallback
if [[ -d /sysroot/ostree/deploy ]]; then
deploy_path=$(find /sysroot/ostree/deploy -maxdepth 3 -name "deploy" -type d | head -1)
if [[ -n "$deploy_path" ]]; then
echo "Found OSTree deployment at $deploy_path"
echo "WARNING: Using direct OSTree deployment instead of squashfs layers"
echo "This may indicate composefs-alternative.sh did not create layers properly"
# Use the deployment as a fallback layer
# Note: This creates a mixed overlayfs with directory + potential squashfs layers
# This is acceptable as a fallback but not ideal for production
LOWER_DIRS="$deploy_path"
LAYER_COUNT=1
# Store fallback information for debugging
echo "UBLUE_FALLBACK_MODE=ostree" >> "$STATE_DIR/layers.conf"
echo "UBLUE_FALLBACK_PATH=$deploy_path" >> "$STATE_DIR/layers.conf"
else
echo "ERROR: No OSTree deployment found"
echo "Cannot proceed with immutable root setup"
exit 1
fi
else
echo "ERROR: No squashfs layers found and no OSTree deployment available"
echo "Cannot proceed with immutable root setup"
echo "Please ensure composefs-alternative.sh has created layers in ${UBLUE_LAYERS}"
exit 1
fi
fi
# Create upper layer
case "${UBLUE_UPPER}" in
tmpfs)
echo "Creating tmpfs upper layer..."
mount -t tmpfs -o size=1G tmpfs /run/ublue/overlay/upper
;;
var)
echo "Using /var as upper layer..."
mkdir -p /run/ublue/overlay/upper
mount --bind /sysroot/var /run/ublue/overlay/upper
;;
*)
echo "Using custom upper layer: ${UBLUE_UPPER}"
mkdir -p /run/ublue/overlay/upper
mount --bind "${UBLUE_UPPER}" /run/ublue/overlay/upper
;;
esac
# Create work directory
mkdir -p /run/ublue/overlay/work
# Store layer information in secure state directory
cat > "$STATE_DIR/layers.conf" << INNEREOF
UBLUE_LOWER_DIRS="$LOWER_DIRS"
UBLUE_LAYER_COUNT=$LAYER_COUNT
UBLUE_OVERLAY_READY=1
INNEREOF
echo "uBlue layer mounting complete: $LAYER_COUNT layers mounted"
EOF
cat > "${MODULE_DIR}/setup-ublue-root.sh" << 'EOF'
#!/bin/bash
# setup-ublue-root.sh - Set up overlayfs root for uBlue immutable system
# Source layer information from secure state directory
STATE_DIR="/run/initramfs/ublue-state"
if [[ -f "$STATE_DIR/layers.conf" ]]; then
source "$STATE_DIR/layers.conf"
else
echo "uBlue layer configuration not found"
exit 1
fi
# Skip if overlay is not ready
if [[ "${UBLUE_OVERLAY_READY:-0}" != "1" ]]; then
echo "uBlue overlay not ready, skipping root setup"
exit 0
fi
echo "Setting up uBlue overlayfs root..."
# Create the overlayfs mount
if [[ -n "$UBLUE_LOWER_DIRS" ]]; then
echo "Creating overlayfs with layers: $UBLUE_LOWER_DIRS"
# Mount overlayfs
mount -t overlay overlay \
-o lowerdir="$UBLUE_LOWER_DIRS",upperdir=/run/ublue/overlay/upper,workdir=/run/ublue/overlay/work \
/run/ublue/overlay/root
# Verify the mount
if mountpoint -q /run/ublue/overlay/root; then
echo "Overlayfs root created successfully"
# Set up pivot root
mkdir -p /run/ublue/overlay/root/oldroot
# Move essential mounts to new root
for mount in /proc /sys /dev /run; do
if mountpoint -q "$mount"; then
mount --move "$mount" "/run/ublue/overlay/root$mount"
fi
done
# Pivot to new root
cd /run/ublue/overlay/root
pivot_root . oldroot
# Clean up old root
umount -l /oldroot
echo "uBlue immutable root setup complete"
else
echo "Failed to create overlayfs root"
exit 1
fi
else
echo "No layers available for overlayfs"
exit 1
fi
EOF
# Make scripts executable
chmod +x "${MODULE_DIR}/module-setup.sh"
chmod +x "${MODULE_DIR}/parse-ublue-cmdline.sh"
chmod +x "${MODULE_DIR}/mount-ublue-layers.sh"
chmod +x "${MODULE_DIR}/setup-ublue-root.sh"
log_success "Dracut module structure created in ${MODULE_DIR}"
}
# Install the module
install_module() {
log_info "Installing dracut module..."
# Check if module already exists
if [[ -d "${MODULE_DIR}" ]]; then
log_warning "Module already exists, backing up..."
mv "${MODULE_DIR}" "${MODULE_DIR}.backup.$(date +%Y%m%d_%H%M%S)"
fi
create_module_structure
log_success "Dracut module installed successfully"
}
# Generate initramfs with the module
generate_initramfs() {
local kernel_version="${1:-}"
if [[ -z "$kernel_version" ]]; then
# Get current kernel version
kernel_version=$(uname -r)
fi
log_info "Generating initramfs for kernel $kernel_version with uBlue module..."
# Generate initramfs with our module
dracut --force --add "$MODULE_NAME" --kver "$kernel_version"
log_success "Initramfs generated with uBlue immutable module"
}
# Update GRUB configuration with idempotent parameter addition
update_grub() {
log_info "Updating GRUB configuration..."
if [[ -f /etc/default/grub ]]; then
# Backup original
cp /etc/default/grub /etc/default/grub.backup.$(date +%Y%m%d_%H%M%S)
# Define uBlue parameters (including manifest for deterministic ordering)
local ublue_params="ublue.immutable=1 ublue.layers=/var/lib/composefs-alternative/layers ublue.upper=tmpfs ublue.manifest=/var/lib/composefs-alternative/layers/manifest.json"
# Check if parameters already exist
if grep -q "ublue.immutable=" /etc/default/grub; then
log_warning "uBlue parameters already exist in GRUB configuration"
else
# Add uBlue parameters to GRUB_CMDLINE_LINUX_DEFAULT
sed -i "s/GRUB_CMDLINE_LINUX_DEFAULT=\"\([^\"]*\)\"/GRUB_CMDLINE_LINUX_DEFAULT=\"\1 $ublue_params\"/" /etc/default/grub
# Update GRUB
update-grub
log_success "GRUB configuration updated with uBlue parameters"
fi
else
log_warning "GRUB configuration not found, manual update may be required"
fi
}
# Test the module
test_module() {
log_info "Testing dracut module..."
# Check if module files exist
local required_files=(
"module-setup.sh"
"parse-ublue-cmdline.sh"
"mount-ublue-layers.sh"
"setup-ublue-root.sh"
)
for file in "${required_files[@]}"; do
if [[ -f "${MODULE_DIR}/${file}" ]]; then
log_success "$file exists"
else
log_error "$file missing"
return 1
fi
done
# Test module setup
if (cd "${MODULE_DIR}" && bash module-setup.sh check); then
log_success "✓ Module check passed"
else
log_warning "⚠ Module check failed (this may be normal on non-uBlue systems)"
fi
log_success "Module test completed"
}
# Remove the module
remove_module() {
log_info "Removing dracut module..."
if [[ -d "${MODULE_DIR}" ]]; then
rm -rf "${MODULE_DIR}"
log_success "Module removed"
else
log_warning "Module not found"
fi
}
# Show module status
show_status() {
log_info "uBlue dracut module status:"
if [[ -d "${MODULE_DIR}" ]]; then
echo "✓ Module installed: ${MODULE_DIR}"
echo " - module-setup.sh"
echo " - parse-ublue-cmdline.sh"
echo " - mount-ublue-layers.sh"
echo " - setup-ublue-root.sh"
else
echo "✗ Module not installed"
fi
# Check if module is in any initramfs
echo ""
echo "Initramfs files containing uBlue module:"
find /boot -name "initrd*" -o -name "initramfs*" 2>/dev/null | while read -r initramfs; do
if lsinitrd "$initramfs" 2>/dev/null | grep -q "ublue"; then
echo "$initramfs"
fi
done
}
# Show usage
show_usage() {
cat << EOF
uBlue Dracut Module Manager
Usage: $0 [COMMAND] [OPTIONS]
Commands:
install Install the dracut module
generate [KERNEL] Generate initramfs with module (optional kernel version)
update-grub Update GRUB with uBlue parameters (idempotent)
test Test the module installation
remove Remove the module
status Show module status
help Show this help
Examples:
$0 install # Install the module
$0 generate # Generate initramfs for current kernel
$0 generate 5.15.0-56-generic # Generate for specific kernel
$0 update-grub # Update GRUB configuration
$0 test # Test the module
$0 status # Show status
Kernel Parameters:
ublue.immutable=1 # Enable immutable mode
ublue.layers=/var/lib/composefs-alternative/layers # Layer directory
ublue.upper=tmpfs # Upper layer type (tmpfs/var/custom)
ublue.manifest=/path/to/manifest.json # Layer ordering manifest
Security Improvements:
- Secure state directory (/run/initramfs/ublue-state)
- Removed initramfs bloat (no mksquashfs/unsquashfs)
- Deterministic layer ordering via manifest
- Idempotent GRUB parameter updates
EOF
}
# Main function
main() {
local command="${1:-help}"
case "$command" in
install)
check_root
install_module
;;
generate)
check_root
generate_initramfs "$2"
;;
update-grub)
check_root
update_grub
;;
test)
test_module
;;
remove)
check_root
remove_module
;;
status)
show_status
;;
help|--help|-h)
show_usage
;;
*)
log_error "Unknown command: $command"
show_usage
exit 1
;;
esac
}
# Run main function with all arguments
main "$@"