てんこ

ブログ名は愛猫(てん)の愛称です。中身は個人のIT系学習記録です。

AnsibleでFortigateのhttpapi利用がうまくいっていない話→Python3に上げたら解消!

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-Cookieccsrftokenの値が返される
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-Cookieccsrftokenの値が返される
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を使っていて当然ですよね…

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なりなんなりでコメントいただけると泣いて喜びます…