diff --git a/build_herodo.sh b/build_herodo.sh index ad2b8f0..280846a 100755 --- a/build_herodo.sh +++ b/build_herodo.sh @@ -26,6 +26,17 @@ cp target/debug/herodo ~/hero/bin/herodo # Check if a script name was provided if [ $# -eq 1 ]; then echo "Running specified test: $1" - herodo "src/herodo/scripts/$1.rhai" + + # Check if the script exists in src/rhaiexamples/ + if [ -f "src/rhaiexamples/$1.rhai" ]; then + herodo "src/rhaiexamples/$1.rhai" + # Check if the script exists in src/herodo/scripts/ + elif [ -f "src/herodo/scripts/$1.rhai" ]; then + herodo "src/herodo/scripts/$1.rhai" + else + echo "Error: Script $1.rhai not found in src/rhaiexamples/ or src/herodo/scripts/" + exit 1 + fi + exit 0 fi diff --git a/examples/package_test.rs b/examples/package_test.rs new file mode 100644 index 0000000..a8012c4 --- /dev/null +++ b/examples/package_test.rs @@ -0,0 +1,100 @@ +//! Example of using the package management module +//! +//! This example demonstrates how to use the package management module +//! to install, remove, and manage packages on different platforms. + +use sal::os::package::{PackHero, Platform}; + +fn main() { + // Create a new PackHero instance + let mut hero = PackHero::new(); + + // Enable debug output + hero.set_debug(true); + + // Detect the platform + let platform = hero.platform(); + println!("Detected platform: {:?}", platform); + + // Only proceed if we're on a supported platform + if platform == Platform::Unknown { + println!("Unsupported platform. This example only works on Ubuntu and macOS."); + return; + } + + // Test package to install/check + let test_package = if platform == Platform::Ubuntu { "wget" } else { "wget" }; + + // Check if the package is installed + match hero.is_installed(test_package) { + Ok(is_installed) => { + println!("Package {} is installed: {}", test_package, is_installed); + + if is_installed { + println!("Package {} is already installed", test_package); + } else { + println!("Package {} is not installed, attempting to install...", test_package); + + // Try to install the package + match hero.install(test_package) { + Ok(_) => println!("Successfully installed package {}", test_package), + Err(e) => println!("Failed to install package {}: {}", test_package, e), + } + + // Check if it was installed successfully + match hero.is_installed(test_package) { + Ok(is_installed_now) => { + if is_installed_now { + println!("Verified package {} was installed successfully", test_package); + } else { + println!("Package {} was not installed successfully", test_package); + } + }, + Err(e) => println!("Error checking if package is installed: {}", e), + } + } + }, + Err(e) => println!("Error checking if package is installed: {}", e), + } + + // Search for packages + let search_term = "wget"; + println!("Searching for packages with term '{}'...", search_term); + match hero.search(search_term) { + Ok(results) => { + println!("Found {} packages matching '{}'", results.len(), search_term); + for (i, package) in results.iter().enumerate().take(5) { + println!(" {}. {}", i + 1, package); + } + if results.len() > 5 { + println!(" ... and {} more", results.len() - 5); + } + }, + Err(e) => println!("Error searching for packages: {}", e), + } + + // List installed packages + println!("Listing installed packages..."); + match hero.list_installed() { + Ok(packages) => { + println!("Found {} installed packages", packages.len()); + println!("First 5 installed packages:"); + for (i, package) in packages.iter().enumerate().take(5) { + println!(" {}. {}", i + 1, package); + } + if packages.len() > 5 { + println!(" ... and {} more", packages.len() - 5); + } + }, + Err(e) => println!("Error listing installed packages: {}", e), + } + + // Update package lists + println!("Updating package lists..."); + match hero.update() { + Ok(_) => println!("Successfully updated package lists"), + Err(e) => println!("Error updating package lists: {}", e), + } + + println!("Package management example completed"); +} \ No newline at end of file diff --git a/rfs_implementation_plan.md b/rfs_implementation_plan.md new file mode 100644 index 0000000..0ce99a1 --- /dev/null +++ b/rfs_implementation_plan.md @@ -0,0 +1,474 @@ +# RFS Wrapper Implementation Plan + +## Overview + +We'll create a Rust wrapper for the RFS (Remote File System) tool that follows the builder pattern, similar to the existing implementations for buildah and nerdctl in the codebase. This wrapper will provide a fluent API for mounting, unmounting, listing mounts, configuring mount options, and packing directories into filesystem layers. + +## Module Structure + +``` +src/virt/rfs/ +├── mod.rs # Module exports and common types +├── cmd.rs # Command execution functions +├── mount.rs # Mount operations +├── pack.rs # Packing operations +├── builder.rs # Builder pattern implementation +├── types.rs # Type definitions +└── error.rs # Error handling +``` + +## Implementation Details + +### 1. Error Handling + +```rust +// error.rs +#[derive(Debug)] +pub enum RfsError { + CommandFailed(String), + InvalidArgument(String), + MountFailed(String), + UnmountFailed(String), + ListFailed(String), + PackFailed(String), + Other(String), +} + +impl std::fmt::Display for RfsError { + // Implementation +} + +impl std::error::Error for RfsError { + // Implementation +} +``` + +### 2. Command Execution + +```rust +// cmd.rs +use crate::process::{run_command, CommandResult}; +use super::error::RfsError; + +pub fn execute_rfs_command(args: &[&str]) -> Result { + // Implementation similar to buildah and nerdctl +} +``` + +### 3. Types + +```rust +// types.rs +#[derive(Debug, Clone)] +pub struct Mount { + pub id: String, + pub source: String, + pub target: String, + pub fs_type: String, + pub options: Vec, +} + +#[derive(Debug, Clone)] +pub enum MountType { + Local, + SSH, + S3, + WebDAV, + // Other mount types +} + +#[derive(Debug, Clone)] +pub struct StoreSpec { + pub spec_type: String, + pub options: std::collections::HashMap, +} + +impl StoreSpec { + pub fn new(spec_type: &str) -> Self { + Self { + spec_type: spec_type.to_string(), + options: std::collections::HashMap::new(), + } + } + + pub fn with_option(mut self, key: &str, value: &str) -> Self { + self.options.insert(key.to_string(), value.to_string()); + self + } + + pub fn to_string(&self) -> String { + let mut result = self.spec_type.clone(); + + if !self.options.is_empty() { + result.push_str(":"); + let options: Vec = self.options + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect(); + result.push_str(&options.join(",")); + } + + result + } +} +``` + +### 4. Builder Pattern + +```rust +// builder.rs +use std::collections::HashMap; +use super::{Mount, MountType, RfsError, execute_rfs_command, StoreSpec}; + +#[derive(Clone)] +pub struct RfsBuilder { + source: String, + target: String, + mount_type: MountType, + options: HashMap, + mount_id: Option, + debug: bool, +} + +impl RfsBuilder { + pub fn new(source: &str, target: &str, mount_type: MountType) -> Self { + Self { + source: source.to_string(), + target: target.to_string(), + mount_type, + options: HashMap::new(), + mount_id: None, + debug: false, + } + } + + pub fn with_option(mut self, key: &str, value: &str) -> Self { + self.options.insert(key.to_string(), value.to_string()); + self + } + + pub fn with_options(mut self, options: HashMap<&str, &str>) -> Self { + for (key, value) in options { + self.options.insert(key.to_string(), value.to_string()); + } + self + } + + pub fn with_debug(mut self, debug: bool) -> Self { + self.debug = debug; + self + } + + pub fn mount(self) -> Result { + // Implementation + } + + pub fn unmount(&self) -> Result<(), RfsError> { + // Implementation + } + + // Other methods +} + +// Packing functionality +pub struct PackBuilder { + directory: String, + output: String, + store_specs: Vec, + debug: bool, +} + +impl PackBuilder { + pub fn new(directory: &str, output: &str) -> Self { + Self { + directory: directory.to_string(), + output: output.to_string(), + store_specs: Vec::new(), + debug: false, + } + } + + pub fn with_store_spec(mut self, store_spec: StoreSpec) -> Self { + self.store_specs.push(store_spec); + self + } + + pub fn with_store_specs(mut self, store_specs: Vec) -> Self { + self.store_specs.extend(store_specs); + self + } + + pub fn with_debug(mut self, debug: bool) -> Self { + self.debug = debug; + self + } + + pub fn pack(self) -> Result<(), RfsError> { + // Implementation for packing a directory into a filesystem layer + let mut args = vec!["pack"]; + + // Add output file + args.push("-m"); + args.push(&self.output); + + // Add store specs + if !self.store_specs.is_empty() { + args.push("-s"); + let specs: Vec = self.store_specs + .iter() + .map(|spec| spec.to_string()) + .collect(); + args.push(&specs.join(",")); + } + + // Add directory + args.push(&self.directory); + + // Convert to string slices for the command + let args_str: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + + // Execute the command + let result = execute_rfs_command(&args_str)?; + + // Check for errors + if !result.success { + return Err(RfsError::PackFailed(result.stderr)); + } + + Ok(()) + } +} +``` + +### 5. Mount Operations + +```rust +// mount.rs +use super::{RfsBuilder, Mount, RfsError, execute_rfs_command}; + +pub fn list_mounts() -> Result, RfsError> { + // Implementation +} + +pub fn unmount_all() -> Result<(), RfsError> { + // Implementation +} + +// Other mount-related functions +``` + +### 6. Pack Operations + +```rust +// pack.rs +use super::{PackBuilder, StoreSpec, RfsError, execute_rfs_command}; + +pub fn pack_directory(directory: &str, output: &str, store_specs: &[StoreSpec]) -> Result<(), RfsError> { + PackBuilder::new(directory, output) + .with_store_specs(store_specs.to_vec()) + .pack() +} + +// Other pack-related functions +``` + +### 7. Module Exports + +```rust +// mod.rs +mod cmd; +mod error; +mod mount; +mod pack; +mod builder; +mod types; + +pub use error::RfsError; +pub use builder::{RfsBuilder, PackBuilder}; +pub use types::{Mount, MountType, StoreSpec}; +pub use mount::{list_mounts, unmount_all}; +pub use pack::pack_directory; + +// Re-export the execute_rfs_command function for use in other modules +pub(crate) use cmd::execute_rfs_command; +``` + +## Usage Examples + +### Mounting Example + +```rust +use crate::virt::rfs::{RfsBuilder, MountType}; + +// Create a new RFS mount with builder pattern +let mount = RfsBuilder::new("user@example.com:/remote/path", "/local/mount/point", MountType::SSH) + .with_option("port", "2222") + .with_option("identity_file", "/path/to/key") + .with_debug(true) + .mount()?; + +// List all mounts +let mounts = list_mounts()?; +for mount in mounts { + println!("Mount ID: {}, Source: {}, Target: {}", mount.id, mount.source, mount.target); +} + +// Unmount +mount.unmount()?; +``` + +### Packing Example + +```rust +use crate::virt::rfs::{PackBuilder, StoreSpec}; + +// Create store specifications +let store_spec1 = StoreSpec::new("file") + .with_option("path", "/path/to/store"); + +let store_spec2 = StoreSpec::new("s3") + .with_option("bucket", "my-bucket") + .with_option("region", "us-east-1"); + +// Pack a directory with builder pattern +let result = PackBuilder::new("/path/to/directory", "output.fl") + .with_store_spec(store_spec1) + .with_store_spec(store_spec2) + .with_debug(true) + .pack()?; + +// Or use the convenience function +pack_directory("/path/to/directory", "output.fl", &[store_spec1, store_spec2])?; +``` + +## Rhai Integration + +We'll also need to create a Rhai module to expose the RFS functionality to Rhai scripts: + +```rust +// src/rhai/rfs.rs +use rhai::{Engine, EvalAltResult, RegisterFn}; +use crate::virt::rfs::{RfsBuilder, MountType, list_mounts, unmount_all, PackBuilder, StoreSpec}; + +pub fn register(engine: &mut Engine) -> Result<(), Box> { + // Register RFS functions + engine.register_fn("rfs_mount", rfs_mount); + engine.register_fn("rfs_unmount", rfs_unmount); + engine.register_fn("rfs_list_mounts", rfs_list_mounts); + engine.register_fn("rfs_unmount_all", rfs_unmount_all); + engine.register_fn("rfs_pack", rfs_pack); + + Ok(()) +} + +// Function implementations +fn rfs_mount(source: &str, target: &str, mount_type: &str, options_map: rhai::Map) -> Result<(), Box> { + // Implementation +} + +fn rfs_unmount(target: &str) -> Result<(), Box> { + // Implementation +} + +fn rfs_list_mounts() -> Result> { + // Implementation +} + +fn rfs_unmount_all() -> Result<(), Box> { + // Implementation +} + +fn rfs_pack(directory: &str, output: &str, store_specs: &str) -> Result<(), Box> { + // Implementation +} +``` + +## Implementation Flow + +Here's a diagram showing the flow of the implementation: + +```mermaid +classDiagram + class RfsBuilder { + +String source + +String target + +MountType mount_type + +HashMap options + +Option~String~ mount_id + +bool debug + +new(source, target, mount_type) + +with_option(key, value) + +with_options(options) + +with_debug(debug) + +mount() + +unmount() + } + + class PackBuilder { + +String directory + +String output + +Vec~StoreSpec~ store_specs + +bool debug + +new(directory, output) + +with_store_spec(store_spec) + +with_store_specs(store_specs) + +with_debug(debug) + +pack() + } + + class Mount { + +String id + +String source + +String target + +String fs_type + +Vec~String~ options + } + + class MountType { + <> + Local + SSH + S3 + WebDAV + } + + class StoreSpec { + +String spec_type + +HashMap options + +new(spec_type) + +with_option(key, value) + +to_string() + } + + class RfsError { + <> + CommandFailed + InvalidArgument + MountFailed + UnmountFailed + ListFailed + PackFailed + Other + } + + RfsBuilder --> Mount : creates + RfsBuilder --> RfsError : may throw + RfsBuilder --> MountType : uses + PackBuilder --> RfsError : may throw + PackBuilder --> StoreSpec : uses + Mount --> RfsError : may throw +``` + +## Implementation Steps + +1. Create the directory structure for the RFS module +2. Implement the error handling module +3. Implement the command execution module +4. Define the types for mounts, mount operations, and store specifications +5. Implement the builder pattern for RFS operations (mount and pack) +6. Implement the mount operations +7. Implement the pack operations +8. Create the module exports +9. Add Rhai integration +10. Write tests for the implementation +11. Update documentation \ No newline at end of file diff --git a/src/docs/docs/sal/rfs.md b/src/docs/docs/sal/rfs.md new file mode 100644 index 0000000..219a962 --- /dev/null +++ b/src/docs/docs/sal/rfs.md @@ -0,0 +1,154 @@ +# RFS (Remote File System) + +The RFS module provides a Rust wrapper for the RFS tool, which allows mounting remote filesystems locally and managing filesystem layers. + +## Overview + +RFS (Remote File System) is a tool that enables mounting various types of remote filesystems locally, as well as creating and managing filesystem layers. The SAL library provides a Rust wrapper for RFS with a fluent builder API, making it easy to use in your applications. + +## Features + +- Mount remote filesystems locally (SSH, S3, WebDAV, etc.) +- List mounted filesystems +- Unmount filesystems +- Pack directories into filesystem layers +- Unpack filesystem layers +- List contents of filesystem layers +- Verify filesystem layers + +## Usage in Rust + +### Mounting a Filesystem + +```rust +use sal::virt::rfs::{RfsBuilder, MountType}; + +// Create a new RFS builder +let mount = RfsBuilder::new("user@example.com:/remote/path", "/local/mount/point", MountType::SSH) + .with_option("port", "2222") + .with_option("identity_file", "/path/to/key") + .with_debug(true) + .mount()?; + +println!("Mounted filesystem with ID: {}", mount.id); +``` + +### Listing Mounts + +```rust +use sal::virt::rfs::list_mounts; + +// List all mounts +let mounts = list_mounts()?; +for mount in mounts { + println!("Mount ID: {}, Source: {}, Target: {}", mount.id, mount.source, mount.target); +} +``` + +### Unmounting a Filesystem + +```rust +use sal::virt::rfs::unmount; + +// Unmount a filesystem +unmount("/local/mount/point")?; +``` + +### Packing a Directory + +```rust +use sal::virt::rfs::{PackBuilder, StoreSpec}; + +// Create store specifications +let store_spec = StoreSpec::new("file") + .with_option("path", "/path/to/store"); + +// Pack a directory with builder pattern +let result = PackBuilder::new("/path/to/directory", "output.fl") + .with_store_spec(store_spec) + .with_debug(true) + .pack()?; +``` + +### Unpacking a Filesystem Layer + +```rust +use sal::virt::rfs::unpack; + +// Unpack a filesystem layer +unpack("input.fl", "/path/to/unpack")?; +``` + +## Usage in Rhai Scripts + +### Mounting a Filesystem + +```rhai +// Create a map for mount options +let options = #{ + "port": "22", + "identity_file": "/path/to/key", + "readonly": "true" +}; + +// Mount the directory +let mount = rfs_mount("user@example.com:/remote/path", "/local/mount/point", "ssh", options); + +print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`); +``` + +### Listing Mounts + +```rhai +// List all mounts +let mounts = rfs_list_mounts(); +print(`Number of mounts: ${mounts.len()}`); + +for mount in mounts { + print(`Mount ID: ${mount.id}, Source: ${mount.source}, Target: ${mount.target}`); +} +``` + +### Unmounting a Filesystem + +```rhai +// Unmount the directory +rfs_unmount("/local/mount/point"); +``` + +### Packing a Directory + +```rhai +// Pack the directory +// Store specs format: "file:path=/path/to/store,s3:bucket=my-bucket" +rfs_pack("/path/to/directory", "output.fl", "file:path=/path/to/store"); +``` + +### Unpacking a Filesystem Layer + +```rhai +// Unpack the filesystem layer +rfs_unpack("output.fl", "/path/to/unpack"); +``` + +## Mount Types + +The RFS module supports various mount types: + +- **Local**: Mount a local directory +- **SSH**: Mount a remote directory via SSH +- **S3**: Mount an S3 bucket +- **WebDAV**: Mount a WebDAV server + +## Store Specifications + +When packing a directory into a filesystem layer, you can specify one or more stores to use. Each store has a type and options: + +- **File**: Store files on the local filesystem + - Options: `path` (path to the store) +- **S3**: Store files in an S3 bucket + - Options: `bucket` (bucket name), `region` (AWS region), `access_key`, `secret_key` + +## Examples + +See the [RFS example script](../../rhaiexamples/rfs_example.rhai) for more examples of how to use the RFS module in Rhai scripts. \ No newline at end of file diff --git a/src/os/package.rs b/src/os/package.rs index 103abcd..2d2a06b 100644 --- a/src/os/package.rs +++ b/src/os/package.rs @@ -393,7 +393,10 @@ impl PackHero { #[cfg(test)] mod tests { + // Import the std::process::Command directly for some test-specific commands + use std::process::Command as StdCommand; use super::*; + use std::sync::{Arc, Mutex}; #[test] fn test_platform_detection() { @@ -415,7 +418,486 @@ mod tests { assert_eq!(thread_local_debug(), false); } - // More tests would be added for each platform-specific implementation - // These would likely be integration tests that are conditionally compiled - // based on the platform they're running on + #[test] + fn test_package_error_display() { + // Test the Display implementation for PackageError + let err1 = PackageError::CommandFailed("command failed".to_string()); + assert_eq!(err1.to_string(), "Command failed: command failed"); + + let err2 = PackageError::UnsupportedPlatform("test platform".to_string()); + assert_eq!(err2.to_string(), "Unsupported platform: test platform"); + + let err3 = PackageError::Other("other error".to_string()); + assert_eq!(err3.to_string(), "Error: other error"); + + // We can't easily test CommandExecutionFailed because std::io::Error doesn't implement PartialEq + } + + // Mock package manager for testing + struct MockPackageManager { + debug: bool, + install_called: Arc>, + remove_called: Arc>, + update_called: Arc>, + upgrade_called: Arc>, + list_installed_called: Arc>, + search_called: Arc>, + is_installed_called: Arc>, + // Control what the mock returns + should_succeed: bool, + } + + impl MockPackageManager { + fn new(debug: bool, should_succeed: bool) -> Self { + Self { + debug, + install_called: Arc::new(Mutex::new(false)), + remove_called: Arc::new(Mutex::new(false)), + update_called: Arc::new(Mutex::new(false)), + upgrade_called: Arc::new(Mutex::new(false)), + list_installed_called: Arc::new(Mutex::new(false)), + search_called: Arc::new(Mutex::new(false)), + is_installed_called: Arc::new(Mutex::new(false)), + should_succeed, + } + } + } + + impl PackageManager for MockPackageManager { + fn install(&self, package: &str) -> Result { + *self.install_called.lock().unwrap() = true; + if self.should_succeed { + Ok(CommandResult { + stdout: format!("Installed package {}", package), + stderr: String::new(), + success: true, + code: 0, + }) + } else { + Err(PackageError::CommandFailed("Mock install failed".to_string())) + } + } + + fn remove(&self, package: &str) -> Result { + *self.remove_called.lock().unwrap() = true; + if self.should_succeed { + Ok(CommandResult { + stdout: format!("Removed package {}", package), + stderr: String::new(), + success: true, + code: 0, + }) + } else { + Err(PackageError::CommandFailed("Mock remove failed".to_string())) + } + } + + fn update(&self) -> Result { + *self.update_called.lock().unwrap() = true; + if self.should_succeed { + Ok(CommandResult { + stdout: "Updated package lists".to_string(), + stderr: String::new(), + success: true, + code: 0, + }) + } else { + Err(PackageError::CommandFailed("Mock update failed".to_string())) + } + } + + fn upgrade(&self) -> Result { + *self.upgrade_called.lock().unwrap() = true; + if self.should_succeed { + Ok(CommandResult { + stdout: "Upgraded packages".to_string(), + stderr: String::new(), + success: true, + code: 0, + }) + } else { + Err(PackageError::CommandFailed("Mock upgrade failed".to_string())) + } + } + + fn list_installed(&self) -> Result, PackageError> { + *self.list_installed_called.lock().unwrap() = true; + if self.should_succeed { + Ok(vec!["package1".to_string(), "package2".to_string()]) + } else { + Err(PackageError::CommandFailed("Mock list_installed failed".to_string())) + } + } + + fn search(&self, query: &str) -> Result, PackageError> { + *self.search_called.lock().unwrap() = true; + if self.should_succeed { + Ok(vec![format!("result1-{}", query), format!("result2-{}", query)]) + } else { + Err(PackageError::CommandFailed("Mock search failed".to_string())) + } + } + + fn is_installed(&self, package: &str) -> Result { + *self.is_installed_called.lock().unwrap() = true; + if self.should_succeed { + Ok(package == "installed-package") + } else { + Err(PackageError::CommandFailed("Mock is_installed failed".to_string())) + } + } + } + + // Custom PackHero for testing with a mock package manager + struct TestPackHero { + platform: Platform, + debug: bool, + mock_manager: MockPackageManager, + } + + impl TestPackHero { + fn new(platform: Platform, debug: bool, should_succeed: bool) -> Self { + Self { + platform, + debug, + mock_manager: MockPackageManager::new(debug, should_succeed), + } + } + + fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> { + match self.platform { + Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager), + Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())), + } + } + + fn install(&self, package: &str) -> Result { + let pm = self.get_package_manager()?; + pm.install(package) + } + + fn remove(&self, package: &str) -> Result { + let pm = self.get_package_manager()?; + pm.remove(package) + } + + fn update(&self) -> Result { + let pm = self.get_package_manager()?; + pm.update() + } + + fn upgrade(&self) -> Result { + let pm = self.get_package_manager()?; + pm.upgrade() + } + + fn list_installed(&self) -> Result, PackageError> { + let pm = self.get_package_manager()?; + pm.list_installed() + } + + fn search(&self, query: &str) -> Result, PackageError> { + let pm = self.get_package_manager()?; + pm.search(query) + } + + fn is_installed(&self, package: &str) -> Result { + let pm = self.get_package_manager()?; + pm.is_installed(package) + } + } + + #[test] + fn test_packhero_with_mock_success() { + // Test PackHero with a mock package manager that succeeds + let hero = TestPackHero::new(Platform::Ubuntu, false, true); + + // Test install + let result = hero.install("test-package"); + assert!(result.is_ok()); + assert!(*hero.mock_manager.install_called.lock().unwrap()); + + // Test remove + let result = hero.remove("test-package"); + assert!(result.is_ok()); + assert!(*hero.mock_manager.remove_called.lock().unwrap()); + + // Test update + let result = hero.update(); + assert!(result.is_ok()); + assert!(*hero.mock_manager.update_called.lock().unwrap()); + + // Test upgrade + let result = hero.upgrade(); + assert!(result.is_ok()); + assert!(*hero.mock_manager.upgrade_called.lock().unwrap()); + + // Test list_installed + let result = hero.list_installed(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["package1".to_string(), "package2".to_string()]); + assert!(*hero.mock_manager.list_installed_called.lock().unwrap()); + + // Test search + let result = hero.search("query"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["result1-query".to_string(), "result2-query".to_string()]); + assert!(*hero.mock_manager.search_called.lock().unwrap()); + + // Test is_installed + let result = hero.is_installed("installed-package"); + assert!(result.is_ok()); + assert!(result.unwrap()); + assert!(*hero.mock_manager.is_installed_called.lock().unwrap()); + + let result = hero.is_installed("not-installed-package"); + assert!(result.is_ok()); + assert!(!result.unwrap()); + } + + #[test] + fn test_packhero_with_mock_failure() { + // Test PackHero with a mock package manager that fails + let hero = TestPackHero::new(Platform::Ubuntu, false, false); + + // Test install + let result = hero.install("test-package"); + assert!(result.is_err()); + assert!(*hero.mock_manager.install_called.lock().unwrap()); + + // Test remove + let result = hero.remove("test-package"); + assert!(result.is_err()); + assert!(*hero.mock_manager.remove_called.lock().unwrap()); + + // Test update + let result = hero.update(); + assert!(result.is_err()); + assert!(*hero.mock_manager.update_called.lock().unwrap()); + + // Test upgrade + let result = hero.upgrade(); + assert!(result.is_err()); + assert!(*hero.mock_manager.upgrade_called.lock().unwrap()); + + // Test list_installed + let result = hero.list_installed(); + assert!(result.is_err()); + assert!(*hero.mock_manager.list_installed_called.lock().unwrap()); + + // Test search + let result = hero.search("query"); + assert!(result.is_err()); + assert!(*hero.mock_manager.search_called.lock().unwrap()); + + // Test is_installed + let result = hero.is_installed("installed-package"); + assert!(result.is_err()); + assert!(*hero.mock_manager.is_installed_called.lock().unwrap()); + } + + #[test] + fn test_packhero_unsupported_platform() { + // Test PackHero with an unsupported platform + let hero = TestPackHero::new(Platform::Unknown, false, true); + + // All operations should fail with UnsupportedPlatform error + let result = hero.install("test-package"); + assert!(result.is_err()); + match result { + Err(PackageError::UnsupportedPlatform(_)) => (), + _ => panic!("Expected UnsupportedPlatform error"), + } + + let result = hero.remove("test-package"); + assert!(result.is_err()); + match result { + Err(PackageError::UnsupportedPlatform(_)) => (), + _ => panic!("Expected UnsupportedPlatform error"), + } + + let result = hero.update(); + assert!(result.is_err()); + match result { + Err(PackageError::UnsupportedPlatform(_)) => (), + _ => panic!("Expected UnsupportedPlatform error"), + } + } + + // Real-world tests that actually install and remove packages on Ubuntu + // These tests will only run on Ubuntu and will be skipped on other platforms + #[test] + fn test_real_package_operations_on_ubuntu() { + // Check if we're on Ubuntu + let platform = Platform::detect(); + if platform != Platform::Ubuntu { + println!("Skipping real package operations test on non-Ubuntu platform: {:?}", platform); + return; + } + + println!("Running real package operations test on Ubuntu"); + + // Create a PackHero instance with debug enabled + let mut hero = PackHero::new(); + hero.set_debug(true); + + // Test package to install/remove + let test_package = "wget"; + + // First, check if the package is already installed + let is_installed_before = match hero.is_installed(test_package) { + Ok(result) => result, + Err(e) => { + println!("Error checking if package is installed: {}", e); + return; + } + }; + + println!("Package {} is installed before test: {}", test_package, is_installed_before); + + // If the package is already installed, we'll remove it first + if is_installed_before { + println!("Removing existing package {} before test", test_package); + match hero.remove(test_package) { + Ok(_) => println!("Successfully removed package {}", test_package), + Err(e) => { + println!("Error removing package {}: {}", test_package, e); + return; + } + } + + // Verify it was removed + match hero.is_installed(test_package) { + Ok(is_installed) => { + if is_installed { + println!("Failed to remove package {}", test_package); + return; + } else { + println!("Verified package {} was removed", test_package); + } + }, + Err(e) => { + println!("Error checking if package is installed after removal: {}", e); + return; + } + } + } + + // Now install the package + println!("Installing package {}", test_package); + match hero.install(test_package) { + Ok(_) => println!("Successfully installed package {}", test_package), + Err(e) => { + println!("Error installing package {}: {}", test_package, e); + return; + } + } + + // Verify it was installed + match hero.is_installed(test_package) { + Ok(is_installed) => { + if !is_installed { + println!("Failed to install package {}", test_package); + return; + } else { + println!("Verified package {} was installed", test_package); + } + }, + Err(e) => { + println!("Error checking if package is installed after installation: {}", e); + return; + } + } + + // Test the search functionality + println!("Searching for packages with 'wget'"); + match hero.search("wget") { + Ok(results) => { + println!("Search results: {:?}", results); + assert!(results.iter().any(|r| r.contains("wget")), "Search results should contain wget"); + }, + Err(e) => { + println!("Error searching for packages: {}", e); + return; + } + } + + // Test listing installed packages + println!("Listing installed packages"); + match hero.list_installed() { + Ok(packages) => { + println!("Found {} installed packages", packages.len()); + // Check if our test package is in the list + assert!(packages.iter().any(|p| p == test_package), + "Installed packages list should contain {}", test_package); + }, + Err(e) => { + println!("Error listing installed packages: {}", e); + return; + } + } + + // Now remove the package if it wasn't installed before + if !is_installed_before { + println!("Removing package {} after test", test_package); + match hero.remove(test_package) { + Ok(_) => println!("Successfully removed package {}", test_package), + Err(e) => { + println!("Error removing package {}: {}", test_package, e); + return; + } + } + + // Verify it was removed + match hero.is_installed(test_package) { + Ok(is_installed) => { + if is_installed { + println!("Failed to remove package {}", test_package); + return; + } else { + println!("Verified package {} was removed", test_package); + } + }, + Err(e) => { + println!("Error checking if package is installed after removal: {}", e); + return; + } + } + } + + // Test update functionality + println!("Testing package list update"); + match hero.update() { + Ok(_) => println!("Successfully updated package lists"), + Err(e) => { + println!("Error updating package lists: {}", e); + return; + } + } + + println!("All real package operations tests passed on Ubuntu"); + } + + // Test to check if apt-get is available on the system + #[test] + fn test_apt_get_availability() { + // This test checks if apt-get is available on the system + let output = StdCommand::new("which") + .arg("apt-get") + .output() + .expect("Failed to execute which apt-get"); + + let success = output.status.success(); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + + println!("apt-get available: {}", success); + if success { + println!("apt-get path: {}", stdout.trim()); + } + + // On Ubuntu, this should pass + if Platform::detect() == Platform::Ubuntu { + assert!(success, "apt-get should be available on Ubuntu"); + } + } } \ No newline at end of file diff --git a/src/rhai/error.rs b/src/rhai/error.rs index dc25d2c..c5e7037 100644 --- a/src/rhai/error.rs +++ b/src/rhai/error.rs @@ -4,6 +4,7 @@ use rhai::{EvalAltResult, Position}; use crate::os::{FsError, DownloadError}; +use crate::os::package::PackageError; /// Convert a FsError to a Rhai EvalAltResult pub fn fs_error_to_rhai_error(err: FsError) -> Box { @@ -23,6 +24,15 @@ pub fn download_error_to_rhai_error(err: DownloadError) -> Box { )) } +/// Convert a PackageError to a Rhai EvalAltResult +pub fn package_error_to_rhai_error(err: PackageError) -> Box { + let err_msg = err.to_string(); + Box::new(EvalAltResult::ErrorRuntime( + err_msg.into(), + Position::NONE + )) +} + /// Register error types with the Rhai engine pub fn register_error_types(engine: &mut rhai::Engine) -> Result<(), Box> { // Register helper functions for error handling @@ -38,6 +48,10 @@ pub fn register_error_types(engine: &mut rhai::Engine) -> Result<(), Box String { + format!("Package management error: {}", err_msg) + }); + Ok(()) } @@ -57,4 +71,10 @@ impl ToRhaiError for Result { fn to_rhai_error(self) -> Result> { self.map_err(download_error_to_rhai_error) } +} + +impl ToRhaiError for Result { + fn to_rhai_error(self) -> Result> { + self.map_err(package_error_to_rhai_error) + } } \ No newline at end of file diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index 77fa96e..0e5a6c7 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -10,6 +10,7 @@ mod buildah; mod nerdctl; mod git; mod text; +mod rfs; #[cfg(test)] mod tests; @@ -53,6 +54,9 @@ pub use nerdctl::{ nerdctl_image_pull, nerdctl_image_commit, nerdctl_image_build }; +// Re-export RFS module +pub use rfs::register as register_rfs_module; + // Re-export git module pub use git::register_git_module; pub use crate::git::{GitTree, GitRepo}; @@ -103,12 +107,16 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { // Register Nerdctl module functions nerdctl::register_nerdctl_module(engine)?; + // Register Git module functions git::register_git_module(engine)?; // Register Text module functions text::register_text_module(engine)?; + // Register RFS module functions + rfs::register(engine)?; + // Future modules can be registered here diff --git a/src/rhai/rfs.rs b/src/rhai/rfs.rs new file mode 100644 index 0000000..de4b0c8 --- /dev/null +++ b/src/rhai/rfs.rs @@ -0,0 +1,292 @@ +use rhai::{Engine, EvalAltResult, Map, Array}; +use crate::virt::rfs::{ + RfsBuilder, MountType, StoreSpec, + list_mounts, unmount_all, unmount, get_mount_info, + pack_directory, unpack, list_contents, verify +}; + +/// Register RFS functions with the Rhai engine +pub fn register(engine: &mut Engine) -> Result<(), Box> { + // Register mount functions + engine.register_fn("rfs_mount", rfs_mount); + engine.register_fn("rfs_unmount", rfs_unmount); + engine.register_fn("rfs_list_mounts", rfs_list_mounts); + engine.register_fn("rfs_unmount_all", rfs_unmount_all); + engine.register_fn("rfs_get_mount_info", rfs_get_mount_info); + + // Register pack functions + engine.register_fn("rfs_pack", rfs_pack); + engine.register_fn("rfs_unpack", rfs_unpack); + engine.register_fn("rfs_list_contents", rfs_list_contents); + engine.register_fn("rfs_verify", rfs_verify); + + Ok(()) +} + +/// Mount a filesystem +/// +/// # Arguments +/// +/// * `source` - Source path or URL +/// * `target` - Target mount point +/// * `mount_type` - Mount type (e.g., "local", "ssh", "s3", "webdav") +/// * `options` - Mount options as a map +/// +/// # Returns +/// +/// * `Result>` - Mount information or error +fn rfs_mount(source: &str, target: &str, mount_type: &str, options: Map) -> Result> { + // Convert mount type string to MountType enum + let mount_type_enum = MountType::from_string(mount_type); + + // Create a builder + let mut builder = RfsBuilder::new(source, target, mount_type_enum); + + // Add options + for (key, value) in options.iter() { + if let Ok(value_str) = value.clone().into_string() { + builder = builder.with_option(key, &value_str); + } + } + + // Mount the filesystem + let mount = builder.mount() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to mount filesystem: {}", e).into(), + rhai::Position::NONE + )))?; + + // Convert Mount to Map + let mut result = Map::new(); + result.insert("id".into(), mount.id.into()); + result.insert("source".into(), mount.source.into()); + result.insert("target".into(), mount.target.into()); + result.insert("fs_type".into(), mount.fs_type.into()); + + let options_array: Array = mount.options.iter() + .map(|opt| opt.clone().into()) + .collect(); + result.insert("options".into(), options_array.into()); + + Ok(result) +} + +/// Unmount a filesystem +/// +/// # Arguments +/// +/// * `target` - Target mount point +/// +/// # Returns +/// +/// * `Result<(), Box>` - Success or error +fn rfs_unmount(target: &str) -> Result<(), Box> { + unmount(target) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to unmount filesystem: {}", e).into(), + rhai::Position::NONE + ))) +} + +/// List all mounted filesystems +/// +/// # Returns +/// +/// * `Result>` - List of mounts or error +fn rfs_list_mounts() -> Result> { + let mounts = list_mounts() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to list mounts: {}", e).into(), + rhai::Position::NONE + )))?; + + let mut result = Array::new(); + + for mount in mounts { + let mut mount_map = Map::new(); + mount_map.insert("id".into(), mount.id.into()); + mount_map.insert("source".into(), mount.source.into()); + mount_map.insert("target".into(), mount.target.into()); + mount_map.insert("fs_type".into(), mount.fs_type.into()); + + let options_array: Array = mount.options.iter() + .map(|opt| opt.clone().into()) + .collect(); + mount_map.insert("options".into(), options_array.into()); + + result.push(mount_map.into()); + } + + Ok(result) +} + +/// Unmount all filesystems +/// +/// # Returns +/// +/// * `Result<(), Box>` - Success or error +fn rfs_unmount_all() -> Result<(), Box> { + unmount_all() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to unmount all filesystems: {}", e).into(), + rhai::Position::NONE + ))) +} + +/// Get information about a mounted filesystem +/// +/// # Arguments +/// +/// * `target` - Target mount point +/// +/// # Returns +/// +/// * `Result>` - Mount information or error +fn rfs_get_mount_info(target: &str) -> Result> { + let mount = get_mount_info(target) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get mount info: {}", e).into(), + rhai::Position::NONE + )))?; + + let mut result = Map::new(); + result.insert("id".into(), mount.id.into()); + result.insert("source".into(), mount.source.into()); + result.insert("target".into(), mount.target.into()); + result.insert("fs_type".into(), mount.fs_type.into()); + + let options_array: Array = mount.options.iter() + .map(|opt| opt.clone().into()) + .collect(); + result.insert("options".into(), options_array.into()); + + Ok(result) +} + +/// Pack a directory into a filesystem layer +/// +/// # Arguments +/// +/// * `directory` - Directory to pack +/// * `output` - Output file +/// * `store_specs` - Store specifications as a string (e.g., "file:path=/path/to/store,s3:bucket=my-bucket") +/// +/// # Returns +/// +/// * `Result<(), Box>` - Success or error +fn rfs_pack(directory: &str, output: &str, store_specs: &str) -> Result<(), Box> { + // Parse store specs + let specs = parse_store_specs(store_specs); + + // Pack the directory + pack_directory(directory, output, &specs) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to pack directory: {}", e).into(), + rhai::Position::NONE + ))) +} + +/// Unpack a filesystem layer +/// +/// # Arguments +/// +/// * `input` - Input file +/// * `directory` - Directory to unpack to +/// +/// # Returns +/// +/// * `Result<(), Box>` - Success or error +fn rfs_unpack(input: &str, directory: &str) -> Result<(), Box> { + unpack(input, directory) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to unpack filesystem layer: {}", e).into(), + rhai::Position::NONE + ))) +} + +/// List the contents of a filesystem layer +/// +/// # Arguments +/// +/// * `input` - Input file +/// +/// # Returns +/// +/// * `Result>` - File listing or error +fn rfs_list_contents(input: &str) -> Result> { + list_contents(input) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to list contents: {}", e).into(), + rhai::Position::NONE + ))) +} + +/// Verify a filesystem layer +/// +/// # Arguments +/// +/// * `input` - Input file +/// +/// # Returns +/// +/// * `Result>` - Whether the layer is valid or error +fn rfs_verify(input: &str) -> Result> { + verify(input) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to verify filesystem layer: {}", e).into(), + rhai::Position::NONE + ))) +} + +/// Parse store specifications from a string +/// +/// # Arguments +/// +/// * `specs_str` - Store specifications as a string +/// +/// # Returns +/// +/// * `Vec` - Store specifications +fn parse_store_specs(specs_str: &str) -> Vec { + let mut result = Vec::new(); + + // Split by comma + for spec_str in specs_str.split(',') { + // Skip empty specs + if spec_str.trim().is_empty() { + continue; + } + + // Split by colon to get type and options + let parts: Vec<&str> = spec_str.split(':').collect(); + + if parts.is_empty() { + continue; + } + + // Get spec type + let spec_type = parts[0].trim(); + + // Create store spec + let mut store_spec = StoreSpec::new(spec_type); + + // Add options if any + if parts.len() > 1 { + let options_str = parts[1]; + + // Split options by comma + for option in options_str.split(',') { + // Split option by equals sign + let option_parts: Vec<&str> = option.split('=').collect(); + + if option_parts.len() == 2 { + store_spec = store_spec.with_option(option_parts[0].trim(), option_parts[1].trim()); + } + } + } + + result.push(store_spec); + } + + result +} \ No newline at end of file diff --git a/src/rhaiexamples/package_management.rhai b/src/rhaiexamples/package_management.rhai index 1701d08..37d55ab 100644 --- a/src/rhaiexamples/package_management.rhai +++ b/src/rhaiexamples/package_management.rhai @@ -1,27 +1,27 @@ -// Example script demonstrating the package management functions +// Example script demonstrating the mypackage management functions // Set debug mode to true to see detailed output package_set_debug(true); -// Function to demonstrate package management on Ubuntu +// Function to demonstrate mypackage management on Ubuntu fn demo_ubuntu() { - print("Demonstrating package management on Ubuntu..."); + print("Demonstrating mypackage management on Ubuntu..."); - // Update package lists - print("Updating package lists..."); + // Update mypackage lists + print("Updating mypackage lists..."); let result = package_update(); print(`Update result: ${result}`); - // Check if a package is installed - let package = "htop"; - print(`Checking if ${package} is installed...`); - let is_installed = package_is_installed(package); - print(`${package} is installed: ${is_installed}`); + // Check if a mypackage is installed + let mypackage = "htop"; + print(`Checking if ${mypackage} is installed...`); + let is_installed = package_is_installed(mypackage); + print(`${mypackage} is installed: ${is_installed}`); - // Install a package if not already installed + // Install a mypackage if not already installed if !is_installed { - print(`Installing ${package}...`); - let install_result = package_install(package); + print(`Installing ${mypackage}...`); + let install_result = package_install(mypackage); print(`Install result: ${install_result}`); } @@ -41,33 +41,33 @@ fn demo_ubuntu() { print(` - ${search_results[i]}`); } - // Remove the package if we installed it + // Remove the mypackage if we installed it if !is_installed { - print(`Removing ${package}...`); - let remove_result = package_remove(package); + print(`Removing ${mypackage}...`); + let remove_result = package_remove(mypackage); print(`Remove result: ${remove_result}`); } } -// Function to demonstrate package management on macOS +// Function to demonstrate mypackage management on macOS fn demo_macos() { - print("Demonstrating package management on macOS..."); + print("Demonstrating mypackage management on macOS..."); - // Update package lists - print("Updating package lists..."); + // Update mypackage lists + print("Updating mypackage lists..."); let result = package_update(); print(`Update result: ${result}`); - // Check if a package is installed - let package = "wget"; - print(`Checking if ${package} is installed...`); - let is_installed = package_is_installed(package); - print(`${package} is installed: ${is_installed}`); + // Check if a mypackage is installed + let mypackage = "wget"; + print(`Checking if ${mypackage} is installed...`); + let is_installed = package_is_installed(mypackage); + print(`${mypackage} is installed: ${is_installed}`); - // Install a package if not already installed + // Install a mypackage if not already installed if !is_installed { - print(`Installing ${package}...`); - let install_result = package_install(package); + print(`Installing ${mypackage}...`); + let install_result = package_install(mypackage); print(`Install result: ${install_result}`); } @@ -87,10 +87,10 @@ fn demo_macos() { print(` - ${search_results[i]}`); } - // Remove the package if we installed it + // Remove the mypackage if we installed it if !is_installed { - print(`Removing ${package}...`); - let remove_result = package_remove(package); + print(`Removing ${mypackage}...`); + let remove_result = package_remove(mypackage); print(`Remove result: ${remove_result}`); } } diff --git a/src/rhaiexamples/rfs_example.rhai b/src/rhaiexamples/rfs_example.rhai new file mode 100644 index 0000000..fb66fc5 --- /dev/null +++ b/src/rhaiexamples/rfs_example.rhai @@ -0,0 +1,121 @@ +// RFS Example Script +// This script demonstrates how to use the RFS wrapper in Rhai + +// Mount a local directory +fn mount_local_example() { + print("Mounting a local directory..."); + + // Create a map for mount options + let options = #{ + "readonly": "true" + }; + + // Mount the directory + let mount = rfs_mount("/source/path", "/target/path", "local", options); + + print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`); + + // List all mounts + let mounts = rfs_list_mounts(); + print(`Number of mounts: ${mounts.len()}`); + + for mount in mounts { + print(`Mount ID: ${mount.id}, Source: ${mount.source}, Target: ${mount.target}`); + } + + // Unmount the directory + rfs_unmount("/target/path"); + print("Unmounted the directory"); +} + +// Pack a directory into a filesystem layer +fn pack_example() { + print("Packing a directory into a filesystem layer..."); + + // Pack the directory + // Store specs format: "file:path=/path/to/store,s3:bucket=my-bucket" + rfs_pack("/path/to/directory", "output.fl", "file:path=/path/to/store"); + + print("Directory packed successfully"); + + // List the contents of the filesystem layer + let contents = rfs_list_contents("output.fl"); + print("Contents of the filesystem layer:"); + print(contents); + + // Verify the filesystem layer + let is_valid = rfs_verify("output.fl"); + print(`Is the filesystem layer valid? ${is_valid}`); + + // Unpack the filesystem layer + rfs_unpack("output.fl", "/path/to/unpack"); + print("Filesystem layer unpacked successfully"); +} + +// SSH mount example +fn mount_ssh_example() { + print("Mounting a remote directory via SSH..."); + + // Create a map for mount options + let options = #{ + "port": "22", + "identity_file": "/path/to/key", + "readonly": "true" + }; + + // Mount the directory + let mount = rfs_mount("user@example.com:/remote/path", "/local/mount/point", "ssh", options); + + print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`); + + // Get mount info + let info = rfs_get_mount_info("/local/mount/point"); + print(`Mount info: ${info}`); + + // Unmount the directory + rfs_unmount("/local/mount/point"); + print("Unmounted the directory"); +} + +// S3 mount example +fn mount_s3_example() { + print("Mounting an S3 bucket..."); + + // Create a map for mount options + let options = #{ + "region": "us-east-1", + "access_key": "your-access-key", + "secret_key": "your-secret-key" + }; + + // Mount the S3 bucket + let mount = rfs_mount("s3://my-bucket", "/mnt/s3", "s3", options); + + print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`); + + // Unmount the S3 bucket + rfs_unmount("/mnt/s3"); + print("Unmounted the S3 bucket"); +} + +// Unmount all example +fn unmount_all_example() { + print("Unmounting all filesystems..."); + + // Unmount all filesystems + rfs_unmount_all(); + + print("All filesystems unmounted"); +} + +// Run the examples +// Note: These are commented out to prevent accidental execution +// Uncomment the ones you want to run + +// mount_local_example(); +// pack_example(); +// mount_ssh_example(); +// mount_s3_example(); +// unmount_all_example(); + +print("RFS example script completed"); \ No newline at end of file diff --git a/src/virt/mod.rs b/src/virt/mod.rs index 382b57b..6f7bf89 100644 --- a/src/virt/mod.rs +++ b/src/virt/mod.rs @@ -1,2 +1,3 @@ pub mod buildah; -pub mod nerdctl; \ No newline at end of file +pub mod nerdctl; +pub mod rfs; \ No newline at end of file diff --git a/src/virt/rfs/builder.rs b/src/virt/rfs/builder.rs new file mode 100644 index 0000000..a4ec10c --- /dev/null +++ b/src/virt/rfs/builder.rs @@ -0,0 +1,280 @@ +use std::collections::HashMap; +use super::{ + error::RfsError, + cmd::execute_rfs_command, + types::{Mount, MountType, StoreSpec}, +}; + +/// Builder for RFS mount operations +#[derive(Clone)] +pub struct RfsBuilder { + /// Source path or URL + source: String, + /// Target mount point + target: String, + /// Mount type + mount_type: MountType, + /// Mount options + options: HashMap, + /// Mount ID + mount_id: Option, + /// Debug mode + debug: bool, +} + +impl RfsBuilder { + /// Create a new RFS builder + /// + /// # Arguments + /// + /// * `source` - Source path or URL + /// * `target` - Target mount point + /// * `mount_type` - Mount type + /// + /// # Returns + /// + /// * `Self` - New RFS builder + pub fn new(source: &str, target: &str, mount_type: MountType) -> Self { + Self { + source: source.to_string(), + target: target.to_string(), + mount_type, + options: HashMap::new(), + mount_id: None, + debug: false, + } + } + + /// Add a mount option + /// + /// # Arguments + /// + /// * `key` - Option key + /// * `value` - Option value + /// + /// # Returns + /// + /// * `Self` - Updated RFS builder for method chaining + pub fn with_option(mut self, key: &str, value: &str) -> Self { + self.options.insert(key.to_string(), value.to_string()); + self + } + + /// Add multiple mount options + /// + /// # Arguments + /// + /// * `options` - Map of option keys to values + /// + /// # Returns + /// + /// * `Self` - Updated RFS builder for method chaining + pub fn with_options(mut self, options: HashMap<&str, &str>) -> Self { + for (key, value) in options { + self.options.insert(key.to_string(), value.to_string()); + } + self + } + + /// Set debug mode + /// + /// # Arguments + /// + /// * `debug` - Whether to enable debug output + /// + /// # Returns + /// + /// * `Self` - Updated RFS builder for method chaining + pub fn with_debug(mut self, debug: bool) -> Self { + self.debug = debug; + self + } + + /// Mount the filesystem + /// + /// # Returns + /// + /// * `Result` - Mount information or error + pub fn mount(self) -> Result { + // Build the command string + let mut cmd = String::from("mount -t "); + cmd.push_str(&self.mount_type.to_string()); + + // Add options if any + if !self.options.is_empty() { + cmd.push_str(" -o "); + let mut first = true; + for (key, value) in &self.options { + if !first { + cmd.push_str(","); + } + cmd.push_str(key); + cmd.push_str("="); + cmd.push_str(value); + first = false; + } + } + + // Add source and target + cmd.push_str(" "); + cmd.push_str(&self.source); + cmd.push_str(" "); + cmd.push_str(&self.target); + + // Split the command into arguments + let args: Vec<&str> = cmd.split_whitespace().collect(); + + // Execute the command + let result = execute_rfs_command(&args)?; + + // Parse the output to get the mount ID + let mount_id = result.stdout.trim().to_string(); + if mount_id.is_empty() { + return Err(RfsError::MountFailed("Failed to get mount ID".to_string())); + } + + // Create and return the Mount struct + Ok(Mount { + id: mount_id, + source: self.source, + target: self.target, + fs_type: self.mount_type.to_string(), + options: self.options.iter().map(|(k, v)| format!("{}={}", k, v)).collect(), + }) + } + + /// Unmount the filesystem + /// + /// # Returns + /// + /// * `Result<(), RfsError>` - Success or error + pub fn unmount(&self) -> Result<(), RfsError> { + // Execute the unmount command + let result = execute_rfs_command(&["unmount", &self.target])?; + + // Check for errors + if !result.success { + return Err(RfsError::UnmountFailed(format!("Failed to unmount {}: {}", self.target, result.stderr))); + } + + Ok(()) + } +} + +/// Builder for RFS pack operations +#[derive(Clone)] +pub struct PackBuilder { + /// Directory to pack + directory: String, + /// Output file + output: String, + /// Store specifications + store_specs: Vec, + /// Debug mode + debug: bool, +} + +impl PackBuilder { + /// Create a new pack builder + /// + /// # Arguments + /// + /// * `directory` - Directory to pack + /// * `output` - Output file + /// + /// # Returns + /// + /// * `Self` - New pack builder + pub fn new(directory: &str, output: &str) -> Self { + Self { + directory: directory.to_string(), + output: output.to_string(), + store_specs: Vec::new(), + debug: false, + } + } + + /// Add a store specification + /// + /// # Arguments + /// + /// * `store_spec` - Store specification + /// + /// # Returns + /// + /// * `Self` - Updated pack builder for method chaining + pub fn with_store_spec(mut self, store_spec: StoreSpec) -> Self { + self.store_specs.push(store_spec); + self + } + + /// Add multiple store specifications + /// + /// # Arguments + /// + /// * `store_specs` - Store specifications + /// + /// # Returns + /// + /// * `Self` - Updated pack builder for method chaining + pub fn with_store_specs(mut self, store_specs: Vec) -> Self { + self.store_specs.extend(store_specs); + self + } + + /// Set debug mode + /// + /// # Arguments + /// + /// * `debug` - Whether to enable debug output + /// + /// # Returns + /// + /// * `Self` - Updated pack builder for method chaining + pub fn with_debug(mut self, debug: bool) -> Self { + self.debug = debug; + self + } + + /// Pack the directory + /// + /// # Returns + /// + /// * `Result<(), RfsError>` - Success or error + pub fn pack(self) -> Result<(), RfsError> { + // Build the command string + let mut cmd = String::from("pack -m "); + cmd.push_str(&self.output); + + // Add store specs if any + if !self.store_specs.is_empty() { + cmd.push_str(" -s "); + let mut first = true; + for spec in &self.store_specs { + if !first { + cmd.push_str(","); + } + let spec_str = spec.to_string(); + cmd.push_str(&spec_str); + first = false; + } + } + + // Add directory + cmd.push_str(" "); + cmd.push_str(&self.directory); + + // Split the command into arguments + let args: Vec<&str> = cmd.split_whitespace().collect(); + + // Execute the command + let result = execute_rfs_command(&args)?; + + // Check for errors + if !result.success { + return Err(RfsError::PackFailed(format!("Failed to pack {}: {}", self.directory, result.stderr))); + } + + Ok(()) + } +} \ No newline at end of file diff --git a/src/virt/rfs/cmd.rs b/src/virt/rfs/cmd.rs new file mode 100644 index 0000000..bb9e366 --- /dev/null +++ b/src/virt/rfs/cmd.rs @@ -0,0 +1,62 @@ +use crate::process::{run_command, CommandResult}; +use super::error::RfsError; +use std::thread_local; +use std::cell::RefCell; + +// Thread-local storage for debug flag +thread_local! { + static DEBUG: RefCell = RefCell::new(false); +} + +/// Set the thread-local debug flag +pub fn set_thread_local_debug(debug: bool) { + DEBUG.with(|d| { + *d.borrow_mut() = debug; + }); +} + +/// Get the current thread-local debug flag +pub fn thread_local_debug() -> bool { + DEBUG.with(|d| { + *d.borrow() + }) +} + +/// Execute an RFS command with the given arguments +/// +/// # Arguments +/// +/// * `args` - Command arguments +/// +/// # Returns +/// +/// * `Result` - Command result or error +pub fn execute_rfs_command(args: &[&str]) -> Result { + let debug = thread_local_debug(); + + // Construct the command string + let mut cmd = String::from("rfs"); + for arg in args { + cmd.push(' '); + cmd.push_str(arg); + } + + if debug { + println!("Executing RFS command: {}", cmd); + } + + // Execute the command + let result = run_command(&cmd) + .map_err(|e| RfsError::CommandFailed(format!("Failed to execute RFS command: {}", e)))?; + + if debug { + println!("RFS command result: {:?}", result); + } + + // Check if the command was successful + if !result.success && !result.stderr.is_empty() { + return Err(RfsError::CommandFailed(result.stderr)); + } + + Ok(result) +} \ No newline at end of file diff --git a/src/virt/rfs/error.rs b/src/virt/rfs/error.rs new file mode 100644 index 0000000..9c9349d --- /dev/null +++ b/src/virt/rfs/error.rs @@ -0,0 +1,43 @@ +use std::fmt; +use std::error::Error; + +/// Error types for RFS operations +#[derive(Debug)] +pub enum RfsError { + /// Command execution failed + CommandFailed(String), + /// Invalid argument provided + InvalidArgument(String), + /// Mount operation failed + MountFailed(String), + /// Unmount operation failed + UnmountFailed(String), + /// List operation failed + ListFailed(String), + /// Pack operation failed + PackFailed(String), + /// Other error + Other(String), +} + +impl fmt::Display for RfsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RfsError::CommandFailed(msg) => write!(f, "RFS command failed: {}", msg), + RfsError::InvalidArgument(msg) => write!(f, "Invalid argument: {}", msg), + RfsError::MountFailed(msg) => write!(f, "Mount failed: {}", msg), + RfsError::UnmountFailed(msg) => write!(f, "Unmount failed: {}", msg), + RfsError::ListFailed(msg) => write!(f, "List failed: {}", msg), + RfsError::PackFailed(msg) => write!(f, "Pack failed: {}", msg), + RfsError::Other(msg) => write!(f, "Other error: {}", msg), + } + } +} + +impl Error for RfsError {} + +impl From for RfsError { + fn from(error: std::io::Error) -> Self { + RfsError::Other(format!("IO error: {}", error)) + } +} \ No newline at end of file diff --git a/src/virt/rfs/mod.rs b/src/virt/rfs/mod.rs new file mode 100644 index 0000000..bd57ccd --- /dev/null +++ b/src/virt/rfs/mod.rs @@ -0,0 +1,15 @@ +mod cmd; +mod error; +mod mount; +mod pack; +mod builder; +mod types; + +pub use error::RfsError; +pub use builder::{RfsBuilder, PackBuilder}; +pub use types::{Mount, MountType, StoreSpec}; +pub use mount::{list_mounts, unmount_all, unmount, get_mount_info}; +pub use pack::{pack_directory, unpack, list_contents, verify}; + +// Re-export the execute_rfs_command function for use in other modules +pub(crate) use cmd::execute_rfs_command; \ No newline at end of file diff --git a/src/virt/rfs/mount.rs b/src/virt/rfs/mount.rs new file mode 100644 index 0000000..c5e8727 --- /dev/null +++ b/src/virt/rfs/mount.rs @@ -0,0 +1,142 @@ +use super::{ + error::RfsError, + cmd::execute_rfs_command, + types::Mount, +}; + +/// List all mounted filesystems +/// +/// # Returns +/// +/// * `Result, RfsError>` - List of mounts or error +pub fn list_mounts() -> Result, RfsError> { + // Execute the list command + let result = execute_rfs_command(&["list", "--json"])?; + + // Parse the JSON output + match serde_json::from_str::(&result.stdout) { + Ok(json) => { + if let serde_json::Value::Array(mounts_json) = json { + let mut mounts = Vec::new(); + + for mount_json in mounts_json { + // Extract mount ID + let id = match mount_json.get("id").and_then(|v| v.as_str()) { + Some(id) => id.to_string(), + None => return Err(RfsError::ListFailed("Missing mount ID".to_string())), + }; + + // Extract source + let source = match mount_json.get("source").and_then(|v| v.as_str()) { + Some(source) => source.to_string(), + None => return Err(RfsError::ListFailed("Missing source".to_string())), + }; + + // Extract target + let target = match mount_json.get("target").and_then(|v| v.as_str()) { + Some(target) => target.to_string(), + None => return Err(RfsError::ListFailed("Missing target".to_string())), + }; + + // Extract filesystem type + let fs_type = match mount_json.get("type").and_then(|v| v.as_str()) { + Some(fs_type) => fs_type.to_string(), + None => return Err(RfsError::ListFailed("Missing filesystem type".to_string())), + }; + + // Extract options + let options = match mount_json.get("options").and_then(|v| v.as_array()) { + Some(options_array) => { + let mut options_vec = Vec::new(); + for option_value in options_array { + if let Some(option_str) = option_value.as_str() { + options_vec.push(option_str.to_string()); + } + } + options_vec + }, + None => Vec::new(), // Empty vector if no options found + }; + + // Create Mount struct and add to vector + mounts.push(Mount { + id, + source, + target, + fs_type, + options, + }); + } + + Ok(mounts) + } else { + Err(RfsError::ListFailed("Expected JSON array".to_string())) + } + }, + Err(e) => { + Err(RfsError::ListFailed(format!("Failed to parse mount list JSON: {}", e))) + } + } +} + +/// Unmount a filesystem by target path +/// +/// # Arguments +/// +/// * `target` - Target mount point +/// +/// # Returns +/// +/// * `Result<(), RfsError>` - Success or error +pub fn unmount(target: &str) -> Result<(), RfsError> { + // Execute the unmount command + let result = execute_rfs_command(&["unmount", target])?; + + // Check for errors + if !result.success { + return Err(RfsError::UnmountFailed(format!("Failed to unmount {}: {}", target, result.stderr))); + } + + Ok(()) +} + +/// Unmount all filesystems +/// +/// # Returns +/// +/// * `Result<(), RfsError>` - Success or error +pub fn unmount_all() -> Result<(), RfsError> { + // Execute the unmount all command + let result = execute_rfs_command(&["unmount", "--all"])?; + + // Check for errors + if !result.success { + return Err(RfsError::UnmountFailed(format!("Failed to unmount all filesystems: {}", result.stderr))); + } + + Ok(()) +} + +/// Get information about a mounted filesystem +/// +/// # Arguments +/// +/// * `target` - Target mount point +/// +/// # Returns +/// +/// * `Result` - Mount information or error +pub fn get_mount_info(target: &str) -> Result { + // Get all mounts + let mounts = list_mounts()?; + + // Find the mount with the specified target + for mount in mounts { + if mount.target == target { + return Ok(mount); + } + } + + // Mount not found + Err(RfsError::Other(format!("No mount found at {}", target))) +} \ No newline at end of file diff --git a/src/virt/rfs/pack.rs b/src/virt/rfs/pack.rs new file mode 100644 index 0000000..c474055 --- /dev/null +++ b/src/virt/rfs/pack.rs @@ -0,0 +1,100 @@ +use super::{ + error::RfsError, + cmd::execute_rfs_command, + types::StoreSpec, + builder::PackBuilder, +}; + +/// Pack a directory into a filesystem layer +/// +/// # Arguments +/// +/// * `directory` - Directory to pack +/// * `output` - Output file +/// * `store_specs` - Store specifications +/// +/// # Returns +/// +/// * `Result<(), RfsError>` - Success or error +pub fn pack_directory(directory: &str, output: &str, store_specs: &[StoreSpec]) -> Result<(), RfsError> { + // Create a new pack builder + let mut builder = PackBuilder::new(directory, output); + + // Add store specs + for spec in store_specs { + builder = builder.with_store_spec(spec.clone()); + } + + // Pack the directory + builder.pack() +} + +/// Unpack a filesystem layer +/// +/// # Arguments +/// +/// * `input` - Input file +/// * `directory` - Directory to unpack to +/// +/// # Returns +/// +/// * `Result<(), RfsError>` - Success or error +pub fn unpack(input: &str, directory: &str) -> Result<(), RfsError> { + // Execute the unpack command + let result = execute_rfs_command(&["unpack", "-m", input, directory])?; + + // Check for errors + if !result.success { + return Err(RfsError::Other(format!("Failed to unpack {}: {}", input, result.stderr))); + } + + Ok(()) +} + +/// List the contents of a filesystem layer +/// +/// # Arguments +/// +/// * `input` - Input file +/// +/// # Returns +/// +/// * `Result` - File listing or error +pub fn list_contents(input: &str) -> Result { + // Execute the list command + let result = execute_rfs_command(&["list", "-m", input])?; + + // Check for errors + if !result.success { + return Err(RfsError::Other(format!("Failed to list contents of {}: {}", input, result.stderr))); + } + + Ok(result.stdout) +} + +/// Verify a filesystem layer +/// +/// # Arguments +/// +/// * `input` - Input file +/// +/// # Returns +/// +/// * `Result` - Whether the layer is valid or error +pub fn verify(input: &str) -> Result { + // Execute the verify command + let result = execute_rfs_command(&["verify", "-m", input])?; + + // Check for errors + if !result.success { + // If the command failed but returned a specific error about verification, + // return false instead of an error + if result.stderr.contains("verification failed") { + return Ok(false); + } + + return Err(RfsError::Other(format!("Failed to verify {}: {}", input, result.stderr))); + } + + Ok(true) +} \ No newline at end of file diff --git a/src/virt/rfs/types.rs b/src/virt/rfs/types.rs new file mode 100644 index 0000000..9887a11 --- /dev/null +++ b/src/virt/rfs/types.rs @@ -0,0 +1,117 @@ +use std::collections::HashMap; + +/// Represents a mounted filesystem +#[derive(Debug, Clone)] +pub struct Mount { + /// Mount ID + pub id: String, + /// Source path or URL + pub source: String, + /// Target mount point + pub target: String, + /// Filesystem type + pub fs_type: String, + /// Mount options + pub options: Vec, +} + +/// Types of mounts supported by RFS +#[derive(Debug, Clone)] +pub enum MountType { + /// Local filesystem + Local, + /// SSH remote filesystem + SSH, + /// S3 object storage + S3, + /// WebDAV remote filesystem + WebDAV, + /// Custom mount type + Custom(String), +} + +impl MountType { + /// Convert mount type to string representation + pub fn to_string(&self) -> String { + match self { + MountType::Local => "local".to_string(), + MountType::SSH => "ssh".to_string(), + MountType::S3 => "s3".to_string(), + MountType::WebDAV => "webdav".to_string(), + MountType::Custom(s) => s.clone(), + } + } + + /// Create a MountType from a string + pub fn from_string(s: &str) -> Self { + match s.to_lowercase().as_str() { + "local" => MountType::Local, + "ssh" => MountType::SSH, + "s3" => MountType::S3, + "webdav" => MountType::WebDAV, + _ => MountType::Custom(s.to_string()), + } + } +} + +/// Store specification for packing operations +#[derive(Debug, Clone)] +pub struct StoreSpec { + /// Store type (e.g., "file", "s3") + pub spec_type: String, + /// Store options + pub options: HashMap, +} + +impl StoreSpec { + /// Create a new store specification + /// + /// # Arguments + /// + /// * `spec_type` - Store type (e.g., "file", "s3") + /// + /// # Returns + /// + /// * `Self` - New store specification + pub fn new(spec_type: &str) -> Self { + Self { + spec_type: spec_type.to_string(), + options: HashMap::new(), + } + } + + /// Add an option to the store specification + /// + /// # Arguments + /// + /// * `key` - Option key + /// * `value` - Option value + /// + /// # Returns + /// + /// * `Self` - Updated store specification for method chaining + pub fn with_option(mut self, key: &str, value: &str) -> Self { + self.options.insert(key.to_string(), value.to_string()); + self + } + + /// Convert the store specification to a string + /// + /// # Returns + /// + /// * `String` - String representation of the store specification + pub fn to_string(&self) -> String { + let mut result = self.spec_type.clone(); + + if !self.options.is_empty() { + result.push_str(":"); + let options: Vec = self.options + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect(); + result.push_str(&options.join(",")); + } + + result + } +} \ No newline at end of file