Compare commits
8 Commits
59d44d7e0f
...
fc1f951c19
Author | SHA1 | Date |
---|---|---|
R Tyler Croy | fc1f951c19 | |
R Tyler Croy | 46dbe64f5f | |
R Tyler Croy | b0cb6533c0 | |
R Tyler Croy | d605bc4229 | |
R Tyler Croy | bc4396a0de | |
R Tyler Croy | 7e3d7fb5ad | |
R Tyler Croy | dc1c67cf28 | |
R Tyler Croy | 184d455b2d |
|
@ -48,6 +48,9 @@ definitions.
|
|||
| `provides`
|
||||
| A relative or absolute path to a file that the task provides. If the file exists, then the task will be skipped.
|
||||
|
||||
| `unless`
|
||||
| A script snippet which can determine whether the task should execute. A non-zero exit status causes the task to execute.
|
||||
|
||||
|===
|
||||
|
||||
.echo.ztask
|
||||
|
|
|
@ -15,7 +15,7 @@ use zap_parser::task::Task;
|
|||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
let opts = MyOptions::parse_args_default_or_exit();
|
||||
let opts = ZapOptions::parse_args_default_or_exit();
|
||||
|
||||
if opts.command.is_none() {
|
||||
println!("Must specify a subcommand!");
|
||||
|
@ -27,22 +27,42 @@ fn main() {
|
|||
let reader = BufReader::new(file);
|
||||
let inventory: Inventory = serde_yaml::from_reader(reader).expect("Failed to read intenvory");
|
||||
|
||||
let runner = match &inventory.config.transport {
|
||||
let mut runner = match &inventory.config.transport {
|
||||
crate::inventory::Transport::Ssh => Ssh::default(),
|
||||
};
|
||||
|
||||
match opts.command.unwrap() {
|
||||
Command::Cmd(opts) => handle_cmd(opts, &runner, inventory),
|
||||
Command::Task(opts) => handle_task(opts, &runner, inventory),
|
||||
Command::Plan(opts) => handle_plan(opts, &runner, inventory),
|
||||
Command::Cmd(opts) => handle_cmd(opts, &mut runner, inventory),
|
||||
Command::Task(opts) => handle_task(opts, &mut runner, inventory),
|
||||
Command::Plan(opts) => handle_plan(opts, &mut runner, inventory),
|
||||
Command::Check(opts) => handle_check(opts),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function simply attempts to parse the given ztask or zplan files in order
|
||||
* to validate that they're properly formatted
|
||||
*/
|
||||
fn handle_check(opts: CheckOpts) {
|
||||
for file in opts.files.iter() {
|
||||
if let Some(ext) = file.as_path().extension() {
|
||||
if ext == "ztask" {
|
||||
let task = Task::from_path(&file).expect("Failed to parse");
|
||||
println!("Parsed task {} properly", task.name);
|
||||
}
|
||||
if ext == "zplan" {
|
||||
let _plan = Plan::from_path(&file).expect("Failed to parse");
|
||||
println!("Parsed plan properly");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will parse and execute a plan
|
||||
*/
|
||||
fn handle_plan(opts: PlanOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
|
||||
fn handle_plan(opts: PlanOpts, runner: &mut dyn crate::transport::Transport, inventory: Inventory) {
|
||||
println!("{}", format!("Running plan with: {:?}", opts).green());
|
||||
let mut exit: i32 = -1;
|
||||
|
||||
|
@ -51,7 +71,13 @@ fn handle_plan(opts: PlanOpts, runner: &dyn crate::transport::Transport, invento
|
|||
info!("Plan located, preparing to execute");
|
||||
for task in plan.tasks {
|
||||
info!("Running executable task: {:?}", task);
|
||||
exit = execute_task_on(opts.targets.clone(), &task, runner, &inventory);
|
||||
exit = execute_task_on(
|
||||
opts.targets.clone(),
|
||||
&task,
|
||||
runner,
|
||||
&inventory,
|
||||
opts.dry_run,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -64,15 +90,16 @@ fn handle_plan(opts: PlanOpts, runner: &dyn crate::transport::Transport, invento
|
|||
fn execute_task_on(
|
||||
targets: String,
|
||||
task: &ExecutableTask,
|
||||
runner: &dyn crate::transport::Transport,
|
||||
runner: &mut dyn crate::transport::Transport,
|
||||
inventory: &Inventory,
|
||||
dry_run: bool,
|
||||
) -> i32 {
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == targets) {
|
||||
return runner.run_group(task, &group, &inventory);
|
||||
return runner.run_group(task, &group, &inventory, dry_run);
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == targets) {
|
||||
return runner.run(task, &target);
|
||||
return runner.run(task, &target, dry_run);
|
||||
}
|
||||
error!("Failed to locate a script to execute for the task!");
|
||||
return -1;
|
||||
|
@ -81,7 +108,7 @@ fn execute_task_on(
|
|||
/**
|
||||
* This function will handle a task
|
||||
*/
|
||||
fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
|
||||
fn handle_task(opts: TaskOpts, runner: &mut dyn crate::transport::Transport, inventory: Inventory) {
|
||||
println!("{}", format!("Running task with: {:?}", opts).green());
|
||||
|
||||
match Task::from_path(&opts.task) {
|
||||
|
@ -102,7 +129,13 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
|
||||
let task = ExecutableTask::new(task, parameters);
|
||||
|
||||
std::process::exit(execute_task_on(opts.targets, &task, runner, &inventory));
|
||||
std::process::exit(execute_task_on(
|
||||
opts.targets,
|
||||
&task,
|
||||
runner,
|
||||
&inventory,
|
||||
opts.dry_run,
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Failed to load task: {:?}", err);
|
||||
|
@ -120,14 +153,20 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
* 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) {
|
||||
fn handle_cmd(opts: CmdOpts, runner: &mut dyn crate::transport::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(opts.targets, &task, runner, &inventory));
|
||||
std::process::exit(execute_task_on(
|
||||
opts.targets,
|
||||
&task,
|
||||
runner,
|
||||
&inventory,
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
struct MyOptions {
|
||||
struct ZapOptions {
|
||||
// Options here can be accepted with any command (or none at all),
|
||||
// but they must come before the command name.
|
||||
#[options(help = "print help message")]
|
||||
|
@ -161,6 +200,8 @@ enum Command {
|
|||
Task(TaskOpts),
|
||||
#[options(help = "Execute a plan on a target(s)")]
|
||||
Plan(PlanOpts),
|
||||
#[options(help = "Check that the specified .ztask or .zplan file is valid")]
|
||||
Check(CheckOpts),
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
|
@ -184,6 +225,8 @@ struct TaskOpts {
|
|||
parameter: Vec<String>,
|
||||
#[options(help = "Name of a target or group")]
|
||||
targets: String,
|
||||
#[options(help = "Run the task in dry-run mode")]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
|
@ -192,6 +235,14 @@ struct PlanOpts {
|
|||
plan: PathBuf,
|
||||
#[options(help = "Name of a target or group")]
|
||||
targets: String,
|
||||
#[options(help = "Run the task in dry-run mode")]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
struct CheckOpts {
|
||||
#[options(free, help = "Files to check")]
|
||||
files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::inventory::{Group, Inventory, Target};
|
||||
use std::path::Path;
|
||||
use zap_parser::plan::ExecutableTask;
|
||||
|
||||
pub mod ssh;
|
||||
|
@ -8,6 +9,14 @@ pub mod ssh;
|
|||
* connecting to targets
|
||||
*/
|
||||
pub trait Transport {
|
||||
fn run_group(&self, cmd: &ExecutableTask, group: &Group, inv: &Inventory) -> i32;
|
||||
fn run(&self, command: &ExecutableTask, target: &Target) -> i32;
|
||||
fn connect(&mut self, target: &Target) -> bool;
|
||||
fn run_group(
|
||||
&mut self,
|
||||
cmd: &ExecutableTask,
|
||||
group: &Group,
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::inventory::{Group, Inventory, Target};
|
||||
use crate::transport::Transport;
|
||||
use colored::*;
|
||||
|
||||
use log::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssh2::Session;
|
||||
use std::convert::TryInto;
|
||||
use std::io::prelude::*;
|
||||
|
@ -12,61 +12,84 @@ use std::path::Path;
|
|||
|
||||
use zap_parser::plan::ExecutableTask;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Ssh {}
|
||||
#[derive(Clone)]
|
||||
pub struct Ssh {
|
||||
session: Session,
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
impl Default for Ssh {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
Self {
|
||||
session: Session::new().unwrap(),
|
||||
connected: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for Ssh {
|
||||
fn run_group(&self, command: &ExecutableTask, group: &Group, inventory: &Inventory) -> i32 {
|
||||
fn run_group(
|
||||
&mut self,
|
||||
command: &ExecutableTask,
|
||||
group: &Group,
|
||||
inventory: &Inventory,
|
||||
dry_run: bool,
|
||||
) -> 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);
|
||||
status = self.run(command, &target, dry_run);
|
||||
}
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
fn run(&self, command: &ExecutableTask, target: &Target) -> i32 {
|
||||
// Connect to the local SSH server
|
||||
fn connect(&mut self, target: &Target) -> bool {
|
||||
if self.connected {
|
||||
return self.connected;
|
||||
}
|
||||
let tcp = TcpStream::connect(format!("{}:22", target.uri)).unwrap();
|
||||
let mut sess = Session::new().unwrap();
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake().unwrap();
|
||||
self.session.set_tcp_stream(tcp);
|
||||
self.session.handshake().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)
|
||||
self.session
|
||||
.userauth_password(&sshconfig.user, &sshconfig.password)
|
||||
.unwrap();
|
||||
authenticated = true;
|
||||
}
|
||||
}
|
||||
if !authenticated {
|
||||
sess.userauth_agent(&std::env::var("USER").unwrap())
|
||||
self.session
|
||||
.userauth_agent(&std::env::var("USER").unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let remote_script = "._zap_command";
|
||||
let args_file = "._zap_args.json";
|
||||
self.connected = true;
|
||||
true
|
||||
}
|
||||
|
||||
fn run(&mut self, command: &ExecutableTask, target: &Target, dry_run: bool) -> i32 {
|
||||
if !self.connect(target) {
|
||||
error!("Failed to connect to {:?}", target);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if let Some(provides) = &command.parameters.get("provides") {
|
||||
debug!(
|
||||
"A `provides` parameter was given, checking to see if {} exists on the remote",
|
||||
provides
|
||||
);
|
||||
if let Err(error) = sess.scp_recv(&Path::new(&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",
|
||||
|
@ -90,50 +113,55 @@ impl Transport for Ssh {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(script) = command.task.script.as_bytes(Some(&command.parameters)) {
|
||||
let mut remote_file = sess
|
||||
.scp_send(
|
||||
Path::new(remote_script),
|
||||
0o700,
|
||||
script
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("Overflow converting the size of the generated file, yikes!"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
remote_file.write(&script).unwrap();
|
||||
// Close the channel and wait for the whole content to be tranferred
|
||||
remote_file.send_eof().unwrap();
|
||||
remote_file.wait_eof().unwrap();
|
||||
remote_file.close().unwrap();
|
||||
remote_file.wait_close().unwrap();
|
||||
let remote_script = "._zap_command";
|
||||
let args_file = "._zap_args.json";
|
||||
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
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 let Some(script) = command.task.script.as_bytes(Some(&command.parameters)) {
|
||||
if dry_run {
|
||||
println!("{}", "Dry-run\n----".yellow());
|
||||
let mut out = std::io::stdout();
|
||||
out.write(&script)
|
||||
.expect("Somehow failed to write to stdout");
|
||||
println!("{}", "\n----".yellow());
|
||||
return 0;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if command.task.script.has_file() {
|
||||
let args = serde_json::to_string(&command.parameters)
|
||||
.expect("Failed to serialize parameters for task");
|
||||
let mut remote_file = sess
|
||||
.scp_send(
|
||||
Path::new(args_file),
|
||||
0o400,
|
||||
args.len().try_into().expect(
|
||||
"Failed converting the size of the generated args file, yikes!",
|
||||
),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
remote_file.write(&args.as_bytes()).unwrap();
|
||||
// Close the channel and wait for the whole content to be tranferred
|
||||
remote_file.send_eof().unwrap();
|
||||
remote_file.wait_eof().unwrap();
|
||||
remote_file.close().unwrap();
|
||||
remote_file.wait_close().unwrap();
|
||||
channel
|
||||
.exec(&format!("./{} {}", remote_script, args_file))
|
||||
.unwrap();
|
||||
if self.send_bytes(Path::new(args_file), &args.into_bytes(), 0o400) {
|
||||
channel
|
||||
.exec(&format!("./{} {}", remote_script, args_file))
|
||||
.unwrap();
|
||||
} else {
|
||||
error!("Failed to upload the arguments file");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
channel.exec(&format!("./{}", remote_script)).unwrap();
|
||||
}
|
||||
|
@ -154,7 +182,7 @@ impl Transport for Ssh {
|
|||
* somehow and I'm not seeing anything that would allow me to just reach
|
||||
* out and remove a file
|
||||
*/
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
let mut channel = self.session.channel_session().unwrap();
|
||||
channel
|
||||
.exec(&format!("rm -f {} {}", remote_script, args_file))
|
||||
.unwrap();
|
||||
|
@ -164,4 +192,26 @@ impl Transport for Ssh {
|
|||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
fn send_bytes(&self, remote_path: &Path, bytes: &Vec<u8>, mode: i32) -> bool {
|
||||
let mut remote_file = self
|
||||
.session
|
||||
.scp_send(
|
||||
remote_path,
|
||||
mode,
|
||||
bytes
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("Failed converting the size of the file to send, yikes!"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
remote_file.write(bytes).unwrap();
|
||||
// Close the channel and wait for the whole content to be tranferred
|
||||
remote_file.send_eof().unwrap();
|
||||
remote_file.wait_eof().unwrap();
|
||||
remote_file.close().unwrap();
|
||||
remote_file.wait_close().unwrap();
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
groups:
|
||||
- name: bsd
|
||||
targets:
|
||||
- zap-freebsd
|
||||
|
||||
targets:
|
||||
- name: zap-freebsd
|
||||
uri: 192.168.1.224
|
||||
config:
|
||||
ssh:
|
||||
user: root
|
||||
password: root
|
||||
|
||||
config:
|
||||
transport: ssh
|
|
@ -0,0 +1 @@
|
|||
../../tasks
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* This plan will set up an nginx server on a FreeBSD machine
|
||||
*/
|
||||
|
||||
|
||||
task 'tasks/install/freebsd.ztask' {
|
||||
packages = 'nginx'
|
||||
provides = '/usr/local/sbin/nginx'
|
||||
}
|
||||
|
||||
|
||||
task 'tasks/shell/sh.ztask' {
|
||||
script = 'sysrc nginx_enable="YES"'
|
||||
}
|
||||
|
|
@ -86,8 +86,9 @@ impl Script {
|
|||
|
||||
let parameters = parameters.unwrap();
|
||||
|
||||
let handlebars = Handlebars::new();
|
||||
match handlebars.render_template(inline, ¶meters) {
|
||||
let mut hb = Handlebars::new();
|
||||
hb.register_escape_fn(handlebars::no_escape);
|
||||
match hb.render_template(inline, ¶meters) {
|
||||
Ok(rendered) => {
|
||||
return Some(rendered.as_bytes().to_vec());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
task FreeBSDPkgInstall {
|
||||
parameters {
|
||||
packages {
|
||||
required = true
|
||||
help = 'One or more space delimited packages for pkg(8) to install'
|
||||
type = string
|
||||
}
|
||||
}
|
||||
|
||||
script {
|
||||
inline = 'pkg install -y {{packages}}'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
task ZypperInstall {
|
||||
parameters {
|
||||
packages {
|
||||
required = true
|
||||
help = 'One or more space delimited packages for zypper(8) to install'
|
||||
type = string
|
||||
}
|
||||
}
|
||||
|
||||
script {
|
||||
inline = 'zypper in -y {{packages}}'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
task Sh {
|
||||
parameters {
|
||||
script {
|
||||
required = true
|
||||
help = 'A script to run via the /bin/sh'
|
||||
type = string
|
||||
}
|
||||
}
|
||||
|
||||
script {
|
||||
inline = '''#!/bin/sh
|
||||
{{script}}
|
||||
'''
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue