ansible/playbook.rs
1//! Ansible playbook execution and management.
2//!
3//! This module provides the [`Playbook`] struct for executing Ansible playbooks
4//! and the [`Play`] enum for representing different types of playbook content.
5
6use crate::command_config::CommandConfig;
7use crate::errors::{AnsibleError, Result};
8use std::ffi::OsStr;
9use std::fmt::{Display, Formatter};
10use std::io::Write;
11use std::process;
12
13/// Ansible playbook executor with comprehensive configuration options.
14///
15/// The `Playbook` struct provides a fluent interface for configuring and executing
16/// Ansible playbooks. It supports all major ansible-playbook command-line options
17/// and provides type-safe configuration.
18///
19/// # Examples
20///
21/// ## Basic Playbook Execution
22///
23/// ```rust,no_run
24/// use ansible::{Playbook, Play};
25///
26/// let mut playbook = Playbook::default();
27/// playbook.set_inventory("hosts.yml");
28///
29/// // Run from file
30/// let result = playbook.run(Play::from_file("site.yml"))?;
31/// println!("Playbook result: {}", result);
32/// # Ok::<(), ansible::AnsibleError>(())
33/// ```
34///
35/// ## Advanced Configuration
36///
37/// ```rust,no_run
38/// use ansible::{Playbook, Play};
39///
40/// let mut playbook = Playbook::default();
41/// playbook
42/// .set_inventory("production")
43/// .set_verbosity(2)
44/// .add_extra_var("env", "production")
45/// .add_extra_var("version", "1.2.3")
46/// .add_tag("deploy")
47/// .add_tag("config")
48/// .set_check_mode(true);
49///
50/// let result = playbook.run(Play::from_file("deploy.yml"))?;
51/// # Ok::<(), ansible::AnsibleError>(())
52/// ```
53///
54/// ## Environment Configuration
55///
56/// ```rust,no_run
57/// use ansible::Playbook;
58///
59/// let mut playbook = Playbook::default();
60/// playbook
61/// .set_system_envs()
62/// .filter_envs(["HOME", "PATH", "USER"])
63/// .add_env("ANSIBLE_HOST_KEY_CHECKING", "False")
64/// .add_env("ANSIBLE_STDOUT_CALLBACK", "json");
65/// # Ok::<(), ansible::AnsibleError>(())
66/// ```
67#[derive(Debug, Clone)]
68pub struct Playbook {
69 pub(crate) command: String,
70 pub(crate) cfg: CommandConfig,
71 pub(crate) inventory: Option<String>,
72}
73
74impl Default for Playbook {
75 fn default() -> Self {
76 Self {
77 command: "ansible-playbook".into(),
78 cfg: CommandConfig::default(),
79 inventory: None,
80 }
81 }
82}
83
84impl Display for Playbook {
85 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
86 write!(f, "{}", self.command)?;
87
88 if let Some(ref inventory) = self.inventory {
89 if !inventory.is_empty() {
90 write!(f, " -i {}", inventory)?;
91 }
92 }
93
94 if !self.cfg.args.is_empty() {
95 write!(f, " {}", self.cfg.args.join(" "))?;
96 }
97
98 Ok(())
99 }
100}
101
102impl Playbook {
103 /// Set environment variables from the current system environment.
104 ///
105 /// This method copies all environment variables from the current process
106 /// to be passed to the ansible-playbook command.
107 ///
108 /// # Examples
109 ///
110 /// ```rust
111 /// use ansible::Playbook;
112 ///
113 /// let mut playbook = Playbook::default();
114 /// playbook.set_system_envs();
115 /// ```
116 pub fn set_system_envs(&mut self) -> &mut Self {
117 self.cfg.set_system_envs();
118 self
119 }
120
121 /// Filter environment variables to only include specified keys.
122 ///
123 /// This method is useful for limiting which environment variables
124 /// are passed to ansible-playbook commands.
125 ///
126 /// # Examples
127 ///
128 /// ```rust
129 /// use ansible::Playbook;
130 ///
131 /// let mut playbook = Playbook::default();
132 /// playbook
133 /// .set_system_envs()
134 /// .filter_envs(["HOME", "PATH", "USER"]);
135 /// ```
136 pub fn filter_envs<T, S>(&mut self, iter: T) -> &mut Self
137 where
138 T: IntoIterator<Item = S>,
139 S: AsRef<OsStr> + Display,
140 {
141 self.cfg.filter_envs(iter);
142 self
143 }
144
145 /// Add a single environment variable.
146 ///
147 /// This method adds or overwrites an environment variable that will
148 /// be passed to the ansible-playbook command.
149 ///
150 /// # Examples
151 ///
152 /// ```rust
153 /// use ansible::Playbook;
154 ///
155 /// let mut playbook = Playbook::default();
156 /// playbook
157 /// .add_env("ANSIBLE_HOST_KEY_CHECKING", "False")
158 /// .add_env("ANSIBLE_STDOUT_CALLBACK", "json");
159 /// ```
160 pub fn add_env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
161 self.cfg.add_env(key, value);
162 self
163 }
164
165 /// Add a single command-line argument.
166 ///
167 /// This method adds a raw command-line argument to the ansible-playbook command.
168 ///
169 /// # Examples
170 ///
171 /// ```rust
172 /// use ansible::Playbook;
173 ///
174 /// let mut playbook = Playbook::default();
175 /// playbook
176 /// .arg("--verbose")
177 /// .arg("--check");
178 /// ```
179 pub fn arg<S: AsRef<OsStr> + Display>(&mut self, arg: S) -> &mut Self {
180 self.cfg.args.push(arg.to_string());
181 self
182 }
183
184 /// Add multiple command-line arguments.
185 ///
186 /// This method adds multiple raw command-line arguments to the ansible-playbook command.
187 ///
188 /// # Examples
189 ///
190 /// ```rust
191 /// use ansible::Playbook;
192 ///
193 /// let mut playbook = Playbook::default();
194 /// playbook.args(["--verbose", "--check", "--diff"]);
195 /// ```
196 pub fn args<T, S>(&mut self, args: T) -> &mut Self
197 where
198 T: IntoIterator<Item = S>,
199 S: AsRef<OsStr> + Display,
200 {
201 for arg in args {
202 self.arg(arg);
203 }
204 self
205 }
206
207 /// Set the inventory file or directory.
208 ///
209 /// This method specifies the inventory file or directory to use
210 /// for host and group information.
211 ///
212 /// # Examples
213 ///
214 /// ```rust
215 /// use ansible::Playbook;
216 ///
217 /// let mut playbook = Playbook::default();
218 /// playbook.set_inventory("hosts.yml");
219 /// playbook.set_inventory("/etc/ansible/hosts");
220 /// playbook.set_inventory("production");
221 /// ```
222 pub fn set_inventory(&mut self, s: &str) -> &mut Self {
223 self.inventory = Some(s.to_string());
224 self
225 }
226
227 /// Configure output to use JSON format.
228 ///
229 /// This method sets environment variables to configure ansible-playbook
230 /// to output results in JSON format.
231 ///
232 /// # Examples
233 ///
234 /// ```rust
235 /// use ansible::Playbook;
236 ///
237 /// let mut playbook = Playbook::default();
238 /// playbook.set_output_json();
239 /// ```
240 pub fn set_output_json(&mut self) -> &mut Self {
241 self.cfg
242 .add_env("ANSIBLE_STDOUT_CALLBACK", "json")
243 .add_env("ANSIBLE_LOAD_CALLBACK_PLUGINS", "True");
244 self
245 }
246
247 /// Set the verbosity level for playbook execution.
248 ///
249 /// This method sets the verbosity level for ansible-playbook output.
250 ///
251 /// # Examples
252 ///
253 /// ```rust
254 /// use ansible::Playbook;
255 ///
256 /// let mut playbook = Playbook::default();
257 /// playbook.set_verbosity(2); // -vv
258 /// ```
259 pub fn set_verbosity(&mut self, level: u8) -> &mut Self {
260 let verbose_arg = format!("-{}", "v".repeat(level as usize));
261 self.arg(verbose_arg);
262 self
263 }
264
265 /// Add an extra variable for playbook execution.
266 ///
267 /// This method adds extra variables that will be passed to the playbook.
268 ///
269 /// # Examples
270 ///
271 /// ```rust
272 /// use ansible::Playbook;
273 ///
274 /// let mut playbook = Playbook::default();
275 /// playbook
276 /// .add_extra_var("env", "production")
277 /// .add_extra_var("version", "1.2.3");
278 /// ```
279 pub fn add_extra_var(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
280 let var_string = format!("{}={}", key.into(), value.into());
281 self.arg("--extra-vars").arg(var_string);
282 self
283 }
284
285 /// Add a tag to limit playbook execution.
286 ///
287 /// This method adds tags to limit which tasks are executed.
288 ///
289 /// # Examples
290 ///
291 /// ```rust
292 /// use ansible::Playbook;
293 ///
294 /// let mut playbook = Playbook::default();
295 /// playbook
296 /// .add_tag("deploy")
297 /// .add_tag("config");
298 /// ```
299 pub fn add_tag(&mut self, tag: impl Into<String>) -> &mut Self {
300 self.arg("--tags").arg(tag.into());
301 self
302 }
303
304 /// Enable check mode (dry run).
305 ///
306 /// This method enables check mode for the playbook execution.
307 ///
308 /// # Examples
309 ///
310 /// ```rust
311 /// use ansible::Playbook;
312 ///
313 /// let mut playbook = Playbook::default();
314 /// playbook.set_check_mode(true);
315 /// ```
316 pub fn set_check_mode(&mut self, enabled: bool) -> &mut Self {
317 if enabled {
318 self.arg("--check");
319 }
320 self
321 }
322
323 /// Execute an Ansible playbook.
324 ///
325 /// This method executes an Ansible playbook from either a file or string content.
326 /// It handles temporary file creation for string content and cleanup afterwards.
327 ///
328 /// # Arguments
329 ///
330 /// * `play` - The playbook source (file path or content)
331 ///
332 /// # Returns
333 ///
334 /// Returns the combined stdout and stderr output from the ansible-playbook command.
335 ///
336 /// # Errors
337 ///
338 /// Returns an error if:
339 /// - The playbook file cannot be read or created
340 /// - The ansible-playbook command fails to execute
341 /// - The command returns a non-zero exit code
342 ///
343 /// # Examples
344 ///
345 /// ## Running from File
346 ///
347 /// ```rust,no_run
348 /// use ansible::{Playbook, Play};
349 ///
350 /// let mut playbook = Playbook::default();
351 /// playbook.set_inventory("hosts.yml");
352 ///
353 /// let result = playbook.run(Play::from_file("site.yml"))?;
354 /// println!("Playbook result: {}", result);
355 /// # Ok::<(), ansible::AnsibleError>(())
356 /// ```
357 ///
358 /// ## Running from Content
359 ///
360 /// ```rust,no_run
361 /// use ansible::{Playbook, Play};
362 ///
363 /// let yaml_content = r#"
364 /// - hosts: all
365 /// tasks:
366 /// - name: Ensure nginx is installed
367 /// package:
368 /// name: nginx
369 /// state: present
370 /// "#;
371 ///
372 /// let mut playbook = Playbook::default();
373 /// let result = playbook.run(Play::from_content(yaml_content))?;
374 /// # Ok::<(), ansible::AnsibleError>(())
375 /// ```
376 pub fn run(&self, play: Play) -> Result<String> {
377 let (playbook_path, is_temp) = match play {
378 Play::File(path) => (path, false),
379 Play::Content(content) => {
380 let temp_dir = std::env::temp_dir();
381 let temp_file = temp_dir.join("ansible_playbook.yaml");
382 let mut f = std::fs::File::create(&temp_file)?;
383 write!(f, "{}", content)?;
384 (temp_file.to_string_lossy().to_string(), true)
385 }
386 };
387 let full_cmd = self.to_string();
388 let cmd_vec: Vec<&str> = full_cmd.split_whitespace().collect();
389 let mut cmd = process::Command::new(&self.command);
390 cmd.envs(&self.cfg.envs);
391 cmd.args(&cmd_vec.as_slice()[1..]);
392 cmd.args(&self.cfg.args);
393 cmd.arg(&playbook_path);
394 let output = cmd.output()?;
395
396 if !output.status.success() {
397 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
398 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
399 return Err(AnsibleError::command_failed(
400 "Ansible playbook execution failed",
401 output.status.code(),
402 Some(stdout),
403 Some(stderr),
404 ));
405 }
406
407 let result = [output.stdout, "\n".as_bytes().to_vec(), output.stderr].concat();
408 let output_str = String::from_utf8_lossy(&result).to_string();
409
410 // Clean up temporary file if it was created
411 if is_temp {
412 std::fs::remove_file(&playbook_path)?;
413 }
414
415 Ok(output_str)
416 }
417}
418
419/// Represents different sources of playbook content.
420///
421/// The `Play` enum allows you to specify playbook content either from
422/// a file on disk or from a string containing YAML content.
423///
424/// # Examples
425///
426/// ## From File
427///
428/// ```rust
429/// use ansible::Play;
430///
431/// let play = Play::from_file("site.yml");
432/// let play = Play::from_file("/path/to/playbook.yml");
433/// ```
434///
435/// ## From Content
436///
437/// ```rust
438/// use ansible::Play;
439///
440/// let yaml_content = r#"
441/// - hosts: all
442/// tasks:
443/// - name: Ensure nginx is installed
444/// package:
445/// name: nginx
446/// state: present
447/// "#;
448///
449/// let play = Play::from_content(yaml_content);
450/// ```
451#[derive(Debug, Clone)]
452pub enum Play {
453 /// Playbook content loaded from a file path
454 ///
455 /// The file should contain valid YAML playbook content.
456 File(String),
457
458 /// Playbook content provided as a string
459 ///
460 /// The string should contain valid YAML playbook content.
461 Content(String),
462}
463
464impl Play {
465 /// Create a Play from a file path.
466 ///
467 /// This method creates a Play that will read playbook content from
468 /// the specified file when executed.
469 ///
470 /// # Arguments
471 ///
472 /// * `path` - Path to the playbook file (relative or absolute)
473 ///
474 /// # Examples
475 ///
476 /// ```rust
477 /// use ansible::Play;
478 ///
479 /// let play = Play::from_file("site.yml");
480 /// let play = Play::from_file("/path/to/playbook.yml");
481 /// let play = Play::from_file("playbooks/deploy.yml");
482 /// ```
483 pub fn from_file(path: impl Into<String>) -> Self {
484 Play::File(path.into())
485 }
486
487 /// Create a Play from string content.
488 ///
489 /// This method creates a Play from YAML content provided as a string.
490 /// The content will be written to a temporary file when executed.
491 ///
492 /// # Arguments
493 ///
494 /// * `content` - YAML playbook content as a string
495 ///
496 /// # Examples
497 ///
498 /// ```rust
499 /// use ansible::Play;
500 ///
501 /// let yaml_content = r#"
502 /// - hosts: all
503 /// become: yes
504 /// tasks:
505 /// - name: Update package cache
506 /// apt:
507 /// update_cache: yes
508 /// when: ansible_os_family == "Debian"
509 ///
510 /// - name: Install essential packages
511 /// package:
512 /// name:
513 /// - curl
514 /// - wget
515 /// - git
516 /// state: present
517 /// "#;
518 ///
519 /// let play = Play::from_content(yaml_content);
520 /// ```
521 pub fn from_content(content: impl Into<String>) -> Self {
522 Play::Content(content.into())
523 }
524}