apt-ostree/src/lib/transaction.rs
robojerk 3dec23f8f7 Fix YAML linting issues and update system requirements to Debian 13+
- Fix trailing spaces and blank lines in Forgejo workflows
- Update system requirements from Ubuntu Jammy/Bookworm to Debian 13+ (Trixie)
- Update test treefile to use Debian Trixie instead of Ubuntu Jammy
- Update documentation to reflect modern system requirements
- Fix yamllint errors for CI/CD functionality
- Ensure compatibility with modern OSTree and libapt versions
2025-08-18 11:39:58 -07:00

801 lines
27 KiB
Rust

use crate::lib::error::{AptOstreeError, AptOstreeResult};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
/// Transaction types for different operations
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TransactionType {
PkgChange, // Package installation/removal
Deploy, // Deployment operations
Rebase, // System rebase operations
Upgrade, // System upgrade operations
Rollback, // Rollback operations
Kargs, // Kernel argument changes
Initramfs, // Initramfs modifications
Override, // Package override changes
UsrOverlay, // User overlay operations
ApplyLive, // Live deployment changes
Finalize, // Deployment finalization
Cleanup, // Cleanup operations
Reload, // Configuration reload
Reset, // Reset operations
RefreshMd, // Metadata refresh
Compose, // Tree composition
Container, // Container operations
Experimental, // Experimental features
}
/// Upgrade-specific transaction data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpgradeTransaction {
pub packages_to_install: Vec<String>,
pub packages_to_remove: Vec<String>,
pub allow_downgrade: bool,
pub require_signatures: bool,
pub reboot_after: bool,
pub preview_mode: bool,
pub cache_only: bool,
pub download_only: bool,
}
impl Default for UpgradeTransaction {
fn default() -> Self {
Self {
packages_to_install: Vec::new(),
packages_to_remove: Vec::new(),
allow_downgrade: false,
require_signatures: true,
reboot_after: false,
preview_mode: false,
cache_only: false,
download_only: false,
}
}
}
/// Transaction states throughout the lifecycle
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum TransactionState {
Initialized, // Transaction created
Preparing, // Preparation phase
Ready, // Ready for execution
Running, // Currently executing
Paused, // Paused for user input
Completed, // Successfully completed
Failed, // Execution failed
Cancelled, // User cancelled
RollingBack, // Rolling back changes
RolledBack, // Successfully rolled back
}
/// Transaction step information for detailed progress tracking
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionStep {
pub id: String,
pub name: String,
pub description: String,
pub status: StepStatus,
pub progress: f64,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub error: Option<String>,
pub metadata: HashMap<String, String>,
}
/// Step status for detailed progress tracking
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum StepStatus {
Pending,
Running,
Completed,
Failed,
Skipped,
}
/// Transaction priority levels
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
pub enum TransactionPriority {
Low = 1,
Normal = 2,
High = 3,
Critical = 4,
}
/// Parameters for creating a transaction with steps
#[derive(Debug, Clone)]
pub struct TransactionCreationParams {
pub transaction_type: TransactionType,
pub user_id: u32,
pub session_id: String,
pub title: String,
pub description: String,
pub steps: Vec<TransactionStep>,
pub priority: TransactionPriority,
}
/// Transaction result information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionResult {
pub success: bool,
pub message: String,
pub details: Option<String>,
pub rollback_required: bool,
pub steps_completed: usize,
pub steps_total: usize,
pub warnings: Vec<String>,
}
/// Transaction object representing a single operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub id: String,
pub transaction_type: TransactionType,
pub user_id: u32,
pub session_id: String,
pub title: String,
pub description: String,
pub state: TransactionState,
pub created_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub progress: f64, // 0.0 to 1.0
pub result: Option<TransactionResult>,
pub metadata: HashMap<String, String>,
pub upgrade_data: Option<UpgradeTransaction>,
pub steps: Vec<TransactionStep>,
pub dependencies: Vec<String>,
pub priority: TransactionPriority,
pub estimated_duration: Option<u64>, // in seconds
pub actual_duration: Option<u64>, // in seconds
}
impl Transaction {
/// Create a new transaction
pub fn new(
id: String,
transaction_type: TransactionType,
user_id: u32,
session_id: String,
title: String,
description: String,
created_at: DateTime<Utc>,
) -> Self {
Self {
id,
transaction_type,
user_id,
session_id,
title,
description,
state: TransactionState::Initialized,
created_at,
started_at: None,
completed_at: None,
progress: 0.0,
result: None,
metadata: HashMap::new(),
upgrade_data: None,
steps: Vec::new(),
dependencies: Vec::new(),
priority: TransactionPriority::Normal,
estimated_duration: None,
actual_duration: None,
}
}
/// Update transaction state
pub fn update_state(&mut self, new_state: TransactionState) {
self.state = new_state;
match new_state {
TransactionState::Running => {
self.started_at = Some(Utc::now());
}
TransactionState::Completed | TransactionState::Failed | TransactionState::RolledBack => {
self.completed_at = Some(Utc::now());
}
_ => {}
}
}
/// Update progress
pub fn update_progress(&mut self, progress: f64) {
self.progress = progress.clamp(0.0, 1.0);
}
/// Set transaction result
pub fn set_result(&mut self, result: TransactionResult) {
self.result = Some(result);
}
/// Add metadata
pub fn add_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
}
/// Add a transaction step
pub fn add_step(&mut self, step: TransactionStep) {
self.steps.push(step);
}
/// Update step status
pub fn update_step_status(&mut self, step_id: &str, status: StepStatus) -> AptOstreeResult<()> {
if let Some(step) = self.steps.iter_mut().find(|s| s.id == step_id) {
step.status = status.clone();
match status {
StepStatus::Running => {
step.started_at = Some(Utc::now());
}
StepStatus::Completed | StepStatus::Failed => {
step.completed_at = Some(Utc::now());
}
_ => {}
}
Ok(())
} else {
Err(AptOstreeError::System(format!("Step {} not found", step_id)))
}
}
/// Get step by ID
pub fn get_step(&self, step_id: &str) -> Option<&TransactionStep> {
self.steps.iter().find(|s| s.id == step_id)
}
/// Calculate overall progress from steps
pub fn calculate_progress_from_steps(&mut self) {
if self.steps.is_empty() {
return;
}
let completed_steps = self.steps.iter()
.filter(|s| s.status == StepStatus::Completed)
.count();
let total_steps = self.steps.len();
self.progress = completed_steps as f64 / total_steps as f64;
}
/// Set transaction priority
pub fn set_priority(&mut self, priority: TransactionPriority) {
self.priority = priority;
}
/// Add dependency
pub fn add_dependency(&mut self, transaction_id: String) {
if !self.dependencies.contains(&transaction_id) {
self.dependencies.push(transaction_id);
}
}
/// Check if dependencies are satisfied
pub fn dependencies_satisfied(&self, completed_transactions: &[String]) -> bool {
self.dependencies.iter().all(|dep| completed_transactions.contains(dep))
}
/// Check if transaction is active
pub fn is_active(&self) -> bool {
matches!(
self.state,
TransactionState::Preparing | TransactionState::Ready | TransactionState::Running | TransactionState::Paused
)
}
/// Check if transaction can be cancelled
pub fn can_cancel(&self) -> bool {
matches!(
self.state,
TransactionState::Preparing | TransactionState::Ready | TransactionState::Running | TransactionState::Paused
)
}
/// Check if transaction can be rolled back
pub fn can_rollback(&self) -> bool {
matches!(
self.state,
TransactionState::Completed | TransactionState::Failed
)
}
}
/// Transaction manager for handling all transactions
#[allow(dead_code, clippy::new_without_default)]
pub struct TransactionManager {
transactions: Arc<RwLock<HashMap<String, Transaction>>>,
}
impl TransactionManager {
/// Create a new transaction manager
pub fn new() -> Self {
Self {
transactions: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl Default for TransactionManager {
fn default() -> Self {
Self::new()
}
}
impl TransactionManager {
/// Create a new transaction
pub async fn create_transaction(
&self,
transaction_type: TransactionType,
user_id: u32,
session_id: String,
title: String,
description: String,
) -> AptOstreeResult<String> {
let transaction_id = Uuid::new_v4().to_string();
let transaction = Transaction::new(
transaction_id.clone(),
transaction_type,
user_id,
session_id,
title,
description,
Utc::now(),
);
// Store transaction
self.transactions
.write()
.await
.insert(transaction_id.clone(), transaction);
Ok(transaction_id)
}
/// Get a transaction by ID
pub async fn get_transaction(&self, transaction_id: &str) -> AptOstreeResult<Transaction> {
let transactions = self.transactions.read().await;
transactions
.get(transaction_id)
.cloned()
.ok_or_else(|| AptOstreeError::System(format!("Transaction {} not found", transaction_id)))
}
/// Update a transaction
pub async fn update_transaction(&self, transaction: &Transaction) -> AptOstreeResult<()> {
let mut transactions = self.transactions.write().await;
transactions.insert(transaction.id.clone(), transaction.clone());
Ok(())
}
/// List all transactions
pub async fn list_transactions(&self) -> AptOstreeResult<Vec<Transaction>> {
let transactions = self.transactions.read().await;
Ok(transactions.values().cloned().collect())
}
/// List active transactions
pub async fn list_active_transactions(&self) -> AptOstreeResult<Vec<Transaction>> {
let transactions = self.transactions.read().await;
Ok(transactions
.values()
.filter(|t| t.is_active())
.cloned()
.collect())
}
/// Cancel a transaction
pub async fn cancel_transaction(&self, transaction_id: &str) -> AptOstreeResult<()> {
let mut transaction = self.get_transaction(transaction_id).await?;
if !transaction.can_cancel() {
return Err(AptOstreeError::System(format!(
"Transaction {} cannot be cancelled in state {:?}",
transaction_id, transaction.state
)));
}
transaction.update_state(TransactionState::Cancelled);
self.update_transaction(&transaction).await?;
Ok(())
}
/// Rollback a transaction
pub async fn rollback_transaction(&self, transaction_id: &str) -> AptOstreeResult<()> {
let mut transaction = self.get_transaction(transaction_id).await?;
if !transaction.can_rollback() {
return Err(AptOstreeError::System(format!(
"Transaction {} cannot be rolled back in state {:?}",
transaction_id, transaction.state
)));
}
transaction.update_state(TransactionState::RollingBack);
self.update_transaction(&transaction).await?;
// TODO: Implement actual rollback logic based on transaction type
transaction.update_state(TransactionState::RolledBack);
self.update_transaction(&transaction).await?;
Ok(())
}
/// Create an upgrade transaction
pub async fn create_upgrade_transaction(
&self,
user_id: u32,
session_id: String,
upgrade_data: UpgradeTransaction,
) -> AptOstreeResult<String> {
let transaction_id = Uuid::new_v4().to_string();
let mut transaction = Transaction::new(
transaction_id.clone(),
TransactionType::Upgrade,
user_id,
session_id,
"System Upgrade".to_string(),
"Upgrading system packages and OSTree tree".to_string(),
Utc::now(),
);
transaction.upgrade_data = Some(upgrade_data);
// Store transaction
self.transactions
.write()
.await
.insert(transaction_id.clone(), transaction);
Ok(transaction_id)
}
/// Execute an upgrade transaction
pub async fn execute_upgrade_transaction(&self, transaction_id: &str) -> AptOstreeResult<()> {
let mut transaction = self.get_transaction(transaction_id).await?;
if transaction.transaction_type != TransactionType::Upgrade {
return Err(AptOstreeError::System(
"Transaction is not an upgrade transaction".to_string()
));
}
// Extract upgrade data before borrowing
let _packages_to_install = if let Some(ref upgrade_data) = transaction.upgrade_data {
upgrade_data.packages_to_install.clone()
} else {
return Err(AptOstreeError::System(
"Upgrade transaction data not found".to_string()
));
};
let _packages_to_remove = if let Some(ref upgrade_data) = transaction.upgrade_data {
upgrade_data.packages_to_remove.len()
} else {
0
};
// Create upgrade steps if they don't exist
if transaction.steps.is_empty() {
let upgrade_steps = vec![
TransactionStep {
id: "validate".to_string(),
name: "Validate System".to_string(),
description: "Check OSTree system status and APT availability".to_string(),
status: StepStatus::Pending,
progress: 0.0,
started_at: None,
completed_at: None,
error: None,
metadata: HashMap::new(),
},
TransactionStep {
id: "update_cache".to_string(),
name: "Update APT Cache".to_string(),
description: "Refresh package lists and metadata".to_string(),
status: StepStatus::Pending,
progress: 0.0,
started_at: None,
completed_at: None,
error: None,
metadata: HashMap::new(),
},
TransactionStep {
id: "check_updates".to_string(),
name: "Check for Updates".to_string(),
description: "Identify available system and package updates".to_string(),
status: StepStatus::Pending,
progress: 0.0,
started_at: None,
completed_at: None,
error: None,
metadata: HashMap::new(),
},
TransactionStep {
id: "download_packages".to_string(),
name: "Download Packages".to_string(),
description: "Download required package updates".to_string(),
status: StepStatus::Pending,
progress: 0.0,
started_at: None,
completed_at: None,
error: None,
metadata: HashMap::new(),
},
TransactionStep {
id: "apply_updates".to_string(),
name: "Apply Updates".to_string(),
description: "Apply package updates and system changes".to_string(),
status: StepStatus::Pending,
progress: 0.0,
started_at: None,
completed_at: None,
error: None,
metadata: HashMap::new(),
},
TransactionStep {
id: "finalize".to_string(),
name: "Finalize Upgrade".to_string(),
description: "Complete upgrade and prepare for reboot if needed".to_string(),
status: StepStatus::Pending,
progress: 0.0,
started_at: None,
completed_at: None,
error: None,
metadata: HashMap::new(),
},
];
for step in upgrade_steps {
transaction.add_step(step);
}
}
// Update transaction state
transaction.update_state(TransactionState::Preparing);
transaction.update_progress(0.1);
self.update_transaction(&transaction).await?;
// Execute upgrade steps
let step_executor = |step: &TransactionStep| -> AptOstreeResult<bool> {
match step.id.as_str() {
"validate" => {
// TODO: Implement real validation logic
Ok(true)
}
"update_cache" => {
// TODO: Implement real APT cache update
Ok(true)
}
"check_updates" => {
// TODO: Implement real update checking
Ok(true)
}
"download_packages" => {
// TODO: Implement real package downloading
Ok(true)
}
"apply_updates" => {
// TODO: Implement real update application
Ok(true)
}
"finalize" => {
// TODO: Implement real upgrade finalization
Ok(true)
}
_ => Ok(true),
}
};
// Execute the transaction with steps
let result = self.execute_transaction_with_steps(transaction_id, step_executor).await?;
// Update the transaction with the result
let mut transaction = self.get_transaction(transaction_id).await?;
transaction.result = Some(result);
self.update_transaction(&transaction).await?;
Ok(())
}
/// Clean up completed transactions
pub async fn cleanup_completed_transactions(&self, max_age_hours: u64) -> AptOstreeResult<usize> {
let mut transactions = self.transactions.write().await;
let cutoff_time = Utc::now() - chrono::Duration::hours(max_age_hours as i64);
let completed_ids: Vec<String> = transactions
.iter()
.filter(|(_, t)| {
matches!(
t.state,
TransactionState::Completed | TransactionState::Failed | TransactionState::Cancelled | TransactionState::RolledBack
) && t.completed_at.is_some_and(|time| time < cutoff_time)
})
.map(|(id, _)| id.clone())
.collect();
let count = completed_ids.len();
for id in completed_ids {
transactions.remove(&id);
}
Ok(count)
}
/// Create a transaction with steps
#[allow(clippy::too_many_arguments)]
pub async fn create_transaction_with_steps(
&self,
params: TransactionCreationParams,
) -> AptOstreeResult<String> {
let transaction_id = Uuid::new_v4().to_string();
let mut transaction = Transaction::new(
transaction_id.clone(),
params.transaction_type,
params.user_id,
params.session_id,
params.title,
params.description,
Utc::now(),
);
transaction.set_priority(params.priority);
for step in params.steps {
transaction.add_step(step);
}
// Store transaction
self.transactions
.write()
.await
.insert(transaction_id.clone(), transaction);
Ok(transaction_id)
}
/// Execute a transaction with step-by-step progress
pub async fn execute_transaction_with_steps(
&self,
transaction_id: &str,
step_executor: impl Fn(&TransactionStep) -> AptOstreeResult<bool> + Send + Sync,
) -> AptOstreeResult<TransactionResult> {
let mut transaction = self.get_transaction(transaction_id).await?;
if !transaction.is_active() {
return Err(AptOstreeError::System(
"Transaction is not in an active state".to_string()
));
}
transaction.update_state(TransactionState::Running);
self.update_transaction(&transaction).await?;
let start_time = std::time::Instant::now();
let mut step_results = Vec::new();
// Execute each step and collect results
for step in &transaction.steps {
if step.status == StepStatus::Pending {
let mut step_result = step.clone();
step_result.status = StepStatus::Running;
step_result.started_at = Some(Utc::now());
// Execute the step
let step_success = match step_executor(step) {
Ok(success) => success,
Err(e) => {
step_result.status = StepStatus::Failed;
step_result.error = Some(e.to_string());
false
}
};
if step_success {
step_result.status = StepStatus::Completed;
step_result.completed_at = Some(Utc::now());
} else {
step_result.status = StepStatus::Failed;
if step_result.error.is_none() {
step_result.error = Some("Step execution failed".to_string());
}
}
// Check if any step failed
if step_result.status == StepStatus::Failed {
let error_message = step_result.error.clone();
let step_name = step_result.name.clone();
step_results.push(step_result);
transaction.update_state(TransactionState::Failed);
transaction.actual_duration = Some(start_time.elapsed().as_secs());
// Update steps in transaction
for (i, result) in step_results.iter().enumerate() {
if i < transaction.steps.len() {
transaction.steps[i] = result.clone();
}
}
self.update_transaction(&transaction).await?;
return Ok(TransactionResult {
success: false,
message: format!("Step '{}' failed", step_name),
details: error_message,
rollback_required: true,
steps_completed: step_results.iter().filter(|s| s.status == StepStatus::Completed).count(),
steps_total: transaction.steps.len(),
warnings: Vec::new(),
});
}
} else {
step_results.push(step.clone());
}
}
// Update transaction with step results
transaction.steps = step_results;
transaction.calculate_progress_from_steps();
// All steps completed successfully
transaction.update_state(TransactionState::Completed);
transaction.actual_duration = Some(start_time.elapsed().as_secs());
let result = TransactionResult {
success: true,
message: "Transaction completed successfully".to_string(),
details: None,
rollback_required: false,
steps_completed: transaction.steps.len(),
steps_total: transaction.steps.len(),
warnings: Vec::new(),
};
transaction.result = Some(result.clone());
self.update_transaction(&transaction).await?;
Ok(result)
}
/// Get transactions by priority
pub async fn get_transactions_by_priority(&self, priority: TransactionPriority) -> AptOstreeResult<Vec<Transaction>> {
let transactions = self.transactions.read().await;
Ok(transactions
.values()
.filter(|t| t.priority == priority)
.cloned()
.collect())
}
/// Get transactions with dependencies
pub async fn get_transactions_with_dependencies(&self) -> AptOstreeResult<Vec<Transaction>> {
let transactions = self.transactions.read().await;
Ok(transactions
.values()
.filter(|t| !t.dependencies.is_empty())
.cloned()
.collect())
}
/// Validate transaction dependencies
pub async fn validate_transaction_dependencies(&self, transaction_id: &str) -> AptOstreeResult<bool> {
let transaction = self.get_transaction(transaction_id).await?;
let completed_transactions = self.list_transactions().await?
.into_iter()
.filter(|t| t.state == TransactionState::Completed)
.map(|t| t.id)
.collect::<Vec<_>>();
Ok(transaction.dependencies_satisfied(&completed_transactions))
}
}