apt-ostree/.notes/rpm-ostree/how-commands-work/03-rollback-command.md

15 KiB

Rollback Command Implementation Guide

Overview

The rollback command is low complexity (80 lines in rpm-ostree) and provides simple deployment rollback functionality with boot configuration updates.

Current Implementation Status

  • Basic rollback command exists in apt-ostree
  • Missing proper boot configuration updates
  • Missing transaction monitoring
  • Missing dry-run support

Implementation Requirements

Phase 1: Option Parsing

Files to Modify:

  • src/main.rs - Add rollback command options
  • src/system.rs - Enhance rollback method

Implementation Steps:

1.1 Update CLI Options (src/main.rs)

#[derive(Debug, Parser)]
pub struct RollbackOpts {
    /// Initiate a reboot after operation is complete
    #[arg(short = 'r', long)]
    reboot: bool,
    
    /// Exit after printing the transaction
    #[arg(short = 'n', long)]
    dry_run: bool,
    
    /// Operate on provided STATEROOT
    #[arg(long)]
    stateroot: Option<String>,
    
    /// Use system root SYSROOT (default: /)
    #[arg(long)]
    sysroot: Option<String>,
    
    /// Force a peer-to-peer connection instead of using the system message bus
    #[arg(long)]
    peer: bool,
    
    /// Avoid printing most informational messages
    #[arg(short = 'q', long)]
    quiet: bool,
}

1.2 Add Option Validation (src/main.rs)

impl RollbackOpts {
    pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
        // Check for valid stateroot if provided
        if let Some(ref stateroot) = self.stateroot {
            if !Path::new(stateroot).exists() {
                return Err(format!("Stateroot '{}' does not exist", stateroot).into());
            }
        }
        
        // Check for valid sysroot if provided
        if let Some(ref sysroot) = self.sysroot {
            if !Path::new(sysroot).exists() {
                return Err(format!("Sysroot '{}' does not exist", sysroot).into());
            }
        }
        
        Ok(())
    }
}

Phase 2: Daemon Communication

Files to Modify:

  • src/system.rs - Add rollback logic
  • src/daemon.rs - Add rollback D-Bus method

Implementation Steps:

2.1 Add Rollback Logic (src/system.rs)

impl AptOstreeSystem {
    pub async fn rollback_deployment(&self, opts: &RollbackOpts) -> Result<String, Box<dyn std::error::Error>> {
        // 1. Load OSTree sysroot
        let sysroot_path = opts.sysroot.as_deref().unwrap_or("/");
        let sysroot = ostree::Sysroot::new_at(libc::AT_FDCWD, sysroot_path);
        sysroot.load(None)?;
        
        // 2. Get current deployments
        let deployments = sysroot.get_deployments();
        if deployments.is_empty() {
            return Err("No deployments found".into());
        }
        
        // 3. Find booted deployment
        let booted_deployment = sysroot.get_booted_deployment();
        if booted_deployment.is_none() {
            return Err("No booted deployment found".into());
        }
        
        let booted = booted_deployment.unwrap();
        let booted_index = deployments.iter().position(|d| d == booted).unwrap();
        
        // 4. Find previous deployment to rollback to
        if booted_index == 0 {
            return Err("No previous deployment to rollback to".into());
        }
        
        let previous_deployment = &deployments[booted_index - 1];
        
        // 5. Handle dry-run
        if opts.dry_run {
            println!("Would rollback from {} to {}", 
                booted.get_csum(), 
                previous_deployment.get_csum());
            return Ok("dry-run-completed".to_string());
        }
        
        // 6. Perform rollback
        let transaction_id = self.perform_rollback(previous_deployment, opts).await?;
        
        Ok(transaction_id)
    }
    
    async fn perform_rollback(&self, target_deployment: &ostree::Deployment, opts: &RollbackOpts) -> Result<String, Box<dyn std::error::Error>> {
        // 1. Create transaction
        let transaction_id = self.create_transaction("rollback").await?;
        
        // 2. Update boot configuration
        self.update_boot_configuration(target_deployment).await?;
        
        // 3. Update deployment state
        self.update_deployment_state(target_deployment).await?;
        
        // 4. Handle reboot if requested
        if opts.reboot {
            self.schedule_reboot().await?;
        }
        
        // 5. Complete transaction
        self.complete_transaction(&transaction_id, true).await?;
        
        Ok(transaction_id)
    }
    
    async fn update_boot_configuration(&self, deployment: &ostree::Deployment) -> Result<(), Box<dyn std::error::Error>> {
        // 1. Get deployment checksum
        let checksum = deployment.get_csum();
        
        // 2. Update GRUB configuration
        self.update_grub_configuration(checksum).await?;
        
        // 3. Update systemd-boot configuration (if applicable)
        self.update_systemd_boot_configuration(checksum).await?;
        
        // 4. Update OSTree boot configuration
        self.update_ostree_boot_configuration(deployment).await?;
        
        Ok(())
    }
    
    async fn update_grub_configuration(&self, checksum: &str) -> Result<(), Box<dyn std::error::Error>> {
        // Update GRUB configuration to boot from the rollback deployment
        let grub_cfg = "/boot/grub/grub.cfg";
        if Path::new(grub_cfg).exists() {
            // Update GRUB configuration to point to rollback deployment
            // This would involve parsing and modifying the GRUB config
            println!("Updated GRUB configuration for rollback deployment");
        }
        
        Ok(())
    }
    
    async fn update_systemd_boot_configuration(&self, checksum: &str) -> Result<(), Box<dyn std::error::Error>> {
        // Update systemd-boot configuration (for UEFI systems)
        let loader_conf = "/boot/loader/loader.conf";
        if Path::new(loader_conf).exists() {
            // Update systemd-boot configuration
            println!("Updated systemd-boot configuration for rollback deployment");
        }
        
        Ok(())
    }
    
    async fn update_ostree_boot_configuration(&self, deployment: &ostree::Deployment) -> Result<(), Box<dyn std::error::Error>> {
        // Update OSTree's internal boot configuration
        let sysroot = ostree::Sysroot::new_default();
        sysroot.load(None)?;
        
        // Set the deployment as the new booted deployment
        sysroot.set_booted_deployment(deployment);
        
        Ok(())
    }
    
    async fn update_deployment_state(&self, deployment: &ostree::Deployment) -> Result<(), Box<dyn std::error::Error>> {
        // Update deployment state in OSTree
        let sysroot = ostree::Sysroot::new_default();
        sysroot.load(None)?;
        
        // Mark the deployment as pending
        sysroot.set_pending_deployment(deployment);
        
        Ok(())
    }
    
    async fn schedule_reboot(&self) -> Result<(), Box<dyn std::error::Error>> {
        // Schedule a reboot using systemctl
        let output = tokio::process::Command::new("systemctl")
            .arg("reboot")
            .output()
            .await?;
        
        if !output.status.success() {
            return Err("Failed to schedule reboot".into());
        }
        
        println!("Reboot scheduled");
        Ok(())
    }
}

2.2 Add Rollback D-Bus Method (src/daemon.rs)

#[dbus_interface(name = "org.aptostree.dev")]
impl AptOstreeDaemon {
    /// Rollback to previous deployment
    async fn rollback(&self, options: HashMap<String, Value>) -> Result<String, Box<dyn std::error::Error>> {
        let system = AptOstreeSystem::new().await?;
        
        // Convert options to RollbackOpts
        let opts = RollbackOpts {
            reboot: options.get("reboot").and_then(|v| v.as_bool()).unwrap_or(false),
            dry_run: options.get("dry-run").and_then(|v| v.as_bool()).unwrap_or(false),
            stateroot: options.get("stateroot").and_then(|v| v.as_str()).map(|s| s.to_string()),
            sysroot: options.get("sysroot").and_then(|v| v.as_str()).map(|s| s.to_string()),
            peer: options.get("peer").and_then(|v| v.as_bool()).unwrap_or(false),
            quiet: options.get("quiet").and_then(|v| v.as_bool()).unwrap_or(false),
        };
        
        let transaction_id = system.rollback_deployment(&opts).await?;
        Ok(transaction_id)
    }
}

Phase 3: Transaction Monitoring

Files to Modify:

  • src/client.rs - Add rollback transaction monitoring
  • src/system.rs - Add transaction management

Implementation Steps:

3.1 Add Rollback Transaction Monitoring (src/client.rs)

impl AptOstreeClient {
    pub async fn monitor_rollback_transaction(&self, transaction_id: &str, opts: &RollbackOpts) -> Result<(), Box<dyn std::error::Error>> {
        if opts.dry_run {
            // For dry-run, just return success
            return Ok(());
        }
        
        // Monitor transaction progress
        let mut progress = 0;
        loop {
            let status = self.get_transaction_status(transaction_id).await?;
            
            match status {
                TransactionStatus::Running(percent) => {
                    if percent != progress && !opts.quiet {
                        progress = percent;
                        println!("Rollback progress: {}%", progress);
                    }
                }
                TransactionStatus::Completed => {
                    if !opts.quiet {
                        println!("Rollback completed successfully");
                    }
                    break;
                }
                TransactionStatus::Failed(error) => {
                    return Err(error.into());
                }
            }
            
            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
        }
        
        Ok(())
    }
}

3.2 Add Transaction Management (src/system.rs)

impl AptOstreeSystem {
    pub async fn create_transaction(&self, operation: &str) -> Result<String, Box<dyn std::error::Error>> {
        // Create unique transaction ID
        let transaction_id = format!("{}-{}", operation, uuid::Uuid::new_v4());
        
        // Store transaction state
        self.store_transaction_state(&transaction_id, "running").await?;
        
        Ok(transaction_id)
    }
    
    pub async fn store_transaction_state(&self, transaction_id: &str, state: &str) -> Result<(), Box<dyn std::error::Error>> {
        // Store transaction state in a file or database
        let state_file = format!("/var/lib/apt-ostree/transactions/{}.state", transaction_id);
        tokio::fs::write(&state_file, state).await?;
        Ok(())
    }
    
    pub async fn complete_transaction(&self, transaction_id: &str, success: bool) -> Result<(), Box<dyn std::error::Error>> {
        let status = if success { "completed" } else { "failed" };
        self.store_transaction_state(transaction_id, status).await?;
        Ok(())
    }
}

Main Rollback Command Implementation

Files to Modify:

  • src/main.rs - Main rollback command logic

Implementation:

async fn rollback_command(opts: RollbackOpts) -> Result<(), Box<dyn std::error::Error>> {
    // 1. Validate options
    opts.validate()?;
    
    // 2. Check permissions
    if !opts.dry_run {
        check_root_permissions()?;
    }
    
    // 3. Perform rollback
    let system = AptOstreeSystem::new().await?;
    let transaction_id = system.rollback_deployment(&opts).await?;
    
    // 4. Monitor transaction (if not dry-run)
    if !opts.dry_run {
        let client = AptOstreeClient::new().await?;
        client.monitor_rollback_transaction(&transaction_id, &opts).await?;
    }
    
    Ok(())
}

fn check_root_permissions() -> Result<(), Box<dyn std::error::Error>> {
    if unsafe { libc::geteuid() } != 0 {
        return Err("Rollback requires root privileges".into());
    }
    Ok(())
}

Testing Strategy

Unit Tests

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]
    async fn test_rollback_options_validation() {
        let opts = RollbackOpts {
            reboot: false,
            dry_run: false,
            stateroot: Some("/nonexistent".to_string()),
            sysroot: None,
            peer: false,
            quiet: false,
        };
        
        assert!(opts.validate().is_err());
        
        let opts = RollbackOpts {
            reboot: false,
            dry_run: false,
            stateroot: None,
            sysroot: None,
            peer: false,
            quiet: false,
        };
        
        assert!(opts.validate().is_ok());
    }
    
    #[tokio::test]
    async fn test_rollback_deployment() {
        let system = AptOstreeSystem::new().await.unwrap();
        let opts = RollbackOpts {
            reboot: false,
            dry_run: true, // Use dry-run for testing
            stateroot: None,
            sysroot: None,
            peer: false,
            quiet: false,
        };
        
        let result = system.rollback_deployment(&opts).await;
        // Test based on system state
    }
    
    #[test]
    fn test_root_permissions_check() {
        // This test would need to be run as root or mocked
        // For now, just test the function exists
        let _ = check_root_permissions();
    }
}

Integration Tests

#[tokio::test]
async fn test_rollback_command_integration() {
    let opts = RollbackOpts {
        reboot: false,
        dry_run: true, // Use dry-run for testing
        stateroot: None,
        sysroot: None,
        peer: false,
        quiet: false,
    };
    
    let result = rollback_command(opts).await;
    assert!(result.is_ok());
}

Error Handling

Files to Modify:

  • src/error.rs - Add rollback-specific errors

Implementation:

#[derive(Debug, thiserror::Error)]
pub enum RollbackError {
    #[error("No deployments found")]
    NoDeployments,
    
    #[error("No booted deployment found")]
    NoBootedDeployment,
    
    #[error("No previous deployment to rollback to")]
    NoPreviousDeployment,
    
    #[error("Failed to update boot configuration: {0}")]
    BootConfigError(String),
    
    #[error("Failed to schedule reboot: {0}")]
    RebootError(String),
    
    #[error("Rollback requires root privileges")]
    PermissionError,
    
    #[error("Invalid stateroot: {0}")]
    InvalidStateroot(String),
    
    #[error("Invalid sysroot: {0}")]
    InvalidSysroot(String),
}

impl From<RollbackError> for Box<dyn std::error::Error> {
    fn from(err: RollbackError) -> Self {
        Box::new(err)
    }
}

Dependencies to Add

Add to Cargo.toml:

[dependencies]
uuid = { version = "1.0", features = ["v4"] }
tokio = { version = "1.0", features = ["process", "time", "fs"] }
libc = "0.2"

Implementation Checklist

  • Add CLI options for rollback command
  • Implement option validation logic
  • Add rollback deployment logic
  • Implement boot configuration updates (GRUB, systemd-boot, OSTree)
  • Add transaction monitoring
  • Handle dry-run mode
  • Add reboot scheduling
  • Add comprehensive error handling
  • Write unit and integration tests
  • Update documentation

References

  • rpm-ostree source: src/app/rpmostree-builtin-rollback.cxx (80 lines)
  • OSTree deployment management
  • GRUB configuration management
  • systemd-boot configuration
  • systemd reboot management