API経由でAWXにインベントリーを作成する

この記事はニフティグループ [Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nifty) の17日目の記事です。
昨日は @kasayu さんで [一般化線形モデルの概要とPythonでの実装](https://qiita.com/kasayu/items/c7ee839a45e8d21baca8)でした。
前提が詳しく書いてあってわかりやすかったです。GLMの使い所、参考にさせてもらいます。

# はじめに
　GUIからAnsibleを触れるAWXはかなり便利です。
　しかし、GUIからしか触ることを想定されているために千台単位の大量の設定変更対象を登録するのはかなりの手間がかかります。というのもインベントリースクリプトを使えば一度に複数台のホストを登録できますが、複数のインベントリーを用意するためにはインベントリーの数だけインベントリースクリプトを登録して、同期して、としなければなりません。
　AWXの1つの利点として多彩なAPIが提供されていることがあります。そこで、今回はAPI経由でインベントリーを登録することで上記のような手間を削減していきます。

# 環境
AWXの環境

```
 ______________ 
<  AWX 10.0.0  >
 -------------- 
        \   ^__^
         \  (oo)\_______
            (__)      A )\/\
                ||----w |
                ||     ||

Ansible 2.9.5
```
今回はpythonを用いてAPIを叩いていきます。
pythonの環境

```
$ python -V
Python 3.8.5

$ pip freeze
requests-html==0.10.0
```

# AWXのAPIについて
提供されているAPIのリストは `http(s)://<your awx ip>/api/v2/`から確認することができます。
さらに、レスポンスや、必須のフィールドが知りたい場合は`/api/v2/`の各リソースをクリックすることで確認できます。
例として`/inventories`の必須フィールドを確認する場合は、以下の画像のように`OPTIONS`から参照できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/72b95571-4d9e-0d47-df24-b32e3da6dbd9.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/6db9b864-baee-9581-395c-27efd288b71e.png)

# インベントリーの作成
今回はインベントリースクリプト経由でインベントリーを作成していきます。
順序としては `インベントリーの作成 > インベントリースクリプトの作成 > インベントリーソースへの登録 >  同期の実行`でホストを登録していきます。
下準備としてインベントリーを所属させたい組織のIDを調べておきます。
組織を選択したときのこちらの数字がIDとなります。画像での `608`がそれにあたります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/31508cbf-bfd3-8f53-3664-a7854463a607.png)

また、インベントリーの作成以下のプログラムは次のライブラリを使用しています。

```python
from requests_html import HTMLSession
# awxの認証情報を保存している
import env
import json
```

## インベントリーの作成
`/api/v2/inventories`に対して インベントリー名`name`と所属させる組織`organization`をdataとしてpostします。


```python
def create_inventory(name):
  awx_url = 'http://<your awx ip>/api/v2/inventories/'
  headers = {'Content-Type': 'application/json'}

  # 608 => default
  data = {
    'name': name,
    'organization': 608
  }
  awx_session = HTMLSession()
  resp = awx_session.post(
      url=awx_url,
      headers=headers,
      auth=(env.awx['login_id'], env.awx['login_pass']),
      data=json.dumps(data),
      verify=False
  )

  return resp.json()['id']
```

### authについて

```python
      auth=(env.awx['login_id'], env.awx['login_pass'])
```
予め`env.py`にAWXのAPI実行権限を持ったユーザーのidとパスワードを記述しています。
ベタ書きなり環境変数に入れるなりしてauthに入れてあげてください。
インベントリースクリプトの作成以下もauthについては同様に処理してください。


## インベントリースクリプトの作成
`/api/v2/inventory_scripts`に対してscriptをpostします。

```python
def upload_inventory_script(name, script):
  awx_url = 'http://<your awx ip>/api/v2/inventory_scripts/'
  headers = {'Content-Type': 'application/json'}

  # 608 => default
  data = {
    'name': f'get_{name}',
    'script': script,
    'organization': 608
  }
  awx_session = HTMLSession()
  resp = awx_session.post(
      url=awx_url,
      headers=headers,
      auth=(env.awx['login_id'], env.awx['login_pass']),
      data=json.dumps(data),
      verify=False
  )

  return resp.json()['id']
```


## インベントリースクリプトの登録
インベントリーとインベントリースクリプトを紐付けます。

```python

def update_inventory_source(name, script_id, inventory_id):
  awx_url = 'http://<your awx ip>/api/v2/inventory_sources/'
  headers = {'Content-Type': 'application/json'}

  data = {
    'name': name,
    'source': 'custom',
    'inventory': inventory_id,
    'source_script': script_id,
  }
  awx_session = HTMLSession()
  resp = awx_session.post(
      url=awx_url,
      headers=headers,
      auth=(env.awx['login_id'], env.awx['login_pass']),
      data=json.dumps(data),
      verify=False
  )

  return resp.json()['id']
```

## インベントリースクリプトの実行
インベントリーの同期を実行します。
`/api/v2/inventory_sources/<id>/update`に対してpostすることで同期ジョブが走ります。

```python

def update_hosts(source_id):
  awx_url = f'http://<your awx ip>/api/v2/inventory_sources/{source_id}/update/'
  headers = {'Content-Type': 'application/json'}

  awx_session = HTMLSession()
  resp = awx_session.post(
      url=awx_url,
      headers=headers,
      auth=(env.awx['login_id'], env.awx['login_pass']),
      data=json.dumps({}),
      verify=False
  )

  return resp.status_code
```

## 実行
投げるスクリプトはjinja2のテンプレートで作成しています。

```text:template.txt
#!/usr/bin/env python
# coding: utf-8
import json
  
dict = {
    {{Account}}: {
        'hosts': {{Hosts}},
  
        'vars': {
            'ansible_port': '22',
            {% raw %}
            'ansible_user': '{{ username }}',
            'ansible_password': '{{ password }}',
            'ansible_become_pass': '{{ password }}'
            {% endraw %}
        }
    }
}
  
print(json.dumps(dict))
```

```python:generate_template.py
from jinja2 import Environment, FileSystemLoader
# inventory scriptを作成
template = Environment(loader=FileSystemLoader('.', 'utf-8')).get_template('template.txt')

# インベントリースクリプトの登録
name = 'test'
hosts = ['192.168.0.0', '192.168.0.1', '192.168.0.2']
script_id = upload_inventory_script(name, template.render({'Account': f'"{name}"', 'Hosts': hosts}))
inventory_id = create_inventory(name)
source_id = update_inventory_source(name, script_id, inventory_id)
update_hosts(source_id)
```

`generate_template.py`を実行することで以下のようなインベントリーが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/f93002f7-e5d6-f533-5b5c-c0ab4e6ed0bb.png)

# おわりに
ポチポチするのが大変な朝にもAPI経由で処理するプログラムを書いておけばボタン一つでサーバーの状態を保つことができます。皆様も良きAWXライフを送ってください。

明日は@shin27さんです。お楽しみに！