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}