use crate::command_config::CommandConfig;
use crate::errors::{AnsibleError, Result};
use std::fmt::{Display, Formatter};
use std::process;
#[derive(Debug, Clone)]
pub struct AnsibleInventory {
pub(crate) command: String,
pub(crate) cfg: CommandConfig,
pub(crate) inventory: Option<String>,
pub(crate) playbook_dir: Option<String>,
}
impl Default for AnsibleInventory {
fn default() -> Self {
Self {
command: "ansible-inventory".into(),
cfg: CommandConfig::default(),
inventory: None,
playbook_dir: None,
}
}
}
impl Display for AnsibleInventory {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.command)?;
if let Some(ref inventory) = self.inventory {
write!(f, " --inventory {}", inventory)?;
}
if let Some(ref playbook_dir) = self.playbook_dir {
write!(f, " --playbook-dir {}", playbook_dir)?;
}
if !self.cfg.args.is_empty() {
write!(f, " {}", self.cfg.args.join(" "))?;
}
Ok(())
}
}
impl AnsibleInventory {
pub fn new() -> Self {
Self::default()
}
pub fn set_inventory(&mut self, inventory: impl Into<String>) -> &mut Self {
self.inventory = Some(inventory.into());
self
}
pub fn set_inventory_file(&mut self, inventory: impl Into<String>) -> &mut Self {
self.set_inventory(inventory)
}
pub fn set_playbook_dir(&mut self, dir: impl Into<String>) -> &mut Self {
self.playbook_dir = Some(dir.into());
self
}
pub fn set_format(&mut self, format: InventoryFormat) -> &mut Self {
self.arg("--output").arg(format.to_string());
self
}
pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
self.cfg.arg(arg.into());
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let args_vec: Vec<String> = args.into_iter().map(|s| s.into()).collect();
self.cfg.args(args_vec);
self
}
pub fn set_system_envs(&mut self) -> &mut Self {
self.cfg.set_system_envs();
self
}
pub fn add_env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.cfg.add_env(key, value);
self
}
fn execute_inventory_command(&self, args: &[String]) -> Result<String> {
let mut cmd = process::Command::new(&self.command);
cmd.envs(&self.cfg.envs);
if let Some(ref inventory) = self.inventory {
cmd.args(["--inventory", inventory]);
}
if let Some(ref playbook_dir) = self.playbook_dir {
cmd.args(["--playbook-dir", playbook_dir]);
}
cmd.args(&self.cfg.args);
cmd.args(args);
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 inventory command failed",
output.status.code(),
Some(stdout),
Some(stderr),
));
}
let result = [output.stdout, "\n".as_bytes().to_vec(), output.stderr].concat();
let s = String::from_utf8_lossy(&result);
Ok(s.to_string())
}
pub fn list(&self) -> Result<String> {
self.execute_inventory_command(&["--list".to_string()])
}
pub fn host(&self, hostname: impl Into<String>) -> Result<String> {
let hostname = hostname.into();
self.execute_inventory_command(&["--host".to_string(), hostname])
}
pub fn graph(&self) -> Result<String> {
self.execute_inventory_command(&["--graph".to_string()])
}
pub fn yaml(&self) -> Result<String> {
self.execute_inventory_command(&["--list".to_string(), "--yaml".to_string()])
}
pub fn json(&self) -> Result<String> {
self.execute_inventory_command(&["--list".to_string()])
}
pub fn parse_inventory_data(&self) -> Result<InventoryData> {
let json_output = self.json()?;
let inventory_data: InventoryData = serde_json::from_str(&json_output)
.map_err(|e| AnsibleError::parsing_failed(&format!("Failed to parse inventory JSON: {}", e)))?;
Ok(inventory_data)
}
pub fn list_hosts(&self, pattern: impl Into<String>) -> Result<String> {
let pattern = pattern.into();
self.execute_inventory_command(&["--list-hosts".to_string(), pattern])
}
pub fn export_host_vars(&self, hostname: impl Into<String>) -> Result<String> {
let hostname = hostname.into();
self.execute_inventory_command(&[
"--host".to_string(),
hostname,
"--export".to_string(),
])
}
pub fn vars_with_format(&self, format: InventoryFormat) -> Result<String> {
self.execute_inventory_command(&[
"--list".to_string(),
format.to_arg(),
])
}
pub fn limit(&mut self, pattern: impl Into<String>) -> &mut Self {
self.cfg.arg("--limit");
self.cfg.arg(pattern.into());
self
}
pub fn verbose(&mut self) -> &mut Self {
self.cfg.arg("-v");
self
}
pub fn verbosity(&mut self, level: u8) -> &mut Self {
let v_arg = "-".to_string() + &"v".repeat(level as usize);
self.cfg.arg(v_arg);
self
}
pub fn get_config(&self) -> &CommandConfig {
&self.cfg
}
pub fn parse(&self) -> Result<InventoryData> {
let json_output = self.json()?;
serde_json::from_str(&json_output)
.map_err(|e| AnsibleError::invalid_inventory(format!("Failed to parse inventory JSON: {}", e)))
}
pub fn groups(&self) -> Result<Vec<String>> {
let data = self.parse()?;
Ok(data.groups())
}
pub fn hosts(&self) -> Result<Vec<String>> {
let data = self.parse()?;
Ok(data.hosts())
}
pub fn hosts_in_group(&self, group: impl Into<String>) -> Result<Vec<String>> {
let group = group.into();
let data = self.parse()?;
Ok(data.hosts_in_group(&group))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InventoryFormat {
Json,
Yaml,
Toml,
}
impl InventoryFormat {
fn to_arg(self) -> String {
match self {
InventoryFormat::Json => "--list".to_string(),
InventoryFormat::Yaml => "--yaml".to_string(),
InventoryFormat::Toml => "--toml".to_string(),
}
}
}
impl Display for InventoryFormat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
InventoryFormat::Json => write!(f, "json"),
InventoryFormat::Yaml => write!(f, "yaml"),
InventoryFormat::Toml => write!(f, "toml"),
}
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct InventoryData {
#[serde(flatten)]
pub groups: std::collections::HashMap<String, InventoryGroup>,
#[serde(rename = "_meta")]
pub meta: Option<InventoryMeta>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct InventoryGroup {
#[serde(default)]
pub hosts: Vec<String>,
#[serde(default)]
pub children: Vec<String>,
#[serde(default)]
pub vars: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct InventoryMeta {
#[serde(default)]
pub hostvars: std::collections::HashMap<String, std::collections::HashMap<String, serde_json::Value>>,
}
impl InventoryData {
pub fn groups(&self) -> Vec<String> {
self.groups.keys().cloned().collect()
}
pub fn hosts(&self) -> Vec<String> {
let mut hosts = std::collections::HashSet::new();
for group in self.groups.values() {
for host in &group.hosts {
hosts.insert(host.clone());
}
}
if let Some(ref meta) = self.meta {
for host in meta.hostvars.keys() {
hosts.insert(host.clone());
}
}
hosts.into_iter().collect()
}
pub fn hosts_in_group(&self, group_name: &str) -> Vec<String> {
self.groups
.get(group_name)
.map(|group| group.hosts.clone())
.unwrap_or_default()
}
pub fn host_vars(&self, hostname: &str) -> std::collections::HashMap<String, serde_json::Value> {
self.meta
.as_ref()
.and_then(|meta| meta.hostvars.get(hostname))
.cloned()
.unwrap_or_default()
}
pub fn group_vars(&self, group_name: &str) -> std::collections::HashMap<String, serde_json::Value> {
self.groups
.get(group_name)
.map(|group| group.vars.clone())
.unwrap_or_default()
}
}