From f27078df520007824969714ce5b442c536128044 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Fri, 1 Feb 2019 06:32:12 +1000 Subject: [PATCH] win_power_plan: fix for Windows 10 and Server 2008 compatibility (#51471) --- .../fragments/win_power_plan-windows10.yaml | 2 + .../modules/windows/win_power_plan.ps1 | 225 ++++++++++++++---- lib/ansible/modules/windows/win_power_plan.py | 2 - .../targets/win_power_plan/tasks/main.yml | 48 ++-- test/sanity/pslint/ignore.txt | 1 - 5 files changed, 197 insertions(+), 81 deletions(-) create mode 100644 changelogs/fragments/win_power_plan-windows10.yaml diff --git a/changelogs/fragments/win_power_plan-windows10.yaml b/changelogs/fragments/win_power_plan-windows10.yaml new file mode 100644 index 00000000000..4bfabf56a19 --- /dev/null +++ b/changelogs/fragments/win_power_plan-windows10.yaml @@ -0,0 +1,2 @@ +bugfixes: +- win_power_plan - Fix issue where win_power_plan failed on newer Windows 10 builds - https://github.com/ansible/ansible/issues/43827 diff --git a/lib/ansible/modules/windows/win_power_plan.ps1 b/lib/ansible/modules/windows/win_power_plan.ps1 index ad565a8fef4..645eefece95 100644 --- a/lib/ansible/modules/windows/win_power_plan.ps1 +++ b/lib/ansible/modules/windows/win_power_plan.ps1 @@ -7,73 +7,204 @@ $params = Parse-Args -arguments $args -supports_check_mode $true $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP # these are your module parameters $name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true -Function Get-PowerPlans { -Param ($PlanName) - If (-not $PlanName) { - Get-CimInstance -Name root\cimv2\power -Class Win32_PowerPlan | - Select-Object -Property ElementName, IsActive | - ForEach-Object -Begin { $ht = @{} } -Process { $ht."$($_.ElementName)" = $_.IsActive } -End { $ht } - } - Else { - Get-CimInstance -Name root\cimv2\power -Class Win32_PowerPlan -Filter "ElementName = '$PlanName'" - } +$result = @{ + changed = $false + power_plan_name = $name + power_plan_enabled = $null + all_available_plans = $null } -#fail if older than 2008r2...need to do it here before Get-PowerPlans function runs further down +$pinvoke_functions = @" +using System; +using System.Runtime.InteropServices; -If ([System.Environment]::OSVersion.Version -lt '6.1') +namespace Ansible.WinPowerPlan { - $result = @{ - changed = $false - power_plan_name = $name - power_plan_enabled = $null - all_available_plans = $null + public enum AccessFlags : uint + { + AccessScheme = 16, + AccessSubgroup = 17, + AccessIndividualSetting = 18 + } + + public class NativeMethods + { + [DllImport("Kernel32.dll", SetLastError = true)] + public static extern IntPtr LocalFree( + IntPtr hMen); + + [DllImport("PowrProf.dll")] + public static extern UInt32 PowerEnumerate( + IntPtr RootPowerKey, + IntPtr SchemeGuid, + IntPtr SubGroupOfPowerSettingsGuid, + AccessFlags AccessFlags, + UInt32 Index, + IntPtr Buffer, + ref UInt32 BufferSize); + + [DllImport("PowrProf.dll")] + public static extern UInt32 PowerGetActiveScheme( + IntPtr UserRootPowerKey, + out IntPtr ActivePolicyGuid); + + [DllImport("PowrProf.dll")] + public static extern UInt32 PowerReadFriendlyName( + IntPtr RootPowerKey, + Guid SchemeGuid, + IntPtr SubGroupOfPowerSettingsGuid, + IntPtr PowerSettingGuid, + IntPtr Buffer, + ref UInt32 BufferSize); + + [DllImport("PowrProf.dll")] + public static extern UInt32 PowerSetActiveScheme( + IntPtr UserRootPowerKey, + Guid SchemeGuid); } - Fail-Json $result "The win_power_plan Ansible module is only available on Server 2008r2 (6.1) and newer" } +"@ +$original_tmp = $env:TMP +$env:TMP = $_remote_tmp +Add-Type -TypeDefinition $pinvoke_functions +$env:TMP = $original_tmp -$result = @{ - changed = $false - power_plan_name = $name - power_plan_enabled = (Get-PowerPlans $name).isactive - all_available_plans = Get-PowerPlans +Function Get-LastWin32ErrorMessage { + param([Int]$ErrorCode) + $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode + $error_msg = "{0} - (Win32 Error Code {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode + return $error_msg } -$all_available_plans = Get-PowerPlans +Function Get-PlanName { + param([Guid]$Plan) -#Terminate if plan is not found on the system -If (! ($all_available_plans.ContainsKey($name)) ) -{ - Fail-Json $result "Defined power_plan: ($name) is not available" + $buffer_size = 0 + $buffer = [IntPtr]::Zero + [Ansible.WinPowerPlan.NativeMethods]::PowerReadFriendlyName([IntPtr]::Zero, $Plan, [IntPtr]::Zero, [IntPtr]::Zero, + $buffer, [ref]$buffer_size) > $null + + $buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buffer_size) + try { + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerReadFriendlyName([IntPtr]::Zero, $Plan, [IntPtr]::Zero, + [IntPtr]::Zero, $buffer, [ref]$buffer_size) + + if ($res -ne 0) { + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + Fail-Json -obj $result -message "Failed to get name for power scheme $Plan - $err_msg" + } + + return [System.Runtime.InteropServices.Marshal]::PtrToStringUni($buffer) + } finally { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer) + } } -#If true, means plan is already active and we exit here with changed: false -#If false, means plan is not active and we move down to enable -#Since the results here are the same whether check mode or not, no specific handling is required -#for check mode. -If ( $all_available_plans.item($name) ) -{ - Exit-Json $result +Function Get-PowerPlans { + $plans = @{} + + $i = 0 + while ($true) { + $buffer_size = 0 + $buffer = [IntPtr]::Zero + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerEnumerate([IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, + [Ansible.WinPowerPlan.AccessFlags]::AccessScheme, $i, $buffer, [ref]$buffer_size) + + if ($res -eq 259) { + # 259 == ERROR_NO_MORE_ITEMS, there are no more power plans to enumerate + break + } elseif ($res -notin @(0, 234)) { + # 0 == ERROR_SUCCESS and 234 == ERROR_MORE_DATA + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + Fail-Json -obj $result -message "Failed to get buffer size on local power schemes at index $i - $err_msg" + } + + $buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buffer_size) + try { + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerEnumerate([IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, + [Ansible.WinPowerPlan.AccessFlags]::AccessScheme, $i, $buffer, [ref]$buffer_size) + + if ($res -eq 259) { + # Server 2008 does not return 259 in the first call above so we do an additional check here + break + } elseif ($res -notin @(0, 234, 259)) { + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + Fail-Json -obj $result -message "Failed to enumerate local power schemes at index $i - $err_msg" + } + $scheme_guid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [Type][Guid]) + } finally { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer) + } + $scheme_name = Get-PlanName -Plan $scheme_guid + $plans.$scheme_name = $scheme_guid + + $i += 1 + } + + return $plans } -Else -{ - Try { - $Null = Invoke-CimMethod -InputObject (Get-PowerPlans $name) -MethodName Activate -ErrorAction Stop -WhatIf:$check_mode + +Function Get-ActivePowerPlan { + $buffer = [IntPtr]::Zero + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerGetActiveScheme([IntPtr]::Zero, [ref]$buffer) + if ($res -ne 0) { + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + Fail-Json -obj $result -message "Failed to get the active power plan - $err_msg" } - Catch { - $result.power_plan_enabled = (Get-PowerPlans $name).IsActive - $result.all_available_plans = Get-PowerPlans - Fail-Json $result "Failed to set the new plan: $($_.Exception.Message)" + + try { + $active_guid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [Type][Guid]) + } finally { + [Ansible.WinPowerPlan.NativeMethods]::LocalFree($buffer) > $null } - #set success parameters and exit + return $active_guid +} + +Function Set-ActivePowerPlan { + [CmdletBinding(SupportsShouldProcess=$true)] + param([Guid]$Plan) + + $res = 0 + if ($PSCmdlet.ShouldProcess($Plan, "Set Power Plan")) { + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerSetActiveScheme([IntPtr]::Zero, $plan_guid) + } + + if ($res -ne 0) { + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + Fail-Json -obj $result -message "Failed to set the active power plan to $name - $err_msg" + } +} + +# Get all local power plans and the current active plan +$plans = Get-PowerPlans +$active_plan = Get-ActivePowerPlan +$result.all_available_plans = @{} +foreach ($plan_info in $plans.GetEnumerator()) { + $result.all_available_plans.($plan_info.Key) = $plan_info.Value -eq $active_plan +} + +if ($name -notin $plans.Keys) { + Fail-Json -obj $result -message "Defined power_plan: ($name) is not available" +} +$plan_guid = $plans.$name +$is_active = $active_plan -eq $plans.$name +$result.power_plan_enabled = $is_active + +if (-not $is_active) { + Set-ActivePowerPlan -Plan $plan_guid -WhatIf:$check_mode $result.changed = $true - $result.power_plan_enabled = (Get-PowerPlans $name).IsActive - $result.all_available_plans = Get-PowerPlans - Exit-Json $result + $result.power_plan_enabled = $true + foreach ($plan_info in $plans.GetEnumerator()) { + $is_active = $plan_info.Value -eq $plan_guid + $result.all_available_plans.($plan_info.Key) = $is_active + } } +Exit-Json -obj $result + diff --git a/lib/ansible/modules/windows/win_power_plan.py b/lib/ansible/modules/windows/win_power_plan.py index 054379c1dcf..6f358993cc0 100644 --- a/lib/ansible/modules/windows/win_power_plan.py +++ b/lib/ansible/modules/windows/win_power_plan.py @@ -17,8 +17,6 @@ description: - Windows defaults to C(balanced) which will cause CPU throttling. In some cases it can be preferable to change the mode to C(high performance) to increase CPU performance. version_added: "2.4" -requirements: - - Windows Server 2008R2 (6.1)/Windows 7 or higher options: name: description: diff --git a/test/integration/targets/win_power_plan/tasks/main.yml b/test/integration/targets/win_power_plan/tasks/main.yml index 0b6f0c315ed..000d8b1a662 100644 --- a/test/integration/targets/win_power_plan/tasks/main.yml +++ b/test/integration/targets/win_power_plan/tasks/main.yml @@ -1,26 +1,16 @@ -- name: register os version (seems integration tests don't gather this fact) - raw: powershell.exe "gwmi Win32_OperatingSystem | select -expand version" - register: os_version - changed_when: False -# ^^ seems "raw" is the only module that works on 2008 non-r2. win_command and win_shell both failed +# I dislike this but 2008 doesn't support the Win32_PowerPlan WMI provider +- name: get current plan details + win_shell: | + $plan_info = powercfg.exe /list + ($plan_info | Select-String -Pattern '\(([\w\s]*)\) \*$').Matches.Groups[1].Value + ($plan_info | Select-String -Pattern '\(([\w\s]*)\)$').Matches.Groups[1].Value + register: plan_info -- name: check if module fails gracefully when older than 2008r2 - win_power_plan: - name: "high performance" - when: os_version.stdout_lines[0] is version('6.1','lt') - check_mode: yes - register: old_os_check - failed_when: old_os_check.msg != 'The win_power_plan Ansible module is only available on Server 2008r2 (6.1) and newer' +- set_fact: + original_plan: '{{ plan_info.stdout_lines[0] }}' + name: '{{ plan_info.stdout_lines[1] }}' - block: - - name: register inactive power plan to test with - win_shell: (Get-CimInstance -Name root\cimv2\power -Class win32_PowerPlan | ? {! $_.IsActive}).ElementName[0] - register: disabled_power_plan - changed_when: False - - - set_fact: - name: "{{ disabled_power_plan.stdout_lines[0] }}" - #Test that plan detects change is needed, but doesn't actually apply change - name: set power plan (check mode) win_power_plan: @@ -28,20 +18,17 @@ register: set_plan_check check_mode: yes -# - debug: -# var: set_plan_check - - name: get result of set power plan (check mode) - win_shell: (Get-CimInstance -Name root\cimv2\power -Class win32_PowerPlan -Filter "ElementName = '{{ name }}'").IsActive + win_shell: (powercfg.exe /list | Select-String -Pattern '\({{ name }}\)').Line register: set_plan_check_result changed_when: False - + # verify that the powershell check is showing the plan as still inactive on the system - name: assert setting plan (check mode) assert: that: - set_plan_check is changed - - set_plan_check_result.stdout == 'False\r\n' + - not set_plan_check_result.stdout_lines[0].endswith('*') #Test that setting plan and that change is applied - name: set power plan @@ -50,7 +37,7 @@ register: set_plan - name: get result of set power plan - win_shell: (Get-CimInstance -Name root\cimv2\power -Class win32_PowerPlan -Filter "ElementName = '{{ name }}'").IsActive + win_shell: (powercfg.exe /list | Select-String -Pattern '\({{ name }}\)').Line register: set_plan_result changed_when: False @@ -58,7 +45,7 @@ assert: that: - set_plan is changed - - set_plan_result.stdout == 'True\r\n' + - set_plan_result.stdout_lines[0].endswith('*') #Test that plan doesn't apply change if it is already set - name: set power plan (idempotent) @@ -71,8 +58,7 @@ that: - set_plan_idempotent is not changed - when: os_version.stdout_lines[0] is version('6.1','ge') always: - - name: always change back plan to high performance when done testing + - name: always change back plan to the original when done testing win_power_plan: - name: high performance + name: '{{ original_plan }}' diff --git a/test/sanity/pslint/ignore.txt b/test/sanity/pslint/ignore.txt index e123acfcb14..2f9327bcb01 100644 --- a/test/sanity/pslint/ignore.txt +++ b/test/sanity/pslint/ignore.txt @@ -54,7 +54,6 @@ lib/ansible/modules/windows/win_pagefile.ps1 PSAvoidUsingPositionalParameters lib/ansible/modules/windows/win_pagefile.ps1 PSAvoidUsingWMICmdlet lib/ansible/modules/windows/win_pagefile.ps1 PSUseDeclaredVarsMoreThanAssignments lib/ansible/modules/windows/win_pagefile.ps1 PSUseSupportsShouldProcess -lib/ansible/modules/windows/win_power_plan.ps1 PSUseDeclaredVarsMoreThanAssignments lib/ansible/modules/windows/win_psmodule.ps1 PSAvoidUsingCmdletAliases lib/ansible/modules/windows/win_rabbitmq_plugin.ps1 PSAvoidUsingCmdletAliases lib/ansible/modules/windows/win_rabbitmq_plugin.ps1 PSAvoidUsingInvokeExpression