Compare commits

...

5 Commits

Author SHA1 Message Date
R Tyler Croy ff3f3c5263 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.
2020-12-31 09:20:22 -08:00
R Tyler Croy 754fd428f8 Allow passing some configuration for user/password on SSH
With some real integration testing it looks like the "set an environment
variable" approach is not really going to work effectively.

I think the script {} will need to be treated like a template instead, since
different shells require different ways of setting env variables and it doesn't
appear that there's a good ssh2-based way to set these environment variables.
2020-12-30 22:46:29 -08:00
R Tyler Croy 7b9066d096 Implement the simple support for running a task
This includes an echo task for funsies
2020-12-30 21:11:35 -08:00
R Tyler Croy b00e9835e8 Allow parameters inside of a task to be optional.
I can imagine a few cases where there wouldn't be any parameters needed
2020-12-30 14:40:52 -08:00
R Tyler Croy 904bf007ce Start working on the task definition parsing grammar, pretty easy. 2020-12-30 14:27:33 -08:00
17 changed files with 835 additions and 37 deletions

345
Cargo.lock generated
View File

@ -1,5 +1,25 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
dependencies = [
"memchr",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
@ -12,6 +32,39 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "block-buffer"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
dependencies = [
"block-padding",
"byte-tools",
"byteorder",
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
dependencies = [
"byte-tools",
]
[[package]]
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "cc"
version = "1.0.66"
@ -24,18 +77,6 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cli"
version = "0.1.0"
dependencies = [
"gumdrop",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"ssh2",
]
[[package]]
name = "cloudabi"
version = "0.0.3"
@ -45,12 +86,66 @@ dependencies = [
"bitflags",
]
[[package]]
name = "colored"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
dependencies = [
"atty",
"lazy_static",
"winapi",
]
[[package]]
name = "digest"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
"generic-array",
]
[[package]]
name = "dtoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
[[package]]
name = "env_logger"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "fake-simd"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]]
name = "generic-array"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
dependencies = [
"typenum",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "gumdrop"
version = "0.8.0"
@ -71,12 +166,50 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
dependencies = [
"libc",
]
[[package]]
name = "humantime"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
dependencies = [
"quick-error 1.2.3",
]
[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.81"
@ -124,6 +257,33 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
dependencies = [
"cfg-if",
]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "memchr"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
name = "opaque-debug"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
[[package]]
name = "openssl-sys"
version = "0.9.60"
@ -161,12 +321,65 @@ dependencies = [
"winapi",
]
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
dependencies = [
"maplit",
"pest",
"sha-1",
]
[[package]]
name = "pkg-config"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]]
name = "pretty_env_logger"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
dependencies = [
"env_logger",
"log",
]
[[package]]
name = "proc-macro2"
version = "1.0.24"
@ -176,6 +389,18 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "quick-error"
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"
@ -191,6 +416,24 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "regex"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-syntax"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
[[package]]
name = "ryu"
version = "1.0.5"
@ -246,6 +489,18 @@ dependencies = [
"yaml-rust",
]
[[package]]
name = "sha-1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
dependencies = [
"block-buffer",
"digest",
"fake-simd",
"opaque-debug",
]
[[package]]
name = "smallvec"
version = "1.5.1"
@ -275,6 +530,36 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "thread_local"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
dependencies = [
"lazy_static",
]
[[package]]
name = "typenum"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-xid"
version = "0.2.1"
@ -303,6 +588,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -317,3 +611,30 @@ checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zap"
version = "0.1.0"
dependencies = [
"colored",
"glob",
"gumdrop",
"handlebars",
"log",
"pretty_env_logger",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"ssh2",
"zap-parser",
]
[[package]]
name = "zap-parser"
version = "0.1.0"
dependencies = [
"log",
"pest",
"pest_derive",
]

View File

@ -1,4 +1,5 @@
[workspace]
members = [
'cli',
'parser',
]

View File

@ -64,10 +64,12 @@ task Install {
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

View File

@ -1,15 +1,16 @@
[package]
name = "cli"
name = "zap"
version = "0.1.0"
authors = ["R. Tyler Croy <rtyler@brokenco.de>"]
edition = "2018"
[[bin]]
name = "zap"
path = "src/main.rs"
[dependencies]
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
# effectively
serde = { version = "~1.0", features = ["derive", "rc"] }
@ -17,3 +18,4 @@ serde_derive = "~1.0"
serde_json = "~1.0"
serde_yaml = "~0.8"
ssh2 = "~0.9.0"
zap-parser = { path = "../parser" }

View File

@ -17,11 +17,23 @@ pub struct Group {
pub struct Target {
pub name: String,
pub uri: String,
pub config: Option<Config>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Config {
#[serde(default = "default_transport")]
pub transport: Transport,
pub ssh: Option<SshConfig>,
}
fn default_transport() -> Transport {
Transport::Ssh
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SshConfig {
pub user: String,
pub password: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]

View File

@ -1,4 +1,7 @@
use colored::*;
use gumdrop::Options;
use log::*;
use std::collections::HashMap;
use std::io::BufReader;
mod inventory;
@ -6,9 +9,10 @@ mod transport;
use crate::inventory::*;
use crate::transport::ssh::Ssh;
use crate::transport::Transport;
use zap_parser::*;
fn main() {
pretty_env_logger::init();
let opts = MyOptions::parse_args_default_or_exit();
if opts.command.is_none() {
@ -26,23 +30,115 @@ fn main() {
};
match opts.command.unwrap() {
Command::Cmd(runopts) => {
println!("run a command: {:?}", runopts);
if let Some(group) = inventory.groups.iter().find(|g| g.name == runopts.targets) {
std::process::exit(runner.run_group(&runopts.command, &group, &inventory));
}
if let Some(target) = inventory.targets.iter().find(|t| t.name == runopts.targets) {
println!("run a command: {:?}", runopts);
std::process::exit(runner.run(&runopts.command, &target));
}
println!("Couldn't find a target named `{}`", runopts.targets);
}
Command::Cmd(opts) => handle_cmd(opts, &runner, inventory),
Command::Task(opts) => handle_task(opts, &runner, inventory),
_ => {}
}
}
fn load_ztasks() -> Vec<Task> {
use glob::glob;
let mut tasks = vec![];
for entry in glob("tasks/**/*.ztask").expect("Failed to read glob pattern") {
match entry {
Ok(path) => {
if let Ok(task) = Task::from_path(&path) {
info!("loaded ztask: {}", task.name);
tasks.push(task);
}
}
Err(e) => println!("{:?}", e),
}
}
tasks
}
/**
* This function will handle a task
*/
fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
println!("running task: {:?}", opts);
for task in load_ztasks() {
if task.name == opts.task {
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 {
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));
}
}
}
}
}
/**
* This function will handle executing a single specified command on the target(s)
* identified in the `opts`.
*
* In the case of a single target, the status code from the executed command will
* be propogated up.
*
* In the case of multiple targets, any non-zero status code will be used to exit
* 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()
);
}
/**
* 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)]
struct MyOptions {
// Options here can be accepted with any command (or none at all),
@ -73,7 +169,9 @@ enum Command {
#[options(help = "show help for a command")]
Help(HelpOpts),
#[options(help = "Run a single command on a target(s)")]
Cmd(RunOpts),
Cmd(CmdOpts),
#[options(help = "Execute a task on a target(s)")]
Task(TaskOpts),
}
#[derive(Debug, Options)]
@ -81,12 +179,44 @@ struct HelpOpts {
#[options(free)]
free: Vec<String>,
}
// Options accepted for the `make` command
#[derive(Debug, Options)]
struct RunOpts {
struct CmdOpts {
#[options(free, help = "Command to execute on the target(s)")]
command: String,
#[options(help = "Name of a target or group")]
targets: String,
}
#[derive(Debug, Options)]
struct TaskOpts {
#[options(free, help = "Task to execute, must exist in ZAP_PATH")]
task: String,
#[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

@ -35,11 +35,25 @@ impl Transport for Ssh {
let mut sess = Session::new().unwrap();
sess.set_tcp_stream(tcp);
sess.handshake().unwrap();
sess.userauth_agent(&std::env::var("USER").unwrap())
.unwrap();
let mut authenticated = false;
if let Some(config) = &target.config {
if let Some(sshconfig) = &config.ssh {
// requires PasswordAuthentication yes
sess.userauth_password(&sshconfig.user, &sshconfig.password)
.unwrap();
authenticated = true;
}
}
if !authenticated {
sess.userauth_agent(&std::env::var("USER").unwrap())
.unwrap();
}
let mut channel = sess.channel_session().unwrap();
channel.exec(command).unwrap();
let mut s = String::new();
channel.read_to_string(&mut s).unwrap();
print!("{}", s);

View File

@ -0,0 +1,20 @@
FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y openssh-server libssl-dev
RUN mkdir /var/run/sshd
RUN echo 'root:root' |chpasswd
RUN sed -ri 's/^#?PermitRootLogin\s+.*/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
RUN mkdir /root/.ssh
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
EXPOSE 22
EXPOSE 80
CMD ["/usr/sbin/sshd", "-D"]

View File

@ -0,0 +1,4 @@
= Managing simple web containers
This is an example from the link:https://puppet.com/docs/bolt/latest/getting_started_with_bolt.html[Bolt documentation]

View File

@ -0,0 +1,14 @@
version: '3'
services:
target1:
build: .
ports:
- '3000:80'
- '2000:22'
container_name: target1
target2:
build: .
ports:
- '3001:80'
- '2001:22'
container_name: target2

View File

@ -9,6 +9,12 @@ targets:
uri: 192.168.1.41
- name: gopher
uri: 192.168.1.41
- name: zap-freebsd
uri: 192.168.1.224
config:
ssh:
user: root
password: root
config:
transport: ssh

0
native-tasks/.gitignore vendored Normal file
View File

10
parser/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "zap-parser"
version = "0.1.0"
authors = ["R. Tyler Croy <rtyler@brokenco.de>"]
edition = "2018"
[dependencies]
log = "0.4"
pest = "~2.1"
pest_derive = "~2.1"

185
parser/src/lib.rs Normal file
View File

@ -0,0 +1,185 @@
#[macro_use]
extern crate pest;
#[macro_use]
extern crate pest_derive;
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"]
struct TaskParser;
pub struct Task {
pub name: String,
inline: Option<String>,
}
impl Task {
pub fn get_script(&self) -> Option<&String> {
self.inline.as_ref()
}
pub fn new(name: &str) -> Self {
Task {
name: name.to_string(),
inline: None,
}
}
fn parse(parser: &mut Pairs<Rule>) -> Result<Self, PestError<Rule>> {
let mut task: Option<Self> = None;
while let Some(parsed) = parser.next() {
match parsed.as_rule() {
Rule::identifier => {
task = Some(Task::new(parsed.as_str()));
}
Rule::script => {
let script = parse_str(&mut parsed.into_inner())?;
if let Some(ref mut task) = task {
task.inline = Some(script);
}
}
_ => {}
}
}
if let Some(task) = task {
return Ok(task);
} else {
return Err(PestError::new_from_pos(
ErrorVariant::CustomError {
message: "Could not find a valid task definition".to_string(),
},
/* TODO: Find a better thing to report */
pest::Position::from_start(""),
));
}
}
pub fn from_str(buf: &str) -> Result<Self, PestError<Rule>> {
let mut parser = TaskParser::parse(Rule::task, buf)?;
while let Some(parsed) = parser.next() {
match parsed.as_rule() {
Rule::task => {
return Task::parse(&mut parsed.into_inner());
}
_ => {}
}
}
return 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>> {
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(""),
));
}
}
}
}
/**
* 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::*;
#[test]
fn parse_simple_script_task() {
let buf = r#"task Install {
parameters {
package {
required = true
help = "Name of package to be installed"
type = string
}
}
script {
inline = "zypper in -y ${ZAP_PACKAGE}"
}
}"#;
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
}
#[test]
fn parse_no_parameters() {
let buf = r#"task PrintEnv {
script {
inline = "env"
}
}"#;
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
}
#[test]
fn parse_task_fn() {
let buf = r#"task PrintEnv {
script {
inline = "env"
}
}"#;
let task = Task::from_str(buf).expect("Failed to parse the task");
assert_eq!(task.name, "PrintEnv");
assert_eq!(task.get_script().unwrap(), "env");
}
}

65
parser/src/task.pest Normal file
View File

@ -0,0 +1,65 @@
/// This describes the task definition grammar for Zap
taskfile = _{ SOI
~ task+
~ EOI }
task = { "task"
~ identifier
~ opening_brace
~ parameters?
~ script
~ closing_brace
}
// An identifier will be used to refer to the task later
identifier = { ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
parameters = { "parameters"
~ opening_brace
~ parameter+
~ closing_brace
}
parameter = { identifier
~ opening_brace
~ required?
~ help
~ ptype
~ closing_brace
}
required = { "required" ~ equals ~ bool }
help = { "help" ~ equals ~ string }
ptype = { "type" ~ equals ~ typedef }
script = { "script"
~ opening_brace
~ (script_inline)
~ closing_brace
}
script_inline = _{ "inline" ~ equals ~ string }
opening_brace = _{ "{" }
closing_brace = _{ "}" }
equals = _{ "=" }
quote = _{ "\"" }
string = { double_quoted }
double_quoted = ${ (quote ~ inner_double_str ~ quote) }
inner_double_str = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner_double_str)? }
escape = @{ "\\" ~ ("\"" | "\\" | "r" | "n" | "t" | "0" | "'" | code | unicode) }
code = @{ "x" ~ hex_digit{2} }
unicode = @{ "u" ~ opening_brace ~ hex_digit{2, 6} ~ closing_brace }
hex_digit = @{ '0'..'9' | 'a'..'f' | 'A'..'F' }
typedef = { string_type }
string_type = { "string" }
bool = { truthy | falsey }
truthy = { "true" }
falsey = { "false" }
block_comment = _{ "/*" ~ (block_comment | !"*/" ~ ANY)* ~ "*/" }
COMMENT = _{ block_comment | ("//" ~ (!NEWLINE~ ANY)*) }
WHITESPACE = _{ " " | "\t" | NEWLINE }

0
tasks/.gitignore vendored Normal file
View File

12
tasks/echo.ztask Normal file
View File

@ -0,0 +1,12 @@
task Echo {
parameters {
msg {
required = true
help = "String to echo back to the client"
type = string
}
}
script {
inline = "echo {{msg}}"
}
}