Compare commits
5 Commits
865850bd64
...
59d44d7e0f
Author | SHA1 | Date |
---|---|---|
R Tyler Croy | 59d44d7e0f | |
R Tyler Croy | b35801a609 | |
R Tyler Croy | d5b8cb6dfc | |
R Tyler Croy | d765cc5703 | |
R Tyler Croy | eda6f523a3 |
|
@ -140,12 +140,6 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "gumdrop"
|
||||
version = "0.8.0"
|
||||
|
@ -503,9 +497,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
|
||||
checksum = "1a55ca5f3b68e41c979bf8c46a6f1da892ca4db8f94023ce0bd32407573b1ac0"
|
||||
|
||||
[[package]]
|
||||
name = "ssh2"
|
||||
|
@ -617,9 +611,7 @@ name = "zap"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"glob",
|
||||
"gumdrop",
|
||||
"handlebars",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"serde",
|
||||
|
@ -634,6 +626,7 @@ dependencies = [
|
|||
name = "zap-parser"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"handlebars",
|
||||
"log",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
|
|
16
README.adoc
16
README.adoc
|
@ -38,7 +38,8 @@ a wrapped shell/ruby/python script which does some specific piece of
|
|||
functionality. Tasks may also take parameters, which allow for some
|
||||
pluggability of new values.
|
||||
|
||||
Tasks have some built-in parameters that should not be overridden.
|
||||
Tasks have some default parameters that should not be overridden in new task
|
||||
definitions.
|
||||
|
||||
.Built-in Parameters
|
||||
|===
|
||||
|
@ -56,13 +57,13 @@ task Echo {
|
|||
parameters {
|
||||
msg {
|
||||
required = true
|
||||
help = "String to echo back to the client"
|
||||
help = 'String to echo back to the client'
|
||||
type = string
|
||||
}
|
||||
}
|
||||
|
||||
script {
|
||||
inline = "echo {{msg}}"
|
||||
inline = 'echo {{msg}}'
|
||||
}
|
||||
}
|
||||
----
|
||||
|
@ -77,11 +78,12 @@ will be executed in the order that they are defined.
|
|||
.simple.zplan
|
||||
[source]
|
||||
----
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
task 'tasks/echo.ztask' {
|
||||
msg = 'Hello from the wonderful world of zplans!'
|
||||
}
|
||||
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "This is nice"
|
||||
task 'tasks/echo.ztask' {
|
||||
msg = 'This is nice'
|
||||
}
|
||||
|
||||
----
|
||||
|
|
|
@ -6,9 +6,7 @@ edition = "2018"
|
|||
|
||||
[dependencies]
|
||||
colored = "2"
|
||||
glob = "0.3"
|
||||
gumdrop = "~0.8.0"
|
||||
handlebars = "~3.5"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
# Needed for deserializing JSON messages _and_ managing our configuration
|
||||
|
|
|
@ -10,7 +10,7 @@ mod transport;
|
|||
|
||||
use crate::inventory::*;
|
||||
use crate::transport::ssh::Ssh;
|
||||
use zap_parser::plan::Plan;
|
||||
use zap_parser::plan::{ExecutableTask, Plan};
|
||||
use zap_parser::task::Task;
|
||||
|
||||
fn main() {
|
||||
|
@ -51,13 +51,7 @@ fn handle_plan(opts: PlanOpts, runner: &dyn crate::transport::Transport, invento
|
|||
info!("Plan located, preparing to execute");
|
||||
for task in plan.tasks {
|
||||
info!("Running executable task: {:?}", task);
|
||||
exit = execute_task_on(
|
||||
opts.targets.clone(),
|
||||
&task.task,
|
||||
&task.parameters,
|
||||
runner,
|
||||
&inventory,
|
||||
);
|
||||
exit = execute_task_on(opts.targets.clone(), &task, runner, &inventory);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -69,21 +63,16 @@ fn handle_plan(opts: PlanOpts, runner: &dyn crate::transport::Transport, invento
|
|||
|
||||
fn execute_task_on(
|
||||
targets: String,
|
||||
task: &Task,
|
||||
parameters: &HashMap<String, String>,
|
||||
task: &ExecutableTask,
|
||||
runner: &dyn crate::transport::Transport,
|
||||
inventory: &Inventory,
|
||||
) -> i32 {
|
||||
if let Some(script) = task.get_script() {
|
||||
let command = render_command(&script, ¶meters);
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == targets) {
|
||||
return runner.run_group(task, &group, &inventory);
|
||||
}
|
||||
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == targets) {
|
||||
return runner.run_group(&command, &group, &inventory);
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == targets) {
|
||||
return runner.run(&command, &target);
|
||||
}
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == targets) {
|
||||
return runner.run(task, &target);
|
||||
}
|
||||
error!("Failed to locate a script to execute for the task!");
|
||||
return -1;
|
||||
|
@ -110,13 +99,10 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
parameters.insert(parts[0].to_string(), parts[1].to_string());
|
||||
}
|
||||
}
|
||||
std::process::exit(execute_task_on(
|
||||
opts.targets,
|
||||
&task,
|
||||
¶meters,
|
||||
runner,
|
||||
&inventory,
|
||||
));
|
||||
|
||||
let task = ExecutableTask::new(task, parameters);
|
||||
|
||||
std::process::exit(execute_task_on(opts.targets, &task, runner, &inventory));
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Failed to load task: {:?}", err);
|
||||
|
@ -135,38 +121,9 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
* non-zero.
|
||||
*/
|
||||
fn handle_cmd(opts: CmdOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
|
||||
let mut task = Task::new("Dynamic");
|
||||
task.inline = Some(opts.command);
|
||||
let parameters = HashMap::new();
|
||||
std::process::exit(execute_task_on(
|
||||
opts.targets,
|
||||
&task,
|
||||
¶meters,
|
||||
runner,
|
||||
&inventory,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* render_command will handle injecting the parameters for a given command
|
||||
* into the string where appropriate, using the Handlebars syntax.
|
||||
*
|
||||
* If the template fails to render, then this will just return the command it
|
||||
* was given
|
||||
*/
|
||||
fn render_command(cmd: &str, parameters: &HashMap<String, String>) -> String {
|
||||
use handlebars::Handlebars;
|
||||
|
||||
let handlebars = Handlebars::new();
|
||||
match handlebars.render_template(cmd, parameters) {
|
||||
Ok(rendered) => {
|
||||
return rendered;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to render command ({:?}): {}", err, cmd);
|
||||
return cmd.to_string();
|
||||
}
|
||||
}
|
||||
let mut task = ExecutableTask::new(Task::new("Dynamic"), HashMap::new());
|
||||
task.task.script.inline = Some(opts.command);
|
||||
std::process::exit(execute_task_on(opts.targets, &task, runner, &inventory));
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
|
@ -238,26 +195,4 @@ struct PlanOpts {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_render_command() {
|
||||
let cmd = "echo \"{{msg}}\"";
|
||||
let mut params = HashMap::new();
|
||||
params.insert("msg".to_string(), "hello".to_string());
|
||||
|
||||
let output = render_command(&cmd, ¶ms);
|
||||
assert_eq!(output, "echo \"hello\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_command_bad_template() {
|
||||
let cmd = "echo \"{{msg\"";
|
||||
let mut params = HashMap::new();
|
||||
params.insert("msg".to_string(), "hello".to_string());
|
||||
|
||||
let output = render_command(&cmd, ¶ms);
|
||||
assert_eq!(output, "echo \"{{msg\"");
|
||||
}
|
||||
}
|
||||
mod tests {}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::inventory::{Group, Inventory, Target};
|
||||
use zap_parser::plan::ExecutableTask;
|
||||
|
||||
pub mod ssh;
|
||||
|
||||
|
@ -7,6 +8,6 @@ pub mod ssh;
|
|||
* connecting to targets
|
||||
*/
|
||||
pub trait Transport {
|
||||
fn run_group(&self, cmd: &str, group: &Group, inv: &Inventory) -> i32;
|
||||
fn run(&self, command: &str, target: &Target) -> i32;
|
||||
fn run_group(&self, cmd: &ExecutableTask, group: &Group, inv: &Inventory) -> i32;
|
||||
fn run(&self, command: &ExecutableTask, target: &Target) -> i32;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
use crate::inventory::{Group, Inventory, Target};
|
||||
use crate::transport::Transport;
|
||||
|
||||
use log::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssh2::Session;
|
||||
use std::convert::TryInto;
|
||||
use std::io::prelude::*;
|
||||
use std::io::BufReader;
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
|
||||
use zap_parser::plan::ExecutableTask;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Ssh {}
|
||||
|
@ -16,7 +22,7 @@ impl Default for Ssh {
|
|||
}
|
||||
|
||||
impl Transport for Ssh {
|
||||
fn run_group(&self, command: &str, group: &Group, inventory: &Inventory) -> i32 {
|
||||
fn run_group(&self, command: &ExecutableTask, group: &Group, inventory: &Inventory) -> i32 {
|
||||
let mut status = 1;
|
||||
for target_name in group.targets.iter() {
|
||||
// XXX: This is inefficient
|
||||
|
@ -29,7 +35,8 @@ impl Transport for Ssh {
|
|||
}
|
||||
status
|
||||
}
|
||||
fn run(&self, command: &str, target: &Target) -> i32 {
|
||||
|
||||
fn run(&self, command: &ExecutableTask, target: &Target) -> i32 {
|
||||
// Connect to the local SSH server
|
||||
let tcp = TcpStream::connect(format!("{}:22", target.uri)).unwrap();
|
||||
let mut sess = Session::new().unwrap();
|
||||
|
@ -51,13 +58,110 @@ impl Transport for Ssh {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
channel.exec(command).unwrap();
|
||||
let remote_script = "._zap_command";
|
||||
let args_file = "._zap_args.json";
|
||||
|
||||
let mut s = String::new();
|
||||
channel.read_to_string(&mut s).unwrap();
|
||||
print!("{}", s);
|
||||
channel.wait_close().expect("Failed to close the channel");
|
||||
return channel.exit_status().unwrap();
|
||||
if let Some(provides) = &command.parameters.get("provides") {
|
||||
debug!(
|
||||
"A `provides` parameter was given, checking to see if {} exists on the remote",
|
||||
provides
|
||||
);
|
||||
if let Err(error) = sess.scp_recv(&Path::new(&provides)) {
|
||||
if error.code() == ssh2::ErrorCode::Session(-28) {
|
||||
debug!(
|
||||
"The provided file ({}) does not exist, the command should be run",
|
||||
provides
|
||||
);
|
||||
} else {
|
||||
error!(
|
||||
"A failure occurred while trying to check the provided file: {:?}",
|
||||
error
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
// If we successfully fetched the provided file, then we should
|
||||
// return 0 and skip the function
|
||||
debug!(
|
||||
"The provided file ({}) was found, avoiding re-running",
|
||||
provides
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(script) = command.task.script.as_bytes(Some(&command.parameters)) {
|
||||
let mut remote_file = sess
|
||||
.scp_send(
|
||||
Path::new(remote_script),
|
||||
0o700,
|
||||
script
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("Overflow converting the size of the generated file, yikes!"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
remote_file.write(&script).unwrap();
|
||||
// Close the channel and wait for the whole content to be tranferred
|
||||
remote_file.send_eof().unwrap();
|
||||
remote_file.wait_eof().unwrap();
|
||||
remote_file.close().unwrap();
|
||||
remote_file.wait_close().unwrap();
|
||||
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
let stderr = channel.stderr();
|
||||
|
||||
if command.task.script.has_file() {
|
||||
let args = serde_json::to_string(&command.parameters)
|
||||
.expect("Failed to serialize parameters for task");
|
||||
let mut remote_file = sess
|
||||
.scp_send(
|
||||
Path::new(args_file),
|
||||
0o400,
|
||||
args.len().try_into().expect(
|
||||
"Failed converting the size of the generated args file, yikes!",
|
||||
),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
remote_file.write(&args.as_bytes()).unwrap();
|
||||
// Close the channel and wait for the whole content to be tranferred
|
||||
remote_file.send_eof().unwrap();
|
||||
remote_file.wait_eof().unwrap();
|
||||
remote_file.close().unwrap();
|
||||
remote_file.wait_close().unwrap();
|
||||
channel
|
||||
.exec(&format!("./{} {}", remote_script, args_file))
|
||||
.unwrap();
|
||||
} else {
|
||||
channel.exec(&format!("./{}", remote_script)).unwrap();
|
||||
}
|
||||
|
||||
let reader = BufReader::new(stderr);
|
||||
for line in reader.lines() {
|
||||
println!("err: {}", line.unwrap());
|
||||
}
|
||||
|
||||
let mut s = String::new();
|
||||
channel.read_to_string(&mut s).unwrap();
|
||||
print!("{}", s);
|
||||
channel.wait_close().expect("Failed to close the channel");
|
||||
let exit = channel.exit_status().unwrap();
|
||||
|
||||
/*
|
||||
* This seems a little dumb and hacky, but we need to clean up the file
|
||||
* somehow and I'm not seeing anything that would allow me to just reach
|
||||
* out and remove a file
|
||||
*/
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
channel
|
||||
.exec(&format!("rm -f {} {}", remote_script, args_file))
|
||||
.unwrap();
|
||||
return exit;
|
||||
} else {
|
||||
error!("No script available to run for task!");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,20 @@
|
|||
* It is expected to be run from the root of the project tree.
|
||||
*/
|
||||
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
task 'tasks/echo.ztask' {
|
||||
msg = 'Hello from the wonderful world of zplans!'
|
||||
}
|
||||
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "This is nice"
|
||||
task 'tasks/echo.ztask' {
|
||||
msg = 'This is nice'
|
||||
}
|
||||
|
||||
task 'tasks/shell/bash.ztask' {
|
||||
script = '''
|
||||
ls -lah
|
||||
touch foo
|
||||
'''
|
||||
|
||||
// Don't run again if the foo file is present
|
||||
provides = 'foo'
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ authors = ["R. Tyler Croy <rtyler@brokenco.de>"]
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
handlebars = "~3.5"
|
||||
log = "0.4"
|
||||
pest = "~2.1"
|
||||
pest_derive = "~2.1"
|
||||
|
|
|
@ -26,11 +26,16 @@ identifier = { ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
|
|||
opening_brace = _{ "{" }
|
||||
closing_brace = _{ "}" }
|
||||
equals = _{ "=" }
|
||||
quote = _{ "\"" }
|
||||
quote = _{ "'" }
|
||||
triple_quote = _{ "'''" }
|
||||
|
||||
string = { triple_quoted | single_quoted }
|
||||
single_quoted = ${ (quote ~ inner_single_str ~ quote) }
|
||||
inner_single_str = @{ (!(quote | "\\") ~ ANY)* ~ (escape ~ inner_single_str)? }
|
||||
|
||||
triple_quoted = ${ (triple_quote ~ inner_triple_str ~ triple_quote) }
|
||||
inner_triple_str = @{ (!(triple_quote | "\\") ~ ANY)* ~ (escape ~ inner_triple_str)? }
|
||||
|
||||
string = { double_quoted }
|
||||
double_quoted = ${ (quote ~ inner_double_str ~ quote) }
|
||||
inner_double_str = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner_double_str)? }
|
||||
escape = @{ "\\" ~ ("\"" | "\\" | "r" | "n" | "t" | "0" | "'" | code | unicode) }
|
||||
code = @{ "x" ~ hex_digit{2} }
|
||||
unicode = @{ "u" ~ opening_brace ~ hex_digit{2, 6} ~ closing_brace }
|
||||
|
|
|
@ -17,7 +17,7 @@ pub struct ExecutableTask {
|
|||
}
|
||||
|
||||
impl ExecutableTask {
|
||||
fn new(task: crate::task::Task, parameters: HashMap<String, String>) -> Self {
|
||||
pub fn new(task: crate::task::Task, parameters: HashMap<String, String>) -> Self {
|
||||
Self { task, parameters }
|
||||
}
|
||||
}
|
||||
|
@ -138,10 +138,16 @@ fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
|
|||
Rule::string => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::double_quoted => {
|
||||
Rule::triple_quoted => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::inner_double_str => {
|
||||
Rule::single_quoted => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::inner_single_str => {
|
||||
return Ok(parsed.as_str().to_string());
|
||||
}
|
||||
Rule::inner_triple_str => {
|
||||
return Ok(parsed.as_str().to_string());
|
||||
}
|
||||
_ => {}
|
||||
|
@ -168,12 +174,12 @@ mod tests {
|
|||
* It is expected to be run from the root of the project tree.
|
||||
*/
|
||||
|
||||
task "../tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
task '../tasks/echo.ztask' {
|
||||
msg = 'Hello from the wonderful world of zplans!'
|
||||
}
|
||||
|
||||
task "../tasks/echo.ztask" {
|
||||
msg = "This can actually take inline shells too: $(date)"
|
||||
task '../tasks/echo.ztask' {
|
||||
msg = 'This can actually take inline shells too: $(date)'
|
||||
}"#;
|
||||
let _plan = PlanParser::parse(Rule::planfile, buf)
|
||||
.unwrap()
|
||||
|
@ -183,12 +189,12 @@ task "../tasks/echo.ztask" {
|
|||
|
||||
#[test]
|
||||
fn parse_plan_fn() {
|
||||
let buf = r#"task "../tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
let buf = r#"task '../tasks/echo.ztask' {
|
||||
msg = 'Hello from the wonderful world of zplans!'
|
||||
}
|
||||
|
||||
task "../tasks/echo.ztask" {
|
||||
msg = "This can actually take inline shells too: $(date)"
|
||||
task '../tasks/echo.ztask' {
|
||||
msg = 'This can actually take inline shells too: $(date)'
|
||||
}"#;
|
||||
let plan = Plan::from_str(buf).expect("Failed to parse the plan");
|
||||
assert_eq!(plan.tasks.len(), 2);
|
||||
|
|
|
@ -32,10 +32,11 @@ ptype = { "type" ~ equals ~ typedef }
|
|||
|
||||
script = { "script"
|
||||
~ opening_brace
|
||||
~ (script_inline)
|
||||
~ (script_inline | script_file)
|
||||
~ closing_brace
|
||||
}
|
||||
script_inline = _{ "inline" ~ equals ~ string }
|
||||
script_inline = { "inline" ~ equals ~ string }
|
||||
script_file = { "file" ~ equals ~ string }
|
||||
|
||||
|
||||
|
||||
|
@ -49,11 +50,16 @@ identifier = { ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
|
|||
opening_brace = _{ "{" }
|
||||
closing_brace = _{ "}" }
|
||||
equals = _{ "=" }
|
||||
quote = _{ "\"" }
|
||||
quote = _{ "'" }
|
||||
triple_quote = _{ "'''" }
|
||||
|
||||
string = { triple_quoted | single_quoted }
|
||||
single_quoted = ${ (quote ~ inner_single_str ~ quote) }
|
||||
inner_single_str = @{ (!(quote | "\\") ~ ANY)* ~ (escape ~ inner_single_str)? }
|
||||
|
||||
triple_quoted = ${ (triple_quote ~ inner_triple_str ~ triple_quote) }
|
||||
inner_triple_str = @{ (!(triple_quote | "\\") ~ ANY)* ~ (escape ~ inner_triple_str)? }
|
||||
|
||||
string = { double_quoted }
|
||||
double_quoted = ${ (quote ~ inner_double_str ~ quote) }
|
||||
inner_double_str = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner_double_str)? }
|
||||
escape = @{ "\\" ~ ("\"" | "\\" | "r" | "n" | "t" | "0" | "'" | code | unicode) }
|
||||
code = @{ "x" ~ hex_digit{2} }
|
||||
unicode = @{ "u" ~ opening_brace ~ hex_digit{2, 6} ~ closing_brace }
|
||||
|
|
|
@ -1,33 +1,125 @@
|
|||
use log::*;
|
||||
use pest::error::Error as PestError;
|
||||
use pest::error::ErrorVariant;
|
||||
use pest::iterators::Pairs;
|
||||
use pest::Parser;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "task.pest"]
|
||||
struct TaskParser;
|
||||
|
||||
/**
|
||||
* A Script represents something that can be executed oa a remote host.
|
||||
*
|
||||
* These come in two variants:
|
||||
* - Inline string of shell commands to run
|
||||
* - A script or binary file to transfer and execute
|
||||
*/
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Script {
|
||||
/**
|
||||
* Inline scripts will have parameters rendered into then with handlebars syntax
|
||||
*/
|
||||
pub inline: Option<String>,
|
||||
/**
|
||||
* File scripts will be executed with the parameters passed as command line
|
||||
* arguments, e.g. the "msg" parameter would be passed as:
|
||||
* ./file --msg=value
|
||||
*/
|
||||
pub file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inline: None,
|
||||
file: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_file(&self) -> bool {
|
||||
self.file.is_some()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the script's contents as bytes
|
||||
*
|
||||
* This is useful for transferring the script to another host for execution
|
||||
*
|
||||
* If the `file` member is defined, that will be preferred, even if `inline` is also defined
|
||||
*/
|
||||
pub fn as_bytes(&self, parameters: Option<&HashMap<String, String>>) -> Option<Vec<u8>> {
|
||||
use handlebars::Handlebars;
|
||||
|
||||
if self.inline.is_some() && self.file.is_some() {
|
||||
warn!("Both inline and file structs are defined for this script, only file will be used!\n({})",
|
||||
self.inline.as_ref().unwrap());
|
||||
}
|
||||
|
||||
if let Some(path) = &self.file {
|
||||
match File::open(path) {
|
||||
Ok(mut file) => {
|
||||
let mut buf = vec![];
|
||||
|
||||
if let Ok(count) = file.read_to_end(&mut buf) {
|
||||
debug!("Read {} bytes of {}", count, path.display());
|
||||
return Some(buf);
|
||||
} else {
|
||||
error!("Failed to read the file {}", path.display());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to open the file at {}, {:?}", path.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(inline) = &self.inline {
|
||||
// Early exit if there are no parameters to render
|
||||
if parameters.is_none() {
|
||||
return Some(inline.as_bytes().to_vec());
|
||||
}
|
||||
|
||||
let parameters = parameters.unwrap();
|
||||
|
||||
let handlebars = Handlebars::new();
|
||||
match handlebars.render_template(inline, ¶meters) {
|
||||
Ok(rendered) => {
|
||||
return Some(rendered.as_bytes().to_vec());
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to render command ({:?}): {}", err, inline);
|
||||
return Some(inline.as_bytes().to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Task {
|
||||
pub name: String,
|
||||
pub inline: Option<String>,
|
||||
pub script: Script,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn get_script(&self) -> Option<&String> {
|
||||
self.inline.as_ref()
|
||||
}
|
||||
|
||||
pub fn new(name: &str) -> Self {
|
||||
Task {
|
||||
name: name.to_string(),
|
||||
inline: None,
|
||||
script: Script::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(parser: &mut Pairs<Rule>) -> Result<Self, PestError<Rule>> {
|
||||
let mut task: Option<Self> = None;
|
||||
let mut inline = None;
|
||||
let mut file = None;
|
||||
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
|
@ -35,17 +127,27 @@ impl Task {
|
|||
task = Some(Task::new(parsed.as_str()));
|
||||
}
|
||||
Rule::script => {
|
||||
let script = parse_str(&mut parsed.into_inner())?;
|
||||
|
||||
if let Some(ref mut task) = task {
|
||||
task.inline = Some(script);
|
||||
for pair in parsed.into_inner() {
|
||||
match pair.as_rule() {
|
||||
Rule::script_inline => {
|
||||
inline = Some(parse_str(&mut pair.into_inner())?);
|
||||
}
|
||||
Rule::script_file => {
|
||||
let path = parse_str(&mut pair.into_inner())?;
|
||||
file = Some(PathBuf::from(path));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(task) = task {
|
||||
if let Some(mut task) = task {
|
||||
task.script.inline = inline;
|
||||
task.script.file = file;
|
||||
|
||||
return Ok(task);
|
||||
} else {
|
||||
return Err(PestError::new_from_pos(
|
||||
|
@ -78,9 +180,6 @@ impl Task {
|
|||
}
|
||||
|
||||
pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
match File::open(path) {
|
||||
Ok(mut file) => {
|
||||
let mut contents = String::new();
|
||||
|
@ -118,10 +217,16 @@ fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
|
|||
Rule::string => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::double_quoted => {
|
||||
Rule::triple_quoted => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::inner_double_str => {
|
||||
Rule::single_quoted => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::inner_single_str => {
|
||||
return Ok(parsed.as_str().to_string());
|
||||
}
|
||||
Rule::inner_triple_str => {
|
||||
return Ok(parsed.as_str().to_string());
|
||||
}
|
||||
_ => {}
|
||||
|
@ -146,12 +251,12 @@ mod tests {
|
|||
parameters {
|
||||
package {
|
||||
required = true
|
||||
help = "Name of package to be installed"
|
||||
help = 'Name of package to be installed'
|
||||
type = string
|
||||
}
|
||||
}
|
||||
script {
|
||||
inline = "zypper in -y ${ZAP_PACKAGE}"
|
||||
inline = 'zypper in -y {{package}}'
|
||||
}
|
||||
}"#;
|
||||
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
|
||||
|
@ -161,7 +266,7 @@ mod tests {
|
|||
fn parse_no_parameters() {
|
||||
let buf = r#"task PrintEnv {
|
||||
script {
|
||||
inline = "env"
|
||||
inline = 'env'
|
||||
}
|
||||
}"#;
|
||||
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
|
||||
|
@ -171,11 +276,28 @@ mod tests {
|
|||
fn parse_task_fn() {
|
||||
let buf = r#"task PrintEnv {
|
||||
script {
|
||||
inline = "env"
|
||||
inline = 'env'
|
||||
}
|
||||
}"#;
|
||||
let task = Task::from_str(buf).expect("Failed to parse the task");
|
||||
assert_eq!(task.name, "PrintEnv");
|
||||
assert_eq!(task.get_script().unwrap(), "env");
|
||||
|
||||
let script = task.script;
|
||||
|
||||
assert_eq!(script.as_bytes(None).unwrap(), "env".as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_task_fn_with_triple_quotes() {
|
||||
let buf = r#"task PrintEnv {
|
||||
script {
|
||||
inline = '''env'''
|
||||
}
|
||||
}"#;
|
||||
let task = Task::from_str(buf).expect("Failed to parse the task");
|
||||
assert_eq!(task.name, "PrintEnv");
|
||||
|
||||
let script = task.script;
|
||||
assert_eq!(script.as_bytes(None).unwrap(), "env".as_bytes());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@ task Echo {
|
|||
parameters {
|
||||
msg {
|
||||
required = true
|
||||
help = "String to echo back to the client"
|
||||
help = 'String to echo back to the client'
|
||||
type = string
|
||||
}
|
||||
}
|
||||
|
||||
script {
|
||||
inline = "echo {{msg}}"
|
||||
inline = 'echo "{{msg}}"'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
task Bash {
|
||||
parameters {
|
||||
script {
|
||||
required = true
|
||||
help = 'A script to run via the bash shell (assumes bash is in the defult PATH)'
|
||||
type = string
|
||||
}
|
||||
}
|
||||
|
||||
script {
|
||||
inline = '''#!/usr/bin/env bash
|
||||
{{script}}
|
||||
'''
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue