#!/bin/bash # OSTree Atomic Package Management - Implementation for apt-layer ostree_compose_install() { local packages=("$@") # Validate input if [[ ${#packages[@]} -eq 0 ]]; then log_error "No packages specified for installation" "apt-layer" log_info "Usage: apt-layer ostree compose install [...]" "apt-layer" return 1 fi log_info "[OSTree] Installing packages and creating atomic commit: ${packages[*]}" "apt-layer" # Check for root privileges if [[ $EUID -ne 0 ]]; then log_error "Root privileges required for OSTree compose install" "apt-layer" return 1 fi # Initialize workspace if needed if ! init_workspace; then log_error "Failed to initialize workspace" "apt-layer" return 1 fi # Start live overlay if not active if ! is_live_overlay_active; then log_info "[OSTree] Starting live overlay for package installation" "apt-layer" if ! start_live_overlay; then log_error "Failed to start live overlay" "apt-layer" return 1 fi fi # Determine if .deb files or package names local has_deb_files=false for pkg in "${packages[@]}"; do if [[ "$pkg" == *.deb ]] || [[ "$pkg" == */*.deb ]]; then has_deb_files=true break fi done # Install packages in live overlay log_info "[OSTree] Installing packages in live overlay" "apt-layer" if [[ "$has_deb_files" == "true" ]]; then log_info "[OSTree] Detected .deb files, using live_dpkg_install" "apt-layer" if ! live_dpkg_install "${packages[@]}"; then log_error "Failed to install .deb packages in overlay" "apt-layer" return 1 fi else log_info "[OSTree] Detected package names, using live_install" "apt-layer" if ! live_install "${packages[@]}"; then log_error "Failed to install packages in overlay" "apt-layer" return 1 fi fi # Create OSTree-style commit local commit_message="Install packages: ${packages[*]}" local commit_id="ostree-$(date +%Y%m%d-%H%M%S)-$$" log_info "[OSTree] Creating atomic commit: $commit_id" "apt-layer" # Create simple commit metadata (avoid complex JSON escaping) local packages_json="[" for i in "${!packages[@]}"; do if [[ $i -gt 0 ]]; then packages_json+="," fi packages_json+="\"${packages[$i]}\"" done packages_json+="]" local commit_data commit_data=$(cat << EOF { "commit_id": "$commit_id", "type": "ostree_compose", "action": "install", "packages": $packages_json, "parent_commit": "$(get_current_deployment)", "commit_message": "Install packages: $(IFS=' '; echo "${packages[*]}")", "created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "composefs_image": "${commit_id}.composefs" } EOF ) # Save commit metadata (for log/history) local commit_log_dir="/var/lib/particle-os/ostree-commits" mkdir -p "$commit_log_dir" echo "$commit_data" > "$commit_log_dir/$commit_id.json" # Commit live overlay changes as new layer log_info "[OSTree] Committing overlay changes as OSTree layer" "apt-layer" if ! commit_live_overlay "$commit_message"; then log_error "Failed to commit overlay changes" "apt-layer" return 1 fi # Get the created layer name (from commit_live_overlay) local layer_name="live-overlay-commit-$(date +%Y%m%d_%H%M%S)" # Create OSTree deployment commit log_info "[OSTree] Creating deployment commit with layer: $layer_name" "apt-layer" local deployment_commit_id deployment_commit_id=$(create_deployment_commit "ostree-base" "$layer_name") # Set as pending deployment (atomic) set_pending_deployment "$deployment_commit_id" log_success "[OSTree] Atomic commit created successfully: $deployment_commit_id" "apt-layer" log_info "[OSTree] Commit includes packages: ${packages[*]}" "apt-layer" log_info "[OSTree] Reboot to activate the new deployment" "apt-layer" return 0 } ostree_compose_remove() { local packages=("$@") # Validate input if [[ ${#packages[@]} -eq 0 ]]; then log_error "No packages specified for removal" "apt-layer" log_info "Usage: apt-layer ostree compose remove [...]" "apt-layer" return 1 fi log_info "[OSTree] Removing packages and creating atomic commit: ${packages[*]}" "apt-layer" # Check for root privileges if [[ $EUID -ne 0 ]]; then log_error "Root privileges required for OSTree compose remove" "apt-layer" return 1 fi # Initialize workspace if needed if ! init_workspace; then log_error "Failed to initialize workspace" "apt-layer" return 1 fi # Start live overlay if not active if ! is_live_overlay_active; then log_info "[OSTree] Starting live overlay for package removal" "apt-layer" if ! start_live_overlay; then log_error "Failed to start live overlay" "apt-layer" return 1 fi fi # Remove packages in live overlay log_info "[OSTree] Removing packages in live overlay" "apt-layer" if ! live_remove "${packages[@]}"; then log_error "Failed to remove packages in overlay" "apt-layer" return 1 fi # Create OSTree-style commit local commit_message="Remove packages: ${packages[*]}" local commit_id="ostree-$(date +%Y%m%d-%H%M%S)-$$" log_info "[OSTree] Creating atomic commit: $commit_id" "apt-layer" # Create simple commit metadata local packages_json="[" for i in "${!packages[@]}"; do if [[ $i -gt 0 ]]; then packages_json+="," fi packages_json+="\"${packages[$i]}\"" done packages_json+="]" local commit_data commit_data=$(cat << EOF { "commit_id": "$commit_id", "type": "ostree_compose", "action": "remove", "packages": $packages_json, "parent_commit": "$(get_current_deployment)", "commit_message": "Remove packages: $(IFS=' '; echo "${packages[*]}")", "created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "composefs_image": "${commit_id}.composefs" } EOF ) # Save commit metadata (for log/history) local commit_log_dir="/var/lib/particle-os/ostree-commits" mkdir -p "$commit_log_dir" echo "$commit_data" > "$commit_log_dir/$commit_id.json" # Commit live overlay changes as new layer log_info "[OSTree] Committing overlay changes as OSTree layer" "apt-layer" if ! commit_live_overlay "$commit_message"; then log_error "Failed to commit overlay changes" "apt-layer" return 1 fi # Get the created layer name (from commit_live_overlay) local layer_name="live-overlay-commit-$(date +%Y%m%d_%H%M%S)" # Create OSTree deployment commit log_info "[OSTree] Creating deployment commit with layer: $layer_name" "apt-layer" local deployment_commit_id deployment_commit_id=$(create_deployment_commit "ostree-base" "$layer_name") # Set as pending deployment (atomic) set_pending_deployment "$deployment_commit_id" log_success "[OSTree] Atomic commit created successfully: $deployment_commit_id" "apt-layer" log_info "[OSTree] Commit includes removed packages: ${packages[*]}" "apt-layer" log_info "[OSTree] Reboot to activate the new deployment" "apt-layer" return 0 } ostree_compose_update() { local packages=("$@") # Validate input if [[ ${#packages[@]} -eq 0 ]]; then log_error "No packages specified for update" "apt-layer" log_info "Usage: apt-layer ostree compose update [package1] [...]" "apt-layer" log_info "Note: If no packages specified, updates all packages" "apt-layer" return 1 fi log_info "[OSTree] Updating packages and creating atomic commit: ${packages[*]}" "apt-layer" # Check for root privileges if [[ $EUID -ne 0 ]]; then log_error "Root privileges required for OSTree compose update" "apt-layer" return 1 fi # Initialize workspace if needed if ! init_workspace; then log_error "Failed to initialize workspace" "apt-layer" return 1 fi # Start live overlay if not active if ! is_live_overlay_active; then log_info "[OSTree] Starting live overlay for package update" "apt-layer" if ! start_live_overlay; then log_error "Failed to start live overlay" "apt-layer" return 1 fi fi # Update packages in live overlay log_info "[OSTree] Updating packages in live overlay" "apt-layer" if ! live_update "${packages[@]}"; then log_error "Failed to update packages in overlay" "apt-layer" return 1 fi # Create OSTree-style commit local commit_message="Update packages: ${packages[*]}" local commit_id="ostree-$(date +%Y%m%d-%H%M%S)-$$" log_info "[OSTree] Creating atomic commit: $commit_id" "apt-layer" # Create simple commit metadata local packages_json="[" for i in "${!packages[@]}"; do if [[ $i -gt 0 ]]; then packages_json+="," fi packages_json+="\"${packages[$i]}\"" done packages_json+="]" local commit_data commit_data=$(cat << EOF { "commit_id": "$commit_id", "type": "ostree_compose", "action": "update", "packages": $packages_json, "parent_commit": "$(get_current_deployment)", "commit_message": "Update packages: $(IFS=' '; echo "${packages[*]}")", "created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "composefs_image": "${commit_id}.composefs" } EOF ) # Save commit metadata (for log/history) local commit_log_dir="/var/lib/particle-os/ostree-commits" mkdir -p "$commit_log_dir" echo "$commit_data" > "$commit_log_dir/$commit_id.json" # Commit live overlay changes as new layer log_info "[OSTree] Committing overlay changes as OSTree layer" "apt-layer" if ! commit_live_overlay "$commit_message"; then log_error "Failed to commit overlay changes" "apt-layer" return 1 fi # Get the created layer name (from commit_live_overlay) local layer_name="live-overlay-commit-$(date +%Y%m%d_%H%M%S)" # Create OSTree deployment commit log_info "[OSTree] Creating deployment commit with layer: $layer_name" "apt-layer" local deployment_commit_id deployment_commit_id=$(create_deployment_commit "ostree-base" "$layer_name") # Set as pending deployment (atomic) set_pending_deployment "$deployment_commit_id" log_success "[OSTree] Atomic commit created successfully: $deployment_commit_id" "apt-layer" log_info "[OSTree] Commit includes updated packages: ${packages[*]}" "apt-layer" log_info "[OSTree] Reboot to activate the new deployment" "apt-layer" return 0 } ostree_log() { local format="${1:-full}" local limit="${2:-10}" log_info "[OSTree] Showing commit log (format: $format, limit: $limit)" "apt-layer" if [[ ! -f "$DEPLOYMENT_DB" ]]; then log_error "No deployment database found" "apt-layer" return 1 fi case "$format" in "full"|"detailed") echo "=== OSTree Commit Log ===" jq -r --arg limit "$limit" ' .deployments | to_entries | sort_by(.value.created) | reverse | .[0:($limit | tonumber)] | .[] | "Commit: " + .key + "\n" + "Message: " + (.value.commit_message // "unknown") + "\n" + "Type: " + (.value.type // "unknown") + "\n" + "Action: " + (.value.action // "unknown") + "\n" + "Created: " + (.value.created // "unknown") + "\n" + "Base: " + (.value.base_image // "unknown") + "\n" + "Layers: " + (.value.layers | join(", ") // "none") + "\n" + "Packages: " + (.value.packages | join(", ") // "none") + "\n" + "---" ' "$DEPLOYMENT_DB" 2>/dev/null || echo "No commits found" ;; "short"|"compact") echo "=== OSTree Commit Log (Compact) ===" jq -r --arg limit "$limit" ' .deployments | to_entries | sort_by(.value.created) | reverse | .[0:($limit | tonumber)] | .[] | "\(.key) - \(.value.commit_message // "unknown") (\(.value.created // "unknown"))" ' "$DEPLOYMENT_DB" 2>/dev/null || echo "No commits found" ;; "json") echo "=== OSTree Commit Log (JSON) ===" jq -r --arg limit "$limit" ' .deployments | to_entries | sort_by(.value.created) | reverse | .[0:($limit | tonumber)] | map({commit_id: .key, details: .value}) ' "$DEPLOYMENT_DB" 2>/dev/null || echo "[]" ;; *) log_error "Invalid format: $format. Use: full, short, or json" "apt-layer" return 1 ;; esac } ostree_diff() { local commit1="${1:-}" local commit2="${2:-}" log_info "[OSTree] Showing diff between commits" "apt-layer" if [[ ! -f "$DEPLOYMENT_DB" ]]; then log_error "No deployment database found" "apt-layer" return 1 fi # If no commits specified, show diff between current and previous if [[ -z "$commit1" ]]; then local current_deployment current_deployment=$(get_current_deployment) if [[ -z "$current_deployment" ]]; then log_error "No current deployment found" "apt-layer" return 1 fi # Get the commit before current commit1=$(jq -r --arg current "$current_deployment" ' .deployments | to_entries | sort_by(.value.created) | map(.key) | index($current) as $idx | if $idx > 0 then .[$idx - 1] else null end ' "$DEPLOYMENT_DB" 2>/dev/null) if [[ -z "$commit1" || "$commit1" == "null" ]]; then log_error "No previous commit found" "apt-layer" return 1 fi commit2="$current_deployment" log_info "[OSTree] Comparing $commit1 -> $commit2" "apt-layer" elif [[ -z "$commit2" ]]; then # If only one commit specified, compare with current local current_deployment current_deployment=$(get_current_deployment) if [[ -z "$current_deployment" ]]; then log_error "No current deployment found" "apt-layer" return 1 fi commit2="$current_deployment" fi # Validate commits exist if ! jq -e ".deployments[\"$commit1\"]" "$DEPLOYMENT_DB" >/dev/null 2>&1; then log_error "Commit not found: $commit1" "apt-layer" return 1 fi if ! jq -e ".deployments[\"$commit2\"]" "$DEPLOYMENT_DB" >/dev/null 2>&1; then log_error "Commit not found: $commit2" "apt-layer" return 1 fi # Get commit data local commit1_data commit1_data=$(jq -r ".deployments[\"$commit1\"]" "$DEPLOYMENT_DB") local commit2_data commit2_data=$(jq -r ".deployments[\"$commit2\"]" "$DEPLOYMENT_DB") echo "=== OSTree Diff: $commit1 -> $commit2 ===" echo "" # Compare commit messages local msg1 msg1=$(echo "$commit1_data" | jq -r '.commit_message // "unknown"') local msg2 msg2=$(echo "$commit2_data" | jq -r '.commit_message // "unknown"') echo "Commit Messages:" echo " $commit1: $msg1" echo " $commit2: $msg2" echo "" # Compare creation times local time1 time1=$(echo "$commit1_data" | jq -r '.created // "unknown"') local time2 time2=$(echo "$commit2_data" | jq -r '.created // "unknown"') echo "Creation Times:" echo " $commit1: $time1" echo " $commit2: $time2" echo "" # Compare layers local layers1 layers1=$(echo "$commit1_data" | jq -r '.layers | join(", ") // "none"') local layers2 layers2=$(echo "$commit2_data" | jq -r '.layers | join(", ") // "none"') echo "Layers:" echo " $commit1: $layers1" echo " $commit2: $layers2" echo "" # Compare packages (if available) local packages1 packages1=$(echo "$commit1_data" | jq -r '.packages | join(", ") // "none"' 2>/dev/null || echo "none") local packages2 packages2=$(echo "$commit2_data" | jq -r '.packages | join(", ") // "none"' 2>/dev/null || echo "none") echo "Packages:" echo " $commit1: $packages1" echo " $commit2: $packages2" echo "" # Show action type local action1 action1=$(echo "$commit1_data" | jq -r '.action // "unknown"') local action2 action2=$(echo "$commit2_data" | jq -r '.action // "unknown"') echo "Actions:" echo " $commit1: $action1" echo " $commit2: $action2" echo "" # Calculate time difference if [[ "$time1" != "unknown" && "$time2" != "unknown" ]]; then local time_diff time_diff=$(($(date -d "$time2" +%s) - $(date -d "$time1" +%s))) echo "Time Difference: $time_diff seconds" echo "" fi return 0 } ostree_rollback() { local target_commit="${1:-}" log_info "[OSTree] Rolling back deployment" "apt-layer" # Check for root privileges if [[ $EUID -ne 0 ]]; then log_error "Root privileges required for OSTree rollback" "apt-layer" return 1 fi # Get current deployment local current_deployment current_deployment=$(get_current_deployment) if [[ -z "$current_deployment" ]]; then log_error "No current deployment found" "apt-layer" return 1 fi # If no target specified, rollback to previous commit if [[ -z "$target_commit" ]]; then log_info "[OSTree] No target specified, rolling back to previous commit" "apt-layer" # Get the commit before current target_commit=$(jq -r --arg current "$current_deployment" ' .deployments | to_entries | sort_by(.value.created) | map(.key) | index($current) as $idx | if $idx > 0 then .[$idx - 1] else null end ' "$DEPLOYMENT_DB" 2>/dev/null) if [[ -z "$target_commit" || "$target_commit" == "null" ]]; then log_error "No previous commit found to rollback to" "apt-layer" return 1 fi log_info "[OSTree] Rolling back to: $target_commit" "apt-layer" fi # Validate target commit exists if ! jq -e ".deployments[\"$target_commit\"]" "$DEPLOYMENT_DB" >/dev/null 2>&1; then log_error "Target commit not found: $target_commit" "apt-layer" return 1 fi # Create rollback commit local rollback_id="rollback-$(date +%Y%m%d-%H%M%S)-$$" local rollback_message="Rollback from $current_deployment to $target_commit" log_info "[OSTree] Creating rollback commit: $rollback_id" "apt-layer" # Get target commit data local target_data target_data=$(jq -r ".deployments[\"$target_commit\"]" "$DEPLOYMENT_DB") local base_image base_image=$(echo "$target_data" | jq -r '.base_image') local layers layers=$(echo "$target_data" | jq -r '.layers | join(" ")') # Create rollback deployment commit local rollback_commit_id rollback_commit_id=$(create_deployment_commit "$base_image" $layers) # Set as pending deployment set_pending_deployment "$rollback_commit_id" log_success "[OSTree] Rollback prepared successfully" "apt-layer" log_info "[OSTree] Rollback from: $current_deployment" "apt-layer" log_info "[OSTree] Rollback to: $target_commit" "apt-layer" log_info "[OSTree] New deployment: $rollback_commit_id" "apt-layer" log_info "[OSTree] Reboot to activate rollback" "apt-layer" return 0 } ostree_status() { log_info "[OSTree] Showing current deployment status" "apt-layer" # Get current and pending deployments local current_deployment current_deployment=$(get_current_deployment) local pending_deployment pending_deployment=$(get_pending_deployment 2>/dev/null | tail -n1) echo "=== OSTree Deployment Status ===" echo "Current Deployment: ${current_deployment:-none}" echo "Pending Deployment: ${pending_deployment:-none}" echo "" # Show recent commits (last 5) echo "=== Recent Commits ===" if [[ -f "$DEPLOYMENT_DB" ]]; then jq -r '.deployments | to_entries | sort_by(.value.created) | reverse | .[0:5] | .[] | "\(.key) - \(.value.commit_message) (\(.value.created))"' "$DEPLOYMENT_DB" 2>/dev/null || echo "No commits found" else echo "No deployment database found" fi echo "" # Show layer information for current deployment if [[ -n "$current_deployment" ]]; then echo "=== Current Deployment Details ===" local commit_data commit_data=$(jq -r ".deployments[\"$current_deployment\"]" "$DEPLOYMENT_DB" 2>/dev/null) if [[ -n "$commit_data" ]]; then echo "Base Image: $(echo "$commit_data" | jq -r '.base_image // "unknown"')" echo "Layers: $(echo "$commit_data" | jq -r '.layers | join(", ") // "none"')" echo "Created: $(echo "$commit_data" | jq -r '.created // "unknown"')" fi fi echo "" # Show available layers echo "=== Available Layers ===" if [[ -d "/var/lib/particle-os/build" ]]; then find /var/lib/particle-os/build -name "*.squashfs" -type f | head -10 | while read -r layer; do local size size=$(du -h "$layer" | cut -f1) local name name=$(basename "$layer") echo "$name ($size)" done else echo "No layers found" fi } ostree_cleanup() { local keep_count="${1:-5}" local dry_run="${2:-false}" log_info "[OSTree] Cleaning up old commits (keeping $keep_count)" "apt-layer" # Check for root privileges if [[ $EUID -ne 0 ]]; then log_error "Root privileges required for OSTree cleanup" "apt-layer" return 1 fi if [[ ! -f "$DEPLOYMENT_DB" ]]; then log_error "No deployment database found" "apt-layer" return 1 fi # Get current and pending deployments (never delete these) local current_deployment current_deployment=$(get_current_deployment) local pending_deployment pending_deployment=$(get_pending_deployment) # Get commits to keep (most recent + current + pending) local keep_commits keep_commits=$(jq -r --arg keep "$keep_count" --arg current "$current_deployment" --arg pending "$pending_deployment" ' .deployments | to_entries | sort_by(.value.created) | reverse | .[0:($keep | tonumber)] | map(.key) + if $current != "" then [$current] else [] end + if $pending != "" and $pending != $current then [$pending] else [] end | unique | join(" ") ' "$DEPLOYMENT_DB" 2>/dev/null) # Get all commits local all_commits all_commits=$(jq -r '.deployments | keys | join(" ")' "$DEPLOYMENT_DB" 2>/dev/null) # Find commits to delete local to_delete=() for commit in $all_commits; do if [[ ! " $keep_commits " =~ " $commit " ]]; then to_delete+=("$commit") fi done if [[ ${#to_delete[@]} -eq 0 ]]; then log_info "[OSTree] No commits to clean up" "apt-layer" return 0 fi echo "=== OSTree Cleanup Summary ===" echo "Keeping commits: $keep_commits" echo "Commits to delete: ${to_delete[*]}" echo "Total to delete: ${#to_delete[@]}" echo "" if [[ "$dry_run" == "true" ]]; then log_info "[OSTree] Dry run - no changes made" "apt-layer" return 0 fi # Confirm deletion echo "Are you sure you want to delete these commits? (y/N)" read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then log_info "[OSTree] Cleanup cancelled" "apt-layer" return 0 fi # Delete commits local deleted_count=0 for commit in "${to_delete[@]}"; do log_info "[OSTree] Deleting commit: $commit" "apt-layer" # Remove from database jq --arg commit "$commit" 'del(.deployments[$commit])' "$DEPLOYMENT_DB" > "${DEPLOYMENT_DB}.tmp" && mv "${DEPLOYMENT_DB}.tmp" "$DEPLOYMENT_DB" # Remove history file rm -f "$DEPLOYMENT_HISTORY_DIR/$commit.json" # Remove associated layers (if not used by other commits) local commit_data commit_data=$(jq -r ".deployments[\"$commit\"]" "$DEPLOYMENT_DB" 2>/dev/null) if [[ -n "$commit_data" ]]; then local layers layers=$(echo "$commit_data" | jq -r '.layers[]?' 2>/dev/null) for layer in $layers; do # Check if layer is used by other commits local layer_used layer_used=$(jq -r --arg layer "$layer" ' .deployments | to_entries | map(select(.value.layers | contains([$layer]))) | length ' "$DEPLOYMENT_DB" 2>/dev/null) if [[ "$layer_used" == "0" ]]; then log_info "[OSTree] Removing unused layer: $layer" "apt-layer" rm -f "/var/lib/particle-os/build/$layer.squashfs" fi done fi ((deleted_count++)) done log_success "[OSTree] Cleanup completed: $deleted_count commits deleted" "apt-layer" return 0 }