Skip to content

Commit

Permalink
Add a new module: lineinfile
Browse files Browse the repository at this point in the history
  • Loading branch information
hahnavi committed Dec 15, 2024
1 parent 7eb26e6 commit b72a0c4
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 6 deletions.
19 changes: 19 additions & 0 deletions examples/lineinfile.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
local host = {
name = "server1",
address = "10.20.30.41",
user = "user1",
private_key_file = os.getenv("HOME") .. "/.ssh/id_ed25519",
}

local task = {
name = "Add a line in a file",
komandan.modules.lineinfile({
path = "/tmp/target_file.txt",
line = "This is a new line",
state = "present",
create = true,
backup = true,
}),
}

komandan.komando(host, task)
7 changes: 3 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use args::Args;
use clap::Parser;
use mlua::{chunk, Error::RuntimeError, Integer, Lua, MultiValue, Table, Value};
use modules::{apt, base_module, cmd, download, script, upload};
use modules::{apt, base_module, cmd, lineinfile, download, script, upload};
use rustyline::DefaultEditor;
use ssh::{ElevateMethod, Elevation, SSHAuthMethod, SSHSession};
use std::{env, path::Path};
Expand Down Expand Up @@ -76,6 +76,7 @@ fn setup_komandan_table(lua: &Lua) -> mlua::Result<()> {
let modules_table = lua.create_table()?;
modules_table.set("apt", lua.create_function(apt)?)?;
modules_table.set("cmd", lua.create_function(cmd)?)?;
modules_table.set("lineinfile", lua.create_function(lineinfile)?)?;
modules_table.set("script", lua.create_function(script)?)?;
modules_table.set("upload", lua.create_function(upload)?)?;
modules_table.set("download", lua.create_function(download)?)?;
Expand Down Expand Up @@ -265,19 +266,16 @@ async fn komando(lua: Lua, (host, task): (Value, Value)) -> mlua::Result<Table>

for pair in env_defaults.pairs() {
let (key, value): (String, String) = pair?;
println!("{}={}", key, value);
ssh.set_env(&key, &value);
}

for pair in env_host.pairs() {
let (key, value): (String, String) = pair?;
println!("{}={}", key, value);
ssh.set_env(&key, &value);
}

for pair in env_task.pairs() {
let (key, value): (String, String) = pair?;
println!("{}={}", key, value);
ssh.set_env(&key, &value);
}

Expand Down Expand Up @@ -449,6 +447,7 @@ mod tests {
let modules_table = komandan_table.get::<Table>("modules").unwrap();
assert!(modules_table.contains_key("apt").unwrap());
assert!(modules_table.contains_key("cmd").unwrap());
assert!(modules_table.contains_key("lineinfile").unwrap());
assert!(modules_table.contains_key("script").unwrap());
assert!(modules_table.contains_key("upload").unwrap());
assert!(modules_table.contains_key("download").unwrap());
Expand Down
231 changes: 231 additions & 0 deletions src/modules/lineinfile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
use mlua::{chunk, Error::RuntimeError, ExternalResult, Lua, Table, Value};
use rand::{distributions::Alphanumeric, Rng};

pub fn lineinfile(lua: &Lua, params: Table) -> mlua::Result<Table> {
let path = match params.get::<String>("path") {
Ok(path) => path,
Err(_) => return Err(RuntimeError(String::from("'path' parameter is required"))),
};

let line = params.get::<Value>("line")?;
let pattern = params.get::<Value>("pattern")?;

let state = match params.get::<String>("state") {
Ok(state) => match state.as_str() {
"present" => state,
"absent" => state,
_ => return Err(RuntimeError(String::from("'state' parameter must be 'present' or 'absent'"))),
},
Err(_) => String::from("present"),
};

if line.is_nil() && pattern.is_nil() {
return Err(RuntimeError(String::from("'line' or 'pattern' parameter is required")));
}

let insert_after = params.get::<Value>("insert_after")?;
let insert_before = params.get::<Value>("insert_before")?;

let create = params.get::<bool>("create").unwrap_or(false);
let backup = params.get::<bool>("backup").unwrap_or(false);

let random_file_name: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.map(char::from)
.take(10)
.collect();

let base_module = super::base_module(&lua);
let module = lua
.load(chunk! {
local module = $base_module:new({ name = "lineinfile" })

function module:run()
local tmpdir = module.ssh:get_tmpdir()
module.remote_script = tmpdir .. "/." .. $random_file_name
module.ssh:write_remote_file(module.remote_script, $LINEINFILE_SCRIPT)
module.ssh:chmod(module.remote_script, "+x")

local cmd = module.remote_script .. " --path \"" .. $path .. "\" --create " .. tostring($create) .. " --backup " .. tostring($backup) .. " --state " .. $state
if $line ~= nil then
cmd = cmd .. " --line \"" .. $line .. "\""
end

if $pattern ~= nil then
cmd = cmd .. " --pattern \"" .. $pattern .. "\""
end

if $insert_after ~= nil then
cmd = cmd .. " --insert_after \"" .. $insert_after .. "\""
end

if $insert_before ~= nil then
cmd = cmd .. " --insert_before \"" .. $insert_before .. "\""
end

module.ssh:cmd(cmd)
end

function module:cleanup()
module.ssh:cmd("rm " .. module.remote_script)
end

return module
})
.eval::<Table>()
.into_lua_err()?;

Ok(module)
}

const LINEINFILE_SCRIPT: &str = r#"#!/bin/sh
# Default values
STATE="present"
CREATE="false"
BACKUP="false"
# Parse command-line arguments
while [ $# -gt 0 ]; do
case "$1" in
--path)
FILE_PATH="$2"
shift 2
;;
--pattern)
REGEXP="$2"
shift 2
;;
--line)
LINE="$2"
shift 2
;;
--state)
STATE="$2"
shift 2
;;
--create)
CREATE="$2"
shift 2
;;
--insert_after)
INSERTAFTER="$2"
shift 2
;;
--insert_before)
INSERTBEFORE="$2"
shift 2
;;
--backup)
BACKUP="$2"
shift 2
;;
*)
echo "Unknown option: $1"
;;
esac
done
# Validate required arguments
if [ -z "$FILE_PATH" ]; then
echo "Error: 'path' is required"
fi
# Check if the file exists
if [ ! -f "$FILE_PATH" -a "$CREATE" = "false" ]; then
echo "Error: File '$FILE_PATH' does not exist and 'create' is set to 'false'"
exit 1
elif [ ! -f "$FILE_PATH" -a "$CREATE" = "true" ]; then
touch "$FILE_PATH"
fi
# Create a backup if requested
if [ "$BACKUP" = "true" ]; then
BACKUP_FILE="$FILE_PATH.$(date +%Y%m%d%H%M%S).bak"
cp "$FILE_PATH" "$BACKUP_FILE"
echo "Backup created: $BACKUP_FILE"
fi
# Handle present state
if [ "$STATE" = "present" ]; then
# Handle insertion or replacement if 'line' is provided
if [ -n "$LINE" ]; then
# Check if line already exists
if grep -q "$LINE" "$FILE_PATH"; then
echo "Line already exists in the file"
exit 0
fi
# Handle replacement
if [ -n "$REGEXP" ]; then
if grep -q "$REGEXP" "$FILE_PATH"; then
# Use a temporary file for sed
sed "s/$REGEXP/$LINE/" "$FILE_PATH" > "$FILE_PATH.tmp"
mv "$FILE_PATH.tmp" "$FILE_PATH"
echo "Line replaced in the file"
exit 0
fi
fi
# Handle insertion
if [ -n "$INSERTAFTER" ]; then
if [ "$INSERTAFTER" = "EOF" ]; then
echo "$LINE" >> "$FILE_PATH"
echo "Line inserted at end of file"
else
# Use a temporary file for sed
sed "/$INSERTAFTER/a $LINE" "$FILE_PATH" > "$FILE_PATH.tmp"
mv "$FILE_PATH.tmp" "$FILE_PATH"
echo "Line inserted after '$INSERTAFTER'"
fi
exit 0
elif [ -n "$INSERTBEFORE" ]; then
if [ "$INSERTBEFORE" = "BOF" ]; then
# Use a temporary file for sed
sed "1s/^/$LINE\n/" "$FILE_PATH" > "$FILE_PATH.tmp"
mv "$FILE_PATH.tmp" "$FILE_PATH"
echo "Line inserted at beginning of file"
else
# Use a temporary file for sed
sed "/$INSERTBEFORE/i $LINE" "$FILE_PATH" > "$FILE_PATH.tmp"
mv "$FILE_PATH.tmp" "$FILE_PATH"
echo "Line inserted before '$INSERTBEFORE'"
fi
exit 0
else
echo "$LINE" >> "$FILE_PATH"
echo "Line appended to the file"
exit 0
fi
# If 'line' is not provided, check for regexp match when state is present
elif [ -n "$REGEXP" ]; then
if ! grep -q "$REGEXP" "$FILE_PATH"; then
echo "No lines match '$REGEXP' when expecting at least one match"
fi
exit 0
else
echo "Error: 'line' or 'pattern' is required when state is 'present'"
exit 1
fi
fi
# Handle absent state
if [ "$STATE" = "absent" ]; then
if [ -z "$REGEXP" -a -z "$LINE" ]; then
echo "Error: Either 'pattern' or 'line' is required when state is 'absent'"
exit 1
fi
if [ -n "$REGEXP" ]; then
# Use a temporary file for sed
sed "/$REGEXP/d" "$FILE_PATH" > "$FILE_PATH.tmp"
mv "$FILE_PATH.tmp" "$FILE_PATH"
echo "Lines matching '$REGEXP' removed from the file"
elif [ -n "$LINE" ]; then
# Use a temporary file for sed
sed "/$(echo "$LINE" | sed 's/[^^]/[&]/g; s/\^/\\^/g')/d" "$FILE_PATH" > "$FILE_PATH.tmp"
mv "$FILE_PATH.tmp" "$FILE_PATH"
echo "Lines matching '$LINE' removed from the file"
fi
exit 0
fi
"#;
2 changes: 2 additions & 0 deletions src/modules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
mod apt;
mod cmd;
mod download;
mod lineinfile;
mod script;
mod upload;

pub use apt::apt;
pub use cmd::cmd;
pub use download::download;
pub use lineinfile::lineinfile;
pub use script::script;
pub use upload::upload;

Expand Down
3 changes: 1 addition & 2 deletions src/modules/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,12 @@ pub fn script(lua: &Lua, params: Table) -> mlua::Result<Table> {
if $interpreter ~= nil then
module.ssh:cmd($interpreter .. " " .. module.remote_path)
else
module.ssh:chmod(module.remote_path, "0755")
module.ssh:chmod(module.remote_path, "+x")
module.ssh:cmd(module.remote_path)
end
end

function module:cleanup()
local tmpdir = module.ssh:get_tmpdir()
module.ssh:cmd("rm " .. module.remote_path)
end

Expand Down

0 comments on commit b72a0c4

Please sign in to comment.