AnsibleでFortigateをREST APIで操作する方法は、以下の2種類が存在します。
- fortiosapi (Legacy Mode)
- httpapi (今後の推奨)
私の環境では、前者のfortiosapiは今のところ問題なく動作しますが、後者のhttpapiはうまく動いていません。
[2019/11/10追記]
Python3にあげることで解消しました!!
いったん棚上げしますが、調べている最中の備忘録です。
Ansible関連の実践記事一覧はこちら。
目的
自身の環境で、httpapi経由でFortigateが操作できない原因を明らかにするため
環境
- FortiOS: 6.0.6
- Ansible実行環境のOS: Windows 10 WSL2上のUbuntu 18.04.3
- 上記環境内のAnsible: 2.9.0
- 上記環境内のPython: 2.7.15+
(ansible29) ansible@ENVY360:~/ansible-study$ ansible --version
ansible 2.9.0
config file = /home/ansible/ansible-study/ansible.cfg
configured module search path = [u'/home/ansible/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /home/ansible/ansible29/local/lib/python2.7/site-packages/ansible
executable location = /home/ansible/ansible29/bin/ansible
python version = 2.7.15 + (default, Oct 7 2019, 17:39:04) [ GCC 7.4.0]
httpapiを利用すると発生するエラー
以下のようなエラーが発生してfailedになります。-vvvつけた結果も、見てもさっぱり…(T_T)
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.module_utils.connection.ConnectionError: addinfourl instance has no attribute 'getheaders'
fw01 failed | msg: MODULE FAILURE
See stdout/stderr for the exact error
fortios系モジュールのAPI利用手順
fortiosapi側
リファレンスがありませんので実測からですが、以下のような流れのようです。
| No. | 方向 | 内容 |
|---|---|---|
| 1. | [Ansible->FGT] | /logincheck に対してPOSTで認証情報を投げる |
| 2. | [Ansible<-FGT] | 上記リクエストに対して、問題ない場合にはSet-Cookieでccsrftokenの値が返される |
| 3. | [Ansible->FGT] | 2.で返されたHTTPヘッダを付けた状態でAPIが利用可能かの確認のため、GET /api/v2/cmdb/system/status?global=1 というリクエストを投げる(これは一例。処理をしようとするエンドポイントに合わせてこの値は変化する) |
| 4. | [Ansible<-FGT] | 上記リクエストに対して、問題ない場合には200 OKのレスポンスが返る |
| 5. | [Ansible->FGT] | 3.と同様、2.で返されたヘッダを付与した状態で、目的のエンドポイントに対してGET/PUTメソッドでリクエストを投げる。(fortios_facts以外はおそらくPUT) |
| 6. | [Ansible<-FGT] | 5.のリクエストに対し、処理を行った結果をレスポンスで返す |
| 7. | [Ansible->FGT] | 処理が完了したら、FIN/ACKでコネクションを切断する |
上記のような動作のため、同一のコネクションを使いまわしています。
httpapi側
fortiosapiとは異なり、常にコネクションを切断→再接続していました。終了の処理もより丁寧になっています。
| No. | 方向 | 内容 |
|---|---|---|
| 1. | [Ansible->FGT] | /logincheck に対してPOSTで認証情報を投げる |
| 2. | [Ansible<-FGT] | 上記リクエストに対して、問題ない場合にはSet-Cookieでccsrftokenの値が返される |
| 3. | [Ansible->FGT] | FIN/ACKでコネクションを切断する。 |
| 4. | [Ansible->FGT] | 2.で返されたHTTPヘッダを付けた状態でAPIが利用可能かの確認のため、GET /api/v2/cmdb/system/status?global=1 というリクエストを投げる(これは一例。処理をしようとするエンドポイントに合わせてこの値は変化する) |
| 5. | [Ansible<-FGT] | 上記リクエストに対して、問題ない場合には200 OKのレスポンスが返る |
| 6. | [Ansible->FGT] | FIN/ACKでコネクションを切断する。 |
| 7. | [Ansible->FGT] | 4.と同様、2.で返されたヘッダを付与した状態で、目的のエンドポイントに対してGET/PUTメソッドでリクエストを投げる。(fortios_facts以外はおそらくPUT) |
| 8. | [Ansible<-FGT] | 7.のリクエストに対し、処理を行った結果をレスポンスで返す |
| 9. | [Ansible->FGT] | FIN/ACKでコネクションを切断する。 |
| 10. | [Ansible->FGT] | /logout に対してPOSTで認証情報を付与したリクエスト投げる |
| 11. | [Ansible<-FGT] | 10.のリクエストに対し、処理を行った結果をレスポンスで返す |
| 12. | [Ansible->FGT] | FIN/ACKでコネクションを切断する。 |
共通していること
Fortigateのモジュールを触っていると、成功する場合は必ずOKが1以上返ってくるのですが、3.の時点でOK=1がカウントされているのだと思います。(ソースを確認したわけではありませんが)。 おそらくは以下のようなカウント方法では無いかと思います。 APIへのリクエストに対し、成功するか否か GETメソッドの場合はOK、PUT(POSTもあるのか?)メソッドの場合はChanged 個人的には上「現在の値が何で、リクエストは実行するべきか?」という、冪等性を担保するための仕組み(GETメソッドでの現状確認)が欲しいなあと思うところではありますが、現状は上記のような動作となるため、設定変更系のモジュールについては冪等性はなさそうです。
httpapiではエラーがどの箇所で発生しているたか
上述のhttpapiのうち、1.は問題なく動作しているのですが、4.のリクエストが出ず、上述した10.のlogoutに対して切断を行う動作となっています。
そのため、4.で必要なCookie情報をヘッダに付与する、という部分でエラーが発生していると理解しています。そうなると、基本的にはAnsibleホスト側の問題です。
切り分け
OS環境を変えてとりあえずやってみる
もしかしたらPythonのバージョンとかが影響するのかなあとなんもかんがえずにCentOSを構築。
- 切り分け環境のOS: CentOS Linux release 7.7.1908 (Core)
- 上記環境内のAnsible: 2.9.0
- 上記環境内のPython: 2.7.5
■CentOS側の環境
(ansible29) [ansible@ansible ansible-playbook]$ ansible --version
ansible 2.9.0
config file = /home/ansible/ansible-playbook/ansible.cfg
configured module search path = [u'/home/ansible/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /home/ansible/ansible29/lib/python2.7/site-packages/ansible
executable location = /home/ansible/ansible29/bin/ansible
python version = 2.7.5 (default, Aug 7 2019, 00:51:29) [ GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
→失敗。
リクエストそのものの違いを見てみる
共通して成功している1.の/logincheckへのPOSTリクエストをキャプチャし、キャプチャデータから各々のリクエストの差を比較してみました。
違いとして見えるのは以下の通り。
| カテゴリ | fortiosapi | httpapi | 備考 |
|---|---|---|---|
| User-Agent | python-requests 2.22 | Python-urllib2.7 | |
| Connection | keepalive | close | httpapiも1.1でリクエストはしているが |
| Content-Type | 定義なし | application/x-www-form-urlencoded | |
| Authenticationヘッダ | なし | Basic認証 | dataとしてはfortiosapiも同じ値を渡している |
Pythonのバージョンを上げてやってみる[2019/11/10追記]
よこちさんの環境はPython3という情報をゲットしました。
おお、3系ですか。
そうですよね…
ちょっと上げて試してみますね。
— tatematsu_san (@tk4_jj) November 10, 2019
まあ、サポート終了考えれば、3を使っていて当然ですよね…
python3を以下の手順でインストールし、venvを作成から実施。
$ sudo yum install python3
$ python3 -V
Python 3.6.8
$ python3 -m venv p3-ansible29
$ source p3-ansible29/bin/activate
$ python -V
Python 3.6.8
python3の環境で実行してみる。
(p3-ansible29) [ansible@ansible kiriwake]$ ansible-playbook forti_getfacts_httpapi.yml
Executing playbook forti_getfacts_httpapi.yml
- fortios on hosts: fortios -
Get Facts....
fortios01 ok
Debug...
fortios01 ok: {
"changed": false,
"getfact_result": {
"ansible_facts": {
"ansible_net_gather_network_resources": [],
"ansible_net_gather_subset": [],
"ansible_network_resources": {
"system_status_select": {
"action": "select",
"build": 272,
"http_method": "GET",
"name": "status",
"path": "system",
"results": {},
"serial": "FGT60D4615093671",
"status": "success",
"vdom": "root",
"version": "v6.0.6"
}
},
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"failed": false
}
}
- Play recap -
fortios01: ok=2 changed=0 unreachable=0 failed=0 rescued=0 ignored=0
(p3-ansible29) [ansible@ansible kiriwake]$
キタ━━━━(゚∀゚)━━━━!!
結論
Python3にあげることで問題なく利用ができるようになりました。 よこちさん、大変ご迷惑をおかけいたしました… 以下に、棚上げ時点での弱音を残しておきますが、実際にどんな差が出てPython2系だとだめなのかはちょっと気になります。すごーーーく時間がある時に調べられたら調べてみたいと思います。 ■↓ここから棚上げ時↓■ ここから先行くのはUser-Agentで定義されている各々のライブラリを確認する必要があるのかなあと思い、python不勉強なのでいったん棚上げします。 もしかすると、よこちさんの環境とはFortiOSの環境も異なるので、そのあたりの差分も影響しているのかもしれません。 ぐぐぐ…悔しいが、前に進みます… どなたか、何か気が付いた点があれば、Twitterなりなんなりでコメントいただけると泣いて喜びます…
https://tenko.hatenablog.jp/entry/2019/11/09/143141
バックアップ日時: 2026年11月11日