AnsibleでWindowsにインストールされているプログラムの情報を取得

プロビジョニング・ツール（環境構築ツール）の[Ansible](http://docs.ansible.com/ansible/)で、Windowsにインストールされているプログラムの情報を取得してみます。

Ansibleは、2.0を使用します（1.9でも適用可能ですが、インベントリ変数名に変更が必要です）。
Windowsは、Windows 7 SP1以降を対象とします。この記事では触れませんが、Windows 7の場合は別途Windows Management Framework 4.0（PowerShell 4.0を含む）をインストールする必要があります。

## PowerShellでインストールされているプログラムの情報を取得する方法

Windows OSで、インストールされているプログラムの情報を取得する方法を調べるとまず出てくるのが WMI（Windows Management Instrumentation）です。WMIをコマンドラインから使うコマンドとしてwmicが提供されています。また、PowerShellから使うコマンドレットとしてGet-WmiObjectが提供されています。

PowerShellから次のコマンドを実行すると、インストールされているプログラムの情報が表示されます。

```
PS > Get-WmiObject Win32_Product

IdentifyingNumber : {5FCE6D76-F5DC-37AB-B2B8-22AB8CEDB1D4}
Name              : Microsoft Visual C++ 2008 Redistributable - x64 9.0.30729.6161
Vendor            : Microsoft Corporation
Version           : 9.0.30729.6161
Caption           : Microsoft Visual C++ 2008 Redistributable - x64 9.0.30729.6161

IdentifyingNumber : {23170F69-40C1-2702-0920-000001000000}
Name              : 7-Zip 9.20 (x64 edition)
Vendor            : Igor Pavlov
Version           : 9.20.00.0
Caption           : 7-Zip 9.20 (x64 edition)

（以下略）
```

ところが、このwmic（Get-WmiObject）での情報取得は少々時間がかかります（手元のWindows 7マシンでは約1分かかります）。
Ansibleで、このコマンドを各マシンで巡回して実行すると相当な時間がかかってしまうので、直接レジストリから情報を取得することにしました。

PowerShellでは、次のコマンドレットで指定したレジストリのサブキー情報（インストールされているソフトウェア情報）を取得できます。

```
PS > Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
```

- 注）64bit OSの場合、32bitプログラムのインストール情報は別なレジストリに記載されるので、上述のコマンドでは64bitプログラムの情報のみ取得されます。

ここのインストール情報から、次の情報を抜粋して取得するとします。

- DisplayName
- DisplayVersion
- InstallDate
- Publisher

```
PS > Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall |
 foreach {Get-ItemProperty $_.PSPath} |
 select DisplayName,DisplayVersion,InstallDate,Publisher

R for Windows 3.2.1   3.2.1                20150626             R Core Team
Speccy                1.25                                      Piriform
WinMerge 2.14.0.63... 2.14.0.63            20141221             Thingamahoochie S...
Microsoft SQL Serv... 12.0.2000.8          20150802             Microsoft Corpora...
（以下略）
```

ここまできたら、次はAnsibleからインストール情報を取得する算段を考えます。

## Ansibleで各管理ノードからプログラムのインストール情報を取得（その１）

とりあえず動かすことを目標に、次のように、Ansibleのplaybookファイル installed.yml、インベントリファイル hosts を作成し、PowerScriptスクリプトファイル get_installed.ps1 をfilesディレクトリ下に作成します。

```:ディレクトリ構成
work
  +-- installed.yml
  +-- hosts
  +-- files/
        +-- get_installed.ps1
```

```yaml:installed.yml
---
- hosts: all
  gather_facts: no
  tasks:
  - name: インストールされているプログラム情報を取得
    script: files/get-installed.ps1
    register: installed_result
  - debug: var=installed_result.stdout_lines
```

```:hosts
[windows]
lima
mike
november

[windows:vars]
ansible_user=foo
ansible_password=fooPassword
ansible_connection=winrm
```

- 注）変数名（例: ansible_user）は、Ansible 2.0で変更になった新しい名前を使用しています。従来の変数名（例:ansible_ssh_user）も非推奨となっていますが引き続き使用可能です。
- パスワードを平文で記述していますが、セキュリティ上配慮が必要な環境ではansible-vaultで暗号化したファイルに記述するとよいでしょう。

```:get-installed.ps1
Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall |
 foreach {Get-ItemProperty $_.PSPath} |
 select DisplayName,DisplayVersion,InstallDate,Publisher
```

これを実行すると

```
work$ ansible-playbook -i hosts installed.yml 

PLAY ***************************************************************************
TASK [インストールされているプログラム情報を取得] ***************************************************
changed: [lima]
changed: [mike]
changed: [november]

TASK [debug] *******************************************************************
ok: [lima] => {
    "installed_result.stdout_lines": [
        "", 
        "DisplayName         DisplayVersion      InstallDate         Publisher          ", 
        "-----------         --------------      -----------         ---------          ", 
        "7-Zip 9.20 (x64 ... 9.20.00.0           20160211            Igor Pavlov        ", 
        "Microsoft Visual... 9.0.30729.6161      20160102            Microsoft Corpor...", 
}
ok: [mike] => {
    "installed_result.stdout_lines": [

（中略）
PLAY RECAP *********************************************************************
lima                       : ok=2    changed=1    unreachable=0    failed=0   
mike                       : ok=2    changed=1    unreachable=0    failed=0 
november                   : ok=2    changed=1    unreachable=0    failed=0 
```

となります。DisplayNameやPublisherが中途で切れてしまう等の問題があります。
そこで、管理ノード（Windows側）でインストール情報をファイルに保存し、そのファイルを取得する方法を（その２）として実施します。

## Ansibleで各管理ノードからプログラムのインストール情報を取得（その２）

```
work
  +-- installed.yml
  +-- hosts
  +-- files/
  |     +-- get_installed.ps1
  +-- outdir/
```

管理ノードからファイルを取得して格納するディレクトリ outdirを用意しておきます。

先に、PowerShellスクリプトを、結果をCSV形式ファイルに保存するよう修正します。ファイルのパスはコマンドラインオプションで指定することとします。

```:get_installed.ps1
Param($csvpath)
Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall |
 foreach {Get-ItemProperty $_.PSPath} |
 select DisplayName,DisplayVersion,InstallDate,Publisher |
 export-csv $csvpath -notype
```

playbookを修正します。

```yaml:installed.yml
---
- hosts: all
  gather_facts: no
  vars:
    csvpath: C:\Windows\Temp\installed_{{ inventory_hostname }}.csv
  tasks:
  - name: インストールされているJavaプログラムの一覧情報をCSVファイルに生成
    script: files/get-installed.ps1 "{{ csvpath }}"
  - name: 生成したcsvファイルを取得
    fetch: src={{ csvpath }} dest=./outdir/ flat=yes
```

- fetchモジュールは、デフォルトでは制御マシンのdestに指定したパスの下に、管理ノードのホスト名 + srcで指定したパスで取得したファイルを保存します。flat=yesを指定することで、destに指定したパスの下にファイルを保存します。ただし、ファイル名を管理ノード毎にユニークになるようにしないとファイル名が衝突するので注意が必要です。

```:fetchでflat=yesを指定しなかった場合のファイル保存先パスの例
work
  +-- outdir
        +-- lima
              +-- C:
                    +-- Windows
                          +-- Temp
                                +-- installed_lima.csv
```


## Ansibleで各管理ノードからプログラムのインストール情報を取得（その３）

収集するインストール情報を、特定のプログラムについて絞り込みたいとします。

```:get_installed.ps1
Param($name, $csvpath)
Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall |
	     foreach {Get-ItemProperty $_.PsPath} |
	     where {$_.DisplayName -match $name} |
	     select DisplayName,DisplayVersion |
	     export-csv $csvpath -notype
```

スクリプトの引数に、絞り込む名前を指定します。指定には正規表現が使えます。

```yaml:installed.yml（変更箇所）
  - name: インストールされているJavaプログラムの一覧情報をCSVファイルに生成
    script: files/get-installed.ps1 "^Java" "{{ csvpath }}"
```

## 参考

- [TechNetライブラリ ソフトウェア インストールの操作](https://technet.microsoft.com/ja-jp/library/dd347651.aspx)
- [TechNetライブラリ Get-ItemPropertyコマンドレットの使用](https://technet.microsoft.com/ja-jp/library/ee176852.aspx)