win_package モジュールのちょっと気になる挙動

Ansible に win_package というモジュールがあります。 このモジュールは Windows に対して MSI または EXE 形式のパッケージをインストールまたはアンインストールすることができます。

今回はこの win_package モジュールを用いたパッケージのアンインストール時のちょっと気になる挙動を確認しました。

前提

Playbook

Playbook は win_package モジュールを使うタスク 1 つだけ用意します。 パラメータには引数で情報を渡すようにして、検証で使い回せるように一部は default(omit) フィルタを設定します。

# playbook.yml
---
- hosts: win2016
  gather_facts: no
  become: yes
  tasks:
    - name: Validate win_package
      win_package:
          path: "{{ path | default(omit) }}"
          product_id: "{{ product_id | default(omit) }}"
          arguments: "{{ arguments | default(omit) }}"
          state: "{{ state }}"

Inventory

ターゲットノードの情報です(雑なのはご勘弁)。

Playbook への引数の渡し方は色々ありますが、今回の検証ではインベントリに変数を設定して、検証パターンによって書き換えます。

# inventory
win2016 ansible_host=192.168.2.30

[all:vars]
ansible_ssh_port=5986
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
ansible_user=ansible
ansible_password=ansible
ansible_become_user=ansible
ansible_become_method=runas

path=<インストーラのパス>
product_id=<パッケージのレジストリ キー>
arguments=<インストーラ実行時の引数>
state=present | absent

インストール

まずは win_package モジュールを使って Windows に パッケージをインストールします。 インベントリ内の変数は以下のように設定しておきます。

# inventory
(snip)
path=\\192.168.1.108\Users\Public\vcredist_x64.exe
product_id="{730ca3c6-815d-4b47-abc9-5082acd0267f}"
arguments=/quiet /norestart
state=present

path はネットワークフォルダ上のインストーラを指定するため、UNC 形式です。

product_idWindowsHKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall にある対象パッケージのレジストリ キーです。 Visual Studio 2013 の Visual C++ 再頒布可能パッケージレジストリ キーは {730ca3c6-815d-4b47-abc9-5082acd0267f} になります。

$ ansible-playbook -i inventory playbook.yml -K -vvv

(snip)

TASK [win_package] ******************************************************************************************************************************************************************************************************************
task path: /home/nnstt1/home-lab/playbook/win_package.yml:6
Using module file /usr/local/lib/python3.6/site-packages/ansible/modules/windows/win_package.ps1
Pipelining is enabled.
<192.168.2.30> ESTABLISH WINRM CONNECTION FOR USER: ansible on PORT 5986 TO 192.168.2.30
EXEC (via pipeline wrapper)
changed: [win2016] => {
    "changed": true,
    "rc": 0,
    "reboot_required": false
}
META: ran handlers
META: ran handlers

PLAY RECAP **************************************************************************************************************************************************************************************************************************
win2016                    : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

無事にインストールされました。

f:id:nnstt1:20200905054610p:plain

アンインストール

次に、いくつかのパターンでアンインストールをしてみます。

レジストリ キー (product_id) のみ設定

設定する変数を product_idstate だけにします。 このパターンは公式ドキュメントの Example にあるものです。

# inventory
(snip)
product_id="{730ca3c6-815d-4b47-abc9-5082acd0267f}"
state=absent

これを実行すると……

$ ansible-playbook -i inventory win_package.yml -K -vvv

(snip)

TASK [win_package] ******************************************************************************************************************************************************************************************************************
task path: /home/nnstt1/home-lab/playbook/win_package.yml:6
Using module file /usr/local/lib/python3.6/site-packages/ansible/modules/windows/win_package.ps1
Pipelining is enabled.
<192.168.2.30> ESTABLISH WINRM CONNECTION FOR USER: ansible on PORT 5986 TO 192.168.2.30
EXEC (via pipeline wrapper)
fatal: [win2016]: FAILED! => {
    "changed": false,
    "msg": "failed to run uninstall process (\"\\\"C:\\ProgramData\\Package Cache\\{730ca3c6-815d-4b47-abc9-5082acd0267f}\\vcredist_x64.exe\\\"  /uninstall\"): \"1\" 個の引数を指定して \"SearchPath\" を呼び出し中に例外が発生しました: \"Could not find file '\\.exe'.\"",
    "reboot_required": false
}

PLAY RECAP **************************************************************************************************************************************************************************************************************************
win2016                    : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   

エラーとなってしまいました。 このパターンでの win_package モジュールの動きは以下のようです。

  1. 変数 product_id からレジストリ キーの UninstallString を参照
  2. UninstallString に設定された文字列(インストーラのパス)を実行してアンインストール

今回対象としている Visual Studio 2013 の Visual C++ 再頒布可能パッケージ の UninstallString は

f:id:nnstt1:20200905062022p:plain

"C:\ProgramData\Package Cache\{730ca3c6-815d-4b47-abc9-5082acd0267f}\vcredist_x64.exe" /uninstall

となっています。 ここに記載された "C:\(snip)\vcredist_x64.exe" のパスを正しく認識できずにエラーとなっているようです。

試しに、UninstallString を色々書き換えて実行してみました。

  1. C:\(snip)\vcredist_x64.exe /uninstall

    → エラー

  2. "C:\(snip)\vcredist_x64.exe /uninstall"

    → エラー

  3. "C:\(snip)\vcredist_x64.exe" + 変数 arguments に /uninstall 追加

    → エラー

  4. C:\(snip)\vcredist_x64.exe + 変数 arguments に /uninstall 追加

    → 成功

どうやらダブルクォートがあることでインストーラのパスを判断できなくなっているようです。 レジストリ書き換えればアンインストールできましたが、実用的ではありません。

インストーラのパス (path) のみ設定

次に、設定する変数を pathstate だけにします。 このパターンも公式ドキュメントの Example にあるにはあります。

# inventory
(snip)
path=\\192.168.1.108\Users\Public\vcredist_x64.exe
state=absent

これを実行すると……

$ ansible-playbook -i inventory win_package.yml -K -vvv

(snip)

TASK [win_package] ******************************************************************************************************************************************************************************************************************
task path: /home/nnstt1/home-lab/playbook/win_package.yml:6
Using module file /usr/local/lib/python3.6/site-packages/ansible/modules/windows/win_package.ps1
Pipelining is enabled.
<192.168.2.30> ESTABLISH WINRM CONNECTION FOR USER: ansible on PORT 5986 TO 192.168.2.30
EXEC (via pipeline wrapper)
fatal: [win2016]: FAILED! => {
    "changed": false,
    "msg": "product_id is required when the path is not an MSI or the path is an MSI but not local",
    "reboot_required": false
}

PLAY RECAP **************************************************************************************************************************************************************************************************************************
win2016                    : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

このパターンもエラーとなりました。 インストーラが EXE の場合は変数 product_id が必須なようです。

レジストリ キー (product_id) とインストーラのパス (path) を設定

では、変数 product_idpath 両方を指定してみます。

# inventory
(snip)
path=\\192.168.1.108\Users\Public\vcredist_x64.exe
product_id="{730ca3c6-815d-4b47-abc9-5082acd0267f}"
state=absent

これを実行すると……

$ ansible-playbook -i inventory win_package.yml -K -vvv

(snip)

TASK [win_package] ******************************************************************************************************************************************************************************************************************
task path: /home/nnstt1/home-lab/playbook/win_package.yml:6
Using module file /usr/local/lib/python3.6/site-packages/ansible/modules/windows/win_package.ps1
Pipelining is enabled.
<192.168.2.30> ESTABLISH WINRM CONNECTION FOR USER: ansible on PORT 5986 TO 192.168.2.30
EXEC (via pipeline wrapper)

このまま応答が返ってきませんでした。 Windows 側のプロセスを確認すると、vcredist_x64.exe が立ち上がっていました。

f:id:nnstt1:20200905154229p:plain

arguments=/quiet が無いため GUI のプロセスとして立ち上がって操作を待ってしまっている状態、と推測しています。 当該プロセスを終了すると、Ansible のほうはエラーとなりました。

fatal: [win2016]: FAILED! => {
    "changed": false,
    "msg": "unexpected rc from uninstall  \\\\192.168.1.108\\Users\\Public\\vcredist_x64.exe: see rc, stdout and stderr for more details",
    "rc": 1,
    "reboot_required": false,
    "stderr": "",
    "stderr_lines": [],
    "stdout": "",
    "stdout_lines": []
}

PLAY RECAP **************************************************************************************************************************************************************************************************************************
win2016                    : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0  

変数すべて設定

結局すべての変数を指定しちゃうのですが、これでいけるでしょう。 インストール時との違いは state=absent なだけです。

#inventory
path=\\192.168.1.108\Users\Public\vcredist_x64.exe
product_id="{730ca3c6-815d-4b47-abc9-5082acd0267f}"
arguments=/quiet /norestart
state=absent

これを実行すると……

$ ansible-playbook -i inventory win_package.yml -K -vvv

(snip)

TASK [win_package] ******************************************************************************************************************************************************************************************************************
task path: /home/nnstt1/home-lab/playbook/win_package.yml:6
Using module file /usr/local/lib/python3.6/site-packages/ansible/modules/windows/win_package.ps1
Pipelining is enabled.
<192.168.2.30> ESTABLISH WINRM CONNECTION FOR USER: ansible on PORT 5986 TO 192.168.2.30
EXEC (via pipeline wrapper)
changed: [win2016] => {
    "changed": true,
    "rc": 0,
    "reboot_required": false
}
META: ran handlers
META: ran handlers

PLAY RECAP **************************************************************************************************************************************************************************************************************************
win2016                    : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

changed=1 になっているし、無事に成功したようです。

「結局 state 変えるだけでいいじゃん」と思いつつ、Windows 側を見てみると……

f:id:nnstt1:20200905155800p:plain

残ってる!!!(分かりづらくてスミマセン。。)

どういうことでしょうか、ちゃんと changed=1 になっているのにアンインストールされていません。

結局どうすれば

上記までのパターンでは、今回対象としている Visual Studio 2013 の Visual C++ 再頒布可能パッケージ のアンインストールはできませんでした。 こうなってしまえばモジュールのソース読むしかありません。

どうやら arguments=/uninstall という設定が必要なようです。 レジストリUninstallString に指定されている /uninstall を自前で用意してあげないとアンインストールの挙動をしてくれません。 結果は割愛しますが、以下の設定でアンインストールできました。

#inventory
path=\\192.168.1.108\Users\Public\vcredist_x64.exe
product_id="{730ca3c6-815d-4b47-abc9-5082acd0267f}"
arguments=<i><b>/uninstall</b></i> /quiet /norestart
state=absent

「じゃあ state=absent にしてインストールすることもできるかも」と考えたのですが、ちゃんと事前にパッケージがインストールされているかレジストリ キーを見て判断しているようです。

おわりに

ということで、win_package モジュールのちょっと気になる挙動を調べてみました。

他のパッケージのレジストリUninstallString を見てみると、ほとんど MsiExec.exe /I{xxxx} となっていました(↓これは ZABBIX Agent)。 これが MSI 形式でインストールされたパッケージなのでしょうか。

f:id:nnstt1:20200905163207p:plain

今回のようなパターンはほとんど遭遇しないのでは、と思います。 ただ、このような想定外な挙動をした場合の調査方法とは今後に活かしていきたいですね。