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

512 lines
No EOL
15 KiB
Markdown

# 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)**
```rust
#[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)**
```rust
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)**
```rust
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)**
```rust
#[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)**
```rust
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)**
```rust
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:
```rust
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
```rust
#[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
```rust
#[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:
```rust
#[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`:
```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