Ansible Towerで実行したテンプレートのジョブのステータスをAPIで確認する


Ansible Towerで実行したテンプレートのジョブステータスをAPIで取得した時のメモです。

# 環境

|      項目     | バージョン |
|---------------|------------|
| OS            | RHEL7.4    |
| Ansible Tower | 3.2.3      |
| python        | 3.4        |

# Ansible Tower APIドキュメント

[http://docs.ansible.com/ansible-tower/3.1.4/html/towerapi/index.html](http://docs.ansible.com/ansible-tower/3.1.4/html/towerapi/index.html)


# ジョブ情報取得URL

テンプレートを実行した時に生成されるジョブIDを元に、そのジョブの情報が以下URLから取得できます。

|       URL        |
|------------------|
| /api/v1/ジョブID |

ジョブIDはテンプレート実行した時のレスポンスBodyに含まれています。
以下がレスポンスの例です。
レスポンスの `url` を元に実行したジョブのステータス情報が取得できます。

```json
{
  "ignored_fields": {},
  "id": 142,
  "type": "job",
  "url": "/api/v1/jobs/142/",
(snip)
}
```

# 検証

## 検証Playbook

```yaml
---
- name: TEST Playbook
  hosts: localhost
  tasks:
    - wait_for: timeout=5
    - debug: msg="Hello World"
```

## 検証テンプレート

![](https://qiita-image-store.s3.amazonaws.com/0/180118/008f84db-caa1-aef2-879c-11a48fcb950b.png)

## 検証ツール

```python:ansible-tower-api-tool.py
#!/usr/bin/env python3
from getpass import getpass
import argparse
import sys
import json
import re
import time
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

class colors:
    GREEN = '\033[32m'
    RED = '\033[91m'
    END = '\033[0m'

def options():
    parser = argparse.ArgumentParser(prog="ansible-tower-api-tool.py",
                                     add_help=True,
                                     description="Ansible Towerのテンプレートを実行するツール")
    parser.add_argument("--host",
                        type=str, required=True,
                        help="Ansible TowerサーバのIPまたはホスト名")
    parser.add_argument("--username", "-u",
                        type=str,default="admin",
                        help="Ansible Towerアカウント(default:admin)")
    parser.add_argument("--password", "-p",
                        type=str,
                        help="Ansible Towerアカウントのパスワード")
    parser.add_argument("--template", "-t",
                        type=str, required=True,
                        help="実行するテンプレート名")
    parser.add_argument("--data", "-d",
                        type=eval,
                        help="テンプレートのextra_vars")

    args = parser.parse_args()
    if(not(args.password)):
        args.password = getpass(prompt="Ansible Tower Password: ")

    return args

def get_header():
    headers = { "Content-Type": "application/json" }
    return headers

def get_cred(username, password):
    cred = {
        'username': username,
        'password': password
    }
    return cred

def create_url(host, path):
    url = "https://%s%s" % (host, path)
    return url

def get_token(host, cred):
    url = create_url(host, "/api/v2/authtoken/")
    r = post(url, get_header(), cred)

    if(r.status_code == 200):
        token = json.loads(r.text)['token']
        return token
    else:
        print('Token acquisition error.')
        sys.exit(1)

def get(url, headers):
    r = requests.get(url,
                     headers=headers,
                     verify=False)

    return r

def post(url, headers, data):
    r = requests.post(url,
                      headers=headers,
                      data=json.dumps(data),
                      verify=False)

    return r

if __name__ == '__main__':
    # オプションを取得
    args = options()

    # 認証Tokenを取得
    cred = get_cred(args.username, args.password)
    token = get_token(args.host, cred)

    # ヘッダー情報を生成
    headers = get_header()
    headers["Authorization"] = "Token " + token

    # テンプレードID取得
    url = create_url(args.host, "/api/v1/job_templates")
    r = get(url, headers)

    for i in json.loads(r.text)['results']:
        if(i["name"] == args.template):
            template_launch_url = i["related"]["launch"]
            break

    if(not("template_launch_url" in locals())):
        print("%s template not found" % args.template)
        sys.exit(1)

    # テンプレートを実行
    url = create_url(args.host, template_launch_url)
    r = post(url, headers, args.data)

    # ジョブIDとURLを取得
    job_id = json.loads(r.text)["job"]
    job_url = json.loads(r.text)["url"]

    # ジョブステータス確認
    Loop = True
    url = create_url(args.host, job_url)
    while Loop:
        r = get(url, headers)
        r = json.loads(r.text)
        if(r["id"] == job_id and re.match(r"new|pending|running", r["status"])):
            msg = "jobID: %s" % r["id"]
            sys.stdout.write(msg.ljust(20) + "[%s]" % r["status"] + "\r")
        elif(r["id"] == job_id and re.match(r"successful|failed|error|canceled", r["status"])):
            msg = "jobID: %s" % r["id"]
            if(r["status"] == "successful"):
                for i in range(1,30): sys.stdout.write(" ")
                sys.stdout.write("\r" + msg.ljust(20) + "[" + colors.GREEN + r["status"] + colors.END + "]" + "\n")
            else:
                for i in range(1,30): sys.stdout.write(" ")
                sys.stdout.write("\r" + msg.ljust(20) + "[" + colors.RED + r["status"] + colors.END + "]" + "\n")
            Loop = False
        time.sleep(1)
```

テンプレートを実行した後にレスポンスから `job_url` を取得してステータスが終了するまでループします。

## 実行例

こんな感じでステータスが確認できます。

![ansible_tower_job_status_check.gif](https://qiita-image-store.s3.amazonaws.com/0/180118/405f96d8-a887-7f59-0fe4-c0cee48b061f.gif)

# 最後に

ジョブ情報の中には `result_stdout` に実行結果が入るので以下のコードを上記ツールの最後に入れて実行すれば

```python
(snip)
    # ジョブ実行結果出力
    r = get(url, headers)
    print(json.loads(r.text)["result_stdout"])
```

こんな感じで結果も見える。

```
[root@localhost ~]# ./ansible-tower-api-tool.py --host 192.168.0.234 -t TEST -d '{}'
Ansible Tower Password:
jobID: 149          [successful]
SSH password:

PLAY [TEST Playbook] ***********************************************************

TASK [Gathering Facts] *********************************************************
ok: [127.0.0.1]

TASK [wait_for] ****************************************************************
ok: [127.0.0.1]

TASK [debug] *******************************************************************
ok: [127.0.0.1] => {
    "msg": "Hello World"
}

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=3    changed=0    unreachable=0    failed=0
```

# 追記

`replace` で文字の `\n` を改行に変更してあげればエラーが改行されて見やすくなる。

```python
    # ジョブ実行結果出力
    r = get(url, headers)
    print(json.loads(r.text)["result_stdout"].replace("\\n","\n"))
```

結果

```
[root@localhost ~]# ./ansible-tower-api-tool.py --host 192.168.0.234 -t TEST -d '{}'
Ansible Tower Password:
jobID: 151          [failed]
SSH password:

PLAY [TEST Playbook] ***********************************************************

TASK [Gathering Facts] *********************************************************
ok: [127.0.0.1]

TASK [debug] *******************************************************************
fatal: [127.0.0.1]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'MSG' is undefined

The error appears to have been in '/var/lib/awx/projects/test/test.yml': line 7, column 7, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

    #- debug: msg=\"Hello World\"
    - debug: msg=\"{{ MSG }}\"
      ^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes.  Always quote template expression brackets when they
start a value. For instance:

    with_items:
      - {{ foo }}

Should be written as:

    with_items:
      - \"{{ foo }}\"

exception type: <class 'ansible.errors.AnsibleUndefinedVariable'>
exception: 'MSG' is undefined"}

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=1    changed=0    unreachable=0    failed=1
```