ansible/
inventory.rs

1//! Ansible inventory parsing and host management.
2//!
3//! This module provides the [`AnsibleInventory`] struct for querying and parsing
4//! Ansible inventories, along with data structures for representing inventory data.
5
6use crate::command_config::CommandConfig;
7use crate::errors::{AnsibleError, Result};
8use std::fmt::{Display, Formatter};
9use std::process;
10
11/// Ansible inventory management and querying utility.
12///
13/// The `AnsibleInventory` struct provides a comprehensive interface for querying
14/// and parsing Ansible inventories. It supports listing hosts, getting host details,
15/// generating graphs, and parsing inventory data into structured formats.
16///
17/// # Examples
18///
19/// ## Basic Inventory Queries
20///
21/// ```rust,no_run
22/// use ansible::AnsibleInventory;
23///
24/// let mut inventory = AnsibleInventory::new();
25/// inventory.set_inventory_file("hosts.yml");
26///
27/// // List all hosts
28/// let hosts = inventory.list()?;
29/// println!("Hosts: {}", hosts);
30///
31/// // Get specific host information
32/// let host_info = inventory.host("web01")?;
33/// println!("Host info: {}", host_info);
34/// # Ok::<(), ansible::AnsibleError>(())
35/// ```
36///
37/// ## Structured Data Parsing
38///
39/// ```rust,no_run
40/// use ansible::{AnsibleInventory, InventoryFormat};
41///
42/// let mut inventory = AnsibleInventory::new();
43/// inventory
44///     .set_inventory_file("hosts.yml")
45///     .set_format(InventoryFormat::Json);
46///
47/// // Parse inventory into structured data
48/// let inventory_data = inventory.parse_inventory_data()?;
49///
50/// for (group_name, group) in &inventory_data.groups {
51///     println!("Group {}: {} hosts", group_name, group.hosts.len());
52///     for host in &group.hosts {
53///         println!("  - {}", host);
54///     }
55/// }
56/// # Ok::<(), ansible::AnsibleError>(())
57/// ```
58///
59/// ## Graph Generation
60///
61/// ```rust,no_run
62/// use ansible::AnsibleInventory;
63///
64/// let mut inventory = AnsibleInventory::new();
65/// inventory.set_inventory_file("hosts.yml");
66///
67/// // Generate inventory graph
68/// let graph = inventory.graph()?;
69/// println!("Inventory graph:\n{}", graph);
70/// # Ok::<(), ansible::AnsibleError>(())
71/// ```
72///
73/// ## Different Output Formats
74///
75/// ```rust,no_run
76/// use ansible::{AnsibleInventory, InventoryFormat};
77///
78/// let mut inventory = AnsibleInventory::new();
79/// inventory.set_inventory_file("hosts.yml");
80///
81/// // JSON format
82/// inventory.set_format(InventoryFormat::Json);
83/// let json_output = inventory.list()?;
84///
85/// // YAML format
86/// inventory.set_format(InventoryFormat::Yaml);
87/// let yaml_output = inventory.list()?;
88/// # Ok::<(), ansible::AnsibleError>(())
89/// ```
90#[derive(Debug, Clone)]
91pub struct AnsibleInventory {
92    pub(crate) command: String,
93    pub(crate) cfg: CommandConfig,
94    pub(crate) inventory: Option<String>,
95    pub(crate) playbook_dir: Option<String>,
96}
97
98impl Default for AnsibleInventory {
99    fn default() -> Self {
100        Self {
101            command: "ansible-inventory".into(),
102            cfg: CommandConfig::default(),
103            inventory: None,
104            playbook_dir: None,
105        }
106    }
107}
108
109impl Display for AnsibleInventory {
110    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
111        write!(f, "{}", self.command)?;
112
113        if let Some(ref inventory) = self.inventory {
114            write!(f, " --inventory {}", inventory)?;
115        }
116
117        if let Some(ref playbook_dir) = self.playbook_dir {
118            write!(f, " --playbook-dir {}", playbook_dir)?;
119        }
120
121        if !self.cfg.args.is_empty() {
122            write!(f, " {}", self.cfg.args.join(" "))?;
123        }
124
125        Ok(())
126    }
127}
128
129impl AnsibleInventory {
130    /// Create a new AnsibleInventory instance
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Set the inventory file or directory
136    pub fn set_inventory(&mut self, inventory: impl Into<String>) -> &mut Self {
137        self.inventory = Some(inventory.into());
138        self
139    }
140
141    /// Set the inventory file or directory (alias for set_inventory)
142    pub fn set_inventory_file(&mut self, inventory: impl Into<String>) -> &mut Self {
143        self.set_inventory(inventory)
144    }
145
146    /// Set the playbook directory
147    pub fn set_playbook_dir(&mut self, dir: impl Into<String>) -> &mut Self {
148        self.playbook_dir = Some(dir.into());
149        self
150    }
151
152    /// Set the output format for inventory commands.
153    ///
154    /// This method sets the format for inventory output.
155    ///
156    /// # Examples
157    ///
158    /// ```rust
159    /// use ansible::{AnsibleInventory, InventoryFormat};
160    ///
161    /// let mut inventory = AnsibleInventory::new();
162    /// inventory.set_format(InventoryFormat::Json);
163    /// ```
164    pub fn set_format(&mut self, format: InventoryFormat) -> &mut Self {
165        self.arg("--output").arg(format.to_string());
166        self
167    }
168
169    /// Add a custom argument
170    pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
171        self.cfg.arg(arg.into());
172        self
173    }
174
175    /// Add multiple arguments
176    pub fn args<I, S>(&mut self, args: I) -> &mut Self
177    where
178        I: IntoIterator<Item = S>,
179        S: Into<String>,
180    {
181        let args_vec: Vec<String> = args.into_iter().map(|s| s.into()).collect();
182        self.cfg.args(args_vec);
183        self
184    }
185
186    /// Set environment variables from the system
187    pub fn set_system_envs(&mut self) -> &mut Self {
188        self.cfg.set_system_envs();
189        self
190    }
191
192    /// Add an environment variable
193    pub fn add_env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
194        self.cfg.add_env(key, value);
195        self
196    }
197
198    /// Execute an inventory command with the given arguments
199    fn execute_inventory_command(&self, args: &[String]) -> Result<String> {
200        let mut cmd = process::Command::new(&self.command);
201        cmd.envs(&self.cfg.envs);
202
203        // Add inventory-specific options
204        if let Some(ref inventory) = self.inventory {
205            cmd.args(["--inventory", inventory]);
206        }
207
208        if let Some(ref playbook_dir) = self.playbook_dir {
209            cmd.args(["--playbook-dir", playbook_dir]);
210        }
211
212        // Add custom arguments
213        cmd.args(&self.cfg.args);
214
215        // Add command-specific arguments
216        cmd.args(args);
217
218        let output = cmd.output()?;
219
220        if !output.status.success() {
221            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
222            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
223            return Err(AnsibleError::command_failed(
224                "Ansible inventory command failed",
225                output.status.code(),
226                Some(stdout),
227                Some(stderr),
228            ));
229        }
230
231        let result = [output.stdout, "\n".as_bytes().to_vec(), output.stderr].concat();
232        let s = String::from_utf8_lossy(&result);
233
234        Ok(s.to_string())
235    }
236
237    /// List all hosts in the inventory
238    pub fn list(&self) -> Result<String> {
239        self.execute_inventory_command(&["--list".to_string()])
240    }
241
242    /// Get information about a specific host
243    pub fn host(&self, hostname: impl Into<String>) -> Result<String> {
244        let hostname = hostname.into();
245        self.execute_inventory_command(&["--host".to_string(), hostname])
246    }
247
248    /// Display inventory as a graph
249    pub fn graph(&self) -> Result<String> {
250        self.execute_inventory_command(&["--graph".to_string()])
251    }
252
253    /// Output inventory in YAML format
254    pub fn yaml(&self) -> Result<String> {
255        self.execute_inventory_command(&["--list".to_string(), "--yaml".to_string()])
256    }
257
258    /// Output inventory in JSON format (default)
259    pub fn json(&self) -> Result<String> {
260        self.execute_inventory_command(&["--list".to_string()])
261    }
262
263    /// Parse inventory data into structured format.
264    ///
265    /// This method retrieves inventory data in JSON format and parses it
266    /// into a structured `InventoryData` object for programmatic access.
267    ///
268    /// # Examples
269    ///
270    /// ```rust,no_run
271    /// use ansible::AnsibleInventory;
272    ///
273    /// let mut inventory = AnsibleInventory::new();
274    /// inventory.set_inventory("hosts.yml");
275    ///
276    /// let data = inventory.parse_inventory_data()?;
277    /// for (group_name, group) in &data.groups {
278    ///     println!("Group {}: {} hosts", group_name, group.hosts.len());
279    /// }
280    /// # Ok::<(), ansible::AnsibleError>(())
281    /// ```
282    pub fn parse_inventory_data(&self) -> Result<InventoryData> {
283        let json_output = self.json()?;
284        let inventory_data: InventoryData = serde_json::from_str(&json_output)
285            .map_err(|e| AnsibleError::parsing_failed(&format!("Failed to parse inventory JSON: {}", e)))?;
286        Ok(inventory_data)
287    }
288
289    /// List hosts matching a pattern
290    pub fn list_hosts(&self, pattern: impl Into<String>) -> Result<String> {
291        let pattern = pattern.into();
292        self.execute_inventory_command(&["--list-hosts".to_string(), pattern])
293    }
294
295    /// Export inventory variables for a host
296    pub fn export_host_vars(&self, hostname: impl Into<String>) -> Result<String> {
297        let hostname = hostname.into();
298        self.execute_inventory_command(&[
299            "--host".to_string(),
300            hostname,
301            "--export".to_string(),
302        ])
303    }
304
305    /// Show inventory variables in a specific format
306    pub fn vars_with_format(&self, format: InventoryFormat) -> Result<String> {
307        self.execute_inventory_command(&[
308            "--list".to_string(),
309            format.to_arg(),
310        ])
311    }
312
313    /// Limit inventory to specific hosts or groups
314    pub fn limit(&mut self, pattern: impl Into<String>) -> &mut Self {
315        self.cfg.arg("--limit");
316        self.cfg.arg(pattern.into());
317        self
318    }
319
320    /// Enable verbose output
321    pub fn verbose(&mut self) -> &mut Self {
322        self.cfg.arg("-v");
323        self
324    }
325
326    /// Set multiple levels of verbosity
327    pub fn verbosity(&mut self, level: u8) -> &mut Self {
328        let v_arg = "-".to_string() + &"v".repeat(level as usize);
329        self.cfg.arg(v_arg);
330        self
331    }
332
333    /// Get a reference to the command configuration (for testing)
334    pub fn get_config(&self) -> &CommandConfig {
335        &self.cfg
336    }
337
338    /// Parse inventory and return structured data
339    pub fn parse(&self) -> Result<InventoryData> {
340        let json_output = self.json()?;
341        serde_json::from_str(&json_output)
342            .map_err(|e| AnsibleError::invalid_inventory(format!("Failed to parse inventory JSON: {}", e)))
343    }
344
345    /// Get all groups in the inventory
346    pub fn groups(&self) -> Result<Vec<String>> {
347        let data = self.parse()?;
348        Ok(data.groups())
349    }
350
351    /// Get all hosts in the inventory
352    pub fn hosts(&self) -> Result<Vec<String>> {
353        let data = self.parse()?;
354        Ok(data.hosts())
355    }
356
357    /// Get hosts in a specific group
358    pub fn hosts_in_group(&self, group: impl Into<String>) -> Result<Vec<String>> {
359        let group = group.into();
360        let data = self.parse()?;
361        Ok(data.hosts_in_group(&group))
362    }
363}
364
365/// Inventory output formats
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub enum InventoryFormat {
368    /// JSON format (default)
369    Json,
370    /// YAML format
371    Yaml,
372    /// TOML format
373    Toml,
374}
375
376impl InventoryFormat {
377    fn to_arg(self) -> String {
378        match self {
379            InventoryFormat::Json => "--list".to_string(),
380            InventoryFormat::Yaml => "--yaml".to_string(),
381            InventoryFormat::Toml => "--toml".to_string(),
382        }
383    }
384}
385
386impl Display for InventoryFormat {
387    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
388        match self {
389            InventoryFormat::Json => write!(f, "json"),
390            InventoryFormat::Yaml => write!(f, "yaml"),
391            InventoryFormat::Toml => write!(f, "toml"),
392        }
393    }
394}
395
396/// Structured representation of Ansible inventory data.
397///
398/// This struct represents the complete inventory data structure as returned
399/// by `ansible-inventory --list` in JSON format. It includes all groups,
400/// hosts, and metadata.
401///
402/// # Examples
403///
404/// ```rust,no_run
405/// use ansible::AnsibleInventory;
406///
407/// let mut inventory = AnsibleInventory::new();
408/// inventory.set_inventory_file("hosts.yml");
409///
410/// let data = inventory.parse_inventory_data()?;
411///
412/// // Get all groups
413/// let groups = data.groups();
414/// println!("Groups: {:?}", groups);
415///
416/// // Get all hosts
417/// let hosts = data.hosts();
418/// println!("Hosts: {:?}", hosts);
419///
420/// // Get hosts in a specific group
421/// let web_hosts = data.hosts_in_group("webservers");
422/// println!("Web servers: {:?}", web_hosts);
423/// # Ok::<(), ansible::AnsibleError>(())
424/// ```
425#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
426pub struct InventoryData {
427    /// All inventory groups indexed by group name
428    #[serde(flatten)]
429    pub groups: std::collections::HashMap<String, InventoryGroup>,
430
431    /// Inventory metadata including host variables
432    #[serde(rename = "_meta")]
433    pub meta: Option<InventoryMeta>,
434}
435
436/// Represents a single inventory group with its hosts, children, and variables.
437///
438/// # Examples
439///
440/// ```rust
441/// use ansible::InventoryGroup;
442/// use std::collections::HashMap;
443///
444/// let group = InventoryGroup {
445///     hosts: vec!["web01".to_string(), "web02".to_string()],
446///     children: vec!["webservers".to_string()],
447///     vars: HashMap::new(),
448/// };
449///
450/// assert_eq!(group.hosts.len(), 2);
451/// ```
452#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
453pub struct InventoryGroup {
454    /// List of hosts in this group
455    #[serde(default)]
456    pub hosts: Vec<String>,
457
458    /// List of child groups
459    #[serde(default)]
460    pub children: Vec<String>,
461
462    /// Group variables
463    #[serde(default)]
464    pub vars: std::collections::HashMap<String, serde_json::Value>,
465}
466
467/// Inventory metadata containing host-specific variables.
468///
469/// This structure contains the `_meta` section of inventory data,
470/// which includes variables for individual hosts.
471///
472/// # Examples
473///
474/// ```rust,no_run
475/// use ansible::AnsibleInventory;
476///
477/// let mut inventory = AnsibleInventory::new();
478/// let data = inventory.parse_inventory_data()?;
479///
480/// if let Some(meta) = &data.meta {
481///     for (hostname, vars) in &meta.hostvars {
482///         println!("Host {}: {} variables", hostname, vars.len());
483///     }
484/// }
485/// # Ok::<(), ansible::AnsibleError>(())
486/// ```
487#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
488pub struct InventoryMeta {
489    /// Host-specific variables indexed by hostname
490    #[serde(default)]
491    pub hostvars: std::collections::HashMap<String, std::collections::HashMap<String, serde_json::Value>>,
492}
493
494impl InventoryData {
495    /// Get all group names
496    pub fn groups(&self) -> Vec<String> {
497        self.groups.keys().cloned().collect()
498    }
499
500    /// Get all host names
501    pub fn hosts(&self) -> Vec<String> {
502        let mut hosts = std::collections::HashSet::new();
503        
504        for group in self.groups.values() {
505            for host in &group.hosts {
506                hosts.insert(host.clone());
507            }
508        }
509        
510        if let Some(ref meta) = self.meta {
511            for host in meta.hostvars.keys() {
512                hosts.insert(host.clone());
513            }
514        }
515        
516        hosts.into_iter().collect()
517    }
518
519    /// Get hosts in a specific group
520    pub fn hosts_in_group(&self, group_name: &str) -> Vec<String> {
521        self.groups
522            .get(group_name)
523            .map(|group| group.hosts.clone())
524            .unwrap_or_default()
525    }
526
527    /// Get variables for a specific host
528    pub fn host_vars(&self, hostname: &str) -> std::collections::HashMap<String, serde_json::Value> {
529        self.meta
530            .as_ref()
531            .and_then(|meta| meta.hostvars.get(hostname))
532            .cloned()
533            .unwrap_or_default()
534    }
535
536    /// Get variables for a specific group
537    pub fn group_vars(&self, group_name: &str) -> std::collections::HashMap<String, serde_json::Value> {
538        self.groups
539            .get(group_name)
540            .map(|group| group.vars.clone())
541            .unwrap_or_default()
542    }
543}