Implement the execution of plans via the CLI
This commit is contained in:
parent
8981370222
commit
fbde2ed863
152
README.adoc
152
README.adoc
|
@ -12,141 +12,65 @@ Zap borrows ideas from
|
|||
link:https://puppet.com/docs/bolt/latest/bolt.html[Puppet Bolt]. but leaves
|
||||
some of the Puppet-based legacy from Bolt behind.
|
||||
|
||||
|
||||
== Goals
|
||||
|
||||
* Support BSD and Linux with ease
|
||||
* Make creating a plan very easy, including adding some simple logic
|
||||
* Explore tags-based task assignment
|
||||
* Pulll dependencies in through git-subtree or other, no inventing a new
|
||||
package distribution mechanism
|
||||
|
||||
== Examples
|
||||
|
||||
[source]
|
||||
----
|
||||
./target/debug/zap task tasks/echo.ztask -p msg="Hello World" -t zap-freebsd
|
||||
❯ zap task tasks/echo.ztask -p msg="Hello World" -t zap-freebsd
|
||||
Running task with: TaskOpts { task: "tasks/echo.ztask", parameter: ["msg=Hello World"], targets: "zap-freebsd" }
|
||||
Hello World
|
||||
|
||||
----
|
||||
|
||||
|
||||
== Sketches
|
||||
|
||||
.install.ztask
|
||||
[source]
|
||||
----
|
||||
/*
|
||||
* The FileSync should be an internal task type
|
||||
*/
|
||||
task FileSync {
|
||||
❯ zap plan ./examples/basic.zplan -t zap-freebsd
|
||||
Running plan with: PlanOpts { plan: "./examples/basic.zplan", targets: "zap-freebsd" }
|
||||
Hello from the wonderful world of zplans!
|
||||
This is nice
|
||||
|
||||
----
|
||||
|
||||
=== Task
|
||||
|
||||
A task is a simple container of some form of execution. Typically this will be
|
||||
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.
|
||||
|
||||
.echo.ztask
|
||||
[source]
|
||||
---
|
||||
task Echo {
|
||||
parameters {
|
||||
localfile {
|
||||
msg {
|
||||
required = true
|
||||
help = "String to echo back to the client"
|
||||
type = string
|
||||
help = "Path on the local system for the file to sync"
|
||||
required = true
|
||||
}
|
||||
remotefile {
|
||||
type = "string"
|
||||
help = "Path on the remote system for the file"
|
||||
required = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sketch of a user-defined task
|
||||
task Install {
|
||||
// The restrict block is a collection of expressions which ensure that the
|
||||
// task is not run anywhere it cannot run. Will fail if it's tried to run
|
||||
// somewhere it cannot be run
|
||||
restrict {
|
||||
// Regular expression to define what
|
||||
match_fact("platform", "(.*)-freebsd")
|
||||
match_fact("hostname", "www1")
|
||||
}
|
||||
|
||||
parameters {
|
||||
package {
|
||||
type = string
|
||||
help = 'The package to be installed'
|
||||
required = true
|
||||
}
|
||||
|
||||
// Unless should be implied on every task
|
||||
unless {
|
||||
type = string
|
||||
help = "Script which when returns zero if the package has been installed, i.e. `test -f /usr/bin/nginx`"
|
||||
}
|
||||
// provides should be implied on every task
|
||||
}
|
||||
|
||||
// Parameters exposed as environment variables
|
||||
// including the "ZAP_NOOP" variable which will be set if the script should
|
||||
// be run in noop
|
||||
script {
|
||||
// either the file or the inline must be defined
|
||||
file = "path/to/file/in/tree"
|
||||
// When using `program`, the task should expect to find:
|
||||
// command-name_x86_64-unknown-linux-gnu
|
||||
program = "/path/to/exes/in/tree"
|
||||
inline = """
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/ Exploring module syntax for namespacing these */
|
||||
module Install {
|
||||
task Pkg {
|
||||
restrict {}
|
||||
parameters {}
|
||||
script {}
|
||||
}
|
||||
|
||||
task Zypper {
|
||||
restrict {}
|
||||
parameters {}
|
||||
script {}
|
||||
inline = "echo {{msg}}"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Sketch of a zplan
|
||||
=== Plan
|
||||
|
||||
A plan is a collection of tasks which can be applied to a target or targets.
|
||||
Tasks are referenced with the parameters that should be passed into them, and
|
||||
will be executed in the order that they are defined.
|
||||
|
||||
|
||||
.simple.zplan
|
||||
[source]
|
||||
----
|
||||
// Should things be just done proceduraly?
|
||||
|
||||
task("sync-tree.ztask") { }
|
||||
|
||||
task('tasks/install.ztask') {
|
||||
name = "install-nginx"
|
||||
package = "nginx"
|
||||
unless = "test -f /usr/bin/nginx"
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
}
|
||||
|
||||
|
||||
// Run another plan from within this plan (supdawg)
|
||||
plan("plans/prepare-website.zplan") {
|
||||
// What parameters make sense here?
|
||||
}
|
||||
|
||||
|
||||
// Relationships between tasks, option A
|
||||
task('tasks/sync-tree.zplan') {
|
||||
source = './foo'
|
||||
destination = '/'
|
||||
|
||||
then = ["install-nginx"]
|
||||
}
|
||||
|
||||
// Relationships between tasks, option B
|
||||
task('tasks/sync-tree.zplan') {
|
||||
source = './foo'
|
||||
destination = '/'
|
||||
|
||||
then {
|
||||
task("tasks/install.ztask") {
|
||||
package = "nginx"
|
||||
unless = "test -f /usr/bin/nginx"
|
||||
}
|
||||
}
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "This is nice"
|
||||
}
|
||||
----
|
||||
|
|
|
@ -10,6 +10,7 @@ mod transport;
|
|||
|
||||
use crate::inventory::*;
|
||||
use crate::transport::ssh::Ssh;
|
||||
use zap_parser::plan::Plan;
|
||||
use zap_parser::task::Task;
|
||||
|
||||
fn main() {
|
||||
|
@ -42,6 +43,50 @@ fn main() {
|
|||
* This function will parse and execute a plan
|
||||
*/
|
||||
fn handle_plan(opts: PlanOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
|
||||
println!("{}", format!("Running plan with: {:?}", opts).green());
|
||||
let mut exit: i32 = -1;
|
||||
|
||||
match Plan::from_path(&opts.plan) {
|
||||
Ok(plan) => {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Failed to load plan: {:?}", err);
|
||||
}
|
||||
}
|
||||
std::process::exit(exit);
|
||||
}
|
||||
|
||||
fn execute_task_on(
|
||||
targets: String,
|
||||
task: &Task,
|
||||
parameters: &HashMap<String, String>,
|
||||
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(&command, &group, &inventory);
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == targets) {
|
||||
return runner.run(&command, &target);
|
||||
}
|
||||
}
|
||||
error!("Failed to locate a script to execute for the task!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,23 +110,17 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
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(&command, &group, &inventory));
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == opts.targets) {
|
||||
std::process::exit(runner.run(&command, &target));
|
||||
}
|
||||
}
|
||||
},
|
||||
std::process::exit(execute_task_on(
|
||||
opts.targets,
|
||||
&task,
|
||||
¶meters,
|
||||
runner,
|
||||
&inventory,
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Failed to load task: {:?}", err);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,19 +135,16 @@ 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) {
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == opts.targets) {
|
||||
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));
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("Couldn't find a target named `{}`", opts.targets).red()
|
||||
);
|
||||
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,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
* It is expected to be run from the root of the project tree.
|
||||
*/
|
||||
|
||||
task "../tasks/echo.ztask" {
|
||||
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 is nice"
|
||||
}
|
||||
|
|
|
@ -1,12 +1,161 @@
|
|||
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::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "plan.pest"]
|
||||
struct PlanParser;
|
||||
|
||||
pub struct Plan {
|
||||
pub tasks: Vec<crate::task::Task>,
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExecutableTask {
|
||||
pub task: crate::task::Task,
|
||||
pub parameters: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl ExecutableTask {
|
||||
fn new(task: crate::task::Task, parameters: HashMap<String, String>) -> Self {
|
||||
Self { task, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Plan {
|
||||
pub tasks: Vec<ExecutableTask>,
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
pub fn new() -> Self {
|
||||
Self { tasks: vec![] }
|
||||
}
|
||||
|
||||
pub fn from_str(buf: &str) -> Result<Self, PestError<Rule>> {
|
||||
let mut parser = PlanParser::parse(Rule::planfile, buf)?;
|
||||
let mut plan = Plan::new();
|
||||
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::task => {
|
||||
let mut raw_task = None;
|
||||
let mut parameters: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for pair in parsed.into_inner() {
|
||||
match pair.as_rule() {
|
||||
Rule::string => {
|
||||
let path = PathBuf::from(parse_str(&mut pair.into_inner())?);
|
||||
|
||||
match crate::task::Task::from_path(&path) {
|
||||
Ok(task) => raw_task = Some(task),
|
||||
Err(err) => {
|
||||
error!("Failed to parse task: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Rule::kwarg => {
|
||||
let (key, val) = parse_kwarg(&mut pair.into_inner())?;
|
||||
parameters.insert(key, val);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(task) = raw_task {
|
||||
plan.tasks.push(ExecutableTask::new(task, parameters));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if let Err(e) = file.read_to_string(&mut contents) {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!("{}", e),
|
||||
},
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
} else {
|
||||
return Self::from_str(&contents);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!("{}", e),
|
||||
},
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_kwarg(parser: &mut Pairs<Rule>) -> Result<(String, String), PestError<Rule>> {
|
||||
let mut identifier = None;
|
||||
let mut arg = None;
|
||||
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::identifier => identifier = Some(parsed.as_str().to_string()),
|
||||
Rule::arg => arg = Some(parse_str(&mut parsed.into_inner())?),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if identifier.is_some() && arg.is_some() {
|
||||
return Ok((identifier.unwrap(), arg.unwrap()));
|
||||
}
|
||||
Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not parse keyword arguments for parameters".to_string(),
|
||||
},
|
||||
/* TODO: Find a better thing to report */
|
||||
pest::Position::from_start(""),
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser utility function to fish out the _actual_ string value for something
|
||||
* that is looking like a string Rule
|
||||
*/
|
||||
fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
|
||||
while let Some(parsed) = parser.next() {
|
||||
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());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not parse out a string value".to_string(),
|
||||
},
|
||||
/* TODO: Find a better thing to report */
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -26,6 +175,22 @@ task "../tasks/echo.ztask" {
|
|||
task "../tasks/echo.ztask" {
|
||||
msg = "This can actually take inline shells too: $(date)"
|
||||
}"#;
|
||||
let _plan = PlanParser::parse(Rule::planfile, buf).unwrap().next().unwrap();
|
||||
let _plan = PlanParser::parse(Rule::planfile, buf)
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_plan_fn() {
|
||||
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)"
|
||||
}"#;
|
||||
let plan = Plan::from_str(buf).expect("Failed to parse the plan");
|
||||
assert_eq!(plan.tasks.len(), 2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,10 @@ use std::path::PathBuf;
|
|||
#[grammar = "task.pest"]
|
||||
struct TaskParser;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Task {
|
||||
pub name: String,
|
||||
inline: Option<String>,
|
||||
pub inline: Option<String>,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
|
@ -67,12 +68,13 @@ impl Task {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
return Err(PestError::new_from_pos(
|
||||
|
||||
Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not find a valid task definition".to_string(),
|
||||
},
|
||||
pest::Position::from_start(buf),
|
||||
));
|
||||
))
|
||||
}
|
||||
|
||||
pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
|
||||
|
@ -109,7 +111,6 @@ impl Task {
|
|||
/**
|
||||
* Parser utility function to fish out the _actual_ string value for something
|
||||
* that is looking like a string Rule
|
||||
*
|
||||
*/
|
||||
fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
|
||||
while let Some(parsed) = parser.next() {
|
||||
|
|
Loading…
Reference in New Issue