diff --git a/Cargo.toml b/Cargo.toml index d607ded..e7ba948 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,10 @@ log = "0.4" # Logging facade rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language rand = "0.8.5" # Random number generation clap = "2.33" # Command-line argument parsing +zinit-client = { git = "https://github.com/threefoldtech/zinit", branch = "json_rpc", package = "zinit-client" } +anyhow = "1.0.98" +jsonrpsee = "0.25.1" +tokio = "1.45.0" # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] diff --git a/examples/zinit/zinit_basic.rhai b/examples/zinit/zinit_basic.rhai new file mode 100644 index 0000000..4c7f7f7 --- /dev/null +++ b/examples/zinit/zinit_basic.rhai @@ -0,0 +1,70 @@ +// Basic example of using the Zinit client in Rhai + +// Socket path for Zinit +let socket_path = "/var/run/zinit.sock"; + +// List all services +print("Listing all services:"); +let services = zinit_list(socket_path); + +for (name, state) in services { + print(`${name}: ${state}`); +} + +// Get status of a specific service +let service_name = "example-service"; +print(`\nGetting status for ${service_name}:`); + +try { + let status = zinit_status(socket_path, service_name); + print(`Service: ${status.name}`); + print(`PID: ${status.pid}`); + print(`State: ${status.state}`); + print(`Target: ${status.target}`); + print("Dependencies:"); + + for (dep, state) in status.after { + print(` ${dep}: ${state}`); + } +} catch(err) { + print(`Error getting status: ${err}`); +} + +// Create a new service +print("\nCreating a new service:"); +let new_service = "rhai-test-service"; +let exec_command = "echo 'Hello from Rhai'"; +let oneshot = true; + +try { + let result = zinit_create_service(socket_path, new_service, exec_command, oneshot); + print(`Service created: ${result}`); + + // Monitor the service + print("\nMonitoring the service:"); + let monitor_result = zinit_monitor(socket_path, new_service); + print(`Service monitored: ${monitor_result}`); + + // Start the service + print("\nStarting the service:"); + let start_result = zinit_start(socket_path, new_service); + print(`Service started: ${start_result}`); + + // Get logs + print("\nGetting logs:"); + let logs = zinit_logs(socket_path, new_service); + + for log in logs { + print(log); + } + + // Clean up + print("\nCleaning up:"); + let forget_result = zinit_forget(socket_path, new_service); + print(`Service forgotten: ${forget_result}`); + + let delete_result = zinit_delete_service(socket_path, new_service); + print(`Service deleted: ${delete_result}`); +} catch(err) { + print(`Error: ${err}`); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index ceda467..564ba63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ pub mod text; pub mod virt; pub mod rhai; pub mod cmd; +pub mod zinit_client; // Version information /// Returns the version of the SAL library diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index d63565e..f69bb6a 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -11,6 +11,7 @@ mod nerdctl; mod git; mod text; mod rfs; +mod zinit; #[cfg(test)] mod tests; @@ -60,6 +61,9 @@ pub use rfs::register as register_rfs_module; pub use git::register_git_module; pub use crate::git::{GitTree, GitRepo}; +// Re-export zinit module +pub use zinit::register_zinit_module; + // Re-export text module pub use text::register_text_module; // Re-export text functions directly from text module @@ -110,6 +114,9 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { // Register Git module functions git::register_git_module(engine)?; + // Register Zinit module functions + zinit::register_zinit_module(engine)?; + // Register Text module functions text::register_text_module(engine)?; diff --git a/src/rhai/zinit.rs b/src/rhai/zinit.rs new file mode 100644 index 0000000..74072ab --- /dev/null +++ b/src/rhai/zinit.rs @@ -0,0 +1,326 @@ +//! Rhai wrappers for Zinit client module functions +//! +//! This module provides Rhai wrappers for the functions in the Zinit client module. + +use rhai::{Engine, EvalAltResult, Array, Dynamic, Map}; +use crate::zinit_client as client; +use tokio::runtime::Runtime; +use serde_json::{json, Value}; +use crate::rhai::error::ToRhaiError; + +/// Register Zinit module functions with the Rhai engine +/// +/// # Arguments +/// +/// * `engine` - The Rhai engine to register the functions with +/// +/// # Returns +/// +/// * `Result<(), Box>` - Ok if registration was successful, Err otherwise +pub fn register_zinit_module(engine: &mut Engine) -> Result<(), Box> { + // Register Zinit client functions + engine.register_fn("zinit_list", zinit_list); + engine.register_fn("zinit_status", zinit_status); + engine.register_fn("zinit_start", zinit_start); + engine.register_fn("zinit_stop", zinit_stop); + engine.register_fn("zinit_restart", zinit_restart); + engine.register_fn("zinit_monitor", zinit_monitor); + engine.register_fn("zinit_forget", zinit_forget); + engine.register_fn("zinit_kill", zinit_kill); + engine.register_fn("zinit_create_service", zinit_create_service); + engine.register_fn("zinit_delete_service", zinit_delete_service); + engine.register_fn("zinit_get_service", zinit_get_service); + engine.register_fn("zinit_logs", zinit_logs); + + Ok(()) +} + +impl ToRhaiError for Result { + fn to_rhai_error(self) -> Result> { + self.map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Zinit error: {}", e).into(), + rhai::Position::NONE + )) + }) + } +} + +// Helper function to get a runtime +fn get_runtime() -> Result> { + tokio::runtime::Runtime::new().map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to create Tokio runtime: {}", e).into(), + rhai::Position::NONE + )) + }) +} + +// +// Zinit Client Function Wrappers +// + +/// Wrapper for zinit_client::list +/// +/// Lists all services managed by Zinit. +pub fn zinit_list(socket_path: &str) -> Result> { + let rt = get_runtime()?; + println!("got runtime: {:?}", rt); + + let result = rt.block_on(async { + client::list(socket_path).await + }); + println!("got result: {:?}", result); + + let services = result.to_rhai_error()?; + println!("got services: {:?}", services); + + // Convert HashMap to Rhai Map + let mut map = Map::new(); + for (name, state) in services { + map.insert(name.into(), Dynamic::from(state)); + } + println!("got map: {:?}", map); + + Ok(map) +} + +/// Wrapper for zinit_client::status +/// +/// Gets the status of a specific service. +pub fn zinit_status(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::status(socket_path, name).await + }); + + let status = result.to_rhai_error()?; + + // Convert Status to Rhai Map + let mut map = Map::new(); + map.insert("name".into(), Dynamic::from(status.name)); + map.insert("pid".into(), Dynamic::from(status.pid)); + map.insert("state".into(), Dynamic::from(status.state)); + map.insert("target".into(), Dynamic::from(status.target)); + + // Convert dependencies + let mut deps_map = Map::new(); + for (dep, state) in status.after { + deps_map.insert(dep.into(), Dynamic::from(state)); + } + map.insert("after".into(), Dynamic::from_map(deps_map)); + + Ok(map) +} + +/// Wrapper for zinit_client::start +/// +/// Starts a service. +pub fn zinit_start(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::start(socket_path, name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::stop +/// +/// Stops a service. +pub fn zinit_stop(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::stop(socket_path, name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::restart +/// +/// Restarts a service. +pub fn zinit_restart(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::restart(socket_path, name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::monitor +/// +/// Starts monitoring a service. +pub fn zinit_monitor(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.monitor(name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::forget +/// +/// Stops monitoring a service. +pub fn zinit_forget(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.forget(name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::kill +/// +/// Sends a signal to a service. +pub fn zinit_kill(socket_path: &str, name: &str, signal: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.kill(name, signal).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::create_service +/// +/// Creates a new service. +pub fn zinit_create_service(socket_path: &str, name: &str, exec: &str, oneshot: bool) -> Result> { + let rt = get_runtime()?; + + // Create service configuration + let content = serde_json::from_value(json!({ + "exec": exec, + "oneshot": oneshot + })).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to create service configuration: {}", e).into(), + rhai::Position::NONE + )) + })?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.create_service(name, content).await + }); + + result.to_rhai_error() +} + +/// Wrapper for zinit_client::delete_service +/// +/// Deletes a service. +pub fn zinit_delete_service(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.delete_service(name).await + }); + + result.to_rhai_error() +} + +/// Wrapper for zinit_client::get_service +/// +/// Gets a service configuration. +pub fn zinit_get_service(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.get_service(name).await + }); + + let value = result.to_rhai_error()?; + + // Convert Value to Dynamic + match value { + Value::Object(map) => { + let mut rhai_map = Map::new(); + for (k, v) in map { + rhai_map.insert(k.into(), value_to_dynamic(v)); + } + Ok(Dynamic::from_map(rhai_map)) + }, + _ => Err(Box::new(EvalAltResult::ErrorRuntime( + "Expected object from get_service".into(), + rhai::Position::NONE + ))) + } +} + +/// Wrapper for zinit_client::logs +/// +/// Gets logs for a service. +pub fn zinit_logs(socket_path: &str, filter: Option<&str>) -> Result> { + let rt = get_runtime()?; + + let filter_string = filter.map(|s| s.to_string()); + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.logs(filter_string).await + }); + + let logs = result.to_rhai_error()?; + + // Convert Vec to Rhai Array + let mut array = Array::new(); + for log in logs { + array.push(Dynamic::from(log)); + } + + Ok(array) +} + +// Helper function to convert serde_json::Value to rhai::Dynamic +fn value_to_dynamic(value: Value) -> Dynamic { + match value { + Value::Null => Dynamic::UNIT, + Value::Bool(b) => Dynamic::from(b), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Dynamic::from(i) + } else if let Some(f) = n.as_f64() { + Dynamic::from(f) + } else { + Dynamic::from(n.to_string()) + } + }, + Value::String(s) => Dynamic::from(s), + Value::Array(arr) => { + let mut rhai_arr = Array::new(); + for item in arr { + rhai_arr.push(value_to_dynamic(item)); + } + Dynamic::from(rhai_arr) + }, + Value::Object(map) => { + let mut rhai_map = Map::new(); + for (k, v) in map { + rhai_map.insert(k.into(), value_to_dynamic(v)); + } + Dynamic::from_map(rhai_map) + } + } +} diff --git a/src/zinit_client/mod.rs b/src/zinit_client/mod.rs new file mode 100644 index 0000000..deb32dd --- /dev/null +++ b/src/zinit_client/mod.rs @@ -0,0 +1,203 @@ +use std::sync::{Arc, Mutex, Once}; +use std::sync::atomic::{AtomicBool, Ordering}; +use lazy_static::lazy_static; +use zinit_client::{Client as ZinitClient, ClientError, Status}; +use std::collections::HashMap; +use serde_json::{Map, Value}; + +// Global Zinit client instance using lazy_static +lazy_static! { + static ref ZINIT_CLIENT: Mutex>> = Mutex::new(None); + static ref INIT: Once = Once::new(); +} + +// Wrapper for Zinit client to handle connection +pub struct ZinitClientWrapper { + client: ZinitClient, + initialized: AtomicBool, +} + +impl ZinitClientWrapper { + // Create a new Zinit client wrapper + fn new(client: ZinitClient) -> Self { + ZinitClientWrapper { + client, + initialized: AtomicBool::new(false), + } + } + + // Initialize the client + async fn initialize(&self) -> Result<(), ClientError> { + if self.initialized.load(Ordering::Relaxed) { + return Ok(()); + } + + // Try to list services to check if the connection works + let _ = self.client.list().await.map_err(|e| { + eprintln!("Failed to initialize Zinit client: {}", e); + e + })?; + + self.initialized.store(true, Ordering::Relaxed); + Ok(()) + } + + // List all services + pub async fn list(&self) -> Result, ClientError> { + self.client.list().await + } + + // Get status of a service + pub async fn status(&self, name: &str) -> Result { + self.client.status(name).await + } + + // Start a service + pub async fn start(&self, name: &str) -> Result<(), ClientError> { + self.client.start(name).await + } + + // Stop a service + pub async fn stop(&self, name: &str) -> Result<(), ClientError> { + self.client.stop(name).await + } + + // Restart a service + pub async fn restart(&self, name: &str) -> Result<(), ClientError> { + self.client.restart(name).await + } + + // Monitor a service + pub async fn monitor(&self, name: &str) -> Result<(), ClientError> { + self.client.monitor(name).await + } + + // Forget a service + pub async fn forget(&self, name: &str) -> Result<(), ClientError> { + self.client.forget(name).await + } + + // Send a signal to a service + pub async fn kill(&self, name: &str, signal: &str) -> Result<(), ClientError> { + self.client.kill(name, signal).await + } + + // Create a new service + pub async fn create_service(&self, name: &str, content: Map) -> Result { + self.client.create_service(name, content).await + } + + // Delete a service + pub async fn delete_service(&self, name: &str) -> Result { + self.client.delete_service(name).await + } + + // Get a service configuration + pub async fn get_service(&self, name: &str) -> Result { + self.client.get_service(name).await + } + + // Shutdown the system + pub async fn shutdown(&self) -> Result<(), ClientError> { + self.client.shutdown().await + } + + // Reboot the system + pub async fn reboot(&self) -> Result<(), ClientError> { + self.client.reboot().await + } + + // Start HTTP server + pub async fn start_http_server(&self, address: &str) -> Result { + self.client.start_http_server(address).await + } + + // Stop HTTP server + pub async fn stop_http_server(&self) -> Result<(), ClientError> { + self.client.stop_http_server().await + } + + // Get logs + pub async fn logs(&self, filter: Option) -> Result, ClientError> { + self.client.logs(filter).await + } +} + +// Get the Zinit client instance +pub async fn get_zinit_client(socket_path: &str) -> Result, ClientError> { + // Check if we already have a client + { + let guard = ZINIT_CLIENT.lock().unwrap(); + if let Some(ref client) = &*guard { + return Ok(Arc::clone(client)); + } + } + + // Create a new client + let client = create_zinit_client(socket_path).await?; + + // Store the client globally + { + let mut guard = ZINIT_CLIENT.lock().unwrap(); + *guard = Some(Arc::clone(&client)); + } + + Ok(client) +} + +// Create a new Zinit client +async fn create_zinit_client(socket_path: &str) -> Result, ClientError> { + // Connect via Unix socket + let client = ZinitClient::unix_socket(socket_path).await?; + let wrapper = Arc::new(ZinitClientWrapper::new(client)); + + // Initialize the client + wrapper.initialize().await?; + + Ok(wrapper) +} + +// Reset the Zinit client +pub async fn reset(socket_path: &str) -> Result<(), ClientError> { + // Clear the existing client + { + let mut client_guard = ZINIT_CLIENT.lock().unwrap(); + *client_guard = None; + } + + // Create a new client, only return error if it fails + get_zinit_client(socket_path).await?; + Ok(()) +} + +// Convenience functions for common operations + +// List all services +pub async fn list(socket_path: &str) -> Result, ClientError> { + let client = get_zinit_client(socket_path).await?; + client.list().await +} + +// Get status of a service +pub async fn status(socket_path: &str, name: &str) -> Result { + let client = get_zinit_client(socket_path).await?; + client.status(name).await +} + +// Start a service +pub async fn start(socket_path: &str, name: &str) -> Result<(), ClientError> { + let client = get_zinit_client(socket_path).await?; + client.start(name).await +} + +// Stop a service +pub async fn stop(socket_path: &str, name: &str) -> Result<(), ClientError> { + let client = get_zinit_client(socket_path).await?; + client.stop(name).await +} + +// Restart a service +pub async fn restart(socket_path: &str, name: &str) -> Result<(), ClientError> { + let client = get_zinit_client(socket_path).await?; + client.restart(name).await +} \ No newline at end of file