apt-ostree/src/script_execution.rs

495 lines
No EOL
18 KiB
Rust

//! Script Execution with Error Handling and Rollback for APT-OSTree
//!
//! This module implements DEB script execution with proper error handling,
//! rollback support, and sandboxed execution environment.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::process::{Command, Stdio};
use tracing::{info, error, debug};
use serde::{Serialize, Deserialize};
use std::pin::Pin;
use std::future::Future;
use crate::error::{AptOstreeError, AptOstreeResult};
/// Script types for DEB package scripts
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ScriptType {
PreInst,
PostInst,
PreRm,
PostRm,
}
/// Script execution result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptResult {
pub script_type: ScriptType,
pub package_name: String,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub success: bool,
pub execution_time: std::time::Duration,
}
/// Script execution state for rollback
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptState {
pub package_name: String,
pub script_type: ScriptType,
pub original_files: Vec<FileBackup>,
pub executed_scripts: Vec<ScriptResult>,
pub rollback_required: bool,
}
/// File backup for rollback
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileBackup {
pub original_path: PathBuf,
pub backup_path: PathBuf,
pub file_type: FileType,
}
/// File types for backup
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FileType {
Regular,
Directory,
Symlink,
}
/// Script execution manager with rollback support
pub struct ScriptExecutionManager {
sandbox_dir: PathBuf,
backup_dir: PathBuf,
script_states: HashMap<String, ScriptState>,
}
/// Script execution configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptConfig {
pub sandbox_directory: PathBuf,
pub backup_directory: PathBuf,
pub timeout_seconds: u64,
pub enable_sandboxing: bool,
pub preserve_environment: bool,
}
impl Default for ScriptConfig {
fn default() -> Self {
Self {
sandbox_directory: PathBuf::from("/var/lib/apt-ostree/scripts/sandbox"),
backup_directory: PathBuf::from("/var/lib/apt-ostree/scripts/backup"),
timeout_seconds: 300, // 5 minutes
enable_sandboxing: true,
preserve_environment: false,
}
}
}
impl ScriptExecutionManager {
/// Create a new script execution manager
pub fn new(config: ScriptConfig) -> AptOstreeResult<Self> {
info!("Creating script execution manager with config: {:?}", config);
// Create directories
fs::create_dir_all(&config.sandbox_directory)?;
fs::create_dir_all(&config.backup_directory)?;
Ok(Self {
sandbox_dir: config.sandbox_directory,
backup_dir: config.backup_directory,
script_states: HashMap::new(),
})
}
/// Execute a script with error handling and rollback support
pub async fn execute_script(
&mut self,
script_path: &Path,
script_type: ScriptType,
package_name: &str,
) -> AptOstreeResult<ScriptResult> {
info!("Executing script: {} ({:?}) for package {}",
script_path.display(), script_type, package_name);
let start_time = std::time::Instant::now();
// Create backup before execution
let backup_created = self.create_backup(package_name, script_type).await?;
// Execute script
let result = self.execute_script_in_sandbox(script_path, script_type, package_name).await?;
let execution_time = start_time.elapsed();
// Update script state
let script_state = self.script_states.entry(package_name.to_string()).or_insert_with(|| ScriptState {
package_name: package_name.to_string(),
script_type: script_type.clone(),
original_files: Vec::new(),
executed_scripts: Vec::new(),
rollback_required: false,
});
script_state.executed_scripts.push(result.clone());
// Handle script failure
if !result.success {
error!("Script execution failed: {} (exit code: {})", script_path.display(), result.exit_code);
script_state.rollback_required = true;
// Perform rollback
self.rollback_script_execution(package_name).await?;
return Err(AptOstreeError::ScriptExecution(
format!("Script failed with exit code {}: {}", result.exit_code, result.stderr)
));
}
info!("Script execution completed successfully in {:?}", execution_time);
Ok(result)
}
/// Execute script in sandboxed environment
async fn execute_script_in_sandbox(
&self,
script_path: &Path,
script_type: ScriptType,
package_name: &str,
) -> AptOstreeResult<ScriptResult> {
// Create sandbox directory
let sandbox_id = format!("{}_{}_{}", package_name, script_type_name(&script_type),
chrono::Utc::now().timestamp());
let sandbox_path = self.sandbox_dir.join(&sandbox_id);
fs::create_dir_all(&sandbox_path)?;
// Copy script to sandbox
let sandbox_script = sandbox_path.join("script");
fs::copy(script_path, &sandbox_script)?;
fs::set_permissions(&sandbox_script, fs::Permissions::from_mode(0o755))?;
// Set up environment
let env_vars = self.get_script_environment(script_type, package_name);
// Execute script
let output = Command::new(&sandbox_script)
.current_dir(&sandbox_path)
.envs(env_vars)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| AptOstreeError::ScriptExecution(format!("Failed to execute script: {}", e)))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
// Clean up sandbox
fs::remove_dir_all(&sandbox_path)?;
Ok(ScriptResult {
script_type,
package_name: package_name.to_string(),
exit_code: output.status.code().unwrap_or(-1),
stdout,
stderr,
success: output.status.success(),
execution_time: std::time::Duration::from_millis(0), // Will be set by caller
})
}
/// Get environment variables for script execution
fn get_script_environment(&self, script_type: ScriptType, package_name: &str) -> HashMap<String, String> {
let mut env = HashMap::new();
// Basic environment
env.insert("PATH".to_string(), "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string());
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
env.insert("DPKG_MAINTSCRIPT_NAME".to_string(), script_type_name(&script_type).to_string());
env.insert("DPKG_MAINTSCRIPT_PACKAGE".to_string(), package_name.to_string());
// Script-specific environment
match script_type {
ScriptType::PreInst => {
env.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string());
env.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string());
}
ScriptType::PostInst => {
env.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string());
env.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string());
}
ScriptType::PreRm => {
env.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string());
env.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string());
}
ScriptType::PostRm => {
env.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string());
env.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string());
}
}
env
}
/// Create backup before script execution
async fn create_backup(&mut self, package_name: &str, script_type: ScriptType) -> AptOstreeResult<bool> {
debug!("Creating backup for package {} script {:?}", package_name, script_type);
let backup_id = format!("{}_{}_{}", package_name, script_type_name(&script_type),
chrono::Utc::now().timestamp());
let backup_path = self.backup_dir.join(&backup_id);
fs::create_dir_all(&backup_path)?;
// TODO: Implement actual file backup
// For now, just create a placeholder backup
let script_state = self.script_states.entry(package_name.to_string()).or_insert_with(|| ScriptState {
package_name: package_name.to_string(),
script_type,
original_files: Vec::new(),
executed_scripts: Vec::new(),
rollback_required: false,
});
// Add placeholder backup
script_state.original_files.push(FileBackup {
original_path: PathBuf::from("/tmp/placeholder"),
backup_path: backup_path.join("placeholder"),
file_type: FileType::Regular,
});
info!("Backup created for package {}: {}", package_name, backup_path.display());
Ok(true)
}
/// Rollback script execution
async fn rollback_script_execution(&mut self, package_name: &str) -> AptOstreeResult<()> {
info!("Rolling back script execution for package: {}", package_name);
// Check if rollback is needed and get backups
let needs_rollback = if let Some(script_state) = self.script_states.get(package_name) {
script_state.rollback_required
} else {
return Ok(());
};
if !needs_rollback {
return Ok(());
}
// Get backups and script state for rollback
let (backups, script_state) = if let Some(script_state) = self.script_states.get(package_name) {
(script_state.original_files.clone(), script_state.clone())
} else {
return Ok(());
};
// Restore original files
for backup in &backups {
self.restore_file_backup(backup).await?;
}
// Execute rollback scripts if available
self.execute_rollback_scripts(&script_state).await?;
// Mark rollback as completed
if let Some(script_state) = self.script_states.get_mut(package_name) {
script_state.rollback_required = false;
}
info!("Rollback completed for package: {}", package_name);
Ok(())
}
/// Restore file from backup
async fn restore_file_backup(&self, backup: &FileBackup) -> AptOstreeResult<()> {
debug!("Restoring file: {} -> {}", backup.backup_path.display(), backup.original_path.display());
if backup.backup_path.exists() {
match backup.file_type {
FileType::Regular => {
if let Some(parent) = backup.original_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&backup.backup_path, &backup.original_path)?;
}
FileType::Directory => {
if backup.original_path.exists() {
fs::remove_dir_all(&backup.original_path)?;
}
self.copy_directory(&backup.backup_path, &backup.original_path).await?;
}
FileType::Symlink => {
if backup.original_path.exists() {
fs::remove_file(&backup.original_path)?;
}
let target = fs::read_link(&backup.backup_path)?;
std::os::unix::fs::symlink(target, &backup.original_path)?;
}
}
}
Ok(())
}
/// Copy directory recursively
fn copy_directory<'a>(&'a self, src: &'a Path, dst: &'a Path) -> Pin<Box<dyn Future<Output = AptOstreeResult<()>> + 'a>> {
Box::pin(async move {
if src.is_dir() {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
self.copy_directory(&src_path, &dst_path).await?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
}
Ok(())
})
}
/// Execute rollback scripts
async fn execute_rollback_scripts(&self, script_state: &ScriptState) -> AptOstreeResult<()> {
debug!("Executing rollback scripts for package: {}", script_state.package_name);
// TODO: Implement rollback script execution
// This would involve executing scripts in reverse order with rollback flags
info!("Rollback scripts executed for package: {}", script_state.package_name);
Ok(())
}
/// Get script execution history
pub fn get_execution_history(&self, package_name: &str) -> Option<&ScriptState> {
self.script_states.get(package_name)
}
/// Check if package has pending rollback
pub fn has_pending_rollback(&self, package_name: &str) -> bool {
self.script_states.get(package_name)
.map(|state| state.rollback_required)
.unwrap_or(false)
}
/// Clean up script states
pub fn cleanup_script_states(&mut self, package_name: &str) -> AptOstreeResult<()> {
if let Some(script_state) = self.script_states.remove(package_name) {
// Clean up backup files
for backup in script_state.original_files {
if backup.backup_path.exists() {
fs::remove_file(&backup.backup_path)?;
}
}
info!("Cleaned up script states for package: {}", package_name);
}
Ok(())
}
}
/// Convert script type to string name
fn script_type_name(script_type: &ScriptType) -> &'static str {
match script_type {
ScriptType::PreInst => "preinst",
ScriptType::PostInst => "postinst",
ScriptType::PreRm => "prerm",
ScriptType::PostRm => "postrm",
}
}
/// Script execution orchestrator
pub struct ScriptOrchestrator {
execution_manager: ScriptExecutionManager,
}
impl ScriptOrchestrator {
/// Create a new script orchestrator
pub fn new(config: ScriptConfig) -> AptOstreeResult<Self> {
let execution_manager = ScriptExecutionManager::new(config)?;
Ok(Self { execution_manager })
}
/// Execute scripts for a package in proper order
pub async fn execute_package_scripts(
&mut self,
package_name: &str,
script_paths: &HashMap<ScriptType, PathBuf>,
) -> AptOstreeResult<Vec<ScriptResult>> {
info!("Executing scripts for package: {}", package_name);
let mut results = Vec::new();
// Execute scripts in proper order: preinst -> postinst
let script_order = [ScriptType::PreInst, ScriptType::PostInst];
for script_type in &script_order {
if let Some(script_path) = script_paths.get(script_type) {
match self.execution_manager.execute_script(script_path, script_type.clone(), package_name).await {
Ok(result) => {
results.push(result);
}
Err(e) => {
error!("Script execution failed: {}", e);
return Err(e);
}
}
}
}
info!("All scripts executed successfully for package: {}", package_name);
Ok(results)
}
/// Execute removal scripts for a package
pub async fn execute_removal_scripts(
&mut self,
package_name: &str,
script_paths: &HashMap<ScriptType, PathBuf>,
) -> AptOstreeResult<Vec<ScriptResult>> {
info!("Executing removal scripts for package: {}", package_name);
let mut results = Vec::new();
// Execute scripts in proper order: prerm -> postrm
let script_order = [ScriptType::PreRm, ScriptType::PostRm];
for script_type in &script_order {
if let Some(script_path) = script_paths.get(script_type) {
match self.execution_manager.execute_script(script_path, script_type.clone(), package_name).await {
Ok(result) => {
results.push(result);
}
Err(e) => {
error!("Script execution failed: {}", e);
return Err(e);
}
}
}
}
info!("All removal scripts executed successfully for package: {}", package_name);
Ok(results)
}
/// Get execution manager reference
pub fn execution_manager(&self) -> &ScriptExecutionManager {
&self.execution_manager
}
/// Get mutable execution manager reference
pub fn execution_manager_mut(&mut self) -> &mut ScriptExecutionManager {
&mut self.execution_manager
}
}