17 KiB
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 optionssrc/system.rs- Enhance upgrade methodsrc/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 checkingsrc/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 checkingsrc/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 APIssrc/daemon.rs- Add upgrade methodssrc/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 monitoringsrc/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