search
LoginSignup
1

More than 1 year has passed since last update.

posted at

updated at

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

本記事は Ansible Advent Calendar 2020 の 12/23 の記事です。
Qiitaの投稿は初投稿ですが、大佐のAnsible本欲しさに投稿しました。

はじめに

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

作成したモジュール

みんなが大好きなこれを無効化したりしなかったりするやつです。
001.PNG
その名もwin_net_adapter_feature
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モジュール開発のウォークスルーが載ってます。基本的にこちらに従い開発を進めていきます。

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さんより、「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の投稿とダダ被りになってしまうことに気づき慌てて方向転換。
とほほ・・余裕持って2,3個ネタは持っておきたい・・・

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1