feat: Add support for virt package
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
- Add sal-virt package to the workspace members - Update MONOREPO_CONVERSION_PLAN.md to reflect the completion of sal-process and sal-virt packages - Update src/lib.rs to include sal-virt - Update src/postgresclient to use sal-virt instead of local virt module - Update tests to use sal-virt
This commit is contained in:
165
virt/src/rfs/README.md
Normal file
165
virt/src/rfs/README.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# SAL RFS (Remote File System) Module (`sal::virt::rfs`)
|
||||
|
||||
## Overview
|
||||
|
||||
The `sal::virt::rfs` module provides a Rust interface for interacting with an underlying `rfs` command-line tool. This tool facilitates mounting various types of remote and local filesystems and managing packed filesystem layers.
|
||||
|
||||
The module allows Rust applications and `herodo` Rhai scripts to:
|
||||
- Mount and unmount filesystems from different sources (e.g., local paths, SSH, S3, WebDAV).
|
||||
- List currently mounted filesystems and retrieve information about specific mounts.
|
||||
- Pack directories into filesystem layers, potentially using specified storage backends.
|
||||
- Unpack, list contents of, and verify these filesystem layers.
|
||||
|
||||
All operations are performed by invoking the `rfs` CLI tool and parsing its output.
|
||||
|
||||
## Key Design Points
|
||||
|
||||
- **CLI Wrapper**: This module acts as a wrapper around an external `rfs` command-line utility. The actual filesystem operations and layer management are delegated to this tool.
|
||||
- **Asynchronous Operations (Implicit)**: While the Rust functions themselves might be synchronous, the underlying `execute_rfs_command` (presumably from `super::cmd`) likely handles command execution, which could be asynchronous or blocking depending on its implementation.
|
||||
- **Filesystem Abstraction**: Supports mounting diverse filesystem types such as `local`, `ssh`, `s3`, and `webdav` through the `rfs` tool's capabilities.
|
||||
- **Layer Management**: Provides functionalities to `pack` directories into portable layers, `unpack` them, `list_contents`, and `verify` their integrity. This is useful for creating and managing reproducible filesystem snapshots or components.
|
||||
- **Store Specifications (`StoreSpec`)**: The packing functionality allows specifying `StoreSpec` types, suggesting that packed layers can be stored or referenced using different backend mechanisms (e.g., local files, S3 buckets). This enables flexible storage and retrieval of filesystem layers.
|
||||
- **Builder Pattern**: Uses `RfsBuilder` for constructing mount commands with various options and `PackBuilder` for packing operations, providing a fluent interface for complex configurations.
|
||||
- **Rhai Scriptability**: Most functionalities are exposed to Rhai scripts via `herodo` through the `sal::rhai::rfs` bridge, enabling automation of filesystem and layer management tasks.
|
||||
- **Structured Error Handling**: Defines `RfsError` for specific error conditions encountered during `rfs` command execution or output parsing.
|
||||
|
||||
## Rhai Scripting with `herodo`
|
||||
|
||||
The `sal::virt::rfs` module is scriptable via `herodo`. The following functions are available in Rhai, prefixed with `rfs_`:
|
||||
|
||||
### Mount Operations
|
||||
|
||||
- `rfs_mount(source: String, target: String, mount_type: String, options: Map) -> Map`
|
||||
- Mounts a filesystem.
|
||||
- `source`: The source path or URL (e.g., `/path/to/local_dir`, `ssh://user@host:/remote/path`, `s3://bucket/key`).
|
||||
- `target`: The local path where the filesystem will be mounted.
|
||||
- `mount_type`: A string specifying the type of filesystem (e.g., "local", "ssh", "s3", "webdav").
|
||||
- `options`: A Rhai map of additional mount options (e.g., `#{ "read_only": true, "uid": 1000 }`).
|
||||
- Returns a map containing details of the mount (id, source, target, fs_type, options) on success.
|
||||
|
||||
- `rfs_unmount(target: String) -> ()`
|
||||
- Unmounts the filesystem at the specified target path.
|
||||
|
||||
- `rfs_list_mounts() -> Array`
|
||||
- Lists all currently mounted filesystems managed by `rfs`.
|
||||
- Returns an array of maps, each representing a mount with its details.
|
||||
|
||||
- `rfs_unmount_all() -> ()`
|
||||
- Unmounts all filesystems currently managed by `rfs`.
|
||||
|
||||
- `rfs_get_mount_info(target: String) -> Map`
|
||||
- Retrieves information about a specific mounted filesystem.
|
||||
- Returns a map with mount details if found.
|
||||
|
||||
### Pack/Layer Operations
|
||||
|
||||
- `rfs_pack(directory: String, output: String, store_specs: String) -> ()`
|
||||
- Packs the contents of a `directory` into an `output` file (layer).
|
||||
- `store_specs`: A comma-separated string defining storage specifications for the layer (e.g., `"file:path=/path/to/local_store,s3:bucket=my-archive,region=us-west-1"`). Each spec is `type:key=value,key2=value2`.
|
||||
|
||||
- `rfs_unpack(input: String, directory: String) -> ()`
|
||||
- Unpacks an `input` layer file into the specified `directory`.
|
||||
|
||||
- `rfs_list_contents(input: String) -> String`
|
||||
- Lists the contents of an `input` layer file.
|
||||
- Returns a string containing the file listing (raw output from the `rfs` tool).
|
||||
|
||||
- `rfs_verify(input: String) -> bool`
|
||||
- Verifies the integrity of an `input` layer file.
|
||||
- Returns `true` if the layer is valid, `false` otherwise.
|
||||
|
||||
### Rhai Example
|
||||
|
||||
```rhai
|
||||
// Example: Mounting a local directory (ensure /mnt/my_local_mount exists)
|
||||
let source_dir = "/tmp/my_data_source"; // Create this directory first
|
||||
let target_mount = "/mnt/my_local_mount";
|
||||
|
||||
// Create source_dir if it doesn't exist for the example to run
|
||||
// In a real script, you might use sal::os::dir_create or ensure it exists.
|
||||
// For this example, assume it's manually created or use: os_run_command(`mkdir -p ${source_dir}`);
|
||||
|
||||
print(`Mounting ${source_dir} to ${target_mount}...`);
|
||||
let mount_result = rfs_mount(source_dir, target_mount, "local", #{});
|
||||
if mount_result.is_ok() {
|
||||
print(`Mount successful: ${mount_result}`);
|
||||
} else {
|
||||
print(`Mount failed: ${mount_result}`);
|
||||
}
|
||||
|
||||
// List mounts
|
||||
print("\nCurrent mounts:");
|
||||
let mounts = rfs_list_mounts();
|
||||
if mounts.is_ok() {
|
||||
for m in mounts {
|
||||
print(` Target: ${m.target}, Source: ${m.source}, Type: ${m.fs_type}`);
|
||||
}
|
||||
} else {
|
||||
print(`Error listing mounts: ${mounts}`);
|
||||
}
|
||||
|
||||
// Example: Packing a directory
|
||||
let dir_to_pack = "/tmp/pack_this_dir"; // Create and populate this directory
|
||||
let packed_file = "/tmp/my_layer.pack";
|
||||
// os_run_command(`mkdir -p ${dir_to_pack}`);
|
||||
// os_run_command(`echo 'hello' > ${dir_to_pack}/file1.txt`);
|
||||
|
||||
print(`\nPacking ${dir_to_pack} to ${packed_file}...`);
|
||||
// Using a file-based store spec for simplicity
|
||||
let pack_store_specs = "file:path=/tmp/rfs_store";
|
||||
// os_run_command(`mkdir -p /tmp/rfs_store`);
|
||||
|
||||
let pack_result = rfs_pack(dir_to_pack, packed_file, pack_store_specs);
|
||||
if pack_result.is_ok() {
|
||||
print("Packing successful.");
|
||||
|
||||
// List contents of the packed file
|
||||
print(`\nContents of ${packed_file}:`);
|
||||
let contents = rfs_list_contents(packed_file);
|
||||
if contents.is_ok() {
|
||||
print(contents);
|
||||
} else {
|
||||
print(`Error listing contents: ${contents}`);
|
||||
}
|
||||
|
||||
// Verify the packed file
|
||||
print(`\nVerifying ${packed_file}...`);
|
||||
let verify_result = rfs_verify(packed_file);
|
||||
if verify_result.is_ok() && verify_result {
|
||||
print("Verification successful: Layer is valid.");
|
||||
} else {
|
||||
print(`Verification failed or error: ${verify_result}`);
|
||||
}
|
||||
|
||||
// Example: Unpacking
|
||||
let unpack_dir = "/tmp/unpacked_layer_here";
|
||||
// os_run_command(`mkdir -p ${unpack_dir}`);
|
||||
print(`\nUnpacking ${packed_file} to ${unpack_dir}...`);
|
||||
let unpack_result = rfs_unpack(packed_file, unpack_dir);
|
||||
if unpack_result.is_ok() {
|
||||
print("Unpacking successful.");
|
||||
// You would typically check contents of unpack_dir here
|
||||
// os_run_command(`ls -la ${unpack_dir}`);
|
||||
} else {
|
||||
print(`Error unpacking: ${unpack_result}`);
|
||||
}
|
||||
|
||||
} else {
|
||||
print(`Error packing: ${pack_result}`);
|
||||
}
|
||||
|
||||
// Cleanup: Unmount the local mount
|
||||
if mount_result.is_ok() {
|
||||
print(`\nUnmounting ${target_mount}...`);
|
||||
rfs_unmount(target_mount);
|
||||
}
|
||||
|
||||
// To run this example, ensure the 'rfs' command-line tool is installed and configured,
|
||||
// and that the necessary directories (/tmp/my_data_source, /mnt/my_local_mount, etc.)
|
||||
// exist and have correct permissions.
|
||||
// You might need to run herodo with sudo for mount/unmount operations.
|
||||
|
||||
print("\nRFS Rhai script finished.");
|
||||
```
|
||||
|
||||
This module provides a flexible way to manage diverse filesystems and filesystem layers, making it a powerful tool for system automation and deployment tasks within the SAL ecosystem.
|
372
virt/src/rfs/builder.rs
Normal file
372
virt/src/rfs/builder.rs
Normal file
@@ -0,0 +1,372 @@
|
||||
use super::{
|
||||
cmd::execute_rfs_command,
|
||||
error::RfsError,
|
||||
types::{Mount, MountType, StoreSpec},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 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<String, String>,
|
||||
/// Mount ID
|
||||
#[allow(dead_code)]
|
||||
mount_id: Option<String>,
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Get the source path
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `&str` - Source path
|
||||
pub fn source(&self) -> &str {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// Get the target path
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `&str` - Target path
|
||||
pub fn target(&self) -> &str {
|
||||
&self.target
|
||||
}
|
||||
|
||||
/// Get the mount type
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `&MountType` - Mount type
|
||||
pub fn mount_type(&self) -> &MountType {
|
||||
&self.mount_type
|
||||
}
|
||||
|
||||
/// Get the options
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `&HashMap<String, String>` - Mount options
|
||||
pub fn options(&self) -> &HashMap<String, String> {
|
||||
&self.options
|
||||
}
|
||||
|
||||
/// Get debug mode
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `bool` - Whether debug mode is enabled
|
||||
pub fn debug(&self) -> bool {
|
||||
self.debug
|
||||
}
|
||||
|
||||
/// Mount the filesystem
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Mount, RfsError>` - Mount information or error
|
||||
pub fn mount(self) -> Result<Mount, RfsError> {
|
||||
// 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<StoreSpec>,
|
||||
/// 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<StoreSpec>) -> 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
|
||||
}
|
||||
|
||||
/// Get the directory path
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `&str` - Directory path
|
||||
pub fn directory(&self) -> &str {
|
||||
&self.directory
|
||||
}
|
||||
|
||||
/// Get the output path
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `&str` - Output path
|
||||
pub fn output(&self) -> &str {
|
||||
&self.output
|
||||
}
|
||||
|
||||
/// Get the store specifications
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `&Vec<StoreSpec>` - Store specifications
|
||||
pub fn store_specs(&self) -> &Vec<StoreSpec> {
|
||||
&self.store_specs
|
||||
}
|
||||
|
||||
/// Get debug mode
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `bool` - Whether debug mode is enabled
|
||||
pub fn debug(&self) -> bool {
|
||||
self.debug
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
}
|
61
virt/src/rfs/cmd.rs
Normal file
61
virt/src/rfs/cmd.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use super::error::RfsError;
|
||||
use sal_process::{run_command, CommandResult};
|
||||
use std::cell::RefCell;
|
||||
use std::thread_local;
|
||||
|
||||
// Thread-local storage for debug flag
|
||||
thread_local! {
|
||||
static DEBUG: RefCell<bool> = RefCell::new(false);
|
||||
}
|
||||
|
||||
/// Set the thread-local debug flag
|
||||
#[allow(dead_code)]
|
||||
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<CommandResult, RfsError>` - Command result or error
|
||||
pub fn execute_rfs_command(args: &[&str]) -> Result<CommandResult, RfsError> {
|
||||
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)
|
||||
}
|
43
virt/src/rfs/error.rs
Normal file
43
virt/src/rfs/error.rs
Normal file
@@ -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<std::io::Error> for RfsError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
RfsError::Other(format!("IO error: {}", error))
|
||||
}
|
||||
}
|
14
virt/src/rfs/mod.rs
Normal file
14
virt/src/rfs/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
142
virt/src/rfs/mount.rs
Normal file
142
virt/src/rfs/mount.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use super::{
|
||||
error::RfsError,
|
||||
cmd::execute_rfs_command,
|
||||
types::Mount,
|
||||
};
|
||||
|
||||
/// List all mounted filesystems
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Vec<Mount>, RfsError>` - List of mounts or error
|
||||
pub fn list_mounts() -> Result<Vec<Mount>, RfsError> {
|
||||
// Execute the list command
|
||||
let result = execute_rfs_command(&["list", "--json"])?;
|
||||
|
||||
// Parse the JSON output
|
||||
match serde_json::from_str::<serde_json::Value>(&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, RfsError>` - Mount information or error
|
||||
pub fn get_mount_info(target: &str) -> Result<Mount, RfsError> {
|
||||
// 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)))
|
||||
}
|
100
virt/src/rfs/pack.rs
Normal file
100
virt/src/rfs/pack.rs
Normal file
@@ -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<String, RfsError>` - File listing or error
|
||||
pub fn list_contents(input: &str) -> Result<String, RfsError> {
|
||||
// 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<bool, RfsError>` - Whether the layer is valid or error
|
||||
pub fn verify(input: &str) -> Result<bool, RfsError> {
|
||||
// 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)
|
||||
}
|
117
virt/src/rfs/types.rs
Normal file
117
virt/src/rfs/types.rs
Normal file
@@ -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<String>,
|
||||
}
|
||||
|
||||
/// 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<String, String>,
|
||||
}
|
||||
|
||||
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<String> = self.options
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}={}", k, v))
|
||||
.collect();
|
||||
result.push_str(&options.join(","));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user