Merge ~ltrager/maas:hmcz_power_driver into maas:master
- Git
- lp:~ltrager/maas
- hmcz_power_driver
- Merge into master
Status: | Merged |
---|---|
Approved by: | Adam Collard |
Approved revision: | 7647dadd034c38d584cdd104d2d5916e2db003a7 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~ltrager/maas:hmcz_power_driver |
Merge into: | maas:master |
Diff against target: |
483 lines (+385/-2) 8 files modified
debian/control (+1/-0) required-packages/base (+1/-0) snap/snapcraft.yaml (+1/-0) src/provisioningserver/drivers/power/hmc.py (+2/-2) src/provisioningserver/drivers/power/hmcz.py (+121/-0) src/provisioningserver/drivers/power/registry.py (+2/-0) src/provisioningserver/drivers/power/tests/test_hmcz.py (+255/-0) utilities/check-imports (+2/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Approve | ||
Adam Collard (community) | Approve | ||
Review via email: mp+398036@code.launchpad.net |
Commit message
Add the HMC power driver for IBM Z
Description of the change
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b hmcz_power_driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 8dd73384509b457
Adam Collard (adam-collard) wrote : | # |
Urgh, failure in import linting
src/provisionin
denied: zhmcclient.Client
denied: zhmcclient.NotFound
denied: zhmcclient.Session
src/provisioni
denied: zhmcclient_
19788 imported names were ALLOWED.
4 imported names were DENIED.
Adam Collard (adam-collard) wrote : | # |
nit pick
- 0270acc... by Lee Trager
-
Allow zhmcclient to be imported
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b hmcz_power_driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS
COMMIT: 0270acc9c0542b8
- 04b9203... by Lee Trager
-
Merge branch 'master' into hmcz_power_driver
- 7647dad... by Lee Trager
-
adam-collard fix
Lee Trager (ltrager) wrote : | # |
Thanks for the review, updated as suggested.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b hmcz_power_driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 7647dadd034c38d
Adam Collard (adam-collard) wrote : | # |
jenkins: !test
Adam Collard (adam-collard) : | # |
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b hmcz_power_driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS
COMMIT: 7647dadd034c38d
Preview Diff
1 | diff --git a/debian/control b/debian/control |
2 | index f83bbad..5e7c67e 100644 |
3 | --- a/debian/control |
4 | +++ b/debian/control |
5 | @@ -154,6 +154,7 @@ Depends: lshw, |
6 | iproute2, |
7 | ${misc:Depends}, |
8 | ${python3:Depends} |
9 | +Suggests: python3-zhmcclient (>= 0.22.0-0ubuntu1) |
10 | Description: MAAS server provisioning libraries (Python 3) |
11 | This package provides the MAAS provisioning server python libraries. |
12 | . |
13 | diff --git a/required-packages/base b/required-packages/base |
14 | index 7a7f382..4426319 100644 |
15 | --- a/required-packages/base |
16 | +++ b/required-packages/base |
17 | @@ -60,6 +60,7 @@ python3-tz |
18 | python3-uvloop |
19 | python3-venv |
20 | python3-yaml |
21 | +python3-zhmcclient |
22 | python3-zope.interface |
23 | syslinux-common |
24 | ubuntu-keyring |
25 | diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml |
26 | index 5d3eef9..1a34213 100644 |
27 | --- a/snap/snapcraft.yaml |
28 | +++ b/snap/snapcraft.yaml |
29 | @@ -149,6 +149,7 @@ parts: |
30 | - python3-txtftp |
31 | - python3-urllib3 # for macaroonbakery |
32 | - python3-yaml |
33 | + - python3-zhmcclient |
34 | - python3-zope.interface |
35 | - rsyslog |
36 | - snmp # APC |
37 | diff --git a/src/provisioningserver/drivers/power/hmc.py b/src/provisioningserver/drivers/power/hmc.py |
38 | index 49cb729..0358a69 100644 |
39 | --- a/src/provisioningserver/drivers/power/hmc.py |
40 | +++ b/src/provisioningserver/drivers/power/hmc.py |
41 | @@ -1,4 +1,4 @@ |
42 | -# Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
43 | +# Copyright 2015-2021 Canonical Ltd. This software is licensed under the |
44 | # GNU Affero General Public License version 3 (see the file LICENSE). |
45 | |
46 | """HMC Power Driver. |
47 | @@ -35,7 +35,7 @@ class HMCPowerDriver(PowerDriver): |
48 | name = "hmc" |
49 | chassis = True |
50 | can_probe = False |
51 | - description = "IBM Hardware Management Console (HMC)" |
52 | + description = "IBM Hardware Management Console (HMC) for PowerPC" |
53 | settings = [ |
54 | make_setting_field("power_address", "IP for HMC", required=True), |
55 | make_setting_field("power_user", "HMC username"), |
56 | diff --git a/src/provisioningserver/drivers/power/hmcz.py b/src/provisioningserver/drivers/power/hmcz.py |
57 | new file mode 100644 |
58 | index 0000000..0f16af1 |
59 | --- /dev/null |
60 | +++ b/src/provisioningserver/drivers/power/hmcz.py |
61 | @@ -0,0 +1,121 @@ |
62 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
63 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
64 | + |
65 | +"""HMC Z Driver. |
66 | + |
67 | +Support for managing DPM partitions via the IBM Hardware Management Console |
68 | +for Z. The HMC for IBM Z has a different API than the HMC for IBM Power, thus |
69 | +two different power drivers. See |
70 | +https://github.com/zhmcclient/python-zhmcclient/issues/494 |
71 | +""" |
72 | + |
73 | +import contextlib |
74 | + |
75 | +from provisioningserver.drivers import ( |
76 | + make_ip_extractor, |
77 | + make_setting_field, |
78 | + SETTING_SCOPE, |
79 | +) |
80 | +from provisioningserver.drivers.power import PowerActionError, PowerDriver |
81 | +from provisioningserver.logger import get_maas_logger |
82 | +from provisioningserver.utils import typed |
83 | +from provisioningserver.utils.twisted import asynchronous, threadDeferred |
84 | + |
85 | +try: |
86 | + from zhmcclient import Client, NotFound, Session |
87 | +except ImportError: |
88 | + no_zhmcclient = True |
89 | +else: |
90 | + no_zhmcclient = False |
91 | + |
92 | +maaslog = get_maas_logger("drivers.power.hmcz") |
93 | + |
94 | + |
95 | +class HMCZPowerDriver(PowerDriver): |
96 | + |
97 | + name = "hmcz" |
98 | + chassis = False |
99 | + can_probe = False |
100 | + description = "IBM Hardware Management Console (HMC) for Z" |
101 | + settings = [ |
102 | + make_setting_field("power_address", "HMC Address", required=True), |
103 | + make_setting_field("power_user", "HMC username", required=True), |
104 | + make_setting_field( |
105 | + "power_pass", "HMC password", field_type="password", required=True |
106 | + ), |
107 | + make_setting_field( |
108 | + "power_partition_name", |
109 | + "HMC partition name", |
110 | + scope=SETTING_SCOPE.NODE, |
111 | + required=True, |
112 | + ), |
113 | + ] |
114 | + ip_extractor = make_ip_extractor("power_address") |
115 | + |
116 | + def detect_missing_packages(self): |
117 | + if no_zhmcclient: |
118 | + return ["python3-zhmcclient"] |
119 | + else: |
120 | + return [] |
121 | + |
122 | + @typed |
123 | + def _get_partition(self, context: dict): |
124 | + session = Session( |
125 | + context["power_address"], |
126 | + context["power_user"], |
127 | + context["power_pass"], |
128 | + ) |
129 | + partition_name = context["power_partition_name"] |
130 | + client = Client(session) |
131 | + # Each HMC manages one or more CPCs(Central Processor Complex). To find |
132 | + # a partition MAAS must iterate over all CPCs. |
133 | + for cpc in client.cpcs.list(): |
134 | + if not cpc.dpm_enabled: |
135 | + maaslog.warning( |
136 | + f"DPM is not enabled on '{cpc.get_property('name')}', " |
137 | + "skipping" |
138 | + ) |
139 | + continue |
140 | + with contextlib.suppress(NotFound): |
141 | + return cpc.partitions.find(name=partition_name) |
142 | + raise PowerActionError(f"Unable to find '{partition_name}' on HMC!") |
143 | + |
144 | + # IBM Z partitions can take awhile to start/stop. Don't wait for completion |
145 | + # so power actions don't consume a thread. |
146 | + |
147 | + @typed |
148 | + @asynchronous |
149 | + @threadDeferred |
150 | + def power_on(self, system_id: str, context: dict): |
151 | + """Power on IBM Z DPM.""" |
152 | + partition = self._get_partition(context) |
153 | + partition.start(wait_for_completion=False) |
154 | + |
155 | + @typed |
156 | + @asynchronous |
157 | + @threadDeferred |
158 | + def power_off(self, system_id: str, context: dict): |
159 | + """Power off IBM Z DPM.""" |
160 | + partition = self._get_partition(context) |
161 | + partition.stop(wait_for_completion=False) |
162 | + |
163 | + @typed |
164 | + @asynchronous |
165 | + @threadDeferred |
166 | + def power_query(self, system_id: str, context: dict): |
167 | + """Power on IBM Z DPM.""" |
168 | + partition = self._get_partition(context) |
169 | + status = partition.get_property("status") |
170 | + # IBM Z takes time to start or stop a partition. It returns a |
171 | + # transitional state during this time. Associate the transitional |
172 | + # state with on or off so MAAS doesn't repeatedly issue a power |
173 | + # on or off command. |
174 | + if status in {"starting", "active"}: |
175 | + # When a partition is starting it can go into a "paused" state. |
176 | + # This isn't on or off, just that the partition isn't currently |
177 | + # executing instructions. |
178 | + return "on" |
179 | + elif status in {"stopping", "stopped"}: |
180 | + return "off" |
181 | + else: |
182 | + return "unknown" |
183 | diff --git a/src/provisioningserver/drivers/power/registry.py b/src/provisioningserver/drivers/power/registry.py |
184 | index 5eb0b66..ca742a5 100644 |
185 | --- a/src/provisioningserver/drivers/power/registry.py |
186 | +++ b/src/provisioningserver/drivers/power/registry.py |
187 | @@ -13,6 +13,7 @@ from provisioningserver.drivers.power.apc import APCPowerDriver |
188 | from provisioningserver.drivers.power.dli import DLIPowerDriver |
189 | from provisioningserver.drivers.power.eaton import EatonPowerDriver |
190 | from provisioningserver.drivers.power.hmc import HMCPowerDriver |
191 | +from provisioningserver.drivers.power.hmcz import HMCZPowerDriver |
192 | from provisioningserver.drivers.power.ipmi import IPMIPowerDriver |
193 | from provisioningserver.drivers.power.manual import ManualPowerDriver |
194 | from provisioningserver.drivers.power.moonshot import MoonshotIPMIPowerDriver |
195 | @@ -55,6 +56,7 @@ power_drivers = [ |
196 | DLIPowerDriver(), |
197 | EatonPowerDriver(), |
198 | HMCPowerDriver(), |
199 | + HMCZPowerDriver(), |
200 | IPMIPowerDriver(), |
201 | ManualPowerDriver(), |
202 | MoonshotIPMIPowerDriver(), |
203 | diff --git a/src/provisioningserver/drivers/power/tests/test_hmcz.py b/src/provisioningserver/drivers/power/tests/test_hmcz.py |
204 | new file mode 100644 |
205 | index 0000000..abaf15c |
206 | --- /dev/null |
207 | +++ b/src/provisioningserver/drivers/power/tests/test_hmcz.py |
208 | @@ -0,0 +1,255 @@ |
209 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
210 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
211 | + |
212 | +"""Tests for `provisioningserver.drivers.power.hmcz`.""" |
213 | + |
214 | +from twisted.internet.defer import inlineCallbacks |
215 | +from zhmcclient_mock import FakedSession |
216 | + |
217 | +from maastesting.factory import factory |
218 | +from maastesting.matchers import MockCalledOnce, MockCalledOnceWith |
219 | +from maastesting.testcase import MAASTestCase, MAASTwistedRunTest |
220 | +from provisioningserver.drivers.power import hmcz as hmcz_module |
221 | +from provisioningserver.drivers.power import PowerActionError |
222 | + |
223 | + |
224 | +class TestHMCZPowerDriver(MAASTestCase): |
225 | + |
226 | + run_tests_with = MAASTwistedRunTest.make_factory(timeout=5) |
227 | + |
228 | + def setUp(self): |
229 | + super().setUp() |
230 | + self.power_address = factory.make_ip_address() |
231 | + self.fake_session = FakedSession( |
232 | + self.power_address, |
233 | + factory.make_name("hmc_name"), |
234 | + # The version and API version below were taken from |
235 | + # the test environment given by IBM. |
236 | + "2.14.1", |
237 | + "2.40", |
238 | + ) |
239 | + self.patch(hmcz_module, "Session").return_value = self.fake_session |
240 | + self.hmcz = hmcz_module.HMCZPowerDriver() |
241 | + |
242 | + def make_context(self, power_partition_name=None): |
243 | + if power_partition_name is None: |
244 | + power_partition_name = factory.make_name("power_partition_name") |
245 | + return { |
246 | + "power_address": self.power_address, |
247 | + "power_user": factory.make_name("power_user"), |
248 | + "power_pass": factory.make_name("power_pass"), |
249 | + "power_partition_name": power_partition_name, |
250 | + } |
251 | + |
252 | + def test_detect_missing_packages(self): |
253 | + hmcz_module.no_zhmcclient = False |
254 | + self.assertEqual([], self.hmcz.detect_missing_packages()) |
255 | + |
256 | + def test_detect_missing_packages_missing(self): |
257 | + hmcz_module.no_zhmcclient = True |
258 | + self.assertEqual( |
259 | + ["python3-zhmcclient"], self.hmcz.detect_missing_packages() |
260 | + ) |
261 | + |
262 | + def test_get_partition(self): |
263 | + power_partition_name = factory.make_name("power_partition_name") |
264 | + cpc = self.fake_session.hmc.cpcs.add( |
265 | + { |
266 | + "name": factory.make_name("cpc"), |
267 | + "dpm-enabled": True, |
268 | + } |
269 | + ) |
270 | + cpc.partitions.add({"name": power_partition_name}) |
271 | + |
272 | + self.assertEqual( |
273 | + power_partition_name, |
274 | + self.hmcz._get_partition( |
275 | + self.make_context(power_partition_name) |
276 | + ).get_property("name"), |
277 | + ) |
278 | + |
279 | + def test_get_partition_ignores_cpcs_with_no_dpm(self): |
280 | + mock_logger = self.patch(hmcz_module.maaslog, "warning") |
281 | + power_partition_name = factory.make_name("power_partition_name") |
282 | + self.fake_session.hmc.cpcs.add( |
283 | + { |
284 | + "name": factory.make_name("cpc"), |
285 | + "dpm-enabled": False, |
286 | + } |
287 | + ) |
288 | + cpc = self.fake_session.hmc.cpcs.add({"dpm-enabled": True}) |
289 | + cpc.partitions.add({"name": power_partition_name}) |
290 | + |
291 | + self.assertEqual( |
292 | + power_partition_name, |
293 | + self.hmcz._get_partition( |
294 | + self.make_context(power_partition_name) |
295 | + ).get_property("name"), |
296 | + ) |
297 | + self.assertThat(mock_logger, MockCalledOnce()) |
298 | + |
299 | + def test_get_partition_doesnt_find_partition(self): |
300 | + cpc = self.fake_session.hmc.cpcs.add( |
301 | + { |
302 | + "name": factory.make_name("cpc"), |
303 | + "dpm-enabled": True, |
304 | + } |
305 | + ) |
306 | + cpc.partitions.add({"name": factory.make_name("power_partition_name")}) |
307 | + |
308 | + self.assertRaises( |
309 | + PowerActionError, self.hmcz._get_partition, self.make_context() |
310 | + ) |
311 | + |
312 | + # zhmcclient_mock doesn't currently support async so MagicMock |
313 | + # must be used for power on/off |
314 | + |
315 | + @inlineCallbacks |
316 | + def test_power_on(self): |
317 | + mock_get_partition = self.patch(self.hmcz, "_get_partition") |
318 | + yield self.hmcz.power_on(None, self.make_context()) |
319 | + self.assertThat( |
320 | + mock_get_partition.return_value.start, |
321 | + MockCalledOnceWith(wait_for_completion=False), |
322 | + ) |
323 | + |
324 | + @inlineCallbacks |
325 | + def test_power_off(self): |
326 | + mock_get_partition = self.patch(self.hmcz, "_get_partition") |
327 | + yield self.hmcz.power_off(None, self.make_context()) |
328 | + self.assertThat( |
329 | + mock_get_partition.return_value.stop, |
330 | + MockCalledOnceWith(wait_for_completion=False), |
331 | + ) |
332 | + |
333 | + @inlineCallbacks |
334 | + def test_power_query_starting(self): |
335 | + power_partition_name = factory.make_name("power_partition_name") |
336 | + cpc = self.fake_session.hmc.cpcs.add( |
337 | + { |
338 | + "name": factory.make_name("cpc"), |
339 | + "dpm-enabled": True, |
340 | + } |
341 | + ) |
342 | + cpc.partitions.add( |
343 | + { |
344 | + "name": power_partition_name, |
345 | + "status": "starting", |
346 | + } |
347 | + ) |
348 | + |
349 | + status = yield self.hmcz.power_query( |
350 | + None, self.make_context(power_partition_name) |
351 | + ) |
352 | + |
353 | + self.assertEqual("on", status) |
354 | + |
355 | + @inlineCallbacks |
356 | + def test_power_query_active(self): |
357 | + power_partition_name = factory.make_name("power_partition_name") |
358 | + cpc = self.fake_session.hmc.cpcs.add( |
359 | + { |
360 | + "name": factory.make_name("cpc"), |
361 | + "dpm-enabled": True, |
362 | + } |
363 | + ) |
364 | + cpc.partitions.add( |
365 | + { |
366 | + "name": power_partition_name, |
367 | + "status": "active", |
368 | + } |
369 | + ) |
370 | + |
371 | + status = yield self.hmcz.power_query( |
372 | + None, self.make_context(power_partition_name) |
373 | + ) |
374 | + |
375 | + self.assertEqual("on", status) |
376 | + |
377 | + @inlineCallbacks |
378 | + def test_power_query_stopping(self): |
379 | + power_partition_name = factory.make_name("power_partition_name") |
380 | + cpc = self.fake_session.hmc.cpcs.add( |
381 | + { |
382 | + "name": factory.make_name("cpc"), |
383 | + "dpm-enabled": True, |
384 | + } |
385 | + ) |
386 | + cpc.partitions.add( |
387 | + { |
388 | + "name": power_partition_name, |
389 | + "status": "stopping", |
390 | + } |
391 | + ) |
392 | + |
393 | + status = yield self.hmcz.power_query( |
394 | + None, self.make_context(power_partition_name) |
395 | + ) |
396 | + |
397 | + self.assertEqual("off", status) |
398 | + |
399 | + @inlineCallbacks |
400 | + def test_power_query_stopped(self): |
401 | + power_partition_name = factory.make_name("power_partition_name") |
402 | + cpc = self.fake_session.hmc.cpcs.add( |
403 | + { |
404 | + "name": factory.make_name("cpc"), |
405 | + "dpm-enabled": True, |
406 | + } |
407 | + ) |
408 | + cpc.partitions.add( |
409 | + { |
410 | + "name": power_partition_name, |
411 | + "status": "stopped", |
412 | + } |
413 | + ) |
414 | + |
415 | + status = yield self.hmcz.power_query( |
416 | + None, self.make_context(power_partition_name) |
417 | + ) |
418 | + |
419 | + self.assertEqual("off", status) |
420 | + |
421 | + @inlineCallbacks |
422 | + def test_power_query_paused(self): |
423 | + power_partition_name = factory.make_name("power_partition_name") |
424 | + cpc = self.fake_session.hmc.cpcs.add( |
425 | + { |
426 | + "name": factory.make_name("cpc"), |
427 | + "dpm-enabled": True, |
428 | + } |
429 | + ) |
430 | + cpc.partitions.add( |
431 | + { |
432 | + "name": power_partition_name, |
433 | + "status": "paused", |
434 | + } |
435 | + ) |
436 | + |
437 | + status = yield self.hmcz.power_query( |
438 | + None, self.make_context(power_partition_name) |
439 | + ) |
440 | + |
441 | + self.assertEqual("unknown", status) |
442 | + |
443 | + @inlineCallbacks |
444 | + def test_power_query_other(self): |
445 | + power_partition_name = factory.make_name("power_partition_name") |
446 | + cpc = self.fake_session.hmc.cpcs.add( |
447 | + { |
448 | + "name": factory.make_name("cpc"), |
449 | + "dpm-enabled": True, |
450 | + } |
451 | + ) |
452 | + cpc.partitions.add( |
453 | + { |
454 | + "name": power_partition_name, |
455 | + "status": factory.make_name("status"), |
456 | + } |
457 | + ) |
458 | + |
459 | + status = yield self.hmcz.power_query( |
460 | + None, self.make_context(power_partition_name) |
461 | + ) |
462 | + |
463 | + self.assertEqual("unknown", status) |
464 | diff --git a/utilities/check-imports b/utilities/check-imports |
465 | index ac9a3a7..dc7b496 100755 |
466 | --- a/utilities/check-imports |
467 | +++ b/utilities/check-imports |
468 | @@ -164,6 +164,7 @@ TestingLibraries = Pattern( |
469 | "testresources|testresources.**", |
470 | "testscenarios|testscenarios.**", |
471 | "testtools|testtools.**", |
472 | + "zhmcclient_mock.**", |
473 | ) |
474 | |
475 | |
476 | @@ -235,6 +236,7 @@ RackControllerRule = Rule( |
477 | Allow("urllib3|urllib3.**"), |
478 | Allow("uvloop"), |
479 | Allow("yaml"), |
480 | + Allow("zhmcclient.**"), |
481 | Allow("zope.interface|zope.interface.**"), |
482 | Allow(StandardLibraries), |
483 | ) |
UNIT TESTS
-b hmcz_power_driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED maas-ci. internal: 8080/job/ maas/job/ branch- tester/ 9211/console 73be918b852fabf 2a617c6151
LOG: http://
COMMIT: 3da16a58b7f1932