512 lines
No EOL
15 KiB
Markdown
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 |