Initial commit: apt-ostree project with 100% rpm-ostree CLI compatibility
This commit is contained in:
commit
a48ad95d70
81 changed files with 28515 additions and 0 deletions
542
.notes/rpm-ostree/how-commands-work/02-upgrade-command.md
Normal file
542
.notes/rpm-ostree/how-commands-work/02-upgrade-command.md
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
# 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)**
|
||||
```rust
|
||||
#[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)**
|
||||
```rust
|
||||
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)**
|
||||
```rust
|
||||
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)**
|
||||
```rust
|
||||
#[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)**
|
||||
```rust
|
||||
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)**
|
||||
```rust
|
||||
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)**
|
||||
```rust
|
||||
#[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)**
|
||||
```rust
|
||||
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)**
|
||||
```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 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:
|
||||
|
||||
```rust
|
||||
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
|
||||
```rust
|
||||
#[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
|
||||
```rust
|
||||
#[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`:
|
||||
```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
|
||||
Loading…
Add table
Add a link
Reference in a new issue