Merge lp:~allenap/maas/revert-ipmi-power-driver into lp:~maas-committers/maas/trunk
- revert-ipmi-power-driver
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Andres Rodriguez | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 4200 | ||||
Proposed branch: | lp:~allenap/maas/revert-ipmi-power-driver | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
654 lines (+130/-469) 6 files modified
etc/maas/templates/power/ipmi.conf (+4/-0) etc/maas/templates/power/ipmi.template (+110/-0) src/provisioningserver/drivers/power/__init__.py (+0/-2) src/provisioningserver/drivers/power/ipmi.py (+0/-154) src/provisioningserver/drivers/power/tests/test_ipmi.py (+0/-312) src/provisioningserver/power/tests/test_poweraction.py (+16/-1) |
||||
To merge this branch: | bzr merge lp:~allenap/maas/revert-ipmi-power-driver | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andres Rodriguez (community) | Approve | ||
Review via email: mp+268258@code.launchpad.net |
Commit message
Revert to the template-based IPMI power driver.
The new driver is not reliable yet. Once the cause is fixed this will be landed again.
Description of the change
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~allenap/maas/revert-ipmi-power-driver into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Get:9 http://
Hit http://
Hit http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,961 kB in 3s (568 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === added file 'etc/maas/templates/power/ipmi.conf' | |||
2 | --- etc/maas/templates/power/ipmi.conf 1970-01-01 00:00:00 +0000 | |||
3 | +++ etc/maas/templates/power/ipmi.conf 2015-08-17 16:53:58 +0000 | |||
4 | @@ -0,0 +1,4 @@ | |||
5 | 1 | Section Chassis_Boot_Flags | ||
6 | 2 | Boot_Flags_Persistent No | ||
7 | 3 | Boot_Device PXE | ||
8 | 4 | EndSection | ||
9 | 0 | 5 | ||
10 | === added file 'etc/maas/templates/power/ipmi.template' | |||
11 | --- etc/maas/templates/power/ipmi.template 1970-01-01 00:00:00 +0000 | |||
12 | +++ etc/maas/templates/power/ipmi.template 2015-08-17 16:53:58 +0000 | |||
13 | @@ -0,0 +1,110 @@ | |||
14 | 1 | # -*- mode: shell-script -*- | ||
15 | 2 | # | ||
16 | 3 | # Control a system via ipmipower | ||
17 | 4 | # | ||
18 | 5 | |||
19 | 6 | # Parameters. | ||
20 | 7 | # If power_change is 'query', echo the current state of the | ||
21 | 8 | # machine: 'on' or 'off'. | ||
22 | 9 | power_change={{power_change}} | ||
23 | 10 | power_address={{power_address}} | ||
24 | 11 | power_user={{power_user}} | ||
25 | 12 | power_pass={{power_pass}} | ||
26 | 13 | power_driver={{power_driver}} | ||
27 | 14 | power_off_mode={{power_off_mode}} | ||
28 | 15 | ipmipower={{ipmipower}} | ||
29 | 16 | ipmi_chassis_config={{ipmi_chassis_config}} | ||
30 | 17 | config={{config_dir}}/{{ipmi_config}} | ||
31 | 18 | |||
32 | 19 | # If ip_address was supplied and power_address is not explicitly set then | ||
33 | 20 | # use ip_address because it gets discovered on-the-fly based on mac_address. | ||
34 | 21 | # We don't want to use it unilaterally because mac_address may be the host's | ||
35 | 22 | # MAC, so only fall back if someone deliberately left power_address empty. | ||
36 | 23 | {{if ip_address and not power_address}} | ||
37 | 24 | power_address={{ip_address}} | ||
38 | 25 | {{endif}} | ||
39 | 26 | |||
40 | 27 | # This workaround is required on many BMCs, and should have no impact | ||
41 | 28 | # on BMCs that don't require it. | ||
42 | 29 | # See https://bugs.launchpad.net/maas/+bug/1287964 | ||
43 | 30 | workarounds="-W opensesspriv" | ||
44 | 31 | |||
45 | 32 | # Determines the power command needed to execute the desired | ||
46 | 33 | # action. This function receives ${power_change} as argument. | ||
47 | 34 | formulate_power_command() { | ||
48 | 35 | case $1 in | ||
49 | 36 | 'on') echo '--cycle --on-if-off' ;; | ||
50 | 37 | 'off') | ||
51 | 38 | if [ "$power_off_mode" = "soft" ]; | ||
52 | 39 | then | ||
53 | 40 | echo '--soft' | ||
54 | 41 | else | ||
55 | 42 | echo '--off' | ||
56 | 43 | fi ;; | ||
57 | 44 | 'query') echo '--stat' ;; | ||
58 | 45 | *) | ||
59 | 46 | echo "Got unknown power state from ipmipower: '$1'" >&2 | ||
60 | 47 | exit 1 | ||
61 | 48 | esac | ||
62 | 49 | } | ||
63 | 50 | |||
64 | 51 | # Issue command to ipmipower, for the given system. | ||
65 | 52 | issue_ipmi_command() { | ||
66 | 53 | # See https://launchpad.net/bugs/1053391 for details of this workaround | ||
67 | 54 | local driver_option="" user_option="" | ||
68 | 55 | if [ -n "$power_driver" ] | ||
69 | 56 | then | ||
70 | 57 | driver_option="--driver-type=${power_driver}" | ||
71 | 58 | fi | ||
72 | 59 | if [ -n "$power_user" ] | ||
73 | 60 | then | ||
74 | 61 | user_option="-u ${power_user}" | ||
75 | 62 | fi | ||
76 | 63 | |||
77 | 64 | if [ "$power_change" != "query" ] | ||
78 | 65 | then | ||
79 | 66 | # Use C locale to force English error messages. | ||
80 | 67 | result=$(echo workaround |\ | ||
81 | 68 | LC_ALL=C ${ipmi_chassis_config} ${workarounds} ${driver_option} -h ${power_address} ${user_option} -p ${power_pass} --commit --filename ${config} 2>&1) | ||
82 | 69 | |||
83 | 70 | if echo $result | grep -q "password invalid" | ||
84 | 71 | then | ||
85 | 72 | echo "Invalid password" >&2 | ||
86 | 73 | exit 2 | ||
87 | 74 | fi | ||
88 | 75 | fi | ||
89 | 76 | |||
90 | 77 | # Use C locale to force English error messages. | ||
91 | 78 | result=$(echo workaround |\ | ||
92 | 79 | LC_ALL=C ${ipmipower} ${workarounds} ${driver_option} -h ${power_address} ${user_option} -p ${power_pass} "$@") | ||
93 | 80 | |||
94 | 81 | exit_status=$? | ||
95 | 82 | |||
96 | 83 | if echo $result | grep -q "password invalid" | ||
97 | 84 | then | ||
98 | 85 | echo "Invalid password" >&2 | ||
99 | 86 | exit 2 | ||
100 | 87 | fi | ||
101 | 88 | |||
102 | 89 | if [ $exit_status -ne 0 ]; then | ||
103 | 90 | echo $result >&2 | ||
104 | 91 | exit 2 | ||
105 | 92 | fi | ||
106 | 93 | |||
107 | 94 | case "$result" in | ||
108 | 95 | *:* ) | ||
109 | 96 | # Result looks like the usual IPMI output: | ||
110 | 97 | # <ipmi-ip-address>: <on/off>, just return the <on|off> | ||
111 | 98 | # part. | ||
112 | 99 | echo ${result} | cut -d ':' -f 2 | ||
113 | 100 | ;; | ||
114 | 101 | * ) | ||
115 | 102 | echo ${result};; | ||
116 | 103 | esac | ||
117 | 104 | } | ||
118 | 105 | |||
119 | 106 | # This script deliberately does not check the current power state | ||
120 | 107 | # before issuing the requested power command. See bug 1171418 for an | ||
121 | 108 | # explanation. | ||
122 | 109 | power_command=$(formulate_power_command ${power_change}) | ||
123 | 110 | issue_ipmi_command ${power_command} | ||
124 | 0 | 111 | ||
125 | === modified file 'src/provisioningserver/drivers/power/__init__.py' | |||
126 | --- src/provisioningserver/drivers/power/__init__.py 2015-08-06 00:36:14 +0000 | |||
127 | +++ src/provisioningserver/drivers/power/__init__.py 2015-08-17 16:53:58 +0000 | |||
128 | @@ -334,7 +334,6 @@ | |||
129 | 334 | 334 | ||
130 | 335 | from provisioningserver.drivers.power.apc import APCPowerDriver | 335 | from provisioningserver.drivers.power.apc import APCPowerDriver |
131 | 336 | from provisioningserver.drivers.power.hmc import HMCPowerDriver | 336 | from provisioningserver.drivers.power.hmc import HMCPowerDriver |
132 | 337 | from provisioningserver.drivers.power.ipmi import IPMIPowerDriver | ||
133 | 338 | from provisioningserver.drivers.power.msftocs import MicrosoftOCSPowerDriver | 337 | from provisioningserver.drivers.power.msftocs import MicrosoftOCSPowerDriver |
134 | 339 | from provisioningserver.drivers.power.mscm import MSCMPowerDriver | 338 | from provisioningserver.drivers.power.mscm import MSCMPowerDriver |
135 | 340 | from provisioningserver.drivers.power.seamicro import SeaMicroPowerDriver | 339 | from provisioningserver.drivers.power.seamicro import SeaMicroPowerDriver |
136 | @@ -345,7 +344,6 @@ | |||
137 | 345 | builtin_power_drivers = [ | 344 | builtin_power_drivers = [ |
138 | 346 | APCPowerDriver(), | 345 | APCPowerDriver(), |
139 | 347 | HMCPowerDriver(), | 346 | HMCPowerDriver(), |
140 | 348 | IPMIPowerDriver(), | ||
141 | 349 | MicrosoftOCSPowerDriver(), | 347 | MicrosoftOCSPowerDriver(), |
142 | 350 | MSCMPowerDriver(), | 348 | MSCMPowerDriver(), |
143 | 351 | SeaMicroPowerDriver(), | 349 | SeaMicroPowerDriver(), |
144 | 352 | 350 | ||
145 | === removed file 'src/provisioningserver/drivers/power/ipmi.py' | |||
146 | --- src/provisioningserver/drivers/power/ipmi.py 2015-08-11 01:13:20 +0000 | |||
147 | +++ src/provisioningserver/drivers/power/ipmi.py 1970-01-01 00:00:00 +0000 | |||
148 | @@ -1,154 +0,0 @@ | |||
149 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
150 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
151 | 3 | |||
152 | 4 | """IPMI Power Driver.""" | ||
153 | 5 | |||
154 | 6 | from __future__ import ( | ||
155 | 7 | absolute_import, | ||
156 | 8 | print_function, | ||
157 | 9 | unicode_literals, | ||
158 | 10 | ) | ||
159 | 11 | |||
160 | 12 | str = None | ||
161 | 13 | |||
162 | 14 | __metaclass__ = type | ||
163 | 15 | __all__ = [] | ||
164 | 16 | |||
165 | 17 | from subprocess import ( | ||
166 | 18 | PIPE, | ||
167 | 19 | Popen, | ||
168 | 20 | ) | ||
169 | 21 | from tempfile import NamedTemporaryFile | ||
170 | 22 | |||
171 | 23 | from provisioningserver.drivers.power import ( | ||
172 | 24 | PowerAuthError, | ||
173 | 25 | PowerDriver, | ||
174 | 26 | PowerFatalError, | ||
175 | 27 | ) | ||
176 | 28 | from provisioningserver.utils.network import find_ip_via_arp | ||
177 | 29 | from provisioningserver.utils.shell import ( | ||
178 | 30 | call_and_check, | ||
179 | 31 | ExternalProcessError, | ||
180 | 32 | ) | ||
181 | 33 | |||
182 | 34 | |||
183 | 35 | IPMI_CONFIG = """\ | ||
184 | 36 | Section Chassis_Boot_Flags | ||
185 | 37 | Boot_Flags_Persistent No | ||
186 | 38 | Boot_Device PXE | ||
187 | 39 | EndSection | ||
188 | 40 | """ | ||
189 | 41 | |||
190 | 42 | |||
191 | 43 | def is_set(setting): | ||
192 | 44 | return not (setting is None or setting == "" or setting.isspace()) | ||
193 | 45 | |||
194 | 46 | |||
195 | 47 | class IPMIPowerDriver(PowerDriver): | ||
196 | 48 | |||
197 | 49 | name = 'ipmi' | ||
198 | 50 | description = "IPMI Power Driver." | ||
199 | 51 | settings = [] | ||
200 | 52 | |||
201 | 53 | @staticmethod | ||
202 | 54 | def _issue_ipmi_chassis_config_command(command, change, address): | ||
203 | 55 | with NamedTemporaryFile() as tmp_config: | ||
204 | 56 | # Write out the chassis configuration. | ||
205 | 57 | tmp_config.write(IPMI_CONFIG) | ||
206 | 58 | tmp_config.flush() | ||
207 | 59 | # Use it when running the chassis config command. | ||
208 | 60 | # XXX: Not using call_and_check here because we | ||
209 | 61 | # need to check stderr. | ||
210 | 62 | command = tuple(command) + ("--filename", tmp_config.name) | ||
211 | 63 | process = Popen(command, stdout=PIPE, stderr=PIPE) | ||
212 | 64 | stdout, stderr = process.communicate() | ||
213 | 65 | stderr = stderr.strip() | ||
214 | 66 | if "password invalid" in stderr: | ||
215 | 67 | raise PowerAuthError("Invalid password.") | ||
216 | 68 | if process.returncode != 0: | ||
217 | 69 | raise PowerFatalError( | ||
218 | 70 | "Failed to power %s %s: %s" % (change, address, stderr)) | ||
219 | 71 | |||
220 | 72 | @staticmethod | ||
221 | 73 | def _issue_ipmi_power_command(command, change, address): | ||
222 | 74 | command = tuple(command) # For consistency when testing. | ||
223 | 75 | try: | ||
224 | 76 | output = call_and_check(command) | ||
225 | 77 | except ExternalProcessError as e: | ||
226 | 78 | raise PowerFatalError( | ||
227 | 79 | "Failed to power %s %s: %s" % ( | ||
228 | 80 | change, address, e.output_as_unicode)) | ||
229 | 81 | else: | ||
230 | 82 | if 'on' in output: | ||
231 | 83 | return 'on' | ||
232 | 84 | elif 'off' in output: | ||
233 | 85 | return 'off' | ||
234 | 86 | else: | ||
235 | 87 | return output | ||
236 | 88 | |||
237 | 89 | def _issue_ipmi_command( | ||
238 | 90 | self, power_change, power_address=None, power_user=None, | ||
239 | 91 | power_pass=None, power_driver=None, power_off_mode=None, | ||
240 | 92 | ipmipower=None, ipmi_chassis_config=None, mac_address=None, | ||
241 | 93 | **extra): | ||
242 | 94 | """Issue command to ipmipower, for the given system.""" | ||
243 | 95 | # This script deliberately does not check the current power state | ||
244 | 96 | # before issuing the requested power command. See bug 1171418 for an | ||
245 | 97 | # explanation. | ||
246 | 98 | |||
247 | 99 | if is_set(mac_address) and not is_set(power_address): | ||
248 | 100 | power_address = find_ip_via_arp(mac_address) | ||
249 | 101 | |||
250 | 102 | # The `-W opensesspriv` workaround is required on many BMCs, and | ||
251 | 103 | # should have no impact on BMCs that don't require it. | ||
252 | 104 | # See https://bugs.launchpad.net/maas/+bug/1287964 | ||
253 | 105 | ipmi_chassis_config_command = [ | ||
254 | 106 | ipmi_chassis_config, '-W', 'opensesspriv'] | ||
255 | 107 | ipmipower_command = [ | ||
256 | 108 | ipmipower, '-W', 'opensesspriv'] | ||
257 | 109 | |||
258 | 110 | # Arguments in common between chassis config and power control. See | ||
259 | 111 | # https://launchpad.net/bugs/1053391 for details of modifying the | ||
260 | 112 | # command for power_driver and power_user. | ||
261 | 113 | common_args = [] | ||
262 | 114 | if is_set(power_driver): | ||
263 | 115 | common_args.extend(("--driver-type", power_driver)) | ||
264 | 116 | common_args.extend(('-h', power_address)) | ||
265 | 117 | if is_set(power_user): | ||
266 | 118 | common_args.extend(("-u", power_user)) | ||
267 | 119 | common_args.extend(('-p', power_pass)) | ||
268 | 120 | |||
269 | 121 | # Update the chassis config and power commands. | ||
270 | 122 | ipmi_chassis_config_command.extend(common_args) | ||
271 | 123 | ipmi_chassis_config_command.append('--commit') | ||
272 | 124 | ipmipower_command.extend(common_args) | ||
273 | 125 | |||
274 | 126 | # Before changing state run the chassis config command. | ||
275 | 127 | if power_change in ("on", "off"): | ||
276 | 128 | self._issue_ipmi_chassis_config_command( | ||
277 | 129 | ipmi_chassis_config_command, power_change, power_address) | ||
278 | 130 | |||
279 | 131 | # Additional arguments for the power command. | ||
280 | 132 | if power_change == 'on': | ||
281 | 133 | ipmipower_command.append('--cycle') | ||
282 | 134 | ipmipower_command.append('--on-if-off') | ||
283 | 135 | elif power_change == 'off': | ||
284 | 136 | if power_off_mode == 'soft': | ||
285 | 137 | ipmipower_command.append('--soft') | ||
286 | 138 | else: | ||
287 | 139 | ipmipower_command.append('--off') | ||
288 | 140 | elif power_change == 'query': | ||
289 | 141 | ipmipower_command.append('--stat') | ||
290 | 142 | |||
291 | 143 | # Update or query the power state. | ||
292 | 144 | return self._issue_ipmi_power_command( | ||
293 | 145 | ipmipower_command, power_change, power_address) | ||
294 | 146 | |||
295 | 147 | def power_on(self, system_id, **kwargs): | ||
296 | 148 | self._issue_ipmi_command('on', **kwargs) | ||
297 | 149 | |||
298 | 150 | def power_off(self, system_id, **kwargs): | ||
299 | 151 | self._issue_ipmi_command('off', **kwargs) | ||
300 | 152 | |||
301 | 153 | def power_query(self, system_id, **kwargs): | ||
302 | 154 | return self._issue_ipmi_command('query', **kwargs) | ||
303 | 155 | 0 | ||
304 | === removed file 'src/provisioningserver/drivers/power/tests/test_ipmi.py' | |||
305 | --- src/provisioningserver/drivers/power/tests/test_ipmi.py 2015-08-11 03:56:48 +0000 | |||
306 | +++ src/provisioningserver/drivers/power/tests/test_ipmi.py 1970-01-01 00:00:00 +0000 | |||
307 | @@ -1,312 +0,0 @@ | |||
308 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
309 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
310 | 3 | |||
311 | 4 | """Tests for `provisioningserver.drivers.power.ipmi`.""" | ||
312 | 5 | |||
313 | 6 | from __future__ import ( | ||
314 | 7 | absolute_import, | ||
315 | 8 | print_function, | ||
316 | 9 | unicode_literals, | ||
317 | 10 | ) | ||
318 | 11 | |||
319 | 12 | str = None | ||
320 | 13 | |||
321 | 14 | __metaclass__ = type | ||
322 | 15 | __all__ = [] | ||
323 | 16 | |||
324 | 17 | import random | ||
325 | 18 | from subprocess import PIPE | ||
326 | 19 | |||
327 | 20 | from maastesting.factory import factory | ||
328 | 21 | from maastesting.matchers import ( | ||
329 | 22 | MockCalledOnceWith, | ||
330 | 23 | MockNotCalled, | ||
331 | 24 | ) | ||
332 | 25 | from maastesting.testcase import MAASTestCase | ||
333 | 26 | from mock import ( | ||
334 | 27 | ANY, | ||
335 | 28 | sentinel, | ||
336 | 29 | ) | ||
337 | 30 | from provisioningserver.drivers.power import ( | ||
338 | 31 | ipmi as ipmi_module, | ||
339 | 32 | PowerAuthError, | ||
340 | 33 | PowerFatalError, | ||
341 | 34 | ) | ||
342 | 35 | from provisioningserver.drivers.power.ipmi import ( | ||
343 | 36 | IPMI_CONFIG, | ||
344 | 37 | IPMIPowerDriver, | ||
345 | 38 | ) | ||
346 | 39 | from provisioningserver.utils.shell import ExternalProcessError | ||
347 | 40 | from testtools.matchers import ( | ||
348 | 41 | Contains, | ||
349 | 42 | Equals, | ||
350 | 43 | ) | ||
351 | 44 | |||
352 | 45 | |||
353 | 46 | def make_parameters(): | ||
354 | 47 | power_address = factory.make_name('power_address') | ||
355 | 48 | power_user = factory.make_name('power_user') | ||
356 | 49 | power_pass = factory.make_name('power_pass') | ||
357 | 50 | power_driver = factory.make_name('power_driver') | ||
358 | 51 | power_off_mode = factory.make_name('power_off_mode') | ||
359 | 52 | ipmipower = factory.make_name('ipmipower') | ||
360 | 53 | ipmi_chassis_config = factory.make_name('ipmi_chassis_config') | ||
361 | 54 | params = { | ||
362 | 55 | 'power_address': power_address, | ||
363 | 56 | 'power_user': power_user, | ||
364 | 57 | 'power_pass': power_pass, | ||
365 | 58 | 'power_driver': power_driver, | ||
366 | 59 | 'power_off_mode': power_off_mode, | ||
367 | 60 | 'ipmipower': ipmipower, | ||
368 | 61 | 'ipmi_chassis_config': ipmi_chassis_config, | ||
369 | 62 | } | ||
370 | 63 | |||
371 | 64 | return ( | ||
372 | 65 | power_address, power_user, power_pass, power_driver, | ||
373 | 66 | power_off_mode, ipmipower, ipmi_chassis_config, params | ||
374 | 67 | ) | ||
375 | 68 | |||
376 | 69 | |||
377 | 70 | def make_ipmi_chassis_config_command( | ||
378 | 71 | ipmi_chassis_config, power_address, power_pass, | ||
379 | 72 | power_driver, power_user, tmp_config_name): | ||
380 | 73 | print(tmp_config_name) | ||
381 | 74 | return ( | ||
382 | 75 | ipmi_chassis_config, '-W', 'opensesspriv', "--driver-type", | ||
383 | 76 | power_driver, '-h', power_address, '-u', power_user, '-p', power_pass, | ||
384 | 77 | '--commit', '--filename', tmp_config_name | ||
385 | 78 | ) | ||
386 | 79 | |||
387 | 80 | |||
388 | 81 | def make_ipmipower_command( | ||
389 | 82 | ipmipower, power_address, power_pass, | ||
390 | 83 | power_driver, power_user): | ||
391 | 84 | return ( | ||
392 | 85 | ipmipower, '-W', 'opensesspriv', "--driver-type", power_driver, '-h', | ||
393 | 86 | power_address, '-u', power_user, '-p', power_pass | ||
394 | 87 | ) | ||
395 | 88 | |||
396 | 89 | |||
397 | 90 | class TestIPMIPowerDriver(MAASTestCase): | ||
398 | 91 | |||
399 | 92 | def test__finds_power_address_from_mac_address(self): | ||
400 | 93 | (power_address, power_user, power_pass, power_driver, power_off_mode, | ||
401 | 94 | ipmipower, ipmi_chassis_config, params) = make_parameters() | ||
402 | 95 | params['mac_address'] = factory.make_mac_address() | ||
403 | 96 | params['power_address'] = random.choice((None, "", " ")) | ||
404 | 97 | |||
405 | 98 | ip_address = factory.make_ipv4_address() | ||
406 | 99 | find_ip_via_arp = self.patch(ipmi_module, 'find_ip_via_arp') | ||
407 | 100 | find_ip_via_arp.return_value = ip_address | ||
408 | 101 | power_change = random.choice(("on", "off")) | ||
409 | 102 | |||
410 | 103 | driver = IPMIPowerDriver() | ||
411 | 104 | self.patch_autospec(driver, "_issue_ipmi_chassis_config_command") | ||
412 | 105 | self.patch_autospec(driver, "_issue_ipmi_power_command") | ||
413 | 106 | driver._issue_ipmi_command(power_change, **params) | ||
414 | 107 | |||
415 | 108 | # The IP address is passed to _issue_ipmi_chassis_config_command. | ||
416 | 109 | self.assertThat( | ||
417 | 110 | driver._issue_ipmi_chassis_config_command, | ||
418 | 111 | MockCalledOnceWith(ANY, power_change, ip_address)) | ||
419 | 112 | # The IP address is also within the command passed to | ||
420 | 113 | # _issue_ipmi_chassis_config_command. | ||
421 | 114 | self.assertThat( | ||
422 | 115 | driver._issue_ipmi_chassis_config_command.call_args[0], | ||
423 | 116 | Contains(ip_address)) | ||
424 | 117 | # The IP address is passed to _issue_ipmi_power_command. | ||
425 | 118 | self.assertThat( | ||
426 | 119 | driver._issue_ipmi_power_command, | ||
427 | 120 | MockCalledOnceWith(ANY, power_change, ip_address)) | ||
428 | 121 | # The IP address is also within the command passed to | ||
429 | 122 | # _issue_ipmi_power_command. | ||
430 | 123 | self.assertThat( | ||
431 | 124 | driver._issue_ipmi_power_command.call_args[0], | ||
432 | 125 | Contains(ip_address)) | ||
433 | 126 | |||
434 | 127 | def test__chassis_config_written_to_temporary_file(self): | ||
435 | 128 | NamedTemporaryFile = self.patch(ipmi_module, "NamedTemporaryFile") | ||
436 | 129 | tmpfile = NamedTemporaryFile.return_value | ||
437 | 130 | tmpfile.__enter__.return_value = tmpfile | ||
438 | 131 | tmpfile.name = factory.make_name("filename") | ||
439 | 132 | |||
440 | 133 | IPMIPowerDriver._issue_ipmi_chassis_config_command( | ||
441 | 134 | ["true"], sentinel.change, sentinel.addr) | ||
442 | 135 | |||
443 | 136 | self.assertThat(NamedTemporaryFile, MockCalledOnceWith()) | ||
444 | 137 | self.assertThat(tmpfile.__enter__, MockCalledOnceWith()) | ||
445 | 138 | self.assertThat(tmpfile.write, MockCalledOnceWith(IPMI_CONFIG)) | ||
446 | 139 | self.assertThat(tmpfile.flush, MockCalledOnceWith()) | ||
447 | 140 | self.assertThat(tmpfile.__exit__, MockCalledOnceWith(None, None, None)) | ||
448 | 141 | |||
449 | 142 | def test__issue_ipmi_command_issues_power_on(self): | ||
450 | 143 | (power_address, power_user, power_pass, power_driver, power_off_mode, | ||
451 | 144 | ipmipower, ipmi_chassis_config, params) = make_parameters() | ||
452 | 145 | ipmi_chassis_config_command = make_ipmi_chassis_config_command( | ||
453 | 146 | ipmi_chassis_config, power_address, power_pass, power_driver, | ||
454 | 147 | power_user, ANY) | ||
455 | 148 | ipmipower_command = make_ipmipower_command( | ||
456 | 149 | ipmipower, power_address, power_pass, power_driver, power_user) | ||
457 | 150 | ipmipower_command += ('--cycle', '--on-if-off') | ||
458 | 151 | ipmi_power_driver = IPMIPowerDriver() | ||
459 | 152 | popen_mock = self.patch(ipmi_module, 'Popen') | ||
460 | 153 | process = popen_mock.return_value | ||
461 | 154 | process.communicate.return_value = (None, '') | ||
462 | 155 | process.returncode = 0 | ||
463 | 156 | call_and_check_mock = self.patch(ipmi_module, 'call_and_check') | ||
464 | 157 | call_and_check_mock.return_value = 'on' | ||
465 | 158 | |||
466 | 159 | result = ipmi_power_driver._issue_ipmi_command('on', **params) | ||
467 | 160 | |||
468 | 161 | self.expectThat( | ||
469 | 162 | popen_mock, MockCalledOnceWith( | ||
470 | 163 | ipmi_chassis_config_command, stdout=PIPE, stderr=PIPE)) | ||
471 | 164 | self.expectThat( | ||
472 | 165 | call_and_check_mock, MockCalledOnceWith(ipmipower_command)) | ||
473 | 166 | self.expectThat(result, Equals('on')) | ||
474 | 167 | |||
475 | 168 | def test__issue_ipmi_command_issues_power_off(self): | ||
476 | 169 | (power_address, power_user, power_pass, power_driver, power_off_mode, | ||
477 | 170 | ipmipower, ipmi_chassis_config, params) = make_parameters() | ||
478 | 171 | ipmi_chassis_config_command = make_ipmi_chassis_config_command( | ||
479 | 172 | ipmi_chassis_config, power_address, power_pass, power_driver, | ||
480 | 173 | power_user, ANY) | ||
481 | 174 | ipmipower_command = make_ipmipower_command( | ||
482 | 175 | ipmipower, power_address, power_pass, power_driver, power_user) | ||
483 | 176 | ipmipower_command += ('--off', ) | ||
484 | 177 | ipmi_power_driver = IPMIPowerDriver() | ||
485 | 178 | popen_mock = self.patch(ipmi_module, 'Popen') | ||
486 | 179 | process = popen_mock.return_value | ||
487 | 180 | process.communicate.return_value = (None, '') | ||
488 | 181 | process.returncode = 0 | ||
489 | 182 | call_and_check_mock = self.patch(ipmi_module, 'call_and_check') | ||
490 | 183 | call_and_check_mock.return_value = 'off' | ||
491 | 184 | |||
492 | 185 | result = ipmi_power_driver._issue_ipmi_command('off', **params) | ||
493 | 186 | |||
494 | 187 | self.expectThat( | ||
495 | 188 | popen_mock, MockCalledOnceWith( | ||
496 | 189 | ipmi_chassis_config_command, stdout=PIPE, stderr=PIPE)) | ||
497 | 190 | self.expectThat( | ||
498 | 191 | call_and_check_mock, MockCalledOnceWith(ipmipower_command)) | ||
499 | 192 | self.expectThat(result, Equals('off')) | ||
500 | 193 | |||
501 | 194 | def test__issue_ipmi_command_issues_power_off_soft_mode(self): | ||
502 | 195 | (power_address, power_user, power_pass, power_driver, power_off_mode, | ||
503 | 196 | ipmipower, ipmi_chassis_config, params) = make_parameters() | ||
504 | 197 | params['power_off_mode'] = 'soft' | ||
505 | 198 | ipmi_chassis_config_command = make_ipmi_chassis_config_command( | ||
506 | 199 | ipmi_chassis_config, power_address, power_pass, power_driver, | ||
507 | 200 | power_user, ANY) | ||
508 | 201 | ipmipower_command = make_ipmipower_command( | ||
509 | 202 | ipmipower, power_address, power_pass, power_driver, power_user) | ||
510 | 203 | ipmipower_command += ('--soft', ) | ||
511 | 204 | ipmi_power_driver = IPMIPowerDriver() | ||
512 | 205 | popen_mock = self.patch(ipmi_module, 'Popen') | ||
513 | 206 | process = popen_mock.return_value | ||
514 | 207 | process.communicate.return_value = (None, '') | ||
515 | 208 | process.returncode = 0 | ||
516 | 209 | call_and_check_mock = self.patch(ipmi_module, 'call_and_check') | ||
517 | 210 | call_and_check_mock.return_value = 'off' | ||
518 | 211 | |||
519 | 212 | result = ipmi_power_driver._issue_ipmi_command('off', **params) | ||
520 | 213 | |||
521 | 214 | self.expectThat( | ||
522 | 215 | popen_mock, MockCalledOnceWith( | ||
523 | 216 | ipmi_chassis_config_command, stdout=PIPE, stderr=PIPE)) | ||
524 | 217 | self.expectThat( | ||
525 | 218 | call_and_check_mock, MockCalledOnceWith(ipmipower_command)) | ||
526 | 219 | self.expectThat(result, Equals('off')) | ||
527 | 220 | |||
528 | 221 | def test__issue_ipmi_command_issues_power_query(self): | ||
529 | 222 | (power_address, power_user, power_pass, power_driver, power_off_mode, | ||
530 | 223 | ipmipower, ipmi_chassis_config, params) = make_parameters() | ||
531 | 224 | ipmipower_command = make_ipmipower_command( | ||
532 | 225 | ipmipower, power_address, power_pass, power_driver, power_user) | ||
533 | 226 | ipmipower_command += ('--stat', ) | ||
534 | 227 | ipmi_power_driver = IPMIPowerDriver() | ||
535 | 228 | popen_mock = self.patch(ipmi_module, 'Popen') | ||
536 | 229 | process = popen_mock.return_value | ||
537 | 230 | process.communicate.return_value = (None, '') | ||
538 | 231 | process.returncode = 0 | ||
539 | 232 | call_and_check_mock = self.patch(ipmi_module, 'call_and_check') | ||
540 | 233 | call_and_check_mock.return_value = 'other' | ||
541 | 234 | |||
542 | 235 | result = ipmi_power_driver._issue_ipmi_command('query', **params) | ||
543 | 236 | |||
544 | 237 | self.expectThat(popen_mock, MockNotCalled()) | ||
545 | 238 | self.expectThat( | ||
546 | 239 | call_and_check_mock, MockCalledOnceWith(ipmipower_command)) | ||
547 | 240 | self.expectThat(result, Equals('other')) | ||
548 | 241 | |||
549 | 242 | def test__issue_ipmi_command_issues_raises_power_auth_error(self): | ||
550 | 243 | _, _, _, _, _, _, _, params = make_parameters() | ||
551 | 244 | ipmi_power_driver = IPMIPowerDriver() | ||
552 | 245 | popen_mock = self.patch(ipmi_module, 'Popen') | ||
553 | 246 | process = popen_mock.return_value | ||
554 | 247 | process.communicate.return_value = (None, 'password invalid') | ||
555 | 248 | process.returncode = 0 | ||
556 | 249 | |||
557 | 250 | self.assertRaises( | ||
558 | 251 | PowerAuthError, ipmi_power_driver._issue_ipmi_command, | ||
559 | 252 | 'on', **params) | ||
560 | 253 | |||
561 | 254 | def test__issue_ipmi_command_issues_raises_power_fatal_error(self): | ||
562 | 255 | _, _, _, _, _, _, _, params = make_parameters() | ||
563 | 256 | ipmi_power_driver = IPMIPowerDriver() | ||
564 | 257 | popen_mock = self.patch(ipmi_module, 'Popen') | ||
565 | 258 | process = popen_mock.return_value | ||
566 | 259 | process.communicate.return_value = (None, '') | ||
567 | 260 | process.returncode = -1 | ||
568 | 261 | |||
569 | 262 | self.assertRaises( | ||
570 | 263 | PowerFatalError, ipmi_power_driver._issue_ipmi_command, | ||
571 | 264 | 'on', **params) | ||
572 | 265 | |||
573 | 266 | def test__issue_ipmi_command_issues_catches_external_process_error(self): | ||
574 | 267 | _, _, _, _, _, _, _, params = make_parameters() | ||
575 | 268 | ipmi_power_driver = IPMIPowerDriver() | ||
576 | 269 | popen_mock = self.patch(ipmi_module, 'Popen') | ||
577 | 270 | process = popen_mock.return_value | ||
578 | 271 | process.communicate.return_value = (None, '') | ||
579 | 272 | process.returncode = 0 | ||
580 | 273 | call_and_check_mock = self.patch(ipmi_module, 'call_and_check') | ||
581 | 274 | call_and_check_mock.side_effect = ( | ||
582 | 275 | ExternalProcessError(1, "ipmipower something")) | ||
583 | 276 | |||
584 | 277 | self.assertRaises( | ||
585 | 278 | PowerFatalError, ipmi_power_driver._issue_ipmi_command, | ||
586 | 279 | 'on', **params) | ||
587 | 280 | |||
588 | 281 | def test_power_on_calls__issue_ipmi_command(self): | ||
589 | 282 | _, _, _, _, _, _, _, params = make_parameters() | ||
590 | 283 | ipmi_power_driver = IPMIPowerDriver() | ||
591 | 284 | _issue_ipmi_command_mock = self.patch( | ||
592 | 285 | ipmi_power_driver, '_issue_ipmi_command') | ||
593 | 286 | system_id = factory.make_name('system_id') | ||
594 | 287 | ipmi_power_driver.power_on(system_id, **params) | ||
595 | 288 | |||
596 | 289 | self.assertThat( | ||
597 | 290 | _issue_ipmi_command_mock, MockCalledOnceWith('on', **params)) | ||
598 | 291 | |||
599 | 292 | def test_power_off_calls__issue_ipmi_command(self): | ||
600 | 293 | _, _, _, _, _, _, _, params = make_parameters() | ||
601 | 294 | ipmi_power_driver = IPMIPowerDriver() | ||
602 | 295 | _issue_ipmi_command_mock = self.patch( | ||
603 | 296 | ipmi_power_driver, '_issue_ipmi_command') | ||
604 | 297 | system_id = factory.make_name('system_id') | ||
605 | 298 | ipmi_power_driver.power_off(system_id, **params) | ||
606 | 299 | |||
607 | 300 | self.assertThat( | ||
608 | 301 | _issue_ipmi_command_mock, MockCalledOnceWith('off', **params)) | ||
609 | 302 | |||
610 | 303 | def test_power_query_calls__issue_ipmi_command(self): | ||
611 | 304 | _, _, _, _, _, _, _, params = make_parameters() | ||
612 | 305 | ipmi_power_driver = IPMIPowerDriver() | ||
613 | 306 | _issue_ipmi_command_mock = self.patch( | ||
614 | 307 | ipmi_power_driver, '_issue_ipmi_command') | ||
615 | 308 | system_id = factory.make_name('system_id') | ||
616 | 309 | ipmi_power_driver.power_query(system_id, **params) | ||
617 | 310 | |||
618 | 311 | self.assertThat( | ||
619 | 312 | _issue_ipmi_command_mock, MockCalledOnceWith('query', **params)) | ||
620 | 313 | 0 | ||
621 | === modified file 'src/provisioningserver/power/tests/test_poweraction.py' | |||
622 | --- src/provisioningserver/power/tests/test_poweraction.py 2015-08-06 00:36:14 +0000 | |||
623 | +++ src/provisioningserver/power/tests/test_poweraction.py 2015-08-17 16:53:58 +0000 | |||
624 | @@ -202,6 +202,21 @@ | |||
625 | 202 | locate_config('templates/power'), | 202 | locate_config('templates/power'), |
626 | 203 | PowerAction(power_type).get_config_basedir()) | 203 | PowerAction(power_type).get_config_basedir()) |
627 | 204 | 204 | ||
628 | 205 | def test_ipmi_script_includes_config_dir(self): | ||
629 | 206 | conf_dir = factory.make_name('power_config_dir') | ||
630 | 207 | self.configure_power_config_dir(conf_dir) | ||
631 | 208 | action = PowerAction('ipmi') | ||
632 | 209 | script = action.render_template( | ||
633 | 210 | action.get_template(), | ||
634 | 211 | action.update_context(dict( | ||
635 | 212 | power_change='on', power_address='mystystem', | ||
636 | 213 | power_user='me', power_pass='me', ipmipower='echo', | ||
637 | 214 | ipmi_chassis_config='echo', config_dir='dir', | ||
638 | 215 | ipmi_config='file.conf', power_driver='LAN', | ||
639 | 216 | ip_address='', power_off_mode='hard')), | ||
640 | 217 | ) | ||
641 | 218 | self.assertIn(conf_dir, script) | ||
642 | 219 | |||
643 | 205 | def test_moonshot_checks_state(self): | 220 | def test_moonshot_checks_state(self): |
644 | 206 | # We can't test the moonshot template in detail (and it may be | 221 | # We can't test the moonshot template in detail (and it may be |
645 | 207 | # customized), but by making it use "echo" instead of a real | 222 | # customized), but by making it use "echo" instead of a real |
646 | @@ -223,7 +238,7 @@ | |||
647 | 223 | class TestTemplateContext(MAASTestCase): | 238 | class TestTemplateContext(MAASTestCase): |
648 | 224 | 239 | ||
649 | 225 | def make_stubbed_power_action(self): | 240 | def make_stubbed_power_action(self): |
651 | 226 | power_action = PowerAction("moonshot") | 241 | power_action = PowerAction("ipmi") |
652 | 227 | render_template = self.patch(power_action, "render_template") | 242 | render_template = self.patch(power_action, "render_template") |
653 | 228 | render_template.return_value = "echo done" | 243 | render_template.return_value = "echo done" |
654 | 229 | return power_action | 244 | return power_action |
lgtm!