diff --git a/lib/ansible/modules/windows/win_optional_feature.ps1 b/lib/ansible/modules/windows/win_optional_feature.ps1 index e4fe8691be7..3505f03d328 100644 --- a/lib/ansible/modules/windows/win_optional_feature.ps1 +++ b/lib/ansible/modules/windows/win_optional_feature.ps1 @@ -7,7 +7,7 @@ $spec = @{ options = @{ - name = @{ type = "str"; required = $true } + name = @{ type = "list"; required = $true } state = @{ type = "str"; default = "present"; choices = @("absent", "present") } source = @{ type = "str" } include_parent = @{ type = "bool"; default = $false } @@ -28,44 +28,56 @@ if (-not (Get-Command -Name Enable-WindowsOptionalFeature -ErrorAction SilentlyC $module.FailJson("This version of Windows does not support the Enable-WindowsOptionalFeature.") } -$feature_state_start = Get-WindowsOptionalFeature -Online -FeatureName $name -if (-not $feature_state_start) { - $module.FailJson("Failed to find feature '$name'") +$changed_features = [System.Collections.Generic.List`1[String]]@() +foreach ($feature_name in $name) { + try { + $feature_state_start = Get-WindowsOptionalFeature -Online -FeatureName $feature_name + } catch [System.Runtime.InteropServices.COMException] { + # Server 2012 raises a COMException and doesn't return $null even with -ErrorAction SilentlyContinue + $feature_state_start = $null + } + if (-not $feature_state_start) { + $module.FailJson("Failed to find feature '$feature_name'") + } + + if ($state -eq "present" -and $feature_state_start.State -notlike "Enabled*") { + # Matches for "Enabled" and "EnabledPending" + $changed_features.Add($feature_name) + } elseif ($state -eq "absent" -and $feature_state_start.State -notlike "Disabled*") { + # Matches for Disabled, DisabledPending, and DisabledWithPayloadRemoved + $changed_features.Add($feature_name) + } } -if ($state -eq "present") { - # Matches for "Enabled" and "EnabledPending" - if ($feature_state_start.State -notlike "Enabled*") { - $install_args = @{ - FeatureName = $name - All = $include_parent - } - if ($source) { - if (-not (Test-Path -LiteralPath $source)) { - $module.FailJson("Path could not be found '$source'") - } - $install_args.Source = $source - } - - if (-not $module.CheckMode) { - $action_result = Enable-WindowsOptionalFeature -Online -NoRestart @install_args - $module.Result.reboot_required = $action_result.RestartNeeded - } - $module.Result.changed = $true +if ($state -eq "present" -and $changed_features.Count -gt 0) { + $install_args = @{ + FeatureName = $changed_features + All = $include_parent } -} else { - # Matches for Disabled, DisabledPending, and DisabledWithPayloadRemoved - if ($feature_state_start.State -notlike "Disabled*") { - $remove_args = @{ - FeatureName = $name - } - if (-not $module.CheckMode) { - $action_result = Disable-WindowsOptionalFeature -Online -NoRestart @remove_args - $module.Result.reboot_required = $action_result.RestartNeeded + if ($source) { + if (-not (Test-Path -LiteralPath $source)) { + $module.FailJson("Path could not be found '$source'") } - $module.Result.changed = $true + $install_args.Source = $source } + + if (-not $module.CheckMode) { + $action_result = Enable-WindowsOptionalFeature -Online -NoRestart @install_args + $module.Result.reboot_required = $action_result.RestartNeeded + } + $module.Result.changed = $true +} elseif ($state -eq "absent" -and $changed_features.Count -gt 0) { + $remove_args = @{ + FeatureName = $changed_features + } + + if (-not $module.CheckMode) { + $action_result = Disable-WindowsOptionalFeature -Online -NoRestart @remove_args + $module.Result.reboot_required = $action_result.RestartNeeded + } + $module.Result.changed = $true } $module.ExitJson() + diff --git a/lib/ansible/modules/windows/win_optional_feature.py b/lib/ansible/modules/windows/win_optional_feature.py index 95918424819..b26188e1d85 100644 --- a/lib/ansible/modules/windows/win_optional_feature.py +++ b/lib/ansible/modules/windows/win_optional_feature.py @@ -22,10 +22,10 @@ description: options: name: description: - - The name of the feature to install. + - The name(s) of the feature to install. - This relates to C(FeatureName) in the Powershell cmdlet. - To list all available features use the PowerShell command C(Get-WindowsOptionalFeature). - type: str + type: list required: yes state: description: @@ -72,6 +72,13 @@ EXAMPLES = r''' - name: Reboot if installing Linux Subsytem as feature requires it win_reboot: when: wsl_status.reboot_required + +- name: Install multiple features in one task + win_optional_feature: + name: + - NetFx3 + - Microsoft-Windows-Subsystem-Linux + state: present ''' RETURN = r''' diff --git a/test/integration/targets/win_optional_feature/tasks/main.yml b/test/integration/targets/win_optional_feature/tasks/main.yml index c0b61c1410a..556590cee4f 100644 --- a/test/integration/targets/win_optional_feature/tasks/main.yml +++ b/test/integration/targets/win_optional_feature/tasks/main.yml @@ -21,5 +21,20 @@ register: run_tests - name: run tests - include_tasks: tests.yml when: run_tests.stdout | trim | bool + block: + - name: ensure we start test with removed features + win_optional_feature: + name: + - SimpleTCP + - TelnetClient + state: absent + - include_tasks: tests.yml + + always: + - name: make sure test features have been removed after test + win_optional_feature: + name: + - SimpleTCP + - TelnetClient + state: absent diff --git a/test/integration/targets/win_optional_feature/tasks/tests.yml b/test/integration/targets/win_optional_feature/tasks/tests.yml index e109c19fb2c..ebec9e8d347 100644 --- a/test/integration/targets/win_optional_feature/tasks/tests.yml +++ b/test/integration/targets/win_optional_feature/tasks/tests.yml @@ -16,6 +16,15 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +- name: fail with invalid feature name + win_optional_feature: + name: + - TelnetClient + - FakeFeature + state: present + register: invalid_name + failed_when: invalid_name.msg != "Failed to find feature 'FakeFeature'" + - name: run with check_mode win_optional_feature: name: TelnetClient @@ -36,7 +45,7 @@ include_parent: true register: real_feature_check -- name: assert feature installed +- name: assert feature installed assert: that: - real_feature_check.changed @@ -53,6 +62,34 @@ that: - not real_feature_check.changed +- name: install feature with list + win_optional_feature: + name: + - SimpleTCP + - TelnetClient + state: present + include_parent: true + register: install_list + +- name: assert install feature with list + assert: + that: + - install_list is changed + +- name: install feature with list (idempotent) + win_optional_feature: + name: + - SimpleTCP + - TelnetClient + state: present + include_parent: true + register: install_list_again + +- name: assert install feature with list (idempotent) + assert: + that: + - not install_list_again is changed + - name: removal run with check_mode win_optional_feature: name: TelnetClient @@ -76,7 +113,7 @@ that: - real_feature_check.changed -- name: test idempotence for removal +- name: test idempotence for removal win_optional_feature: name: TelnetClient state: absent @@ -86,3 +123,29 @@ assert: that: - not real_feature_check.changed + +- name: remove feature with list + win_optional_feature: + name: + - SimpleTCP + - TelnetClient + state: absent + register: remove_feature_list + +- name: assert remove feature with list + assert: + that: + - remove_feature_list is changed + +- name: remove feature with list (idempotent) + win_optional_feature: + name: + - SimpleTCP + - TelnetClient + state: absent + register: remove_feature_list_again + +- name: assert remove feature with list (idempotent) + assert: + that: + - not remove_feature_list_again is changed