diff --git a/Cargo.lock b/Cargo.lock index d4d4e0e..1b2d06a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,6 +336,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + [[package]] name = "komandan" version = "0.1.0" @@ -347,6 +353,7 @@ dependencies = [ "rand", "regex", "rustyline", + "serde_json", "ssh2", "tokio", ] @@ -823,6 +830,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -859,6 +872,18 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 4e8b6a5..7bd00a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,6 @@ mlua = { version = "0.10.1", features = ["anyhow", "async", "luajit", "macros", rand = "0.8.5" regex = "1.11.1" rustyline = "15.0.0" +serde_json = "1.0.133" ssh2 = { version = "0.9.4", features = ["vendored-openssl"] } tokio = { version = "1.42.0", features = ["io-std", "macros", "rt"] } diff --git a/README.md b/README.md index fdd34e6..88b66d5 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ For detailed explanations, arguments, and examples of each module, please refer Komandan offers built-in functions to enhance scripting capabilities: - **`komandan.filter_hosts`**: Filters a list of hosts based on a pattern. +- **`komandan.parse_hosts_json`**: Parses a JSON file containing hosts information. - **`komandan.set_defaults`**: Sets default values for host connection parameters. For detailed descriptions and usage examples of these functions, please visit the [Built-in Functions section of the Komandan Documentation Site](https://komandan.vercel.app/docs/functions/). diff --git a/examples/parse_hosts_json.lua b/examples/parse_hosts_json.lua new file mode 100644 index 0000000..cfd9767 --- /dev/null +++ b/examples/parse_hosts_json.lua @@ -0,0 +1,15 @@ +local hosts = komandan.parse_hosts_json("/path/to/hosts.json") + +komandan.set_defaults({ + user = "user1", + private_key_file = os.getenv("HOME") .. "/.ssh/id_ed25519", +}) + +for _, host in pairs(hosts) do + komandan.komando(host, { + name = "Create a directory", + komandan.modules.cmd({ + cmd = "mkdir /tmp/newdir1", + }), + }) +end diff --git a/src/main.rs b/src/main.rs index 7946df2..065c60f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ mod ssh; mod util; mod validator; -use util::{dprint, filter_hosts, regex_is_match}; +use util::{dprint, filter_hosts, parse_hosts_json, regex_is_match}; use validator::{validate_host, validate_module, validate_task}; #[tokio::main(flavor = "current_thread")] @@ -70,6 +70,7 @@ fn setup_komandan_table(lua: &Lua) -> mlua::Result<()> { // Add utils komandan.set("regex_is_match", lua.create_function(regex_is_match)?)?; komandan.set("filter_hosts", lua.create_function(filter_hosts)?)?; + komandan.set("parse_hosts_json", lua.create_function(parse_hosts_json)?)?; komandan.set("dprint", lua.create_function(dprint)?)?; // Add core modules @@ -443,6 +444,7 @@ mod tests { assert!(komandan_table.contains_key("komando").unwrap()); assert!(komandan_table.contains_key("regex_is_match").unwrap()); assert!(komandan_table.contains_key("filter_hosts").unwrap()); + assert!(komandan_table.contains_key("parse_hosts_json").unwrap()); assert!(komandan_table.contains_key("dprint").unwrap()); let modules_table = komandan_table.get::("modules").unwrap(); diff --git a/src/util.rs b/src/util.rs index 339f427..6c7626d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,7 @@ -use crate::args::Args; +use crate::{args::Args, validator::validate_host}; use clap::Parser; -use mlua::{chunk, Lua, Table, Value}; +use mlua::{chunk, Error::RuntimeError, Lua, LuaSerdeExt, Table, Value}; +use std::{fs::File, io::Read}; pub fn dprint(lua: &Lua, value: Value) -> mlua::Result<()> { let args = Args::parse(); @@ -91,6 +92,58 @@ pub fn filter_hosts(lua: &Lua, (hosts, pattern): (Value, Value)) -> mlua::Result Ok(matched_hosts) } +pub fn parse_hosts_json(lua: &Lua, src: Value) -> mlua::Result
{ + let src = match src.as_string_lossy() { + Some(s) => s, + None => return Err(RuntimeError(String::from("Invalid src path"))), + }; + + let mut file = match File::open(&src) { + Ok(f) => f, + Err(_) => return Err(RuntimeError(String::from("Failed to open JSON file"))), + }; + + let mut content = String::new(); + match file.read_to_string(&mut content) { + Ok(_) => (), + Err(_) => return Err(RuntimeError(String::from("Failed to read JSON file"))), + }; + + let json: serde_json::Value = match serde_json::from_str(&content) { + Ok(j) => j, + Err(_) => return Err(RuntimeError(String::from("Failed to parse JSON file"))), + }; + + let hosts = lua.create_table()?; + + let lua_value = match lua.to_value(&json) { + Ok(o) => o, + Err(_) => return Err(RuntimeError(String::from("Failed to convert JSON to Lua"))), + }; + + let lua_table = match lua_value.as_table() { + Some(t) => t, + None => return Err(RuntimeError(String::from("JSON does not contain a table"))), + }; + + for pair in lua_table.pairs() { + let (_, value): (Value, Value) = pair?; + match validate_host(&lua, value) { + Ok(host) => { + hosts.set(hosts.len()? + 1, host)?; + } + Err(_) => {} + }; + } + + dprint( + lua, + lua.to_value(&format!("Loaded {} hosts from '{}'", hosts.len()?, src))?, + )?; + + Ok(hosts) +} + pub fn regex_is_match( _: &Lua, (text, pattern): (mlua::String, mlua::String),