Ansible Tower(AWX)にREST API経由で変数を渡すと使用できない仕様について調べてみた

Ansible Tower(AWX)にREST APIで変数を渡すと変数が展開されないので仕様について調べて見ました。

# 1. 変数展開されない事象について

まずは、変数が展開されない事象について確認しています。

## 1-1. 動作確認方針

extra_varsに変数を格納してテンプレート埋め込みとREST API経由から実行して動作を確認してみます。

## 1-2. テスト用Playbook

使用するPlaybookは以下のものです。
extra_varsから `msg` に `{{ message }}` を格納して実行し `test message` が表示されれば問題ありません。

```yaml
---
- name: var output test
  hosts: all
  gather_facts: no
  vars:
    message: test message
  tasks:
    - debug: msg="{{ msg }}"
    - debug: msg="{{ msg | type_debug }}"
```

## 1-3. テンプレート設定

### 1-3-1. テンプレートに変数埋め込み

![スクリーンショット 2018-07-28 21.38.13.png](https://qiita-image-store.s3.amazonaws.com/0/180118/c47bcd17-3f6a-fdb5-a89c-88f73b92b36e.png)

### 1-3-2. 実行

これを実行すると問題なく `{{ message }}` 変数が展開され `test message` が表示されています。

![スクリーンショット 2018-07-28 21.39.30.png](https://qiita-image-store.s3.amazonaws.com/0/180118/33d99dcf-5610-0564-6300-a4fafff54ab7.png)

### 1-3-3. REST APIから渡す

埋め込んだ変数を削除してREST APIからextra_varsを渡します。

![スクリーンショット 2018-07-28 21.46.11.png](https://qiita-image-store.s3.amazonaws.com/0/180118/1a5e4198-fa1e-5cee-9008-a3ffc1360f95.png)


### 1-3-4. 実行

以下のソースを使って実行します。

```python
#!/usr/bin/env python3
from collections import defaultdict
import json
import requests

def main():
    url = "https://192.168.0.236/api/v1/job_templates/7/launch/"
    headers = {"Content-Type":"application/json"}

    nested_dict = lambda: defaultdict(nested_dict)
    data = nested_dict()
    data['extra_vars']['msg'] = "{{ message }}"

    r = requests.post(url,
                      headers=headers,
                      auth=("admin","redhat"),
                      data=json.dumps(data),
                      verify=False)

    print(r.status_code)
    print(json.dumps(json.loads(r.text), indent=2))

if __name__ == "__main__":
    main()
```

REST API経由から実行すると変数は展開されず、そのまま文字列として出力されます。
変数の型も `AnsibleunsafeText` になっていることが確認できます。

![スクリーンショット 2018-07-28 21.46.57.png](https://qiita-image-store.s3.amazonaws.com/0/180118/23deeda5-d406-7e5b-0d99-dd488b430d3f.png)

# 2. 原因

REST API経由からextra_varsを渡すと `UnsafeProxy` で文字列に変換され戻りで `AnsibleUnsafeText` になるようです。

```python:/usr/lib/python2.7/site-packages/ansible/utils/unsafe_proxy.py
(snip)
class UnsafeProxy(object):
    def __new__(cls, obj, *args, **kwargs):
        # In our usage we should only receive unicode strings.
        # This conditional and conversion exists to sanity check the values
        # we're given but we may want to take it out for testing and sanitize
        # our input instead.
        if isinstance(obj, string_types):
            obj = to_text(obj, errors='surrogate_or_strict')
            return AnsibleUnsafeText(obj)
        return obj
(snip)
```

https://github.com/ansible/ansible/blob/cbb6a7f4e831abd2d56375fb0aa22465e0cc7c0c/lib/ansible/utils/unsafe_proxy.py#L73
https://github.com/ansible/ansible/blob/cbb6a7f4e831abd2d56375fb0aa22465e0cc7c0c/lib/ansible/utils/unsafe_proxy.py#L80

# 3. 解決方法

## 3-1. Ansible本体のソースを改修

一先ず動作確認として `UnsafeProxy` の部分を変更してみます。

```python:/usr/lib/python2.7/site-packages/ansible/utils/unsafe_proxy.py
(snip)
class UnsafeProxy(object):
    def __new__(cls, obj, *args, **kwargs):
        # In our usage we should only receive unicode strings.
        # This conditional and conversion exists to sanity check the values
        # we're given but we may want to take it out for testing and sanitize
        # our input instead.
        if isinstance(obj, string_types):
            obj = to_text(obj, errors='surrogate_or_strict')
            return obj
            #return AnsibleUnsafeText(obj)
        return obj
(snip)
```

実行すると変数が展開されていることが確認できます。

![スクリーンショット 2018-07-28 21.55.05.png](https://qiita-image-store.s3.amazonaws.com/0/180118/7cf833ac-7495-e2c5-2035-7cc104b6dfd4.png)

しかし、これは本体のソースをいじって無理やりやっている形なので影響度が不明です。

以下は `return obj(左)` と `return AnsibleUnsafeText(obj)(右)` の時の `UnsafeProxy` で実行された結果の差分です。
UnsafeProxyで処理されたobjの名前と型を出力して比較しています。

![スクリーンショット 2018-07-28 22.09.59.png](https://qiita-image-store.s3.amazonaws.com/0/180118/62e0e7df-78d1-de71-a343-c98857786fb8.png)

左の場合は `ansible.parsing.yaml.objects.AnsibleUnicode` のclassのためPlaybookを実行すると変数が展開されます。

## 3-2. dict/listを文字列として渡す

dictを文字列として渡してみます。

```python
#!/usr/bin/env python3
from collections import defaultdict
import json
import requests

def main():
    url = "https://192.168.0.236/api/v1/job_templates/7/launch/"
    headers = {"Content-Type":"application/json"}

    nested_dict = lambda: defaultdict(nested_dict)
    data = nested_dict()
    data['extra_vars']['msg'] = "{'msg': message }"

    r = requests.post(url,
                      headers=headers,
                      auth=("admin","redhat"),
                      data=json.dumps(data),
                      verify=False)

    print(r.status_code)
    print(json.dumps(json.loads(r.text), indent=2))

if __name__ == "__main__":
    main()
```

![スクリーンショット 2018-07-28 22.20.57.png](https://qiita-image-store.s3.amazonaws.com/0/180118/01d35ff1-7e45-dd35-694f-b71ec368ce4c.png)

listを文字列として渡してみます。

```python
#!/usr/bin/env python3
from collections import defaultdict
import json
import requests

def main():
    url = "https://192.168.0.236/api/v1/job_templates/7/launch/"
    headers = {"Content-Type":"application/json"}

    nested_dict = lambda: defaultdict(nested_dict)
    data = nested_dict()
    data['extra_vars']['msg'] = "[ message ]"

    r = requests.post(url,
                      headers=headers,
                      auth=("admin","redhat"),
                      data=json.dumps(data),
                      verify=False)

    print(r.status_code)
    print(json.dumps(json.loads(r.text), indent=2))

if __name__ == "__main__":
    main()
```

![スクリーンショット 2018-07-28 22.21.49.png](https://qiita-image-store.s3.amazonaws.com/0/180118/9367a623-f842-bd49-156e-85b2f0c78e15.png)

両方とも `message` 変数が展開されていることが確認できます。

# 4. 最後に

Ansible Tower(AWX)のREST APIから変数を渡すというやり方は簡単にできないような感じです。
ちょっと一癖ありますね。。。
辞書やリストを使う前提の変数であれば、文字列で渡せばなんとかなりそうです。
フィルター噛ませたりしてやりくりするとできたりするのだろうか。

# 5. 参考

[to_textのソース](https://github.com/ansible/ansible/blob/fc5d71de0da187658899cf12e0a6b3cdb210f5fc/lib/ansible/module_utils/_text.py)
[string_typesのソース](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/six/__init__.py)