- 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
612 lines
18 KiB
Bash
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 "$@"
|