495 lines
No EOL
18 KiB
Rust
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
|
|
}
|
|
}
|