apt-ostree/.notes/rpm-ostree/how-commands-work/02-upgrade-command.md

17 KiB

Upgrade Command Implementation Guide

Overview

The upgrade command is high complexity (247 lines in rpm-ostree) and handles system upgrades with automatic update integration, driver registration checking, and multiple upgrade paths.

Current Implementation Status

  • Basic upgrade command exists in apt-ostree
  • Missing automatic update policy integration
  • Missing driver registration checking
  • Missing preview/check modes
  • Missing multiple upgrade APIs

Implementation Requirements

Phase 1: Option Parsing and Validation

Files to Modify:

  • src/main.rs - Add upgrade command options
  • src/system.rs - Enhance upgrade method
  • src/daemon.rs - Add upgrade D-Bus methods

Implementation Steps:

1.1 Update CLI Options (src/main.rs)

#[derive(Debug, Parser)]
pub struct UpgradeOpts {
    /// Initiate a reboot after operation is complete
    #[arg(short = 'r', long)]
    reboot: bool,
    
    /// Permit deployment of chronologically older trees
    #[arg(long)]
    allow_downgrade: bool,
    
    /// Just preview package differences (implies --unchanged-exit-77)
    #[arg(long)]
    preview: bool,
    
    /// Just check if an upgrade is available (implies --unchanged-exit-77)
    #[arg(long)]
    check: bool,
    
    /// Do not download latest ostree and APT data
    #[arg(short = 'C', long)]
    cache_only: bool,
    
    /// Just download latest ostree and APT data, don't deploy
    #[arg(long)]
    download_only: bool,
    
    /// If no new deployment made, exit 77
    #[arg(long)]
    unchanged_exit_77: bool,
    
    /// Force an upgrade even if an updates driver is registered
    #[arg(long)]
    bypass_driver: bool,
    
    /// Prevent automatic deployment finalization on shutdown
    #[arg(long)]
    lock_finalization: bool,
    
    /// For automated use only; triggered by automatic timer
    #[arg(long)]
    trigger_automatic_update_policy: bool,
}

1.2 Add Option Validation (src/main.rs)

impl UpgradeOpts {
    pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
        // Check incompatible options
        if self.reboot && self.preview {
            return Err("Cannot specify both --reboot and --preview".into());
        }
        
        if self.reboot && self.check {
            return Err("Cannot specify both --reboot and --check".into());
        }
        
        if self.preview && (self.install_packages.is_some() || self.uninstall_packages.is_some()) {
            return Err("Cannot specify both --preview and --install/--uninstall".into());
        }
        
        // Set implied options
        if self.preview {
            // preview implies unchanged_exit_77
        }
        
        if self.check {
            // check implies unchanged_exit_77
        }
        
        Ok(())
    }
}

Phase 2: Automatic Update Policy Check

Files to Modify:

  • src/system.rs - Add automatic update policy checking
  • src/daemon.rs - Add automatic update trigger method

Implementation Steps:

2.1 Add Automatic Update Policy (src/system.rs)

impl AptOstreeSystem {
    pub async fn get_automatic_update_policy(&self) -> Result<Option<String>, Box<dyn std::error::Error>> {
        // Check systemd service status
        let output = tokio::process::Command::new("systemctl")
            .args(["is-enabled", "apt-ostreed-automatic.timer"])
            .output()
            .await?;
        
        if output.status.success() {
            // Check policy configuration
            let policy_file = Path::new("/etc/apt-ostree/automatic.conf");
            if policy_file.exists() {
                let content = tokio::fs::read_to_string(policy_file).await?;
                // Parse policy (stage, check, etc.)
                Ok(Some("stage".to_string())) // Default for now
            } else {
                Ok(Some("check".to_string())) // Default policy
            }
        } else {
            Ok(None) // Automatic updates disabled
        }
    }
    
    pub async fn trigger_automatic_update(&self, mode: &str) -> Result<bool, Box<dyn std::error::Error>> {
        // Check if automatic updates are enabled
        let policy = self.get_automatic_update_policy().await?;
        
        if policy.is_none() {
            return Ok(false); // Automatic updates disabled
        }
        
        // Trigger automatic update based on mode
        match mode {
            "check" => {
                // Just check for updates
                self.check_for_updates().await
            }
            "auto" => {
                // Perform automatic update
                self.perform_automatic_update().await
            }
            _ => Err("Invalid automatic update mode".into())
        }
    }
}

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

#[dbus_interface(name = "org.aptostree.dev")]
impl AptOstreeDaemon {
    /// Trigger automatic update
    async fn trigger_automatic_update(&self, options: HashMap<String, Value>) -> Result<bool, Box<dyn std::error::Error>> {
        let mode = options.get("mode")
            .and_then(|v| v.as_str())
            .unwrap_or("auto");
        
        let system = AptOstreeSystem::new().await?;
        let enabled = system.trigger_automatic_update(mode).await?;
        
        Ok(enabled)
    }
}

Phase 3: Driver Registration Check

Files to Modify:

  • src/system.rs - Add driver registration checking
  • src/daemon.rs - Add driver management

Implementation Steps:

3.1 Add Driver Registration Check (src/system.rs)

impl AptOstreeSystem {
    pub async fn check_driver_registration(&self) -> Result<Option<String>, Box<dyn std::error::Error>> {
        // Check for registered update drivers
        // This would check for systemd services or other update mechanisms
        
        // For now, check for common update services
        let services = [
            "apt-daily.timer",
            "apt-daily-upgrade.timer",
            "unattended-upgrades",
        ];
        
        for service in &services {
            let output = tokio::process::Command::new("systemctl")
                .args(["is-active", service])
                .output()
                .await?;
            
            if output.status.success() {
                return Ok(Some(service.to_string()));
            }
        }
        
        Ok(None) // No drivers registered
    }
    
    pub async fn error_if_driver_registered(&self) -> Result<(), Box<dyn std::error::Error>> {
        if let Some(driver) = self.check_driver_registration().await? {
            return Err(format!("Update driver '{}' is registered. Use --bypass-driver to override.", driver).into());
        }
        Ok(())
    }
}

Phase 4: API Selection and Daemon Communication

Files to Modify:

  • src/system.rs - Add multiple upgrade APIs
  • src/daemon.rs - Add upgrade methods
  • src/client.rs - Add client communication

Implementation Steps:

4.1 Add Multiple Upgrade APIs (src/system.rs)

impl AptOstreeSystem {
    pub async fn upgrade_system(&self, opts: &UpgradeOpts) -> Result<String, Box<dyn std::error::Error>> {
        // Build options dictionary
        let mut options = HashMap::new();
        options.insert("reboot".to_string(), Value::Bool(opts.reboot));
        options.insert("allow-downgrade".to_string(), Value::Bool(opts.allow_downgrade));
        options.insert("cache-only".to_string(), Value::Bool(opts.cache_only));
        options.insert("download-only".to_string(), Value::Bool(opts.download_only));
        options.insert("lock-finalization".to_string(), Value::Bool(opts.lock_finalization));
        
        // Choose API based on options
        if opts.install_packages.is_some() || opts.uninstall_packages.is_some() {
            // Use UpdateDeployment API for package changes
            self.update_deployment_with_packages(opts, &options).await
        } else {
            // Use Upgrade API for system upgrade
            self.perform_system_upgrade(&options).await
        }
    }
    
    async fn perform_system_upgrade(&self, options: &HashMap<String, Value>) -> Result<String, Box<dyn std::error::Error>> {
        // 1. Check for available updates
        let updates = self.check_for_updates().await?;
        
        if updates.is_empty() {
            return Err("No updates available".into());
        }
        
        // 2. Download updates (if not cache-only)
        if !options.get("cache-only").and_then(|v| v.as_bool()).unwrap_or(false) {
            self.download_updates(&updates).await?;
        }
        
        // 3. Create new deployment (if not download-only)
        if !options.get("download-only").and_then(|v| v.as_bool()).unwrap_or(false) {
            let new_commit = self.create_upgrade_commit(&updates).await?;
            self.update_deployment(&new_commit).await?;
        }
        
        // 4. Handle reboot
        if options.get("reboot").and_then(|v| v.as_bool()).unwrap_or(false) {
            self.schedule_reboot().await?;
        }
        
        Ok("upgrade-completed".to_string())
    }
    
    async fn update_deployment_with_packages(&self, opts: &UpgradeOpts, options: &HashMap<String, Value>) -> Result<String, Box<dyn std::error::Error>> {
        // Handle package installation/removal during upgrade
        let install_packages = opts.install_packages.as_ref().unwrap_or(&Vec::new());
        let uninstall_packages = opts.uninstall_packages.as_ref().unwrap_or(&Vec::new());
        
        // Create new deployment with package changes
        let new_commit = self.create_deployment_with_packages(install_packages, uninstall_packages).await?;
        self.update_deployment(&new_commit).await?;
        
        Ok("deployment-updated".to_string())
    }
}

4.2 Add Upgrade D-Bus Methods (src/daemon.rs)

#[dbus_interface(name = "org.aptostree.dev")]
impl AptOstreeDaemon {
    /// Perform system upgrade
    async fn upgrade(&self, options: HashMap<String, Value>) -> Result<String, Box<dyn std::error::Error>> {
        let system = AptOstreeSystem::new().await?;
        let transaction_id = system.perform_system_upgrade(&options).await?;
        Ok(transaction_id)
    }
    
    /// Update deployment with package changes
    async fn update_deployment(&self, options: HashMap<String, Value>) -> Result<String, Box<dyn std::error::Error>> {
        let system = AptOstreeSystem::new().await?;
        let transaction_id = system.update_deployment_with_packages(&options).await?;
        Ok(transaction_id)
    }
}

Phase 5: Transaction Monitoring

Files to Modify:

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

Implementation Steps:

5.1 Add Transaction Monitoring (src/client.rs)

impl AptOstreeClient {
    pub async fn monitor_upgrade_transaction(&self, transaction_id: &str, opts: &UpgradeOpts) -> Result<(), Box<dyn std::error::Error>> {
        // 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 {
                        progress = percent;
                        println!("Progress: {}%", progress);
                    }
                }
                TransactionStatus::Completed => {
                    println!("Upgrade completed successfully");
                    break;
                }
                TransactionStatus::Failed(error) => {
                    return Err(error.into());
                }
            }
            
            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
        }
        
        // Handle unchanged exit 77
        if opts.unchanged_exit_77 {
            // Check if any changes were made
            if !self.were_changes_made(transaction_id).await? {
                std::process::exit(77);
            }
        }
        
        Ok(())
    }
}

#[derive(Debug)]
enum TransactionStatus {
    Running(u32),
    Completed,
    Failed(String),
}

5.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 update_transaction_progress(&self, transaction_id: &str, progress: u32) -> Result<(), Box<dyn std::error::Error>> {
        // Update transaction progress
        self.store_transaction_progress(transaction_id, progress).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 Upgrade Command Implementation

Files to Modify:

  • src/main.rs - Main upgrade command logic

Implementation:

async fn upgrade_command(opts: UpgradeOpts) -> Result<(), Box<dyn std::error::Error>> {
    // 1. Validate options
    opts.validate()?;
    
    // 2. Check automatic update policy
    if !opts.trigger_automatic_update_policy {
        let system = AptOstreeSystem::new().await?;
        if let Some(policy) = system.get_automatic_update_policy().await? {
            println!("note: automatic updates ({}) are enabled", policy);
        }
    }
    
    // 3. Check driver registration (unless bypassed)
    if !opts.bypass_driver {
        let system = AptOstreeSystem::new().await?;
        system.error_if_driver_registered().await?;
    }
    
    // 4. Handle automatic update trigger
    if opts.trigger_automatic_update_policy || opts.preview || opts.check {
        let client = AptOstreeClient::new().await?;
        let mode = if opts.preview || opts.check { "check" } else { "auto" };
        
        let mut options = HashMap::new();
        options.insert("mode".to_string(), Value::String(mode.to_string()));
        
        let enabled = client.trigger_automatic_update(options).await?;
        if !enabled {
            println!("Automatic updates are not enabled; exiting...");
            return Ok(());
        }
        
        return Ok(());
    }
    
    // 5. Perform manual upgrade
    let system = AptOstreeSystem::new().await?;
    let transaction_id = system.upgrade_system(&opts).await?;
    
    // 6. Monitor transaction
    let client = AptOstreeClient::new().await?;
    client.monitor_upgrade_transaction(&transaction_id, &opts).await?;
    
    Ok(())
}

Testing Strategy

Unit Tests

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]
    async fn test_upgrade_options_validation() {
        let mut opts = UpgradeOpts {
            reboot: true,
            preview: true,
            ..Default::default()
        };
        
        assert!(opts.validate().is_err());
        
        opts.preview = false;
        assert!(opts.validate().is_ok());
    }
    
    #[tokio::test]
    async fn test_driver_registration_check() {
        let system = AptOstreeSystem::new().await.unwrap();
        let driver = system.check_driver_registration().await.unwrap();
        // Test based on system state
    }
    
    #[tokio::test]
    async fn test_automatic_update_policy() {
        let system = AptOstreeSystem::new().await.unwrap();
        let policy = system.get_automatic_update_policy().await.unwrap();
        // Test policy detection
    }
}

Integration Tests

#[tokio::test]
async fn test_upgrade_command_integration() {
    let opts = UpgradeOpts {
        reboot: false,
        allow_downgrade: false,
        preview: false,
        check: false,
        cache_only: false,
        download_only: false,
        unchanged_exit_77: false,
        bypass_driver: false,
        lock_finalization: false,
        trigger_automatic_update_policy: false,
        install_packages: None,
        uninstall_packages: None,
    };
    
    let result = upgrade_command(opts).await;
    assert!(result.is_ok());
}

Dependencies to Add

Add to Cargo.toml:

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

Implementation Checklist

  • Add CLI options for all upgrade modes
  • Implement option validation logic
  • Add automatic update policy checking
  • Implement driver registration checking
  • Add multiple upgrade APIs (Upgrade, UpdateDeployment)
  • Implement automatic update trigger
  • Add transaction monitoring
  • Handle unchanged exit 77 logic
  • Add comprehensive error handling
  • Write unit and integration tests
  • Update documentation

References

  • rpm-ostree source: src/app/rpmostree-builtin-upgrade.cxx (247 lines)
  • systemd service management
  • APT automatic update configuration
  • OSTree deployment management