Compare commits

...

5 Commits

Author SHA1 Message Date
R Tyler Croy 35e186f828 Explore syntax for built-in tasks. Implementation is still ... crap 2021-01-01 15:39:06 -08:00
R Tyler Croy e2a5f74c3a More refactoring to clean up the ssh transport a bit 2021-01-01 15:14:21 -08:00
R Tyler Croy c6d62687b4 Add Transport.file_exists to clean up the run method for builtin tasks 2021-01-01 15:03:14 -08:00
R Tyler Croy 4888aa9ecf Make the .ztask suffix not required when loading in the zplan
Fixes #3
2021-01-01 14:41:42 -08:00
R Tyler Croy 50ab1a5b14 Restructuring the file tree to make it easier to start adding "native tasks"
Some number of tasks I will want to be "builtin" and need to know a little bit
more about the transport in order to make that work.

The traits, they are coming.

See #1
2021-01-01 14:32:51 -08:00
14 changed files with 245 additions and 107 deletions

96
Cargo.lock generated
View File

@ -131,6 +131,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]]
name = "form_urlencoded"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "generic-array"
version = "0.12.3"
@ -192,6 +202,17 @@ dependencies = [
"quick-error 1.2.3",
]
[[package]]
name = "idna"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "itoa"
version = "0.4.7"
@ -266,6 +287,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "matches"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "memchr"
version = "2.3.4"
@ -315,6 +342,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pest"
version = "2.1.3"
@ -515,9 +548,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.56"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9802ddde94170d186eeee5005b798d9c159fa970403f1be19976d0cfb939b72"
checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6"
dependencies = [
"proc-macro2",
"quote",
@ -542,6 +575,21 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "tinyvec"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "typenum"
version = "1.12.0"
@ -554,12 +602,42 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-bidi"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
dependencies = [
"matches",
]
[[package]]
name = "unicode-normalization"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "url"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "vcpkg"
version = "0.2.11"
@ -608,26 +686,30 @@ dependencies = [
[[package]]
name = "zap-cli"
version = "0.1.1"
version = "0.2.0"
dependencies = [
"colored",
"gumdrop",
"log",
"pretty_env_logger",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"ssh2",
"zap-model",
]
[[package]]
name = "zap-model"
version = "0.1.1"
version = "0.2.0"
dependencies = [
"colored",
"handlebars",
"log",
"pest",
"pest_derive",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"ssh2",
"url",
]

View File

@ -139,11 +139,11 @@ will be executed in the order that they are defined.
.simple.zplan
[source]
----
task 'tasks/echo.ztask' {
task 'tasks/echo' {
msg = 'Hello from the wonderful world of zplans!'
}
task 'tasks/echo.ztask' {
task 'tasks/echo' {
msg = 'This is nice'
}

View File

@ -1,6 +1,6 @@
[package]
name = "zap-cli"
version = "0.1.1"
version = "0.2.0"
authors = ["R. Tyler Croy <rtyler@brokenco.de>"]
edition = "2018"
description = "A simple configuration management and orchestration tool"
@ -13,17 +13,11 @@ keywords = ["sysadmin", "management"]
name = "zap"
path = "src/main.rs"
[dependencies]
colored = "2"
gumdrop = "~0.8.0"
log = "0.4"
pretty_env_logger = "0.4"
# Needed for deserializing JSON messages _and_ managing our configuration
# effectively
serde = { version = "~1.0", features = ["derive", "rc"] }
serde_derive = "~1.0"
serde_json = "~1.0"
serde_yaml = "~0.8"
ssh2 = "~0.9.0"
zap-model = { version = "~0.1", path = "../model" }
zap-model = { version = "~0.2", path = "../model" }

View File

@ -5,14 +5,10 @@ use std::collections::HashMap;
use std::io::BufReader;
use std::path::PathBuf;
mod inventory;
mod transport;
use crate::inventory::*;
use crate::transport::ssh::Ssh;
use zap_model::plan::Plan;
use zap_model::task::Task;
use zap_model::inventory::Inventory;
use zap_model::transport::ssh::Ssh;
use zap_model::ExecutableTask;
use zap_model::{Plan, Task, Transport};
fn main() {
pretty_env_logger::init();
@ -29,7 +25,7 @@ fn main() {
let inventory: Inventory = serde_yaml::from_reader(reader).expect("Failed to read intenvory");
let mut runner = match &inventory.config.transport {
crate::inventory::Transport::Ssh => Ssh::default(),
zap_model::inventory::Transport::Ssh => Ssh::default(),
};
match opts.command.unwrap() {
@ -63,7 +59,7 @@ fn handle_check(opts: CheckOpts) {
/**
* This function will parse and execute a plan
*/
fn handle_plan(opts: PlanOpts, runner: &mut dyn crate::transport::Transport, inventory: Inventory) {
fn handle_plan(opts: PlanOpts, runner: &mut dyn Transport, inventory: Inventory) {
println!("{}", format!("Running plan with: {:?}", opts).green());
let mut exit: i32 = -1;
@ -91,7 +87,7 @@ fn handle_plan(opts: PlanOpts, runner: &mut dyn crate::transport::Transport, inv
fn execute_task_on(
targets: String,
task: &ExecutableTask,
runner: &mut dyn crate::transport::Transport,
runner: &mut dyn Transport,
inventory: &Inventory,
dry_run: bool,
) -> i32 {
@ -109,7 +105,7 @@ fn execute_task_on(
/**
* This function will handle a task
*/
fn handle_task(opts: TaskOpts, runner: &mut dyn crate::transport::Transport, inventory: Inventory) {
fn handle_task(opts: TaskOpts, runner: &mut dyn Transport, inventory: Inventory) {
println!("{}", format!("Running task with: {:?}", opts).green());
match Task::from_path(&opts.task) {
@ -154,7 +150,7 @@ fn handle_task(opts: TaskOpts, runner: &mut dyn crate::transport::Transport, inv
* In the case of multiple targets, any non-zero status code will be used to exit
* non-zero.
*/
fn handle_cmd(opts: CmdOpts, runner: &mut dyn crate::transport::Transport, inventory: Inventory) {
fn handle_cmd(opts: CmdOpts, runner: &mut dyn Transport, inventory: Inventory) {
let mut task = ExecutableTask::new(Task::new("Dynamic"), HashMap::new());
task.task.script.inline = Some(opts.command);
std::process::exit(execute_task_on(

View File

@ -4,20 +4,16 @@
* It is expected to be run from the root of the project tree.
*/
task 'tasks/echo.ztask' {
task 'tasks/echo' {
msg = 'Hello from the wonderful world of zplans!'
}
task 'tasks/echo.ztask' {
task 'tasks/echo' {
msg = 'This is nice'
}
task 'tasks/shell/bash.ztask' {
task 'zap://sh' {
script = '''
ls -lah
touch foo
pwd
'''
// Don't run again if the foo file is present
provides = 'foo'
}

View File

@ -1,6 +1,6 @@
[package]
name = "zap-model"
version = "0.1.1"
version = "0.2.0"
authors = ["R. Tyler Croy <rtyler@brokenco.de>"]
edition = "2018"
description = "Internal models for zap, a simple configuration management tool"
@ -10,7 +10,16 @@ license = "AGPL-3.0+"
keywords = ["sysadmin", "management"]
[dependencies]
colored = "2"
handlebars = "~3.5"
log = "0.4"
pest = "~2.1"
pest_derive = "~2.1"
# Needed for deserializing JSON messages _and_ managing our configuration
# effectively
serde = { version = "~1.0", features = ["derive", "rc"] }
serde_derive = "~1.0"
serde_json = "~1.0"
serde_yaml = "~0.8"
ssh2 = "~0.9.0"
url = "~2.2"

View File

@ -4,9 +4,17 @@ extern crate pest;
extern crate pest_derive;
use std::collections::HashMap;
use std::path::PathBuf;
pub mod inventory;
pub mod plan;
pub mod task;
pub mod tasks;
pub mod transport;
pub use crate::plan::Plan;
pub use crate::task::Task;
pub use crate::transport::{Transport, TransportError};
/**
* An ExecutableTask is a light container over a Task execpt with user-provided information and is
@ -14,12 +22,24 @@ pub mod task;
*/
#[derive(Clone, Debug)]
pub struct ExecutableTask {
pub task: task::Task,
pub task: Task,
pub parameters: HashMap<String, String>,
}
impl ExecutableTask {
pub fn new(task: task::Task, parameters: HashMap<String, String>) -> Self {
pub fn new(task: Task, parameters: HashMap<String, String>) -> Self {
Self { task, parameters }
}
/**
* Provides will return the files that the ExecutableTask provides
*
* If these files exist, the task should not be executed
*/
pub fn provides() -> Vec<PathBuf> {
vec![]
}
}
#[cfg(test)]
mod tests {}

View File

@ -6,7 +6,7 @@ use pest::Parser;
use std::collections::HashMap;
use std::path::PathBuf;
use crate::ExecutableTask;
use crate::{ExecutableTask, Task};
#[derive(Parser)]
#[grammar = "plan.pest"]
@ -35,9 +35,16 @@ impl Plan {
for pair in parsed.into_inner() {
match pair.as_rule() {
Rule::string => {
let path = PathBuf::from(parse_str(&mut pair.into_inner())?);
let name = parse_str(&mut pair.into_inner())?;
let task = match name.starts_with("zap://") {
true => Task::from_url(&name),
false => {
let path = PathBuf::from(format!("{}.ztask", name));
Task::from_path(&path)
}
};
match crate::task::Task::from_path(&path) {
match task {
Ok(task) => raw_task = Some(task),
Err(err) => {
error!("Failed to parse task: {:?}", err);
@ -164,11 +171,11 @@ mod tests {
* It is expected to be run from the root of the project tree.
*/
task '../tasks/echo.ztask' {
task '../tasks/echo' {
msg = 'Hello from the wonderful world of zplans!'
}
task '../tasks/echo.ztask' {
task '../tasks/echo' {
msg = 'This can actually take inline shells too: $(date)'
}"#;
let _plan = PlanParser::parse(Rule::planfile, buf)
@ -179,11 +186,11 @@ task '../tasks/echo.ztask' {
#[test]
fn parse_plan_fn() {
let buf = r#"task '../tasks/echo.ztask' {
let buf = r#"task '../tasks/echo' {
msg = 'Hello from the wonderful world of zplans!'
}
task '../tasks/echo.ztask' {
task '../tasks/echo' {
msg = 'This can actually take inline shells too: $(date)'
}"#;
let plan = Plan::from_str(buf).expect("Failed to parse the plan");

View File

@ -7,6 +7,7 @@ use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use url::Url;
#[derive(Parser)]
#[grammar = "task.pest"]
@ -180,6 +181,28 @@ impl Task {
))
}
pub fn from_url(url: &str) -> Result<Self, PestError<Rule>> {
if let Ok(url) = Url::parse(url) {
println!("UR: {:?}", url);
if let Some(name) = url.host_str() {
// XXX: Temporary hard-coding see #5
let mut task = Task::new(name);
assert_eq!(name, "sh");
// This is a hacky temporary workaround for now too
// a real builtin shouldn't need to bother with a handlebars template
task.script.inline = Some("#!/bin/sh\n{{script}}".into());
return Ok(task);
}
}
Err(PestError::new_from_pos(
ErrorVariant::CustomError {
message: "Could not find a valid task definition".to_string(),
},
pest::Position::from_start(url),
))
}
pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
match File::open(path) {
Ok(mut file) => {
@ -317,4 +340,10 @@ mod tests {
let script = task.script;
assert_eq!(script.as_bytes(None).unwrap(), "env".as_bytes());
}
#[test]
fn task_from_url() {
let task = Task::from_url("zap://sh").expect("Failed to load task from URL");
assert_eq!(task.name, "sh");
}
}

1
model/src/tasks/mod.rs Normal file
View File

@ -0,0 +1 @@

View File

@ -1,9 +1,14 @@
use crate::inventory::{Group, Inventory, Target};
use crate::ExecutableTask;
use std::path::Path;
use zap_model::ExecutableTask;
pub mod ssh;
pub enum TransportError {
GeneralError(String),
}
/**
* The Transport trait allows for multiple transports to be implemented for
* connecting to targets
@ -11,6 +16,9 @@ pub mod ssh;
pub trait Transport {
fn connect(&mut self, target: &Target) -> bool;
fn disconnect(&mut self);
fn file_exists(&self, path: &Path) -> Result<bool, TransportError>;
fn run(&mut self, command: &ExecutableTask, target: &Target, dry_run: bool) -> i32;
fn run_script(&mut self, script: &str) -> i32;
fn run_group(
&mut self,
cmd: &ExecutableTask,
@ -18,6 +26,5 @@ pub trait Transport {
inv: &Inventory,
dry_run: bool,
) -> i32;
fn run(&mut self, command: &ExecutableTask, target: &Target, dry_run: bool) -> i32;
fn send_bytes(&self, remote_path: &Path, bytes: &Vec<u8>, mode: i32) -> bool;
}

View File

@ -1,5 +1,7 @@
use crate::inventory::{Group, Inventory, Target};
use crate::transport::Transport;
use crate::{ExecutableTask, TransportError};
use colored::*;
use log::*;
@ -10,7 +12,7 @@ use std::io::BufReader;
use std::net::TcpStream;
use std::path::Path;
use zap_model::ExecutableTask;
const REMOTE_SCRIPT: &str = "._zap_command";
#[derive(Clone)]
pub struct Ssh {
@ -90,6 +92,45 @@ impl Transport for Ssh {
true
}
fn file_exists(&self, path: &Path) -> Result<bool, TransportError> {
if let Err(error) = self.session.scp_recv(path) {
if error.code() == ssh2::ErrorCode::Session(-28) {
debug!("The file ({}) does not exist", path.display());
} else {
error!(
"A failure occurred while trying to check a file exists: {:?}",
error
);
return Err(TransportError::GeneralError(
"Failed to check that file exists".into(),
));
}
} else {
// If we successfully fetched the provided file, then we should
// return 0 and skip the function
trace!("The file exists: {}", path.display());
return Ok(true);
}
return Ok(false);
}
/**
* run_script will copy the given string over and execute it
*/
fn run_script(&mut self, script: &str) -> i32 {
if self.send_bytes(Path::new(REMOTE_SCRIPT), &script.as_bytes().to_vec(), 0o700) {
let mut channel = self.session.channel_session().unwrap();
channel.exec(&format!("./{}", REMOTE_SCRIPT));
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();
}
return -10;
}
fn run(&mut self, command: &ExecutableTask, target: &Target, dry_run: bool) -> i32 {
if !self.connect(target) {
error!("Failed to connect to {:?}", target);
@ -102,47 +143,21 @@ impl Transport for Ssh {
provides
);
if let Err(error) = self.session.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;
if let Ok(found) = self.file_exists(Path::new(provides)) {
if found {
debug!("File {} exists, skipping task", provides);
return 0;
}
} 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;
return -1;
}
}
let remote_script = "._zap_command";
let args_file = "._zap_args.json";
if let Some(unless) = &command.parameters.get("unless") {
debug!("An `unless` parameter was given, running {}", unless);
if self.send_bytes(Path::new(remote_script), &unless.as_bytes().to_vec(), 0o700) {
let mut channel = self.session.channel_session().unwrap();
channel.exec(&format!("./{}", remote_script));
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();
if exit == 0 {
debug!("Unless script returned success, so bailing out early");
return 0;
}
if 0 == self.run_script(unless) {
debug!("`unless` script returned 0, skipping the task");
return 0;
}
}
@ -156,27 +171,28 @@ impl Transport for Ssh {
return 0;
}
if !self.send_bytes(Path::new(remote_script), &script, 0o700) {
if !self.send_bytes(Path::new(REMOTE_SCRIPT), &script, 0o700) {
error!("Failed to upload script file for execution");
return -1;
}
let mut channel = self.session.channel_session().unwrap();
let stderr = channel.stderr();
let args_file = "._zap_args.json";
if command.task.script.has_file() {
let args = serde_json::to_string(&command.parameters)
.expect("Failed to serialize parameters for task");
if self.send_bytes(Path::new(args_file), &args.into_bytes(), 0o400) {
channel
.exec(&format!("./{} {}", remote_script, args_file))
.exec(&format!("./{} {}", REMOTE_SCRIPT, args_file))
.unwrap();
} else {
error!("Failed to upload the arguments file");
return -1;
}
} else {
channel.exec(&format!("./{}", remote_script)).unwrap();
channel.exec(&format!("./{}", REMOTE_SCRIPT)).unwrap();
}
let reader = BufReader::new(stderr);
@ -197,7 +213,7 @@ impl Transport for Ssh {
*/
let mut channel = self.session.channel_session().unwrap();
channel
.exec(&format!("rm -f {} {}", remote_script, args_file))
.exec(&format!("rm -f {} {}", REMOTE_SCRIPT, args_file))
.unwrap();
return exit;
} else {

View File

@ -1,19 +0,0 @@
/*
* The sh task is a simple passthrough to /bin/sh on the target machine
*/
task Sh {
parameters {
script {
required = true
help = 'A script to run via the /bin/sh'
type = string
}
}
script {
inline = '''#!/bin/sh
{{script}}
'''
}
}