Implement the execution of plans via the CLI

This commit is contained in:
R Tyler Croy 2020-12-31 11:10:12 -08:00
parent 8981370222
commit fbde2ed863
5 changed files with 278 additions and 152 deletions

View File

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

View File

@ -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, &parameters);
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, &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(&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,
&parameters,
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,
&parameters,
runner,
&inventory,
));
}
/**

View File

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

View File

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

View File

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