particle-os-tools/src/apt-layer/scriptlets/15-ostree-atomic.sh
2025-07-14 01:09:07 -07:00

735 lines
No EOL
25 KiB
Bash

#!/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 <package1|.deb> [...]" "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 <package1> [...]" "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
}