Move away from environment variables and switch to rendering scripts as handlebars
This will make sure there's a better cross-platform approach for getting parameters into these commands.
This commit is contained in:
parent
754fd428f8
commit
ff3f3c5263
|
@ -166,6 +166,20 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "handlebars"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "964d0e99a61fe9b1b347389b77ebf8b7e1587b70293676aaca7d27e59b9073b2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"quick-error 2.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.17"
|
||||
|
@ -181,7 +195,7 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
|
||||
dependencies = [
|
||||
"quick-error",
|
||||
"quick-error 1.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -381,6 +395,12 @@ version = "1.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ac73b1112776fc109b2e61909bc46c7e1bf0d7f690ffb1676553acce16d5cda"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.8"
|
||||
|
@ -599,6 +619,7 @@ dependencies = [
|
|||
"colored",
|
||||
"glob",
|
||||
"gumdrop",
|
||||
"handlebars",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"serde",
|
||||
|
|
|
@ -8,6 +8,7 @@ edition = "2018"
|
|||
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
|
||||
|
|
|
@ -26,7 +26,9 @@ pub struct Config {
|
|||
pub transport: Transport,
|
||||
pub ssh: Option<SshConfig>,
|
||||
}
|
||||
fn default_transport() -> Transport { Transport::Ssh }
|
||||
fn default_transport() -> Transport {
|
||||
Transport::Ssh
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct SshConfig {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
use colored::*;
|
||||
use gumdrop::Options;
|
||||
use log::*;
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
|
||||
mod inventory;
|
||||
mod transport;
|
||||
|
||||
use zap_parser::*;
|
||||
use crate::inventory::*;
|
||||
use crate::transport::ssh::Ssh;
|
||||
use crate::transport::Transport;
|
||||
use zap_parser::*;
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
@ -43,12 +43,11 @@ fn load_ztasks() -> Vec<Task> {
|
|||
for entry in glob("tasks/**/*.ztask").expect("Failed to read glob pattern") {
|
||||
match entry {
|
||||
Ok(path) => {
|
||||
let task = Task::from_path(&path).unwrap();
|
||||
if let Ok(task) = Task::from_path(&path) {
|
||||
info!("loaded ztask: {}", task.name);
|
||||
tasks.push(task);
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => println!("{:?}", e),
|
||||
}
|
||||
}
|
||||
|
@ -63,25 +62,30 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
|
||||
for task in load_ztasks() {
|
||||
if task.name == opts.task {
|
||||
let mut env = crate::transport::EnvVars::new();
|
||||
let mut parameters = HashMap::new();
|
||||
|
||||
/*
|
||||
* XXX: This is very primitive way, there must be a better way to take
|
||||
* arbitrary command line parameters than this.
|
||||
*/
|
||||
for parameter in opts.parameter.iter() {
|
||||
let parts: Vec<&str> = parameter.split("=").collect();
|
||||
if parts.len() == 2 {
|
||||
env.insert(parts[0].to_string(), parts[1].to_string());
|
||||
parameters.insert(parts[0].to_string(), parts[1].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(script) = task.get_script() {
|
||||
let command = render_command(&script, ¶meters);
|
||||
|
||||
// TODO: refactor with handle_cmd
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == opts.targets) {
|
||||
std::process::exit(runner.run_group(&script, &group, &inventory, Some(env)));
|
||||
std::process::exit(runner.run_group(&command, &group, &inventory));
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == opts.targets) {
|
||||
std::process::exit(runner.run(&script, &target, Some(&env)));
|
||||
std::process::exit(runner.run(&command, &target));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,15 +103,40 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
*/
|
||||
fn handle_cmd(opts: CmdOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == opts.targets) {
|
||||
std::process::exit(runner.run_group(&opts.command, &group, &inventory, None));
|
||||
std::process::exit(runner.run_group(&opts.command, &group, &inventory));
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == opts.targets) {
|
||||
println!("{}", format!("run a command: {:?}", opts).green());
|
||||
std::process::exit(runner.run(&opts.command, &target, None));
|
||||
std::process::exit(runner.run(&opts.command, &target));
|
||||
}
|
||||
|
||||
println!("{}", format!("Couldn't find a target named `{}`", opts.targets).red());
|
||||
println!(
|
||||
"{}",
|
||||
format!("Couldn't find a target named `{}`", opts.targets).red()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
|
@ -161,8 +190,33 @@ struct CmdOpts {
|
|||
struct TaskOpts {
|
||||
#[options(free, help = "Task to execute, must exist in ZAP_PATH")]
|
||||
task: String,
|
||||
#[options(short="p", help = "Parameter values")]
|
||||
#[options(short = "p", help = "Parameter values")]
|
||||
parameter: Vec<String>,
|
||||
#[options(help = "Name of a target or group")]
|
||||
targets: String,
|
||||
}
|
||||
|
||||
#[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\"");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
use crate::inventory::{Group, Inventory, Target};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
pub type EnvVars = HashMap<String, String>;
|
||||
|
||||
pub mod ssh;
|
||||
|
||||
|
@ -11,6 +7,6 @@ pub mod ssh;
|
|||
* connecting to targets
|
||||
*/
|
||||
pub trait Transport {
|
||||
fn run_group(&self, cmd: &str, group: &Group, inv: &Inventory, env: Option<EnvVars>) -> i32;
|
||||
fn run(&self, command: &str, target: &Target, env: Option<&EnvVars>) -> i32;
|
||||
fn run_group(&self, cmd: &str, group: &Group, inv: &Inventory) -> i32;
|
||||
fn run(&self, command: &str, target: &Target) -> i32;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::inventory::{Group, Inventory, Target};
|
||||
use crate::transport::EnvVars;
|
||||
use crate::transport::Transport;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -17,20 +16,20 @@ impl Default for Ssh {
|
|||
}
|
||||
|
||||
impl Transport for Ssh {
|
||||
fn run_group(&self, command: &str, group: &Group, inventory: &Inventory, env: Option<EnvVars>) -> i32 {
|
||||
fn run_group(&self, command: &str, group: &Group, inventory: &Inventory) -> i32 {
|
||||
let mut status = 1;
|
||||
for target_name in group.targets.iter() {
|
||||
// XXX: This is inefficient
|
||||
for target in inventory.targets.iter() {
|
||||
if &target.name == target_name {
|
||||
println!("Running on `{}`", target.name);
|
||||
status = self.run(command, &target, env.as_ref());
|
||||
status = self.run(command, &target);
|
||||
}
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
fn run(&self, command: &str, target: &Target, env: Option<&EnvVars>) -> i32 {
|
||||
fn run(&self, command: &str, target: &Target) -> i32 {
|
||||
// Connect to the local SSH server
|
||||
let tcp = TcpStream::connect(format!("{}:22", target.uri)).unwrap();
|
||||
let mut sess = Session::new().unwrap();
|
||||
|
@ -42,28 +41,19 @@ impl Transport for Ssh {
|
|||
if let Some(config) = &target.config {
|
||||
if let Some(sshconfig) = &config.ssh {
|
||||
// requires PasswordAuthentication yes
|
||||
sess.userauth_password(&sshconfig.user, &sshconfig.password).unwrap();
|
||||
sess.userauth_password(&sshconfig.user, &sshconfig.password)
|
||||
.unwrap();
|
||||
authenticated = true;
|
||||
}
|
||||
}
|
||||
if ! authenticated {
|
||||
if !authenticated {
|
||||
sess.userauth_agent(&std::env::var("USER").unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
channel.exec(command).unwrap();
|
||||
|
||||
let mut segments = vec![];
|
||||
|
||||
if let Some(env) = env {
|
||||
for (key, val) in env.iter() {
|
||||
channel.setenv(key, val);
|
||||
segments.push(format!("export ZAP_{}=\"{}\"", key.to_uppercase(), val));
|
||||
}
|
||||
}
|
||||
segments.push(command.to_string());
|
||||
|
||||
channel.exec(&segments.join(";")).unwrap();
|
||||
let mut s = String::new();
|
||||
channel.read_to_string(&mut s).unwrap();
|
||||
print!("{}", s);
|
||||
|
|
|
@ -3,17 +3,16 @@ extern crate pest;
|
|||
#[macro_use]
|
||||
extern crate pest_derive;
|
||||
|
||||
use pest::Parser;
|
||||
use pest::error::Error as PestError;
|
||||
use pest::error::ErrorVariant;
|
||||
use pest::iterators::Pairs;
|
||||
use pest::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar="task.pest"]
|
||||
#[grammar = "task.pest"]
|
||||
struct TaskParser;
|
||||
|
||||
|
||||
pub struct Task {
|
||||
pub name: String,
|
||||
inline: Option<String>,
|
||||
|
@ -25,7 +24,7 @@ impl Task {
|
|||
}
|
||||
|
||||
pub fn new(name: &str) -> Self {
|
||||
Task {
|
||||
Task {
|
||||
name: name.to_string(),
|
||||
inline: None,
|
||||
}
|
||||
|
@ -38,7 +37,7 @@ impl Task {
|
|||
match parsed.as_rule() {
|
||||
Rule::identifier => {
|
||||
task = Some(Task::new(parsed.as_str()));
|
||||
},
|
||||
}
|
||||
Rule::script => {
|
||||
let script = parse_str(&mut parsed.into_inner())?;
|
||||
|
||||
|
@ -46,14 +45,13 @@ impl Task {
|
|||
task.inline = Some(script);
|
||||
}
|
||||
}
|
||||
_ => {},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(task) = task {
|
||||
return Ok(task);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not find a valid task definition".to_string(),
|
||||
|
@ -70,8 +68,8 @@ impl Task {
|
|||
match parsed.as_rule() {
|
||||
Rule::task => {
|
||||
return Task::parse(&mut parsed.into_inner());
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return Err(PestError::new_from_pos(
|
||||
|
@ -123,10 +121,10 @@ fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
|
|||
match parsed.as_rule() {
|
||||
Rule::string => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
},
|
||||
}
|
||||
Rule::double_quoted => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
},
|
||||
}
|
||||
Rule::inner_double_str => {
|
||||
return Ok(parsed.as_str().to_string());
|
||||
}
|
||||
|
@ -160,8 +158,7 @@ mod tests {
|
|||
inline = "zypper in -y ${ZAP_PACKAGE}"
|
||||
}
|
||||
}"#;
|
||||
let _task = TaskParser::parse(Rule::task, buf)
|
||||
.unwrap().next().unwrap();
|
||||
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -171,8 +168,7 @@ mod tests {
|
|||
inline = "env"
|
||||
}
|
||||
}"#;
|
||||
let task = TaskParser::parse(Rule::task, buf)
|
||||
.unwrap().next().unwrap();
|
||||
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -7,6 +7,6 @@ task Echo {
|
|||
}
|
||||
}
|
||||
script {
|
||||
inline = "env; echo ${ZAP_MSG}"
|
||||
inline = "echo {{msg}}"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue