ansible-rs 1.1.0

A Rust wrapper library for Ansible command-line tools (Linux/Unix only)
Documentation
//! 异步 Playbook 执行
//!
//! 提供异步版本的 Ansible Playbook 执行功能。

use crate::{Playbook, Play, AnsibleError};
use crate::command_config::CommandConfig;
use crate::async_support::{AsyncResult, AsyncExecute, IntoAsync, FromAsync};
use std::ffi::OsStr;
use std::fmt::Display;
use tokio::process::Command;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

/// 异步 Playbook 执行器
///
/// `AsyncPlaybook` 提供与 [`Playbook`] 相同的功能,但所有执行操作都是异步的。
///
/// # Examples
///
/// ## 基本异步 Playbook 执行
///
/// ```rust,no_run
/// use ansible::{AsyncPlaybook, AsyncPlay};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let mut playbook = AsyncPlaybook::default();
///     playbook
///         .set_inventory("hosts.yml")
///         .set_verbosity(2);
///
///     let result = playbook.run(AsyncPlay::from_file("site.yml")).await?;
///     println!("Playbook result: {}", result);
///
///     Ok(())
/// }
/// ```
///
/// ## 并发执行多个 Playbook
///
/// ```rust,no_run
/// use ansible::{AsyncPlaybook, AsyncPlay};
/// use tokio::try_join;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let mut playbook1 = AsyncPlaybook::default();
///     playbook1.set_inventory("production");
///
///     let mut playbook2 = AsyncPlaybook::default();
///     playbook2.set_inventory("staging");
///
///     let (prod_result, staging_result) = try_join!(
///         playbook1.run(AsyncPlay::from_file("deploy.yml")),
///         playbook2.run(AsyncPlay::from_file("test.yml"))
///     )?;
///
///     println!("Production: {}", prod_result);
///     println!("Staging: {}", staging_result);
///
///     Ok(())
/// }
/// ```
#[derive(Debug, Clone)]
pub struct AsyncPlaybook {
    command: String,
    cfg: CommandConfig,
    inventory: Option<String>,
}

impl Default for AsyncPlaybook {
    fn default() -> Self {
        Self {
            command: "ansible-playbook".to_string(),
            cfg: CommandConfig::default(),
            inventory: None,
        }
    }
}

impl AsyncPlaybook {
    /// 创建新的 AsyncPlaybook 实例
    pub fn new() -> Self {
        Self::default()
    }

    /// 设置系统环境变量
    pub fn set_system_envs(&mut self) -> &mut Self {
        self.cfg.set_system_envs();
        self
    }

    /// 过滤环境变量
    pub fn filter_envs<T, S>(&mut self, iter: T) -> &mut Self
    where
        T: IntoIterator<Item = S>,
        S: AsRef<OsStr> + Display,
    {
        self.cfg.filter_envs(iter);
        self
    }

    /// 添加环境变量
    pub fn add_env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
        self.cfg.add_env(key, value);
        self
    }

    /// 添加命令行参数
    pub fn arg<S: AsRef<OsStr> + Display>(&mut self, arg: S) -> &mut Self {
        self.cfg.arg(arg);
        self
    }

    /// 添加多个命令行参数
    pub fn args<T, S>(&mut self, args: T) -> &mut Self
    where
        T: IntoIterator<Item = S>,
        S: AsRef<OsStr> + Display,
    {
        self.cfg.args(args);
        self
    }

    /// 设置清单文件
    pub fn set_inventory(&mut self, inventory: &str) -> &mut Self {
        self.inventory = Some(inventory.to_string());
        self
    }

    /// 设置 JSON 输出格式
    pub fn set_output_json(&mut self) -> &mut Self {
        self.cfg
            .add_env("ANSIBLE_STDOUT_CALLBACK", "json")
            .add_env("ANSIBLE_LOAD_CALLBACK_PLUGINS", "True");
        self
    }

    /// 设置详细程度
    pub fn set_verbosity(&mut self, level: u8) -> &mut Self {
        let verbose_arg = format!("-{}", "v".repeat(level as usize));
        self.arg(verbose_arg);
        self
    }

    /// 添加额外变量
    pub fn add_extra_var(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
        let var_string = format!("{}={}", key.into(), value.into());
        self.arg("--extra-vars").arg(var_string);
        self
    }

    /// 添加标签
    pub fn add_tag(&mut self, tag: impl Into<String>) -> &mut Self {
        self.arg("--tags").arg(tag.into());
        self
    }

    /// 设置检查模式
    pub fn set_check_mode(&mut self, enabled: bool) -> &mut Self {
        if enabled {
            self.arg("--check");
        }
        self
    }

    /// 异步执行 Playbook
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// use ansible::{AsyncPlaybook, AsyncPlay};
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let mut playbook = AsyncPlaybook::default();
    ///     playbook.set_inventory("hosts.yml");
    ///
    ///     let yaml_content = r#"
    ///     - hosts: all
    ///       tasks:
    ///         - name: Test connection
    ///           ping:
    ///     "#;
    ///
    ///     let result = playbook.run(AsyncPlay::from_content(yaml_content)).await?;
    ///     println!("Result: {}", result);
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn run(&self, play: AsyncPlay) -> AsyncResult<String> {
        let playbook_path = match &play.inner {
            Play::File(path) => path.clone(),
            Play::Content(content) => {
                // 创建临时文件
                let temp_file = tempfile::NamedTempFile::new()
                    .map_err(|e| AnsibleError::io_error(&format!("Failed to create temp file: {}", e)))?;
                let temp_path = temp_file.path().to_string_lossy().to_string();
                
                // 异步写入内容
                let mut file = File::create(&temp_path).await
                    .map_err(|e| AnsibleError::io_error(&format!("Failed to create playbook file: {}", e)))?;
                file.write_all(content.as_bytes()).await
                    .map_err(|e| AnsibleError::io_error(&format!("Failed to write playbook content: {}", e)))?;
                file.flush().await
                    .map_err(|e| AnsibleError::io_error(&format!("Failed to flush playbook file: {}", e)))?;
                
                temp_path
            }
        };

        let full_cmd = self.to_string();
        let cmd_vec: Vec<&str> = full_cmd.split_whitespace().collect();
        
        let mut cmd = Command::new(&self.command);
        cmd.envs(&self.cfg.envs);
        cmd.args(&cmd_vec.as_slice()[1..]);
        cmd.args(&self.cfg.args);
        cmd.arg(&playbook_path);

        let output = cmd.output().await?;
        
        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 playbook execution 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())
    }

    /// 构建完整的命令字符串(内部使用)
    fn to_string(&self) -> String {
        let mut parts = vec![self.command.clone()];

        if let Some(ref inventory) = self.inventory {
            parts.push("-i".to_string());
            parts.push(inventory.clone());
        }

        // 添加配置中的参数
        for arg in &self.cfg.args {
            parts.push(arg.to_string());
        }

        parts.join(" ")
    }
}

/// 异步 Playbook 内容表示
///
/// 与 [`Play`] 相同,但用于异步操作。
#[derive(Debug, Clone)]
pub struct AsyncPlay {
    inner: Play,
}

impl AsyncPlay {
    /// 从文件路径创建 AsyncPlay
    pub fn from_file(path: impl Into<String>) -> Self {
        Self {
            inner: Play::from_file(path),
        }
    }

    /// 从字符串内容创建 AsyncPlay
    pub fn from_content(content: impl Into<String>) -> Self {
        Self {
            inner: Play::from_content(content),
        }
    }
}

impl AsyncExecute for AsyncPlaybook {
    async fn execute_async(&self) -> AsyncResult<String> {
        // 默认执行一个简单的 ping playbook
        let ping_playbook = r#"
---
- hosts: all
  gather_facts: no
  tasks:
    - name: Test connection
      ping:
"#;
        self.run(AsyncPlay::from_content(ping_playbook)).await
    }
}

impl IntoAsync<AsyncPlaybook> for Playbook {
    fn into_async(self) -> AsyncPlaybook {
        AsyncPlaybook {
            command: self.command,
            cfg: self.cfg,
            inventory: self.inventory,
        }
    }
}

impl FromAsync<AsyncPlaybook> for Playbook {
    fn from_async(async_playbook: AsyncPlaybook) -> Self {
        Playbook {
            command: async_playbook.command,
            cfg: async_playbook.cfg,
            inventory: async_playbook.inventory,
        }
    }
}

impl IntoAsync<AsyncPlay> for Play {
    fn into_async(self) -> AsyncPlay {
        AsyncPlay { inner: self }
    }
}

impl FromAsync<AsyncPlay> for Play {
    fn from_async(async_play: AsyncPlay) -> Self {
        async_play.inner
    }
}

impl std::fmt::Display for AsyncPlaybook {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.to_string())
    }
}