Refactor some code to make the Ssh a "transport"

I'm still not thrilled with the need to hard-code some things here, and tried to
use the enum_dispatch crate to make the transport mapping go directly from
inventory::Transport to transport::Ssh. Didn't work because it needs to be
struct/tuple syntax
This commit is contained in:
R Tyler Croy 2020-12-29 16:49:46 -08:00
parent e82ca1eff0
commit 822eceb0d6
4 changed files with 121 additions and 58 deletions

View File

@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Inventory {
pub groups: Vec<Group>,
pub targets: Vec<Target>,
pub config: Config,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Group {
pub name: String,
pub targets: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Target {
pub name: String,
pub uri: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Config {
pub transport: Transport,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Transport {
Ssh,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_with_transport() {
let buf = r#"
---
targets: []
groups: []
config:
transport: ssh"#;
let _i: Inventory = serde_yaml::from_str(&buf).expect("Failed to deser");
}
}

View File

@ -1,27 +1,12 @@
use gumdrop::Options; use gumdrop::Options;
use serde::Deserialize;
use std::io::BufReader; use std::io::BufReader;
use std::io::prelude::*;
use std::net::{TcpStream};
use ssh2::Session;
#[derive(Clone, Debug, Deserialize)] mod inventory;
struct Inventory { mod transport;
groups: Vec<Group>,
targets: Vec<Target>,
}
#[derive(Clone, Debug, Deserialize)] use crate::inventory::*;
struct Target { use crate::transport::ssh::Ssh;
name: String, use crate::transport::Transport;
uri: String,
}
#[derive(Clone, Debug, Deserialize)]
struct Group {
name: String,
targets: Vec<String>,
}
fn main() { fn main() {
let opts = MyOptions::parse_args_default_or_exit(); let opts = MyOptions::parse_args_default_or_exit();
@ -31,59 +16,33 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
let file = std::fs::File::open("inventory.yml").expect("Failed to load the inventory.ymll file"); let file =
std::fs::File::open("inventory.yml").expect("Failed to load the inventory.ymll file");
let reader = BufReader::new(file); let reader = BufReader::new(file);
let inventory: Inventory = serde_yaml::from_reader(reader).expect("Failed to read intenvory"); let inventory: Inventory = serde_yaml::from_reader(reader).expect("Failed to read intenvory");
let runner = match &inventory.config.transport {
crate::inventory::Transport::Ssh => Ssh::default(),
};
match opts.command.unwrap() { match opts.command.unwrap() {
Command::Cmd(runopts) => { Command::Cmd(runopts) => {
println!("run a command: {:?}", runopts);
if let Some(group) = inventory.groups.iter().find(|g| g.name == runopts.targets) { if let Some(group) = inventory.groups.iter().find(|g| g.name == runopts.targets) {
std::process::exit(run_group(&runopts.command, &group, &inventory)); std::process::exit(runner.run_group(&runopts.command, &group, &inventory));
} }
if let Some(target) = inventory.targets.iter().find(|t| t.name == runopts.targets) { if let Some(target) = inventory.targets.iter().find(|t| t.name == runopts.targets) {
println!("run a command: {:?}", runopts); println!("run a command: {:?}", runopts);
std::process::exit(run(&runopts.command, &target)); std::process::exit(runner.run(&runopts.command, &target));
} }
println!("Couldn't find a target named `{}`", runopts.targets); println!("Couldn't find a target named `{}`", runopts.targets);
},
_ => {},
}
}
fn run_group(command: &str, group: &Group, inventory: &Inventory) -> 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 = run(command, &target);
}
} }
_ => {}
} }
status
} }
fn run(command: &str, target: &Target) -> i32 {
// Connect to the local SSH server
let tcp = TcpStream::connect(format!("{}:22", target.uri)).unwrap();
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 channel = sess.channel_session().unwrap();
channel.exec(command).unwrap();
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();
}
#[derive(Debug, Options)] #[derive(Debug, Options)]
struct MyOptions { struct MyOptions {
// Options here can be accepted with any command (or none at all), // Options here can be accepted with any command (or none at all),
@ -113,7 +72,7 @@ enum Command {
// Names can be explicitly specified using `#[options(name = "...")]` // Names can be explicitly specified using `#[options(name = "...")]`
#[options(help = "show help for a command")] #[options(help = "show help for a command")]
Help(HelpOpts), Help(HelpOpts),
#[options(help="Run a single command on a target(s)")] #[options(help = "Run a single command on a target(s)")]
Cmd(RunOpts), Cmd(RunOpts),
} }
@ -126,7 +85,7 @@ struct HelpOpts {
// Options accepted for the `make` command // Options accepted for the `make` command
#[derive(Debug, Options)] #[derive(Debug, Options)]
struct RunOpts { struct RunOpts {
#[options(free, help="Command to execute on the target(s)")] #[options(free, help = "Command to execute on the target(s)")]
command: String, command: String,
#[options(help = "Name of a target or group")] #[options(help = "Name of a target or group")]
targets: String, targets: String,

8
cli/src/transport/mod.rs Normal file
View File

@ -0,0 +1,8 @@
use crate::inventory::{Group, Inventory, Target};
pub mod ssh;
pub trait Transport {
fn run_group(&self, cmd: &str, group: &Group, inv: &Inventory) -> i32;
fn run(&self, command: &str, target: &Target) -> i32;
}

49
cli/src/transport/ssh.rs Normal file
View File

@ -0,0 +1,49 @@
use crate::inventory::{Group, Inventory, Target};
use crate::transport::Transport;
use serde::{Deserialize, Serialize};
use ssh2::Session;
use std::io::prelude::*;
use std::net::TcpStream;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Ssh {}
impl Default for Ssh {
fn default() -> Self {
Self {}
}
}
impl Transport for Ssh {
fn run_group(&self, command: &str, group: &Group, inventory: &Inventory) -> 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
}
fn run(&self, command: &str, target: &Target) -> i32 {
// Connect to the local SSH server
let tcp = TcpStream::connect(format!("{}:22", target.uri)).unwrap();
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 channel = sess.channel_session().unwrap();
channel.exec(command).unwrap();
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();
}
}