use crate::command_config::CommandConfig;
use crate::errors::{AnsibleError, Result};
use std::ffi::OsStr;
use std::fmt::{Display, Formatter};
use std::io::Write;
use std::process;
#[derive(Debug, Clone)]
pub struct Playbook {
pub(crate) command: String,
pub(crate) cfg: CommandConfig,
pub(crate) inventory: Option<String>,
}
impl Default for Playbook {
fn default() -> Self {
Self {
command: "ansible-playbook".into(),
cfg: CommandConfig::default(),
inventory: None,
}
}
}
impl Display for Playbook {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.command)?;
if let Some(ref inventory) = self.inventory {
if !inventory.is_empty() {
write!(f, " -i {}", inventory)?;
}
}
if !self.cfg.args.is_empty() {
write!(f, " {}", self.cfg.args.join(" "))?;
}
Ok(())
}
}
impl Playbook {
pub fn set_system_envs(&mut self) -> &mut Self {
self.cfg.set_system_envs();
self
}
pub fn filter_envs<T, S>(&mut self, iter: T) -> &mut Self
where
T: IntoIterator<Item = S>,
S: AsRef<OsStr> + Display,
{
self.cfg.filter_envs(iter);
self
}
pub fn add_env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.cfg.add_env(key, value);
self
}
pub fn arg<S: AsRef<OsStr> + Display>(&mut self, arg: S) -> &mut Self {
self.cfg.args.push(arg.to_string());
self
}
pub fn args<T, S>(&mut self, args: T) -> &mut Self
where
T: IntoIterator<Item = S>,
S: AsRef<OsStr> + Display,
{
for arg in args {
self.arg(arg);
}
self
}
pub fn set_inventory(&mut self, s: &str) -> &mut Self {
self.inventory = Some(s.to_string());
self
}
pub fn set_output_json(&mut self) -> &mut Self {
self.cfg
.add_env("ANSIBLE_STDOUT_CALLBACK", "json")
.add_env("ANSIBLE_LOAD_CALLBACK_PLUGINS", "True");
self
}
pub fn set_verbosity(&mut self, level: u8) -> &mut Self {
let verbose_arg = format!("-{}", "v".repeat(level as usize));
self.arg(verbose_arg);
self
}
pub fn add_extra_var(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
let var_string = format!("{}={}", key.into(), value.into());
self.arg("--extra-vars").arg(var_string);
self
}
pub fn add_tag(&mut self, tag: impl Into<String>) -> &mut Self {
self.arg("--tags").arg(tag.into());
self
}
pub fn set_check_mode(&mut self, enabled: bool) -> &mut Self {
if enabled {
self.arg("--check");
}
self
}
pub fn run(&self, play: Play) -> Result<String> {
let (playbook_path, is_temp) = match play {
Play::File(path) => (path, false),
Play::Content(content) => {
let temp_dir = std::env::temp_dir();
let temp_file = temp_dir.join("ansible_playbook.yaml");
let mut f = std::fs::File::create(&temp_file)?;
write!(f, "{}", content)?;
(temp_file.to_string_lossy().to_string(), true)
}
};
let full_cmd = self.to_string();
let cmd_vec: Vec<&str> = full_cmd.split_whitespace().collect();
let mut cmd = process::Command::new(&self.command);
cmd.envs(&self.cfg.envs);
cmd.args(&cmd_vec.as_slice()[1..]);
cmd.args(&self.cfg.args);
cmd.arg(&playbook_path);
let output = cmd.output()?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(AnsibleError::command_failed(
"Ansible playbook execution failed",
output.status.code(),
Some(stdout),
Some(stderr),
));
}
let result = [output.stdout, "\n".as_bytes().to_vec(), output.stderr].concat();
let output_str = String::from_utf8_lossy(&result).to_string();
if is_temp {
std::fs::remove_file(&playbook_path)?;
}
Ok(output_str)
}
}
#[derive(Debug, Clone)]
pub enum Play {
File(String),
Content(String),
}
impl Play {
pub fn from_file(path: impl Into<String>) -> Self {
Play::File(path.into())
}
pub fn from_content(content: impl Into<String>) -> Self {
Play::Content(content.into())
}
}