Merge ~ltrager/maas:hmcz_power_driver into maas:master

Proposed by Lee Trager
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)
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

To post a comment you must log in.
Revision history for this message
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://maas-ci.internal:8080/job/maas/job/branch-tester/9211/console
COMMIT: 3da16a58b7f193273be918b852fabf2a617c6151

review: Needs Fixing
Revision history for this message
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://maas-ci.internal:8080/job/maas/job/branch-tester/9212/console
COMMIT: 8dd73384509b457a3538ffb8cfb08359ccf5dc4e

review: Needs Fixing
Revision history for this message
Adam Collard (adam-collard) wrote :

Urgh, failure in import linting

src/provisioningserver/drivers/power/hmcz.py
   denied: zhmcclient.Client
   denied: zhmcclient.NotFound
   denied: zhmcclient.Session
 src/provisioningserver/drivers/power/tests/test_hmcz.py
   denied: zhmcclient_mock.FakedSession

 19788 imported names were ALLOWED.
 4 imported names were DENIED.

Revision history for this message
Adam Collard (adam-collard) wrote :

nit pick

~ltrager/maas:hmcz_power_driver updated
0270acc... by Lee Trager

Allow zhmcclient to be imported

Revision history for this message
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: 0270acc9c0542b8bde175d0a0cb9ea89a475a1fe

review: Approve
~ltrager/maas:hmcz_power_driver updated
04b9203... by Lee Trager

Merge branch 'master' into hmcz_power_driver

7647dad... by Lee Trager

adam-collard fix

Revision history for this message
Lee Trager (ltrager) wrote :

Thanks for the review, updated as suggested.

Revision history for this message
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://maas-ci.internal:8080/job/maas/job/branch-tester/9257/console
COMMIT: 7647dadd034c38d584cdd104d2d5916e2db003a7

review: Needs Fixing
Revision history for this message
Adam Collard (adam-collard) wrote :

jenkins: !test

Revision history for this message
Adam Collard (adam-collard) :
review: Approve
Revision history for this message
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: 7647dadd034c38d584cdd104d2d5916e2db003a7

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/control b/debian/control
2index 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 .
13diff --git a/required-packages/base b/required-packages/base
14index 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
25diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
26index 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
37diff --git a/src/provisioningserver/drivers/power/hmc.py b/src/provisioningserver/drivers/power/hmc.py
38index 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"),
56diff --git a/src/provisioningserver/drivers/power/hmcz.py b/src/provisioningserver/drivers/power/hmcz.py
57new file mode 100644
58index 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"
183diff --git a/src/provisioningserver/drivers/power/registry.py b/src/provisioningserver/drivers/power/registry.py
184index 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(),
203diff --git a/src/provisioningserver/drivers/power/tests/test_hmcz.py b/src/provisioningserver/drivers/power/tests/test_hmcz.py
204new file mode 100644
205index 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)
464diff --git a/utilities/check-imports b/utilities/check-imports
465index 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 )

Subscribers

People subscribed via source and target branches