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:
R Tyler Croy 2020-12-31 09:20:22 -08:00
parent 754fd428f8
commit ff3f3c5263
8 changed files with 115 additions and 55 deletions

23
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -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 {

View File

@ -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, &parameters);
// 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, &params);
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, &params);
assert_eq!(output, "echo \"{{msg\"");
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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]

View File

@ -7,6 +7,6 @@ task Echo {
}
}
script {
inline = "env; echo ${ZAP_MSG}"
inline = "echo {{msg}}"
}
}