ansible-rs 1.1.0

A Rust wrapper library for Ansible command-line tools (Linux/Unix only)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
//! Ansible inventory parsing and host management.
//!
//! This module provides the [`AnsibleInventory`] struct for querying and parsing
//! Ansible inventories, along with data structures for representing inventory data.

use crate::command_config::CommandConfig;
use crate::errors::{AnsibleError, Result};
use std::fmt::{Display, Formatter};
use std::process;

/// Ansible inventory management and querying utility.
///
/// The `AnsibleInventory` struct provides a comprehensive interface for querying
/// and parsing Ansible inventories. It supports listing hosts, getting host details,
/// generating graphs, and parsing inventory data into structured formats.
///
/// # Examples
///
/// ## Basic Inventory Queries
///
/// ```rust,no_run
/// use ansible::AnsibleInventory;
///
/// let mut inventory = AnsibleInventory::new();
/// inventory.set_inventory_file("hosts.yml");
///
/// // List all hosts
/// let hosts = inventory.list()?;
/// println!("Hosts: {}", hosts);
///
/// // Get specific host information
/// let host_info = inventory.host("web01")?;
/// println!("Host info: {}", host_info);
/// # Ok::<(), ansible::AnsibleError>(())
/// ```
///
/// ## Structured Data Parsing
///
/// ```rust,no_run
/// use ansible::{AnsibleInventory, InventoryFormat};
///
/// let mut inventory = AnsibleInventory::new();
/// inventory
///     .set_inventory_file("hosts.yml")
///     .set_format(InventoryFormat::Json);
///
/// // Parse inventory into structured data
/// let inventory_data = inventory.parse_inventory_data()?;
///
/// for (group_name, group) in &inventory_data.groups {
///     println!("Group {}: {} hosts", group_name, group.hosts.len());
///     for host in &group.hosts {
///         println!("  - {}", host);
///     }
/// }
/// # Ok::<(), ansible::AnsibleError>(())
/// ```
///
/// ## Graph Generation
///
/// ```rust,no_run
/// use ansible::AnsibleInventory;
///
/// let mut inventory = AnsibleInventory::new();
/// inventory.set_inventory_file("hosts.yml");
///
/// // Generate inventory graph
/// let graph = inventory.graph()?;
/// println!("Inventory graph:\n{}", graph);
/// # Ok::<(), ansible::AnsibleError>(())
/// ```
///
/// ## Different Output Formats
///
/// ```rust,no_run
/// use ansible::{AnsibleInventory, InventoryFormat};
///
/// let mut inventory = AnsibleInventory::new();
/// inventory.set_inventory_file("hosts.yml");
///
/// // JSON format
/// inventory.set_format(InventoryFormat::Json);
/// let json_output = inventory.list()?;
///
/// // YAML format
/// inventory.set_format(InventoryFormat::Yaml);
/// let yaml_output = inventory.list()?;
/// # Ok::<(), ansible::AnsibleError>(())
/// ```
#[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 {
    /// Create a new AnsibleInventory instance
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the inventory file or directory
    pub fn set_inventory(&mut self, inventory: impl Into<String>) -> &mut Self {
        self.inventory = Some(inventory.into());
        self
    }

    /// Set the inventory file or directory (alias for set_inventory)
    pub fn set_inventory_file(&mut self, inventory: impl Into<String>) -> &mut Self {
        self.set_inventory(inventory)
    }

    /// Set the playbook directory
    pub fn set_playbook_dir(&mut self, dir: impl Into<String>) -> &mut Self {
        self.playbook_dir = Some(dir.into());
        self
    }

    /// Set the output format for inventory commands.
    ///
    /// This method sets the format for inventory output.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use ansible::{AnsibleInventory, InventoryFormat};
    ///
    /// let mut inventory = AnsibleInventory::new();
    /// inventory.set_format(InventoryFormat::Json);
    /// ```
    pub fn set_format(&mut self, format: InventoryFormat) -> &mut Self {
        self.arg("--output").arg(format.to_string());
        self
    }

    /// Add a custom argument
    pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
        self.cfg.arg(arg.into());
        self
    }

    /// Add multiple arguments
    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
    }

    /// Set environment variables from the system
    pub fn set_system_envs(&mut self) -> &mut Self {
        self.cfg.set_system_envs();
        self
    }

    /// Add an environment variable
    pub fn add_env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
        self.cfg.add_env(key, value);
        self
    }

    /// Execute an inventory command with the given arguments
    fn execute_inventory_command(&self, args: &[String]) -> Result<String> {
        let mut cmd = process::Command::new(&self.command);
        cmd.envs(&self.cfg.envs);

        // Add inventory-specific options
        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]);
        }

        // Add custom arguments
        cmd.args(&self.cfg.args);

        // Add command-specific arguments
        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())
    }

    /// List all hosts in the inventory
    pub fn list(&self) -> Result<String> {
        self.execute_inventory_command(&["--list".to_string()])
    }

    /// Get information about a specific host
    pub fn host(&self, hostname: impl Into<String>) -> Result<String> {
        let hostname = hostname.into();
        self.execute_inventory_command(&["--host".to_string(), hostname])
    }

    /// Display inventory as a graph
    pub fn graph(&self) -> Result<String> {
        self.execute_inventory_command(&["--graph".to_string()])
    }

    /// Output inventory in YAML format
    pub fn yaml(&self) -> Result<String> {
        self.execute_inventory_command(&["--list".to_string(), "--yaml".to_string()])
    }

    /// Output inventory in JSON format (default)
    pub fn json(&self) -> Result<String> {
        self.execute_inventory_command(&["--list".to_string()])
    }

    /// Parse inventory data into structured format.
    ///
    /// This method retrieves inventory data in JSON format and parses it
    /// into a structured `InventoryData` object for programmatic access.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// use ansible::AnsibleInventory;
    ///
    /// let mut inventory = AnsibleInventory::new();
    /// inventory.set_inventory("hosts.yml");
    ///
    /// let data = inventory.parse_inventory_data()?;
    /// for (group_name, group) in &data.groups {
    ///     println!("Group {}: {} hosts", group_name, group.hosts.len());
    /// }
    /// # Ok::<(), ansible::AnsibleError>(())
    /// ```
    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)
    }

    /// List hosts matching a pattern
    pub fn list_hosts(&self, pattern: impl Into<String>) -> Result<String> {
        let pattern = pattern.into();
        self.execute_inventory_command(&["--list-hosts".to_string(), pattern])
    }

    /// Export inventory variables for a host
    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(),
        ])
    }

    /// Show inventory variables in a specific format
    pub fn vars_with_format(&self, format: InventoryFormat) -> Result<String> {
        self.execute_inventory_command(&[
            "--list".to_string(),
            format.to_arg(),
        ])
    }

    /// Limit inventory to specific hosts or groups
    pub fn limit(&mut self, pattern: impl Into<String>) -> &mut Self {
        self.cfg.arg("--limit");
        self.cfg.arg(pattern.into());
        self
    }

    /// Enable verbose output
    pub fn verbose(&mut self) -> &mut Self {
        self.cfg.arg("-v");
        self
    }

    /// Set multiple levels of verbosity
    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
    }

    /// Get a reference to the command configuration (for testing)
    pub fn get_config(&self) -> &CommandConfig {
        &self.cfg
    }

    /// Parse inventory and return structured data
    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)))
    }

    /// Get all groups in the inventory
    pub fn groups(&self) -> Result<Vec<String>> {
        let data = self.parse()?;
        Ok(data.groups())
    }

    /// Get all hosts in the inventory
    pub fn hosts(&self) -> Result<Vec<String>> {
        let data = self.parse()?;
        Ok(data.hosts())
    }

    /// Get hosts in a specific group
    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))
    }
}

/// Inventory output formats
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InventoryFormat {
    /// JSON format (default)
    Json,
    /// YAML format
    Yaml,
    /// TOML format
    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"),
        }
    }
}

/// Structured representation of Ansible inventory data.
///
/// This struct represents the complete inventory data structure as returned
/// by `ansible-inventory --list` in JSON format. It includes all groups,
/// hosts, and metadata.
///
/// # Examples
///
/// ```rust,no_run
/// use ansible::AnsibleInventory;
///
/// let mut inventory = AnsibleInventory::new();
/// inventory.set_inventory_file("hosts.yml");
///
/// let data = inventory.parse_inventory_data()?;
///
/// // Get all groups
/// let groups = data.groups();
/// println!("Groups: {:?}", groups);
///
/// // Get all hosts
/// let hosts = data.hosts();
/// println!("Hosts: {:?}", hosts);
///
/// // Get hosts in a specific group
/// let web_hosts = data.hosts_in_group("webservers");
/// println!("Web servers: {:?}", web_hosts);
/// # Ok::<(), ansible::AnsibleError>(())
/// ```
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct InventoryData {
    /// All inventory groups indexed by group name
    #[serde(flatten)]
    pub groups: std::collections::HashMap<String, InventoryGroup>,

    /// Inventory metadata including host variables
    #[serde(rename = "_meta")]
    pub meta: Option<InventoryMeta>,
}

/// Represents a single inventory group with its hosts, children, and variables.
///
/// # Examples
///
/// ```rust
/// use ansible::InventoryGroup;
/// use std::collections::HashMap;
///
/// let group = InventoryGroup {
///     hosts: vec!["web01".to_string(), "web02".to_string()],
///     children: vec!["webservers".to_string()],
///     vars: HashMap::new(),
/// };
///
/// assert_eq!(group.hosts.len(), 2);
/// ```
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct InventoryGroup {
    /// List of hosts in this group
    #[serde(default)]
    pub hosts: Vec<String>,

    /// List of child groups
    #[serde(default)]
    pub children: Vec<String>,

    /// Group variables
    #[serde(default)]
    pub vars: std::collections::HashMap<String, serde_json::Value>,
}

/// Inventory metadata containing host-specific variables.
///
/// This structure contains the `_meta` section of inventory data,
/// which includes variables for individual hosts.
///
/// # Examples
///
/// ```rust,no_run
/// use ansible::AnsibleInventory;
///
/// let mut inventory = AnsibleInventory::new();
/// let data = inventory.parse_inventory_data()?;
///
/// if let Some(meta) = &data.meta {
///     for (hostname, vars) in &meta.hostvars {
///         println!("Host {}: {} variables", hostname, vars.len());
///     }
/// }
/// # Ok::<(), ansible::AnsibleError>(())
/// ```
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct InventoryMeta {
    /// Host-specific variables indexed by hostname
    #[serde(default)]
    pub hostvars: std::collections::HashMap<String, std::collections::HashMap<String, serde_json::Value>>,
}

impl InventoryData {
    /// Get all group names
    pub fn groups(&self) -> Vec<String> {
        self.groups.keys().cloned().collect()
    }

    /// Get all host names
    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()
    }

    /// Get hosts in a specific group
    pub fn hosts_in_group(&self, group_name: &str) -> Vec<String> {
        self.groups
            .get(group_name)
            .map(|group| group.hosts.clone())
            .unwrap_or_default()
    }

    /// Get variables for a specific host
    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()
    }

    /// Get variables for a specific group
    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()
    }
}