コントリビュートcommunity.windows～モジュール作成編

本記事は [Ansible Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ansible_01) の 12/23 の記事です。
Qiitaの投稿は初投稿ですが、大佐のAnsible本欲しさに投稿しました。

#はじめに
Windowsの自作モジュールは作ったけど、libraryで管理し続けるのが辛い・・・
Ansible環境が変わるたびにlibraryにモジュール配置疲れしている人は多いのではないでしょうか？
そんな自作モジュール、コントリビュートしてみませんか？
今回は作成したモジュールを例にWindowsモジュール作成のお作法やPRからマージまでの経緯、Windowsならではのポイントなどについてまとめていきたいと思います。

# 作成したモジュール
みんなが大好きなこれを無効化したりしなかったりするやつです。
![001.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189641/2b0637c5-3b7a-688b-f1b5-89d039d41f7c.png)
その名も[win_net_adapter_feature](https://github.com/ansible-collections/community.windows/blob/main/docs/community.windows.win_net_adapter_feature_module.rst)。
community.windows v1.2.0でマージされました。


# 背景
世の中のWindows設定手順書の8割以上がアダプターのIPv6を無効にしているのではないだろうか。。
元々はwin_shellモジュールでGet-xxxxで状態確認して、Set-xxxxで設定を行う
冪等性担保のためにchanged_when: falseやcheck_mode: falseなどを駆使した長いRoleを使ってました。
そのRoleを何度もお世話になる局面が有り、モジュール化したら需要があるのでは？と思いモジュール作成に至りました。
（何故これが今まで放置されていたのかが疑問）

## Windowsモジュール開発のお作法
Ansible公式ドキュメントに[Windowsモジュール開発のウォークスルー](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general_windows.html#id4)が載ってます。基本的にこちらに従い開発を進めていきます。

## ps1ファイルとpyファイル
Windows以外のモジュールに比べ、Windowsモジュールは実際の処理部分を記載するps1ファイルとは別に、exampleやモジュールの説明などのドキュメント文を記載するpyファイルを作成する必要があります。

## Windowsモジュールのユーティリティ
Windowsモジュールでは、PowershellのユーティリティとC#のユーティリティを利用することができます。
以下はWindowsモジュールで多く使用されているユーティリティです。


|言語  |ユーティリティ名  |
|---|---|
|Powershell  |Ansible.ModuleUtils.Legacy.psm1  |
|C#  |Ansible.Basic.cs  |

ウォークスルーの説明を見ていくと、Ansible.Basic.csはAnsible2.8から出てきた後発のmodule_utils。
どちらを使用しても同じことができるが、C#を使用したほうが簡単に実装できる場合があるみたい。
実際に作成したwin_net_adapter_featureモジュールも、当初Ansible.ModuleUtils.Legacyを使用していましたが、PR後にレビュワーの[Jborean93](https://github.com/jborean93)さんより、「Ansible.ModuleUtils.LegacyよりAnsible.Basic使ったほうがいいよ」とご指摘があり大幅に修正。

以下にBefore/Afterを貼りますが、C#のmodule_utilsを使用した方が40行も減りました。
おそらくモジュールのパラメータ不足時のエラーハンドリング（Fail-Json）をmodule_utils側で吸収しているのかと思われます。

```Ansible.ModuleUtils.Legacy.psm1を使った場合のparam定義部分
# WANT_JSON
# POWERSHELL_COMMON
# temporarily disable strictmode, for this module only
#Requires -Module Ansible.ModuleUtils.Legacy

$ErrorActionPreference = "Stop"

$Interface_name = [string[]]
$componentID_name = [string[]]
$params = Parse-Args $args -supports_check_mode $true
$Interface_name = Get-AnsibleParam -obj $params -name "Interface"
$state = Get-AnsibleParam -obj $params -name state -type "str"
$componentID_name = Get-AnsibleParam -obj $params -name "componentID"
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false

$result = @{
    changed = $false
}

If(!$Interface_name) { Fail-Json -message "Absolute Interface is not specified for Interface" }
If(!$state) { Fail-Json -message "Absolute state is not specified for state" }
If(!$componentID_name) { Fail-Json -message "Absolute componentID is not specified for componentID" }

If ($state -eq 'enable'){
    $state = $true
}ElseIf ($state -eq 'disable') {
    $state = $false
}Else {
    Fail-Json -message "Specify the state as 'enable' or 'disable'"
}

If($Interface_name -is [string]) {
    If($Interface_name.Length -gt 0) {
        $Interfaces = @($Interface_name)
    }
    Else {
        $Interfaces = @()
    }
}
Else {
        $Interfaces = $Interface_name
}
If($componentID_name -is [string]) {
    If($componentID_name.Length -gt 0) {
        $componentIDs = @($componentID_name)
    }
    Else {
        $componentIDs = @()
    }
}
Else {
    $componentIDs = $componentID_name
}
```

```Ansible.Basic.AnsibleModule.csを使ったparam定義部分
$spec = @{
    options = @{
        interface = @{ type = 'list'; elements = 'str'; required = $true }
        state = @{ type = 'str'; choices = 'disabled', 'enabled'; default = 'enabled' }
        component_id = @{ type = 'list'; elements = 'str'; required = $true }
    }
    supports_check_mode = $true
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
$interface = $module.Params.interface
$state = $module.Params.state
$component_id = $module.Params.component_id
$check_mode = $module.CheckMode
```
## 開発したモジュールのテスト
Windowsモジュールではユニットテストは行う必要はありませんが、integrationテストを行う必要があります。Forkしたcommunity.windowsリポジトリの./test/integration/targets/以下にテスト用Playbookを作成します。
テストケースとしては以下のようなものを記載していきます。

### モジュールの必須オプションが存在しない場合
今回のmoduleですと、interface名（Ethernet等）が必須のオプションなのですが、interfaceオプションが存在しない場合にfailedで終わるようなテストケース

```
---
- name: fail when interface isn't set
  win_net_adapter_feature:
    state: enabled
    component_id: ms_tcpip6
  register: failed_task
  failed_when: not failed_task is failed
```
### パラメータが実機側に存在しない場合
Ethernet10のような実機に存在しないinterface名を指定した場合にfailedで終わるテストケース

```
- name: fail when interface doesn't exist
  win_net_adapter_feature:
    interface: Ethernet10
    state: enabled
    component_id: ms_tcpip6
  register: failed_task
  failed_when: not failed_task is failed

```
### check modeで実行して、設定が変わったりしてないか
check_modeにもサポートするよう心がけましょう。
check_modeで実行後、win_shellモジュール等でPowershellコマンドレットを使って状態確認し、設定が変わっていないことを確認するテストケースを作ります。

```
- name: disable multiple interfaces of multiple interfaces - check mode
  win_net_adapter_feature:
    interface:
    - '{{ network_adapter_name }}'
    - '{{ network_adapter_name2 }}'
    state: disabled
    component_id:
    - ms_tcpip6
    - ms_server
  check_mode: yes
  register: mutliple_multiple_disable_check

- name: get the status of disable multiple interfaces of multiple interfaces - check mode
  ansible.windows.win_shell: |
    $info = Get-NetAdapterBinding -Name '{{ network_adapter_name }}', '{{ network_adapter_name2 }}'
    @{
        '{{ network_adapter_name }}' = @{
            ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled
            ms_server = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_server'}).Enabled
        }
        '{{ network_adapter_name2 }}' = @{
            ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled
            ms_server = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_server'}).Enabled
        }
    } | ConvertTo-Json
  register: mutliple_multiple_disable_check_actual

- name: assert disable multiple interfaces of multiple interfaces - check mode
  assert:
    that:
      - mutliple_multiple_disable_check is changed
      - (mutliple_multiple_disable_check_actual.stdout | from_json)[network_adapter_name]['ms_tcpip6'] == True
      - (mutliple_multiple_disable_check_actual.stdout | from_json)[network_adapter_name]['ms_server'] == True
      - (mutliple_multiple_disable_check_actual.stdout | from_json)[network_adapter_name2]['ms_tcpip6'] == True
      - (mutliple_multiple_disable_check_actual.stdout | from_json)[network_adapter_name2]['ms_server'] == True
```


## 最後に
本モジュール作成にあたり、レビュアーのJborean93さんには感謝につきます。
Jborean93さんはWindowsモジュール系のメインコントリビューターであり、ご自身でもたくさんのIssueやモジュールを作成されてます。
今回のPRに関し、様々なご支援をいただきました。
OSSにコントリビュートと聞くとかなりハードルが高いと思われるかもしれませんが、Ansibleは成熟したコミュニティであり、優しいレビュワーの皆様がリードしてくれますので、皆様がお持ちのモジュールをPRしてはどうでしょうか。

# 余談
当初はOCIとAnsibleネタの投稿を考えていましたが、二日前に@ussvgrさんのAnsible Advent Calendar [12/17の投稿](https://qiita.com/ussvgr/items/128e3c0b2670bf362fdb)とダダ被りになってしまうことに気づき慌てて方向転換。
とほほ・・余裕持って2,3個ネタは持っておきたい・・・