PythonからAPIを使ってAnsibleを実行する

# 初めに
Ansibleって便利ですよね。同じ作業をたくさんのサーバで行う時は神様だと思います。

Playbookを叩くだけでも十分自動化している感ありますが、Playbookの実行も良い感じに自動化したいと思いました。
シェルスクリプトを書いても良いんですが、結果などをパースするのは面倒です。

そこで今回は、Pythonから良い感じにAnsibleを叩いてみたいと思います。
意外と記事が見つからなかったです。

# ソースコード
`__main__`から読むとわかりやすいです。
`192.168.0.1`で`ls -la /`を実行するPlaybookです。
Ansibleをコマンドで使ったことがあれば簡単に読めると思います。
所々簡単なコメントを入れているのでご参考に。

```
pip3 install ansible
```

```python
import json
import shutil
from ansible.module_utils.common.collections import ImmutableDict
from ansible.parsing.dataloader import DataLoader
from ansible.vars.manager import VariableManager
from ansible.inventory.manager import InventoryManager
from ansible.playbook.play import Play
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.plugins.callback import CallbackBase
from ansible import context
import ansible.constants as C


class ResultCallback(CallbackBase):
    def __init__(self, *args, **kwargs):
        super(ResultCallback, self).__init__(*args, **kwargs)
        self.host_ok = {}
        self.host_unreachable = {}
        self.host_failed = {}

    def v2_runner_on_unreachable(self, result):
        host = result._host
        self.host_unreachable[host.get_name()] = result

    def v2_runner_on_ok(self, result, *args, **kwargs):
        host = result._host
        self.host_ok[host.get_name()] = result

    def v2_runner_on_failed(self, result, *args, **kwargs):
        host = result._host
        self.host_failed[host.get_name()] = result

def ansible_run(play_source, host_list):
    # ansible-playbookで指定できる引数と同じ
    context.CLIARGS = ImmutableDict(
        tags={}, 
        listtags=False, 
        listtasks=False, 
        listhosts=False, 
        syntax=False, 
        connection='ssh',                
        module_path=None, 
        forks=100, 
        private_key_file=None,
        ssh_common_args=None, 
        ssh_extra_args=None, 
        sftp_extra_args=None, 
        scp_extra_args=None, 
        become=False,
        become_method='Sudo', 
        become_user='root', 
        verbosity=True, 
        check=False, 
        start_at_task=None
    )

    # 鍵認証が優先され、パスワードを聞かれた場合のみ利用する。書きたくない場合は適当でOK
    passwords = dict(vault_pass='secret')

    # コールバックのインスタンス化
    results_callback = ResultCallback()

    # インベントリを１ライナー用フォーマットに変換
    sources = ','.join(host_list)
    if len(host_list) == 1:
        sources += ','
    loader = DataLoader()
    inventory = InventoryManager(loader=loader, sources=sources)

    # 値をセット
    variable_manager = VariableManager(loader=loader, inventory=inventory)
    play = Play().load(play_source, variable_manager=variable_manager, loader=loader)

    # 実行
    tqm = None
    try:
        tqm = TaskQueueManager(
                inventory=inventory,
                variable_manager=variable_manager,
                loader=loader,
                passwords=passwords,
                stdout_callback=results_callback, 
            )
        result = tqm.run(play)
    finally:
        # 終了後に一時ファイルを削除している
        if tqm is not None:
            tqm.cleanup()
        # Remove ansible tmpdir
        shutil.rmtree(C.DEFAULT_LOCAL_TMP, True)
        return results_callback

if __name__ == "__main__":
    # 実行ホストを指定（インベントリにも指定される）
    host_list = [ "root@192.168.0.1" ]

    # プレイブックを定義
    play_source =  dict(
        name = "Ansible Play",
        hosts = host_list,
        gather_facts = 'no',
        tasks = [
            dict(action=dict(module='shell', args='ls -l /'), register='shell_out')
        ]
    )
    
    results = ansible_run(play_source=play_source, host_list=host_list)

    for host, result in results.host_ok.items():
        print(host)
        print(json.dumps(result._result, indent=4))
    
    for host, result in results.host_failed.items():
        print(host)
        print(json.dumps(result._result, indent=4))
    
    for host, result in results.host_unreachable.items():
        print(host)
        print(json.dumps(result._result, indent=4))
    
```

実行結果の辞書はこんな感じで取得できます。以下のはJsonで出力したものを抜粋してます。

```json
{
    "cmd": "ls -l /",
    "stdout": "total 28\nlrwxrwxrwx.    1 root root    7 May 11  2019 bin -> usr/bin\ndr-xr-xr-x.    6 root root 4096 Nov 12 18:17 boot\ndrwxr-xr-x.    7 root root   65 Nov 17 00:41 data\ndrwxr-xr-x.   21 root root 3580 Nov 23 12:10 dev\ndrwxr-xr-x.  104 root root 8192 Nov 22 14:11 etc\ndrwxr-xr-x.    6 root root 4096 Nov 20 13:06 gvolume0\ndrwxr-xr-x.    3 root root 4096 Nov 17 00:47 gvolume1\ndrwxr-xr-x.    3 root root   19 Nov 10 01:24 home\nlrwxrwxrwx.    1 root root    7 May 11  2019 lib -> usr/lib\nlrwxrwxrwx.    1 root root    9 May 11  2019 lib64 -> usr/lib64\ndrwxr-xr-x.    2 root root    6 May 11  2019 media\ndrwxr-xr-x.    2 root root    6 May 11  2019 mnt\ndrwxr-xr-x.    2 root root    6 May 11  2019 opt\ndr-xr-xr-x. 1056 root root    0 Nov 22 14:04 proc\ndr-xr-x---.    4 root root  192 Nov 23 11:27 root\ndrwxr-xr-x.   32 root root 1100 Nov 22 14:46 run\nlrwxrwxrwx.    1 root root    8 May 11  2019 sbin -> usr/sbin\ndrwxr-xr-x.    2 root root    6 May 11  2019 srv\ndr-xr-xr-x.   13 root root    0 Nov 22 14:04 sys\ndrwxrwxrwt.    9 root root  212 Nov 23 22:51 tmp\ndrwxr-xr-x.   12 root root  144 Nov 10 01:22 usr\ndrwxr-xr-x.   21 root root 4096 Nov 10 01:28 var",
    "stderr": "",
    "rc": 0,
    "start": "2020-11-23 22:51:11.787866",
    "end": "2020-11-23 22:51:11.793951",
    "delta": "0:00:00.006085",
    "changed": true,
    "invocation": {
        "module_args": {
            "_raw_params": "ls -l /",
            "_uses_shell": true,
            "warn": true,
            "stdin_add_newline": true,
            "strip_empty_ends": true,
            "argv": null,
            "chdir": null,
            "executable": null,
            "creates": null,
            "removes": null,
            "stdin": null
        }
    },
    "stdout_lines": [
        "total 28",
        "lrwxrwxrwx.    1 root root    7 May 11  2019 bin -> usr/bin",
        "dr-xr-xr-x.    6 root root 4096 Nov 12 18:17 boot",
        "drwxr-xr-x.    7 root root   65 Nov 17 00:41 data",
        "drwxr-xr-x.   21 root root 3580 Nov 23 12:10 dev",
        "drwxr-xr-x.  104 root root 8192 Nov 22 14:11 etc",
        "drwxr-xr-x.    6 root root 4096 Nov 20 13:06 gvolume0",
        "drwxr-xr-x.    3 root root 4096 Nov 17 00:47 gvolume1",
        "drwxr-xr-x.    3 root root   19 Nov 10 01:24 home",
        "lrwxrwxrwx.    1 root root    7 May 11  2019 lib -> usr/lib",
        "lrwxrwxrwx.    1 root root    9 May 11  2019 lib64 -> usr/lib64",
        "drwxr-xr-x.    2 root root    6 May 11  2019 media",
        "drwxr-xr-x.    2 root root    6 May 11  2019 mnt",
        "drwxr-xr-x.    2 root root    6 May 11  2019 opt",
        "dr-xr-xr-x. 1056 root root    0 Nov 22 14:04 proc",
        "dr-xr-x---.    4 root root  192 Nov 23 11:27 root",
        "drwxr-xr-x.   32 root root 1100 Nov 22 14:46 run",
        "lrwxrwxrwx.    1 root root    8 May 11  2019 sbin -> usr/sbin",
        "drwxr-xr-x.    2 root root    6 May 11  2019 srv",
        "dr-xr-xr-x.   13 root root    0 Nov 22 14:04 sys",
        "drwxrwxrwt.    9 root root  212 Nov 23 22:51 tmp",
        "drwxr-xr-x.   12 root root  144 Nov 10 01:22 usr",
        "drwxr-xr-x.   21 root root 4096 Nov 10 01:28 var"
    ],
    "stderr_lines": [],
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "_ansible_no_log": false
}
```

# 終わりに
結果もPythonで使いやすいので他のシステムとの結合も簡単です。
私は[趣味で書いているPythonのシステム](https://github.com/hibiki31/virty)からAnsibleを叩きたいと思ってこの方法を探してました。
他の方のお役に立てれば何よりです。

# 参考
- [Red Hat, Inc. - Ansible公式のAPI例1](https://docs.ansible.com/ansible/2.9_ja//dev_guide/developing_api.html)
- [Red Hat, Inc. - Ansible公式のAPI例2](https://docs.ansible.com/ansible/latest/dev_guide/developing_api.html)
- [it-swarm-ja.tech - Python APIを使用してansible-playbookを実行](https://www.it-swarm-ja.tech/ja/python/python-api%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6ansibleplaybook%E3%82%92%E5%AE%9F%E8%A1%8C/1050193047/)