Merge lp:~newell-jensen/maas/add-mscm-power-query-bug-1384428 into lp:~maas-committers/maas/trunk
- add-mscm-power-query-bug-1384428
- Merge into trunk
Proposed by
Newell Jensen
Status: | Merged |
---|---|
Approved by: | Newell Jensen |
Approved revision: | no longer in the source branch. |
Merged at revision: | 3325 |
Proposed branch: | lp:~newell-jensen/maas/add-mscm-power-query-bug-1384428 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
545 lines (+259/-128) 4 files modified
etc/maas/templates/power/mscm.template (+58/-8) src/provisioningserver/drivers/hardware/mscm.py (+44/-15) src/provisioningserver/drivers/hardware/tests/test_mscm.py (+156/-104) src/provisioningserver/rpc/power.py (+1/-1) |
To merge this branch: | bzr merge lp:~newell-jensen/maas/add-mscm-power-query-bug-1384428 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Blake Rouse (community) | Approve | ||
Review via email: mp+239917@code.launchpad.net |
Commit message
This branch adds power querying capabilities to MSCM. Many of the tests for MSCM have also been cleaned up and reorganized.
Description of the change
This was tested with packages on an MSCM system.
To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote : | # |
Revision history for this message
Christian Reis (kiko) wrote : | # |
Could you ask Sean F. or Narinder to test this?
Revision history for this message
Newell Jensen (newell-jensen) wrote : | # |
Kiko,
Sean tested it and says it works good. I also tested it.
Revision history for this message
Sean Feole (sfeole) wrote : | # |
Tested and verified on maas 1.7
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Looks great.
Only one comment inline.
review:
Approve
Revision history for this message
Raphaël Badin (rvb) wrote : | # |
Looks good, couple of remarks but nothing major.
review:
Approve
Revision history for this message
Newell Jensen (newell-jensen) wrote : | # |
In commit I said "fixed error" but should have said "fixed exception message".
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'etc/maas/templates/power/mscm.template' | |||
2 | --- etc/maas/templates/power/mscm.template 2014-07-18 17:05:57 +0000 | |||
3 | +++ etc/maas/templates/power/mscm.template 2014-10-30 20:06:36 +0000 | |||
4 | @@ -2,13 +2,63 @@ | |||
5 | 2 | # | 2 | # |
6 | 3 | # Control a system via Moonshot HP iLO Chassis Manager (MSCM). | 3 | # Control a system via Moonshot HP iLO Chassis Manager (MSCM). |
7 | 4 | 4 | ||
8 | 5 | # Exit with failure message. | ||
9 | 6 | # Parameters: exit code, and error message. | ||
10 | 7 | fail() { | ||
11 | 8 | echo "$2" >&2 | ||
12 | 9 | exit $1 | ||
13 | 10 | } | ||
14 | 11 | |||
15 | 12 | issue_mscm_command() { | ||
16 | 5 | python - << END | 13 | python - << END |
17 | 14 | import sys | ||
18 | 6 | from provisioningserver.drivers.hardware.mscm import power_control_mscm | 15 | from provisioningserver.drivers.hardware.mscm import power_control_mscm |
27 | 7 | power_control_mscm( | 16 | try: |
28 | 8 | {{escape_py_literal(power_address) | safe}}, | 17 | power_control_mscm( |
29 | 9 | {{escape_py_literal(power_user) | safe}}, | 18 | {{escape_py_literal(power_address) | safe}}, |
30 | 10 | {{escape_py_literal(power_pass) | safe}}, | 19 | {{escape_py_literal(power_user) | safe}}, |
31 | 11 | {{escape_py_literal(node_id) | safe}}, | 20 | {{escape_py_literal(power_pass) | safe}}, |
32 | 12 | {{escape_py_literal(power_change) | safe}}, | 21 | {{escape_py_literal(node_id) | safe}}, |
33 | 13 | ) | 22 | {{escape_py_literal(power_change) | safe}}, |
34 | 14 | END | 23 | ) |
35 | 24 | except Exception as e: | ||
36 | 25 | # This gets in the node event log: print the exception's message | ||
37 | 26 | # and not the stacktrace. | ||
38 | 27 | print(unicode(e)) | ||
39 | 28 | sys.exit(1) | ||
40 | 29 | END | ||
41 | 30 | } | ||
42 | 31 | |||
43 | 32 | query_state() { | ||
44 | 33 | python - << END | ||
45 | 34 | import sys | ||
46 | 35 | from provisioningserver.drivers.hardware.mscm import power_state_mscm | ||
47 | 36 | try: | ||
48 | 37 | print(power_state_mscm( | ||
49 | 38 | {{escape_py_literal(power_address) | safe}}, | ||
50 | 39 | {{escape_py_literal(power_user) | safe}}, | ||
51 | 40 | {{escape_py_literal(power_pass) | safe}}, | ||
52 | 41 | {{escape_py_literal(node_id) | safe}}, | ||
53 | 42 | )) | ||
54 | 43 | except Exception as e: | ||
55 | 44 | # This gets in the node event log: print the exception's message | ||
56 | 45 | # and not the stacktrace. | ||
57 | 46 | print(unicode(e)) | ||
58 | 47 | sys.exit(1) | ||
59 | 48 | END | ||
60 | 49 | } | ||
61 | 50 | |||
62 | 51 | main() { | ||
63 | 52 | case $1 in | ||
64 | 53 | 'on'|'off') | ||
65 | 54 | issue_mscm_command | ||
66 | 55 | ;; | ||
67 | 56 | 'query') | ||
68 | 57 | query_state | ||
69 | 58 | ;; | ||
70 | 59 | *) | ||
71 | 60 | fail 2 "Unknown power command: '$1'" | ||
72 | 61 | esac | ||
73 | 62 | } | ||
74 | 63 | |||
75 | 64 | main "{{power_change}}" | ||
76 | 15 | 65 | ||
77 | === modified file 'src/provisioningserver/drivers/hardware/mscm.py' | |||
78 | --- src/provisioningserver/drivers/hardware/mscm.py 2014-09-24 22:20:11 +0000 | |||
79 | +++ src/provisioningserver/drivers/hardware/mscm.py 2014-10-30 20:06:36 +0000 | |||
80 | @@ -18,6 +18,7 @@ | |||
81 | 18 | __metaclass__ = type | 18 | __metaclass__ = type |
82 | 19 | __all__ = [ | 19 | __all__ = [ |
83 | 20 | 'power_control_mscm', | 20 | 'power_control_mscm', |
84 | 21 | 'power_state_mscm', | ||
85 | 21 | 'probe_and_enlist_mscm', | 22 | 'probe_and_enlist_mscm', |
86 | 22 | ] | 23 | ] |
87 | 23 | 24 | ||
88 | @@ -43,6 +44,15 @@ | |||
89 | 43 | } | 44 | } |
90 | 44 | 45 | ||
91 | 45 | 46 | ||
92 | 47 | class MSCMState: | ||
93 | 48 | OFF = "Off" | ||
94 | 49 | ON = "On" | ||
95 | 50 | |||
96 | 51 | |||
97 | 52 | class MSCMError(Exception): | ||
98 | 53 | """Failure communicating to MSCM. """ | ||
99 | 54 | |||
100 | 55 | |||
101 | 46 | class MSCM_CLI_API: | 56 | class MSCM_CLI_API: |
102 | 47 | """An API for interacting with the Moonshot iLO CM CLI.""" | 57 | """An API for interacting with the Moonshot iLO CM CLI.""" |
103 | 48 | 58 | ||
104 | @@ -117,7 +127,7 @@ | |||
105 | 117 | else: | 127 | else: |
106 | 118 | return cartridge_mapping['Default'] | 128 | return cartridge_mapping['Default'] |
107 | 119 | 129 | ||
109 | 120 | def get_node_power_status(self, node_id): | 130 | def get_node_power_state(self, node_id): |
110 | 121 | """Get power state of node (on/off). | 131 | """Get power state of node (on/off). |
111 | 122 | 132 | ||
112 | 123 | Example of stdout from running "show node power <node_id>": | 133 | Example of stdout from running "show node power <node_id>": |
113 | @@ -152,30 +162,49 @@ | |||
114 | 152 | of 'mscm'. | 162 | of 'mscm'. |
115 | 153 | """ | 163 | """ |
116 | 154 | mscm = MSCM_CLI_API(host, username, password) | 164 | mscm = MSCM_CLI_API(host, username, password) |
117 | 155 | power_status = mscm.get_node_power_status(node_id) | ||
118 | 156 | 165 | ||
119 | 157 | if power_change == 'off': | 166 | if power_change == 'off': |
120 | 158 | mscm.power_node_off(node_id) | 167 | mscm.power_node_off(node_id) |
131 | 159 | return | 168 | elif power_change == 'on': |
132 | 160 | 169 | if mscm.get_node_power_state(node_id) == MSCMState.ON: | |
133 | 161 | if power_change != 'on': | 170 | mscm.power_node_off(node_id) |
134 | 162 | raise AssertionError('Unexpected maas power mode.') | 171 | mscm.configure_node_bootonce_pxe(node_id) |
135 | 163 | 172 | mscm.power_node_on(node_id) | |
136 | 164 | if power_status == 'On': | 173 | else: |
137 | 165 | mscm.power_node_off(node_id) | 174 | raise MSCMError("Unexpected maas power mode.") |
138 | 166 | 175 | ||
139 | 167 | mscm.configure_node_bootonce_pxe(node_id) | 176 | |
140 | 168 | mscm.power_node_on(node_id) | 177 | def power_state_mscm(host, username, password, node_id): |
141 | 178 | """Return the power state for the mscm machine.""" | ||
142 | 179 | mscm = MSCM_CLI_API(host, username, password) | ||
143 | 180 | try: | ||
144 | 181 | power_state = mscm.get_node_power_state(node_id) | ||
145 | 182 | except: | ||
146 | 183 | raise MSCMError("Failed to retrieve power state.") | ||
147 | 184 | |||
148 | 185 | if power_state == MSCMState.OFF: | ||
149 | 186 | return 'off' | ||
150 | 187 | elif power_state == MSCMState.ON: | ||
151 | 188 | return 'on' | ||
152 | 189 | raise MSCMError('Unknown power state: %s' % power_state) | ||
153 | 169 | 190 | ||
154 | 170 | 191 | ||
155 | 171 | def probe_and_enlist_mscm(host, username, password): | 192 | def probe_and_enlist_mscm(host, username, password): |
157 | 172 | """ Extracts all of nodes from mscm, sets all of them to boot via HDD by, | 193 | """ Extracts all of nodes from mscm, sets all of them to boot via M.2 by, |
158 | 173 | default, sets them to bootonce via PXE, and then enlists them into MAAS. | 194 | default, sets them to bootonce via PXE, and then enlists them into MAAS. |
159 | 174 | """ | 195 | """ |
160 | 175 | mscm = MSCM_CLI_API(host, username, password) | 196 | mscm = MSCM_CLI_API(host, username, password) |
162 | 176 | nodes = mscm.discover_nodes() | 197 | try: |
163 | 198 | # if discover_nodes works, we have access to the system | ||
164 | 199 | nodes = mscm.discover_nodes() | ||
165 | 200 | except: | ||
166 | 201 | raise MSCMError( | ||
167 | 202 | "Failed to probe nodes for mscm with host=%s, " | ||
168 | 203 | "username=%s, password=%s" | ||
169 | 204 | % (host, username, password)) | ||
170 | 205 | |||
171 | 177 | for node_id in nodes: | 206 | for node_id in nodes: |
173 | 178 | # Set default boot to HDD | 207 | # Set default boot to M.2 |
174 | 179 | mscm.configure_node_boot_m2(node_id) | 208 | mscm.configure_node_boot_m2(node_id) |
175 | 180 | params = { | 209 | params = { |
176 | 181 | 'power_address': host, | 210 | 'power_address': host, |
177 | 182 | 211 | ||
178 | === modified file 'src/provisioningserver/drivers/hardware/tests/test_mscm.py' | |||
179 | --- src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-09-18 12:44:38 +0000 | |||
180 | +++ src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-10-30 20:06:36 +0000 | |||
181 | @@ -19,16 +19,24 @@ | |||
182 | 19 | from StringIO import StringIO | 19 | from StringIO import StringIO |
183 | 20 | 20 | ||
184 | 21 | from maastesting.factory import factory | 21 | from maastesting.factory import factory |
186 | 22 | from maastesting.matchers import MockCalledOnceWith | 22 | from maastesting.matchers import ( |
187 | 23 | MockAnyCall, | ||
188 | 24 | MockCalledOnceWith, | ||
189 | 25 | MockCalledWith, | ||
190 | 26 | ) | ||
191 | 23 | from maastesting.testcase import MAASTestCase | 27 | from maastesting.testcase import MAASTestCase |
192 | 24 | from mock import Mock | 28 | from mock import Mock |
193 | 25 | from provisioningserver.drivers.hardware.mscm import ( | 29 | from provisioningserver.drivers.hardware.mscm import ( |
194 | 26 | cartridge_mapping, | 30 | cartridge_mapping, |
195 | 27 | MSCM_CLI_API, | 31 | MSCM_CLI_API, |
196 | 32 | MSCMError, | ||
197 | 33 | MSCMState, | ||
198 | 28 | power_control_mscm, | 34 | power_control_mscm, |
199 | 35 | power_state_mscm, | ||
200 | 29 | probe_and_enlist_mscm, | 36 | probe_and_enlist_mscm, |
201 | 30 | ) | 37 | ) |
202 | 31 | import provisioningserver.utils as utils | 38 | import provisioningserver.utils as utils |
203 | 39 | from testtools.matchers import Equals | ||
204 | 32 | 40 | ||
205 | 33 | 41 | ||
206 | 34 | def make_mscm_api(): | 42 | def make_mscm_api(): |
207 | @@ -56,10 +64,19 @@ | |||
208 | 56 | for _ in xrange(length)) | 64 | for _ in xrange(length)) |
209 | 57 | 65 | ||
210 | 58 | 66 | ||
215 | 59 | class TestRunCliCommand(MAASTestCase): | 67 | class TestMSCMCliApi(MAASTestCase): |
216 | 60 | """Tests for ``MSCM_CLI_API.run_cli_command``.""" | 68 | """Tests for `MSCM_CLI_API`.""" |
217 | 61 | 69 | ||
218 | 62 | def test_returns_output(self): | 70 | scenarios = [ |
219 | 71 | ('power_node_on', | ||
220 | 72 | dict(method='power_node_on')), | ||
221 | 73 | ('power_node_off', | ||
222 | 74 | dict(method='power_node_off')), | ||
223 | 75 | ('configure_node_bootonce_pxe', | ||
224 | 76 | dict(method='configure_node_bootonce_pxe')), | ||
225 | 77 | ] | ||
226 | 78 | |||
227 | 79 | def test_run_cli_command_returns_output(self): | ||
228 | 63 | api = make_mscm_api() | 80 | api = make_mscm_api() |
229 | 64 | ssh_mock = self.patch(api, '_ssh') | 81 | ssh_mock = self.patch(api, '_ssh') |
230 | 65 | expected = factory.make_name('output') | 82 | expected = factory.make_name('output') |
231 | @@ -69,18 +86,18 @@ | |||
232 | 69 | output = api._run_cli_command(factory.make_name('command')) | 86 | output = api._run_cli_command(factory.make_name('command')) |
233 | 70 | self.assertEqual(expected, output) | 87 | self.assertEqual(expected, output) |
234 | 71 | 88 | ||
236 | 72 | def test_connects_and_closes_ssh_client(self): | 89 | def test_run_cli_command_connects_and_closes_ssh_client(self): |
237 | 73 | api = make_mscm_api() | 90 | api = make_mscm_api() |
238 | 74 | ssh_mock = self.patch(api, '_ssh') | 91 | ssh_mock = self.patch(api, '_ssh') |
239 | 75 | ssh_mock.exec_command = Mock(return_value=factory.make_streams()) | 92 | ssh_mock.exec_command = Mock(return_value=factory.make_streams()) |
240 | 76 | api._run_cli_command(factory.make_name('command')) | 93 | api._run_cli_command(factory.make_name('command')) |
242 | 77 | self.assertThat( | 94 | self.expectThat( |
243 | 78 | ssh_mock.connect, | 95 | ssh_mock.connect, |
244 | 79 | MockCalledOnceWith( | 96 | MockCalledOnceWith( |
245 | 80 | api.host, username=api.username, password=api.password)) | 97 | api.host, username=api.username, password=api.password)) |
247 | 81 | self.assertThat(ssh_mock.close, MockCalledOnceWith()) | 98 | self.expectThat(ssh_mock.close, MockCalledOnceWith()) |
248 | 82 | 99 | ||
250 | 83 | def test_closes_when_exception_raised(self): | 100 | def test_run_cli_command_closes_when_exception_raised(self): |
251 | 84 | api = make_mscm_api() | 101 | api = make_mscm_api() |
252 | 85 | ssh_mock = self.patch(api, '_ssh') | 102 | ssh_mock = self.patch(api, '_ssh') |
253 | 86 | 103 | ||
254 | @@ -90,11 +107,7 @@ | |||
255 | 90 | ssh_mock.exec_command = Mock(side_effect=fail) | 107 | ssh_mock.exec_command = Mock(side_effect=fail) |
256 | 91 | command = factory.make_name('command') | 108 | command = factory.make_name('command') |
257 | 92 | self.assertRaises(Exception, api._run_cli_command, command) | 109 | self.assertRaises(Exception, api._run_cli_command, command) |
263 | 93 | self.assertThat(ssh_mock.close, MockCalledOnceWith()) | 110 | self.expectThat(ssh_mock.close, MockCalledOnceWith()) |
259 | 94 | |||
260 | 95 | |||
261 | 96 | class TestDiscoverNodes(MAASTestCase): | ||
262 | 97 | """Tests for ``MSCM_CLI_API.discover_nodes``.""" | ||
264 | 98 | 111 | ||
265 | 99 | def test_discover_nodes(self): | 112 | def test_discover_nodes(self): |
266 | 100 | api = make_mscm_api() | 113 | api = make_mscm_api() |
267 | @@ -106,10 +119,6 @@ | |||
268 | 106 | output = api.discover_nodes() | 119 | output = api.discover_nodes() |
269 | 107 | self.assertEqual(expected, output) | 120 | self.assertEqual(expected, output) |
270 | 108 | 121 | ||
271 | 109 | |||
272 | 110 | class TestNodeMACAddress(MAASTestCase): | ||
273 | 111 | """Tests for ``MSCM_CLI_API.get_node_macaddr``.""" | ||
274 | 112 | |||
275 | 113 | def test_get_node_macaddr(self): | 122 | def test_get_node_macaddr(self): |
276 | 114 | api = make_mscm_api() | 123 | api = make_mscm_api() |
277 | 115 | expected = make_show_node_macaddr() | 124 | expected = make_show_node_macaddr() |
278 | @@ -120,10 +129,6 @@ | |||
279 | 120 | self.assertEqual(re.findall(r':'.join(['[0-9a-f]{2}'] * 6), | 129 | self.assertEqual(re.findall(r':'.join(['[0-9a-f]{2}'] * 6), |
280 | 121 | expected), output) | 130 | expected), output) |
281 | 122 | 131 | ||
282 | 123 | |||
283 | 124 | class TestNodeArch(MAASTestCase): | ||
284 | 125 | """Tests for ``MSCM_CLI_API.get_node_arch``.""" | ||
285 | 126 | |||
286 | 127 | def test_get_node_arch(self): | 132 | def test_get_node_arch(self): |
287 | 128 | api = make_mscm_api() | 133 | api = make_mscm_api() |
288 | 129 | expected = '\r\n Product Name: ProLiant Moonshot Cartridge\r\n' | 134 | expected = '\r\n Product Name: ProLiant Moonshot Cartridge\r\n' |
289 | @@ -134,36 +139,17 @@ | |||
290 | 134 | key = expected.split('Product Name: ')[1].splitlines()[0] | 139 | key = expected.split('Product Name: ')[1].splitlines()[0] |
291 | 135 | self.assertEqual(cartridge_mapping[key], output) | 140 | self.assertEqual(cartridge_mapping[key], output) |
292 | 136 | 141 | ||
298 | 137 | 142 | def test_get_node_power_state(self): | |
294 | 138 | class TestGetNodePowerStatus(MAASTestCase): | ||
295 | 139 | """Tests for ``MSCM_CLI_API.get_node_power_status``.""" | ||
296 | 140 | |||
297 | 141 | def test_get_node_power_status(self): | ||
299 | 142 | api = make_mscm_api() | 143 | api = make_mscm_api() |
300 | 143 | expected = '\r\n Node #1\r\n Power State: On\r\n' | 144 | expected = '\r\n Node #1\r\n Power State: On\r\n' |
301 | 144 | cli_mock = self.patch(api, '_run_cli_command') | 145 | cli_mock = self.patch(api, '_run_cli_command') |
302 | 145 | cli_mock.return_value = expected | 146 | cli_mock.return_value = expected |
303 | 146 | node_id = make_node_id() | 147 | node_id = make_node_id() |
305 | 147 | output = api.get_node_power_status(node_id) | 148 | output = api.get_node_power_state(node_id) |
306 | 148 | self.assertEqual(expected.split('Power State: ')[1].splitlines()[0], | 149 | self.assertEqual(expected.split('Power State: ')[1].splitlines()[0], |
307 | 149 | output) | 150 | output) |
308 | 150 | 151 | ||
325 | 151 | 152 | def test_power_and_configure_node_returns_expected_outout(self): | |
310 | 152 | class TestPowerAndConfigureNode(MAASTestCase): | ||
311 | 153 | """Tests for ``MSCM_CLI_API.configure_node_bootonce_pxe, | ||
312 | 154 | MSCM_CLI_API.power_node_on, and MSCM_CLI_API.power_node_off``. | ||
313 | 155 | """ | ||
314 | 156 | |||
315 | 157 | scenarios = [ | ||
316 | 158 | ('power_node_on()', | ||
317 | 159 | dict(method='power_node_on')), | ||
318 | 160 | ('power_node_off()', | ||
319 | 161 | dict(method='power_node_off')), | ||
320 | 162 | ('configure_node_bootonce_pxe()', | ||
321 | 163 | dict(method='configure_node_bootonce_pxe')), | ||
322 | 164 | ] | ||
323 | 165 | |||
324 | 166 | def test_returns_expected_outout(self): | ||
326 | 167 | api = make_mscm_api() | 153 | api = make_mscm_api() |
327 | 168 | ssh_mock = self.patch(api, '_ssh') | 154 | ssh_mock = self.patch(api, '_ssh') |
328 | 169 | expected = factory.make_name('output') | 155 | expected = factory.make_name('output') |
329 | @@ -174,60 +160,8 @@ | |||
330 | 174 | self.assertEqual(expected, output) | 160 | self.assertEqual(expected, output) |
331 | 175 | 161 | ||
332 | 176 | 162 | ||
387 | 177 | class TestPowerControlMSCM(MAASTestCase): | 163 | class TestMSCMProbeAndEnlist(MAASTestCase): |
388 | 178 | """Tests for ``power_control_ucsm``.""" | 164 | """Tests for `probe_and_enlist_mscm`.""" |
335 | 179 | |||
336 | 180 | def test_power_control_mscm_on_on(self): | ||
337 | 181 | # power_change and power_status are both 'on' | ||
338 | 182 | host = factory.make_hostname('mscm') | ||
339 | 183 | username = factory.make_name('user') | ||
340 | 184 | password = factory.make_name('password') | ||
341 | 185 | node_id = make_node_id() | ||
342 | 186 | bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe') | ||
343 | 187 | power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') | ||
344 | 188 | power_status_mock.return_value = 'On' | ||
345 | 189 | power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on') | ||
346 | 190 | power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off') | ||
347 | 191 | |||
348 | 192 | power_control_mscm(host, username, password, node_id, | ||
349 | 193 | power_change='on') | ||
350 | 194 | self.assertThat(bootonce_mock, MockCalledOnceWith(node_id)) | ||
351 | 195 | self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id)) | ||
352 | 196 | self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id)) | ||
353 | 197 | |||
354 | 198 | def test_power_control_mscm_on_off(self): | ||
355 | 199 | # power_change is 'on' and power_status is 'off' | ||
356 | 200 | host = factory.make_hostname('mscm') | ||
357 | 201 | username = factory.make_name('user') | ||
358 | 202 | password = factory.make_name('password') | ||
359 | 203 | node_id = make_node_id() | ||
360 | 204 | bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe') | ||
361 | 205 | power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') | ||
362 | 206 | power_status_mock.return_value = 'Off' | ||
363 | 207 | power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on') | ||
364 | 208 | |||
365 | 209 | power_control_mscm(host, username, password, node_id, | ||
366 | 210 | power_change='on') | ||
367 | 211 | self.assertThat(bootonce_mock, MockCalledOnceWith(node_id)) | ||
368 | 212 | self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id)) | ||
369 | 213 | |||
370 | 214 | def test_power_control_mscm_off_on(self): | ||
371 | 215 | # power_change is 'off' and power_status is 'on' | ||
372 | 216 | host = factory.make_hostname('mscm') | ||
373 | 217 | username = factory.make_name('user') | ||
374 | 218 | password = factory.make_name('password') | ||
375 | 219 | node_id = make_node_id() | ||
376 | 220 | power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') | ||
377 | 221 | power_status_mock.return_value = 'On' | ||
378 | 222 | power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off') | ||
379 | 223 | |||
380 | 224 | power_control_mscm(host, username, password, node_id, | ||
381 | 225 | power_change='off') | ||
382 | 226 | self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id)) | ||
383 | 227 | |||
384 | 228 | |||
385 | 229 | class TestProbeAndEnlistMSCM(MAASTestCase): | ||
386 | 230 | """Tests for ``probe_and_enlist_mscm``.""" | ||
389 | 231 | 165 | ||
390 | 232 | def test_probe_and_enlist(self): | 166 | def test_probe_and_enlist(self): |
391 | 233 | host = factory.make_hostname('mscm') | 167 | host = factory.make_hostname('mscm') |
392 | @@ -244,16 +178,134 @@ | |||
393 | 244 | node_macs_mock = self.patch(MSCM_CLI_API, 'get_node_macaddr') | 178 | node_macs_mock = self.patch(MSCM_CLI_API, 'get_node_macaddr') |
394 | 245 | node_macs_mock.return_value = macs | 179 | node_macs_mock.return_value = macs |
395 | 246 | create_node_mock = self.patch(utils, 'create_node') | 180 | create_node_mock = self.patch(utils, 'create_node') |
396 | 247 | probe_and_enlist_mscm(host, username, password) | ||
397 | 248 | self.assertThat(discover_nodes_mock, MockCalledOnceWith()) | ||
398 | 249 | self.assertThat(boot_m2_mock, MockCalledOnceWith(node_id)) | ||
399 | 250 | self.assertThat(node_arch_mock, MockCalledOnceWith(node_id)) | ||
400 | 251 | self.assertThat(node_macs_mock, MockCalledOnceWith(node_id)) | ||
401 | 252 | params = { | 181 | params = { |
402 | 253 | 'power_address': host, | 182 | 'power_address': host, |
403 | 254 | 'power_user': username, | 183 | 'power_user': username, |
404 | 255 | 'power_pass': password, | 184 | 'power_pass': password, |
405 | 256 | 'node_id': node_id, | 185 | 'node_id': node_id, |
406 | 257 | } | 186 | } |
408 | 258 | self.assertThat(create_node_mock, | 187 | |
409 | 188 | probe_and_enlist_mscm(host, username, password) | ||
410 | 189 | self.expectThat(discover_nodes_mock, MockAnyCall()) | ||
411 | 190 | self.expectThat(boot_m2_mock, MockCalledWith(node_id)) | ||
412 | 191 | self.expectThat(node_arch_mock, MockCalledOnceWith(node_id)) | ||
413 | 192 | self.expectThat(node_macs_mock, MockCalledOnceWith(node_id)) | ||
414 | 193 | self.expectThat(create_node_mock, | ||
415 | 259 | MockCalledOnceWith(macs, arch, 'mscm', params)) | 194 | MockCalledOnceWith(macs, arch, 'mscm', params)) |
416 | 195 | |||
417 | 196 | def test_probe_and_enlist_discover_nodes_failure(self): | ||
418 | 197 | host = factory.make_hostname('mscm') | ||
419 | 198 | username = factory.make_name('user') | ||
420 | 199 | password = factory.make_name('password') | ||
421 | 200 | discover_nodes_mock = self.patch(MSCM_CLI_API, 'discover_nodes') | ||
422 | 201 | discover_nodes_mock.side_effect = MSCMError('error') | ||
423 | 202 | self.assertRaises( | ||
424 | 203 | MSCMError, probe_and_enlist_mscm, host, username, password) | ||
425 | 204 | |||
426 | 205 | |||
427 | 206 | class TestMSCMPowerControl(MAASTestCase): | ||
428 | 207 | """Tests for `power_control_mscm`.""" | ||
429 | 208 | |||
430 | 209 | def test_power_control_error_on_unknown_power_change(self): | ||
431 | 210 | host = factory.make_hostname('mscm') | ||
432 | 211 | username = factory.make_name('user') | ||
433 | 212 | password = factory.make_name('password') | ||
434 | 213 | node_id = make_node_id() | ||
435 | 214 | power_change = factory.make_name('error') | ||
436 | 215 | self.assertRaises( | ||
437 | 216 | MSCMError, power_control_mscm, host, | ||
438 | 217 | username, password, node_id, power_change) | ||
439 | 218 | |||
440 | 219 | def test_power_control_power_change_on_power_state_on(self): | ||
441 | 220 | # power_change and current power_state are both 'on' | ||
442 | 221 | host = factory.make_hostname('mscm') | ||
443 | 222 | username = factory.make_name('user') | ||
444 | 223 | password = factory.make_name('password') | ||
445 | 224 | node_id = make_node_id() | ||
446 | 225 | power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state') | ||
447 | 226 | power_state_mock.return_value = MSCMState.ON | ||
448 | 227 | power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off') | ||
449 | 228 | bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe') | ||
450 | 229 | power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on') | ||
451 | 230 | |||
452 | 231 | power_control_mscm(host, username, password, node_id, | ||
453 | 232 | power_change='on') | ||
454 | 233 | self.expectThat(power_state_mock, MockCalledOnceWith(node_id)) | ||
455 | 234 | self.expectThat(power_node_off_mock, MockCalledOnceWith(node_id)) | ||
456 | 235 | self.expectThat(bootonce_mock, MockCalledOnceWith(node_id)) | ||
457 | 236 | self.expectThat(power_node_on_mock, MockCalledOnceWith(node_id)) | ||
458 | 237 | |||
459 | 238 | def test_power_control_power_change_on_power_state_off(self): | ||
460 | 239 | # power_change is 'on' and current power_state is 'off' | ||
461 | 240 | host = factory.make_hostname('mscm') | ||
462 | 241 | username = factory.make_name('user') | ||
463 | 242 | password = factory.make_name('password') | ||
464 | 243 | node_id = make_node_id() | ||
465 | 244 | power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state') | ||
466 | 245 | power_state_mock.return_value = MSCMState.OFF | ||
467 | 246 | bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe') | ||
468 | 247 | power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on') | ||
469 | 248 | |||
470 | 249 | power_control_mscm(host, username, password, node_id, | ||
471 | 250 | power_change='on') | ||
472 | 251 | self.expectThat(power_state_mock, MockCalledOnceWith(node_id)) | ||
473 | 252 | self.expectThat(bootonce_mock, MockCalledOnceWith(node_id)) | ||
474 | 253 | self.expectThat(power_node_on_mock, MockCalledOnceWith(node_id)) | ||
475 | 254 | |||
476 | 255 | def test_power_control_power_change_off_power_state_on(self): | ||
477 | 256 | # power_change is 'off' and current power_state is 'on' | ||
478 | 257 | host = factory.make_hostname('mscm') | ||
479 | 258 | username = factory.make_name('user') | ||
480 | 259 | password = factory.make_name('password') | ||
481 | 260 | node_id = make_node_id() | ||
482 | 261 | power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off') | ||
483 | 262 | |||
484 | 263 | power_control_mscm(host, username, password, node_id, | ||
485 | 264 | power_change='off') | ||
486 | 265 | self.expectThat(power_node_off_mock, MockCalledOnceWith(node_id)) | ||
487 | 266 | |||
488 | 267 | |||
489 | 268 | class TestMSCMPowerState(MAASTestCase): | ||
490 | 269 | """Tests for `power_state_mscm`.""" | ||
491 | 270 | |||
492 | 271 | def test_power_state_failed_to_get_state(self): | ||
493 | 272 | host = factory.make_hostname('mscm') | ||
494 | 273 | username = factory.make_name('user') | ||
495 | 274 | password = factory.make_name('password') | ||
496 | 275 | node_id = make_node_id() | ||
497 | 276 | power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state') | ||
498 | 277 | power_state_mock.side_effect = MSCMError('error') | ||
499 | 278 | self.assertRaises( | ||
500 | 279 | MSCMError, power_state_mscm, host, username, password, node_id) | ||
501 | 280 | |||
502 | 281 | def test_power_state_get_off(self): | ||
503 | 282 | host = factory.make_hostname('mscm') | ||
504 | 283 | username = factory.make_name('user') | ||
505 | 284 | password = factory.make_name('password') | ||
506 | 285 | node_id = make_node_id() | ||
507 | 286 | power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state') | ||
508 | 287 | power_state_mock.return_value = MSCMState.OFF | ||
509 | 288 | self.assertThat( | ||
510 | 289 | power_state_mscm(host, username, password, node_id), | ||
511 | 290 | Equals('off')) | ||
512 | 291 | |||
513 | 292 | def test_power_state_get_on(self): | ||
514 | 293 | host = factory.make_hostname('mscm') | ||
515 | 294 | username = factory.make_name('user') | ||
516 | 295 | password = factory.make_name('password') | ||
517 | 296 | node_id = make_node_id() | ||
518 | 297 | power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state') | ||
519 | 298 | power_state_mock.return_value = MSCMState.ON | ||
520 | 299 | self.assertThat( | ||
521 | 300 | power_state_mscm(host, username, password, node_id), | ||
522 | 301 | Equals('on')) | ||
523 | 302 | |||
524 | 303 | def test_power_state_error_on_unknown_state(self): | ||
525 | 304 | host = factory.make_hostname('mscm') | ||
526 | 305 | username = factory.make_name('user') | ||
527 | 306 | password = factory.make_name('password') | ||
528 | 307 | node_id = make_node_id() | ||
529 | 308 | power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state') | ||
530 | 309 | power_state_mock.return_value = factory.make_name('error') | ||
531 | 310 | self.assertRaises( | ||
532 | 311 | MSCMError, power_state_mscm, host, username, password, node_id) | ||
533 | 260 | 312 | ||
534 | === modified file 'src/provisioningserver/rpc/power.py' | |||
535 | --- src/provisioningserver/rpc/power.py 2014-10-27 11:56:50 +0000 | |||
536 | +++ src/provisioningserver/rpc/power.py 2014-10-30 20:06:36 +0000 | |||
537 | @@ -61,7 +61,7 @@ | |||
538 | 61 | # state for these power types. | 61 | # state for these power types. |
539 | 62 | # This is meant to be temporary until all the power types support | 62 | # This is meant to be temporary until all the power types support |
540 | 63 | # querying the power state of a node. | 63 | # querying the power state of a node. |
542 | 64 | QUERY_POWER_TYPES = ['amt', 'ipmi', 'virsh'] | 64 | QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'virsh'] |
543 | 65 | 65 | ||
544 | 66 | 66 | ||
545 | 67 | # Timeout for change_power_state(). We set it to 2 minutes by default, | 67 | # Timeout for change_power_state(). We set it to 2 minutes by default, |
This passed CI testing as well.