use std::process::Command;
use anyhow::{Context, Result};
use crate::config::Host;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Output {
pub stdout: Vec<u8>,
pub stderr: String,
pub code: i32,
}
impl Output {
pub fn ok(stdout: impl Into<Vec<u8>>) -> Self {
Self {
stdout: stdout.into(),
stderr: String::new(),
code: 0,
}
}
pub fn text(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(&self.stdout)
}
pub fn fail(code: i32, stderr: impl Into<String>) -> Self {
Self {
stdout: Vec::new(),
stderr: stderr.into(),
code,
}
}
}
pub trait Transport {
fn run_unlocked(&self, host: &Host, script: &str) -> Result<Output>;
fn master_alive(&self, host: &Host) -> bool;
fn run(&self, host: &Host, script: &str) -> Result<Output> {
crate::lock::with_lock(&host.name, || self.run_unlocked(host, script))?
}
}
fn base_args(host: &Host) -> Vec<String> {
vec![
"-S".into(),
host.socket.display().to_string(),
"-o".into(),
"BatchMode=yes".into(),
"-o".into(),
"ControlMaster=no".into(),
"-o".into(),
"ProxyCommand=none".into(),
host.target.clone(),
]
}
pub fn probe_args(host: &Host) -> Vec<String> {
let mut args = base_args(host);
args.extend(["-O".into(), "check".into()]);
args
}
pub fn run_args(host: &Host, script: &str) -> Vec<String> {
let mut args = base_args(host);
args.push(script.to_string());
args
}
fn base_command(host: &Host) -> Command {
let mut cmd = Command::new("ssh");
cmd.args(base_args(host));
cmd
}
fn ssh_agent_state() -> crate::errors::AgentState {
match Command::new("ssh-add").arg("-l").output() {
Ok(output) if output.status.success() => crate::errors::AgentState::Keys,
Ok(output) if output.status.code() == Some(1) => crate::errors::AgentState::NoKeys,
Ok(output) if output.status.code() == Some(2) => crate::errors::AgentState::Unreachable,
Ok(_) | Err(_) => crate::errors::AgentState::Unknown,
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Ssh;
impl Transport for Ssh {
fn run_unlocked(&self, host: &Host, script: &str) -> Result<Output> {
let out = base_command(host)
.arg(script)
.output()
.with_context(|| format!("spawning ssh for host {}", host.name))?;
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
if let Some(error) = crate::errors::classify(&stderr, ssh_agent_state) {
return Err(error.into());
}
Ok(Output {
stdout: out.stdout,
stderr,
code: out.status.code().unwrap_or(-1),
})
}
fn master_alive(&self, host: &Host) -> bool {
base_command(host)
.args(["-O", "check"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}
#[derive(Debug, Default)]
pub struct Fake {
scripts: std::sync::Mutex<Vec<String>>,
outputs: std::sync::Mutex<std::collections::VecDeque<Output>>,
master: bool,
}
impl Fake {
pub fn new() -> Self {
Self {
master: true,
..Default::default()
}
}
pub fn no_master() -> Self {
Self::default()
}
pub fn push(&self, out: Output) -> &Self {
self.outputs.lock().unwrap().push_back(out);
self
}
pub fn scripts(&self) -> Vec<String> {
self.scripts.lock().unwrap().clone()
}
}
impl Transport for Fake {
fn run_unlocked(&self, _host: &Host, script: &str) -> Result<Output> {
self.scripts.lock().unwrap().push(script.to_string());
Ok(self
.outputs
.lock()
.unwrap()
.pop_front()
.unwrap_or_else(|| Output::ok("")))
}
fn master_alive(&self, _host: &Host) -> bool {
self.master
}
}