Browse code

win_power_plan: fix for Windows 10 and Server 2008 compatibility (#51471)

(cherry picked from commit f27078df520007824969714ce5b442c536128044)

Jordan Borean authored on 2019/02/01 05:32:12
Showing 5 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+bugfixes:
1
+- win_power_plan - Fix issue where win_power_plan failed on newer Windows 10 builds - https://github.com/ansible/ansible/issues/43827
... ...
@@ -7,73 +7,204 @@
7 7
 
8 8
 $params = Parse-Args -arguments $args -supports_check_mode $true
9 9
 $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
10
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
10 11
 
11 12
 # these are your module parameters
12 13
 $name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
13 14
 
14
-Function Get-PowerPlans {
15
-Param ($PlanName)
16
-    If (-not $PlanName) {
17
-        Get-CimInstance -Name root\cimv2\power -Class Win32_PowerPlan |
18
-        Select-Object -Property ElementName, IsActive |
19
-        ForEach-Object -Begin { $ht = @{} } -Process { $ht."$($_.ElementName)" = $_.IsActive } -End { $ht }
20
-    }
21
-    Else {
22
-        Get-CimInstance -Name root\cimv2\power -Class Win32_PowerPlan -Filter "ElementName = '$PlanName'"
23
-    }
15
+$result = @{
16
+    changed = $false
17
+    power_plan_name = $name
18
+    power_plan_enabled = $null
19
+    all_available_plans = $null
24 20
 }
25 21
 
26
-#fail if older than 2008r2...need to do it here before Get-PowerPlans function runs further down
22
+$pinvoke_functions = @"
23
+using System;
24
+using System.Runtime.InteropServices;
27 25
 
28
-If ([System.Environment]::OSVersion.Version -lt '6.1')
26
+namespace Ansible.WinPowerPlan
29 27
 {
30
-    $result = @{
31
-        changed = $false
32
-        power_plan_name = $name
33
-        power_plan_enabled = $null
34
-        all_available_plans = $null
28
+    public enum AccessFlags : uint
29
+    {
30
+        AccessScheme = 16,
31
+        AccessSubgroup = 17,
32
+        AccessIndividualSetting = 18
33
+    }
34
+
35
+    public class NativeMethods
36
+    {
37
+        [DllImport("Kernel32.dll", SetLastError = true)]
38
+        public static extern IntPtr LocalFree(
39
+            IntPtr hMen);
40
+
41
+        [DllImport("PowrProf.dll")]
42
+        public static extern UInt32 PowerEnumerate(
43
+            IntPtr RootPowerKey,
44
+            IntPtr SchemeGuid,
45
+            IntPtr SubGroupOfPowerSettingsGuid,
46
+            AccessFlags AccessFlags,
47
+            UInt32 Index,
48
+            IntPtr Buffer,
49
+            ref UInt32 BufferSize);
50
+
51
+        [DllImport("PowrProf.dll")]
52
+        public static extern UInt32 PowerGetActiveScheme(
53
+            IntPtr UserRootPowerKey,
54
+            out IntPtr ActivePolicyGuid);
55
+
56
+        [DllImport("PowrProf.dll")]
57
+        public static extern UInt32 PowerReadFriendlyName(
58
+            IntPtr RootPowerKey,
59
+            Guid SchemeGuid,
60
+            IntPtr SubGroupOfPowerSettingsGuid,
61
+            IntPtr PowerSettingGuid,
62
+            IntPtr Buffer,
63
+            ref UInt32 BufferSize);
64
+
65
+        [DllImport("PowrProf.dll")]
66
+        public static extern UInt32 PowerSetActiveScheme(
67
+            IntPtr UserRootPowerKey,
68
+            Guid SchemeGuid);
35 69
     }
36
-    Fail-Json $result "The win_power_plan Ansible module is only available on Server 2008r2 (6.1) and newer"
37 70
 }
71
+"@
72
+$original_tmp = $env:TMP
73
+$env:TMP = $_remote_tmp
74
+Add-Type -TypeDefinition $pinvoke_functions
75
+$env:TMP = $original_tmp
38 76
 
39
-$result = @{
40
-    changed = $false
41
-    power_plan_name = $name
42
-    power_plan_enabled = (Get-PowerPlans $name).isactive
43
-    all_available_plans = Get-PowerPlans
77
+Function Get-LastWin32ErrorMessage {
78
+    param([Int]$ErrorCode)
79
+    $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
80
+    $error_msg = "{0} - (Win32 Error Code {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode
81
+    return $error_msg
44 82
 }
45 83
 
46
-$all_available_plans = Get-PowerPlans
84
+Function Get-PlanName {
85
+    param([Guid]$Plan)
47 86
 
48
-#Terminate if plan is not found on the system
49
-If (! ($all_available_plans.ContainsKey($name)) )
50
-{
51
-    Fail-Json $result "Defined power_plan: ($name) is not available"
87
+    $buffer_size = 0
88
+    $buffer = [IntPtr]::Zero
89
+    [Ansible.WinPowerPlan.NativeMethods]::PowerReadFriendlyName([IntPtr]::Zero, $Plan, [IntPtr]::Zero, [IntPtr]::Zero,
90
+        $buffer, [ref]$buffer_size) > $null
91
+
92
+    $buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buffer_size)
93
+    try {
94
+        $res = [Ansible.WinPowerPlan.NativeMethods]::PowerReadFriendlyName([IntPtr]::Zero, $Plan, [IntPtr]::Zero,
95
+            [IntPtr]::Zero, $buffer, [ref]$buffer_size)
96
+
97
+        if ($res -ne 0) {
98
+            $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
99
+            Fail-Json -obj $result -message "Failed to get name for power scheme $Plan - $err_msg"
100
+        }
101
+
102
+        return [System.Runtime.InteropServices.Marshal]::PtrToStringUni($buffer)
103
+    } finally {
104
+        [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer)
105
+    }
52 106
 }
53 107
 
54
-#If true, means plan is already active and we exit here with changed: false
55
-#If false, means plan is not active and we move down to enable
56
-#Since the results here are the same whether check mode or not, no specific handling is required
57
-#for check mode.
58
-If ( $all_available_plans.item($name) )
59
-{
60
-    Exit-Json $result
108
+Function Get-PowerPlans {
109
+    $plans = @{}
110
+
111
+    $i = 0
112
+    while ($true) {
113
+        $buffer_size = 0
114
+        $buffer = [IntPtr]::Zero
115
+        $res = [Ansible.WinPowerPlan.NativeMethods]::PowerEnumerate([IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero,
116
+            [Ansible.WinPowerPlan.AccessFlags]::AccessScheme, $i, $buffer, [ref]$buffer_size)
117
+
118
+        if ($res -eq 259) {
119
+            # 259 == ERROR_NO_MORE_ITEMS, there are no more power plans to enumerate
120
+            break
121
+        } elseif ($res -notin @(0, 234)) {
122
+            # 0 == ERROR_SUCCESS and 234 == ERROR_MORE_DATA
123
+            $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
124
+            Fail-Json -obj $result -message "Failed to get buffer size on local power schemes at index $i - $err_msg"
125
+        }
126
+
127
+        $buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buffer_size)
128
+        try {
129
+            $res = [Ansible.WinPowerPlan.NativeMethods]::PowerEnumerate([IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero,
130
+                [Ansible.WinPowerPlan.AccessFlags]::AccessScheme, $i, $buffer, [ref]$buffer_size)
131
+
132
+            if ($res -eq 259) {
133
+                # Server 2008 does not return 259 in the first call above so we do an additional check here
134
+                break
135
+            } elseif ($res -notin @(0, 234, 259)) {
136
+                $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
137
+                Fail-Json -obj $result -message "Failed to enumerate local power schemes at index $i - $err_msg"
138
+            }
139
+            $scheme_guid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [Type][Guid])
140
+        } finally {
141
+            [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer)
142
+        }
143
+        $scheme_name = Get-PlanName -Plan $scheme_guid
144
+        $plans.$scheme_name = $scheme_guid
145
+
146
+        $i += 1
147
+    }
148
+
149
+    return $plans
61 150
 }
62
-Else
63
-{
64
-    Try {
65
-        $Null = Invoke-CimMethod -InputObject (Get-PowerPlans $name) -MethodName Activate -ErrorAction Stop -WhatIf:$check_mode
151
+
152
+Function Get-ActivePowerPlan {
153
+    $buffer = [IntPtr]::Zero
154
+    $res = [Ansible.WinPowerPlan.NativeMethods]::PowerGetActiveScheme([IntPtr]::Zero, [ref]$buffer)
155
+    if ($res -ne 0) {
156
+        $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
157
+        Fail-Json -obj $result -message "Failed to get the active power plan - $err_msg"
66 158
     }
67
-    Catch {
68
-        $result.power_plan_enabled = (Get-PowerPlans $name).IsActive
69
-        $result.all_available_plans = Get-PowerPlans
70
-        Fail-Json $result "Failed to set the new plan: $($_.Exception.Message)"
159
+
160
+    try {
161
+        $active_guid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [Type][Guid])
162
+    } finally {
163
+        [Ansible.WinPowerPlan.NativeMethods]::LocalFree($buffer) > $null
71 164
     }
72 165
 
73
-    #set success parameters and exit
166
+    return $active_guid
167
+}
168
+
169
+Function Set-ActivePowerPlan {
170
+    [CmdletBinding(SupportsShouldProcess=$true)]
171
+    param([Guid]$Plan)
172
+
173
+    $res = 0
174
+    if ($PSCmdlet.ShouldProcess($Plan, "Set Power Plan")) {
175
+        $res = [Ansible.WinPowerPlan.NativeMethods]::PowerSetActiveScheme([IntPtr]::Zero, $plan_guid)
176
+    }
177
+
178
+    if ($res -ne 0) {
179
+        $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
180
+        Fail-Json -obj $result -message "Failed to set the active power plan to $name - $err_msg"
181
+    }
182
+}
183
+
184
+# Get all local power plans and the current active plan
185
+$plans = Get-PowerPlans
186
+$active_plan = Get-ActivePowerPlan
187
+$result.all_available_plans = @{}
188
+foreach ($plan_info in $plans.GetEnumerator()) {
189
+    $result.all_available_plans.($plan_info.Key) = $plan_info.Value -eq $active_plan
190
+}
191
+
192
+if ($name -notin $plans.Keys) {
193
+    Fail-Json -obj $result -message "Defined power_plan: ($name) is not available"
194
+}
195
+$plan_guid = $plans.$name
196
+$is_active = $active_plan -eq $plans.$name
197
+$result.power_plan_enabled = $is_active
198
+
199
+if (-not $is_active) {
200
+    Set-ActivePowerPlan -Plan $plan_guid -WhatIf:$check_mode
74 201
     $result.changed = $true
75
-    $result.power_plan_enabled = (Get-PowerPlans $name).IsActive
76
-    $result.all_available_plans = Get-PowerPlans
77
-    Exit-Json $result
202
+    $result.power_plan_enabled = $true
203
+    foreach ($plan_info in $plans.GetEnumerator()) {
204
+        $is_active = $plan_info.Value -eq $plan_guid
205
+        $result.all_available_plans.($plan_info.Key) = $is_active
206
+    }
78 207
 }
79 208
 
209
+Exit-Json -obj $result
210
+
... ...
@@ -25,8 +25,6 @@ options:
25 25
       - String value that indicates the desired power plan. The power plan must already be
26 26
         present on the system. Commonly there will be options for C(balanced) and C(high performance).
27 27
     required: yes
28
-requirements:
29
-  - Windows Server 2008R2 (6.1)/Windows 7 or higher
30 28
 '''
31 29
 
32 30
 EXAMPLES = '''
... ...
@@ -1,26 +1,16 @@
1
-- name: register os version (seems integration tests don't gather this fact)
2
-  raw: powershell.exe "gwmi Win32_OperatingSystem | select -expand version"
3
-  register: os_version
4
-  changed_when: False
5
-# ^^ seems "raw" is the only module that works on 2008 non-r2. win_command and win_shell both failed
1
+# I dislike this but 2008 doesn't support the Win32_PowerPlan WMI provider
2
+- name: get current plan details
3
+  win_shell: |
4
+      $plan_info = powercfg.exe /list
5
+      ($plan_info | Select-String -Pattern '\(([\w\s]*)\) \*$').Matches.Groups[1].Value
6
+      ($plan_info | Select-String -Pattern '\(([\w\s]*)\)$').Matches.Groups[1].Value
7
+  register: plan_info
6 8
 
7
-- name: check if module fails gracefully when older than 2008r2
8
-  win_power_plan:
9
-    name: "high performance"
10
-  when: os_version.stdout_lines[0]  is version('6.1','lt')
11
-  check_mode: yes
12
-  register: old_os_check
13
-  failed_when: old_os_check.msg != 'The win_power_plan Ansible module is only available on Server 2008r2 (6.1) and newer'
9
+- set_fact:
10
+    original_plan: '{{ plan_info.stdout_lines[0] }}'
11
+    name: '{{ plan_info.stdout_lines[1] }}'
14 12
 
15 13
 - block:
16
-  - name: register inactive power plan to test with
17
-    win_shell: (Get-CimInstance -Name root\cimv2\power -Class win32_PowerPlan | ? {! $_.IsActive}).ElementName[0]
18
-    register: disabled_power_plan
19
-    changed_when: False
20
-
21
-  - set_fact:
22
-      name: "{{ disabled_power_plan.stdout_lines[0] }}"
23
-
24 14
   #Test that plan detects change is needed, but doesn't actually apply change
25 15
   - name: set power plan (check mode)
26 16
     win_power_plan:
... ...
@@ -28,20 +18,17 @@
28 28
     register: set_plan_check
29 29
     check_mode: yes
30 30
 
31
-#  - debug:
32
-#      var: set_plan_check
33
-
34 31
   - name: get result of set power plan (check mode)
35
-    win_shell: (Get-CimInstance -Name root\cimv2\power -Class win32_PowerPlan -Filter "ElementName = '{{ name }}'").IsActive
32
+    win_shell: (powercfg.exe /list | Select-String -Pattern '\({{ name }}\)').Line
36 33
     register: set_plan_check_result
37 34
     changed_when: False
38
-    
35
+
39 36
   # verify that the powershell check is showing the plan as still inactive on the system
40 37
   - name: assert setting plan (check mode)
41 38
     assert:
42 39
       that:
43 40
       - set_plan_check is changed
44
-      - set_plan_check_result.stdout == 'False\r\n'
41
+      - not set_plan_check_result.stdout_lines[0].endswith('*')
45 42
 
46 43
   #Test that setting plan and that change is applied
47 44
   - name: set power plan
... ...
@@ -50,7 +37,7 @@
50 50
     register: set_plan
51 51
 
52 52
   - name: get result of set power plan
53
-    win_shell: (Get-CimInstance -Name root\cimv2\power -Class win32_PowerPlan -Filter "ElementName = '{{ name }}'").IsActive
53
+    win_shell: (powercfg.exe /list | Select-String -Pattern '\({{ name }}\)').Line
54 54
     register: set_plan_result
55 55
     changed_when: False
56 56
 
... ...
@@ -58,7 +45,7 @@
58 58
     assert:
59 59
       that:
60 60
       - set_plan is changed
61
-      - set_plan_result.stdout == 'True\r\n'
61
+      - set_plan_result.stdout_lines[0].endswith('*')
62 62
 
63 63
   #Test that plan doesn't apply change if it is already set
64 64
   - name: set power plan (idempotent)
... ...
@@ -71,8 +58,7 @@
71 71
       that:
72 72
       - set_plan_idempotent is not changed
73 73
 
74
-  when: os_version.stdout_lines[0]  is version('6.1','ge')
75 74
   always:
76
-  - name: always change back plan to high performance when done testing
75
+  - name: always change back plan to the original when done testing
77 76
     win_power_plan:
78
-      name: high performance
77
+      name: '{{ original_plan }}'
... ...
@@ -56,7 +56,6 @@ lib/ansible/modules/windows/win_pagefile.ps1 PSAvoidUsingPositionalParameters
56 56
 lib/ansible/modules/windows/win_pagefile.ps1 PSAvoidUsingWMICmdlet
57 57
 lib/ansible/modules/windows/win_pagefile.ps1 PSUseDeclaredVarsMoreThanAssignments
58 58
 lib/ansible/modules/windows/win_pagefile.ps1 PSUseSupportsShouldProcess
59
-lib/ansible/modules/windows/win_power_plan.ps1 PSUseDeclaredVarsMoreThanAssignments
60 59
 lib/ansible/modules/windows/win_psmodule.ps1 PSAvoidUsingCmdletAliases
61 60
 lib/ansible/modules/windows/win_rabbitmq_plugin.ps1 PSAvoidUsingCmdletAliases
62 61
 lib/ansible/modules/windows/win_rabbitmq_plugin.ps1 PSAvoidUsingInvokeExpression