herolib-mos 0.3.13

Mycelium Operating System (MOS) - Network and VM abstraction layer
Documentation
use super::error::{NetworkError, Result};
use std::path::PathBuf;
use std::process::Command;

const NETNS_PATH: &str = "/var/run/netns";

/// Network namespace handle
#[derive(Debug, Clone)]
pub struct NetNamespace {
    pub name: String,
    pub path: PathBuf,
}

/// Create a new persistent network namespace using `ip netns add`
pub fn create_namespace(name: &str) -> Result<NetNamespace> {
    let ns_path = PathBuf::from(NETNS_PATH).join(name);

    // Check if namespace already exists
    if ns_path.exists() {
        return get_namespace_by_name(name);
    }

    // Create namespace using ip netns add
    let output = Command::new("ip")
        .args(["netns", "add", name])
        .output()
        .map_err(|e| NetworkError::Namespace(format!("Failed to execute ip netns add: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Namespace(format!(
            "Failed to create namespace {}: {}",
            name,
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(NetNamespace {
        name: name.to_string(),
        path: ns_path,
    })
}

/// Get an existing network namespace by name
pub fn get_namespace_by_name(name: &str) -> Result<NetNamespace> {
    let ns_path = PathBuf::from(NETNS_PATH).join(name);

    if !ns_path.exists() {
        return Err(NetworkError::NotFound(format!(
            "Namespace {} not found",
            name
        )));
    }

    Ok(NetNamespace {
        name: name.to_string(),
        path: ns_path,
    })
}

/// Check if a namespace exists
pub fn namespace_exists(name: &str) -> bool {
    PathBuf::from(NETNS_PATH).join(name).exists()
}

/// Delete the network namespace using `ip netns delete`
pub fn delete_namespace(ns: NetNamespace) -> Result<()> {
    let output = Command::new("ip")
        .args(["netns", "delete", &ns.name])
        .output()
        .map_err(|e| NetworkError::Namespace(format!("Failed to execute ip netns delete: {}", e)))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        // Ignore "No such file" error (namespace already deleted)
        if !stderr.contains("No such file") {
            return Err(NetworkError::Namespace(format!(
                "Failed to delete namespace {}: {}",
                ns.name, stderr
            )));
        }
    }

    Ok(())
}

/// Execute a command within a namespace using `ip netns exec`
pub fn exec_in_ns(ns: &NetNamespace, cmd: &str, args: &[&str]) -> Result<std::process::Output> {
    let mut command = Command::new("ip");
    command.args(["netns", "exec", &ns.name, cmd]);
    command.args(args);

    let output = command
        .output()
        .map_err(|e| NetworkError::Namespace(format!("Failed to exec in namespace: {}", e)))?;

    Ok(output)
}

/// Execute a command within a namespace by name
pub fn exec_in_ns_by_name(name: &str, cmd: &str, args: &[&str]) -> Result<std::process::Output> {
    if !namespace_exists(name) {
        return Err(NetworkError::NotFound(format!(
            "Namespace {} not found",
            name
        )));
    }

    let mut command = Command::new("ip");
    command.args(["netns", "exec", name, cmd]);
    command.args(args);

    let output = command
        .output()
        .map_err(|e| NetworkError::Namespace(format!("Failed to exec in namespace: {}", e)))?;

    Ok(output)
}


/// List all network namespaces
pub fn list_namespaces() -> Result<Vec<String>> {
    let output = Command::new("ip")
        .args(["netns", "list"])
        .output()
        .map_err(|e| NetworkError::Namespace(format!("Failed to list namespaces: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Namespace(format!(
            "Failed to list namespaces: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let namespaces: Vec<String> = stdout
        .lines()
        .filter_map(|line| {
            // Format is "name (id: X)" or just "name"
            line.split_whitespace().next().map(|s| s.to_string())
        })
        .collect();

    Ok(namespaces)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_netnamespace_struct() {
        let ns = NetNamespace {
            name: "test_ns".to_string(),
            path: PathBuf::from("/var/run/netns/test_ns"),
        };
        assert_eq!(ns.name, "test_ns");
        assert_eq!(ns.path.to_str().unwrap(), "/var/run/netns/test_ns");
    }

    #[test]
    fn test_netns_path_constant() {
        assert_eq!(NETNS_PATH, "/var/run/netns");
    }

    #[test]
    fn test_namespace_path_construction() {
        let name = "my_namespace";
        let expected_path = PathBuf::from(NETNS_PATH).join(name);
        assert_eq!(
            expected_path.to_str().unwrap(),
            "/var/run/netns/my_namespace"
        );
    }

    #[test]
    fn test_namespace_exists_false() {
        assert!(!namespace_exists("nonexistent_ns_12345"));
    }
}