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
diff --git a/debian/control b/debian/control
index f83bbad..5e7c67e 100644
--- a/debian/control
+++ b/debian/control
@@ -154,6 +154,7 @@ Depends: lshw,
154 iproute2,154 iproute2,
155 ${misc:Depends},155 ${misc:Depends},
156 ${python3:Depends}156 ${python3:Depends}
157Suggests: python3-zhmcclient (>= 0.22.0-0ubuntu1)
157Description: MAAS server provisioning libraries (Python 3)158Description: MAAS server provisioning libraries (Python 3)
158 This package provides the MAAS provisioning server python libraries.159 This package provides the MAAS provisioning server python libraries.
159 .160 .
diff --git a/required-packages/base b/required-packages/base
index 7a7f382..4426319 100644
--- a/required-packages/base
+++ b/required-packages/base
@@ -60,6 +60,7 @@ python3-tz
60python3-uvloop60python3-uvloop
61python3-venv61python3-venv
62python3-yaml62python3-yaml
63python3-zhmcclient
63python3-zope.interface64python3-zope.interface
64syslinux-common65syslinux-common
65ubuntu-keyring66ubuntu-keyring
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 5d3eef9..1a34213 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -149,6 +149,7 @@ parts:
149 - python3-txtftp149 - python3-txtftp
150 - python3-urllib3 # for macaroonbakery150 - python3-urllib3 # for macaroonbakery
151 - python3-yaml151 - python3-yaml
152 - python3-zhmcclient
152 - python3-zope.interface153 - python3-zope.interface
153 - rsyslog154 - rsyslog
154 - snmp # APC155 - snmp # APC
diff --git a/src/provisioningserver/drivers/power/hmc.py b/src/provisioningserver/drivers/power/hmc.py
index 49cb729..0358a69 100644
--- a/src/provisioningserver/drivers/power/hmc.py
+++ b/src/provisioningserver/drivers/power/hmc.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the1# Copyright 2015-2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""HMC Power Driver.4"""HMC Power Driver.
@@ -35,7 +35,7 @@ class HMCPowerDriver(PowerDriver):
35 name = "hmc"35 name = "hmc"
36 chassis = True36 chassis = True
37 can_probe = False37 can_probe = False
38 description = "IBM Hardware Management Console (HMC)"38 description = "IBM Hardware Management Console (HMC) for PowerPC"
39 settings = [39 settings = [
40 make_setting_field("power_address", "IP for HMC", required=True),40 make_setting_field("power_address", "IP for HMC", required=True),
41 make_setting_field("power_user", "HMC username"),41 make_setting_field("power_user", "HMC username"),
diff --git a/src/provisioningserver/drivers/power/hmcz.py b/src/provisioningserver/drivers/power/hmcz.py
42new file mode 10064442new file mode 100644
index 0000000..0f16af1
--- /dev/null
+++ b/src/provisioningserver/drivers/power/hmcz.py
@@ -0,0 +1,121 @@
1# Copyright 2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""HMC Z Driver.
5
6Support for managing DPM partitions via the IBM Hardware Management Console
7for Z. The HMC for IBM Z has a different API than the HMC for IBM Power, thus
8two different power drivers. See
9https://github.com/zhmcclient/python-zhmcclient/issues/494
10"""
11
12import contextlib
13
14from provisioningserver.drivers import (
15 make_ip_extractor,
16 make_setting_field,
17 SETTING_SCOPE,
18)
19from provisioningserver.drivers.power import PowerActionError, PowerDriver
20from provisioningserver.logger import get_maas_logger
21from provisioningserver.utils import typed
22from provisioningserver.utils.twisted import asynchronous, threadDeferred
23
24try:
25 from zhmcclient import Client, NotFound, Session
26except ImportError:
27 no_zhmcclient = True
28else:
29 no_zhmcclient = False
30
31maaslog = get_maas_logger("drivers.power.hmcz")
32
33
34class HMCZPowerDriver(PowerDriver):
35
36 name = "hmcz"
37 chassis = False
38 can_probe = False
39 description = "IBM Hardware Management Console (HMC) for Z"
40 settings = [
41 make_setting_field("power_address", "HMC Address", required=True),
42 make_setting_field("power_user", "HMC username", required=True),
43 make_setting_field(
44 "power_pass", "HMC password", field_type="password", required=True
45 ),
46 make_setting_field(
47 "power_partition_name",
48 "HMC partition name",
49 scope=SETTING_SCOPE.NODE,
50 required=True,
51 ),
52 ]
53 ip_extractor = make_ip_extractor("power_address")
54
55 def detect_missing_packages(self):
56 if no_zhmcclient:
57 return ["python3-zhmcclient"]
58 else:
59 return []
60
61 @typed
62 def _get_partition(self, context: dict):
63 session = Session(
64 context["power_address"],
65 context["power_user"],
66 context["power_pass"],
67 )
68 partition_name = context["power_partition_name"]
69 client = Client(session)
70 # Each HMC manages one or more CPCs(Central Processor Complex). To find
71 # a partition MAAS must iterate over all CPCs.
72 for cpc in client.cpcs.list():
73 if not cpc.dpm_enabled:
74 maaslog.warning(
75 f"DPM is not enabled on '{cpc.get_property('name')}', "
76 "skipping"
77 )
78 continue
79 with contextlib.suppress(NotFound):
80 return cpc.partitions.find(name=partition_name)
81 raise PowerActionError(f"Unable to find '{partition_name}' on HMC!")
82
83 # IBM Z partitions can take awhile to start/stop. Don't wait for completion
84 # so power actions don't consume a thread.
85
86 @typed
87 @asynchronous
88 @threadDeferred
89 def power_on(self, system_id: str, context: dict):
90 """Power on IBM Z DPM."""
91 partition = self._get_partition(context)
92 partition.start(wait_for_completion=False)
93
94 @typed
95 @asynchronous
96 @threadDeferred
97 def power_off(self, system_id: str, context: dict):
98 """Power off IBM Z DPM."""
99 partition = self._get_partition(context)
100 partition.stop(wait_for_completion=False)
101
102 @typed
103 @asynchronous
104 @threadDeferred
105 def power_query(self, system_id: str, context: dict):
106 """Power on IBM Z DPM."""
107 partition = self._get_partition(context)
108 status = partition.get_property("status")
109 # IBM Z takes time to start or stop a partition. It returns a
110 # transitional state during this time. Associate the transitional
111 # state with on or off so MAAS doesn't repeatedly issue a power
112 # on or off command.
113 if status in {"starting", "active"}:
114 # When a partition is starting it can go into a "paused" state.
115 # This isn't on or off, just that the partition isn't currently
116 # executing instructions.
117 return "on"
118 elif status in {"stopping", "stopped"}:
119 return "off"
120 else:
121 return "unknown"
diff --git a/src/provisioningserver/drivers/power/registry.py b/src/provisioningserver/drivers/power/registry.py
index 5eb0b66..ca742a5 100644
--- a/src/provisioningserver/drivers/power/registry.py
+++ b/src/provisioningserver/drivers/power/registry.py
@@ -13,6 +13,7 @@ from provisioningserver.drivers.power.apc import APCPowerDriver
13from provisioningserver.drivers.power.dli import DLIPowerDriver13from provisioningserver.drivers.power.dli import DLIPowerDriver
14from provisioningserver.drivers.power.eaton import EatonPowerDriver14from provisioningserver.drivers.power.eaton import EatonPowerDriver
15from provisioningserver.drivers.power.hmc import HMCPowerDriver15from provisioningserver.drivers.power.hmc import HMCPowerDriver
16from provisioningserver.drivers.power.hmcz import HMCZPowerDriver
16from provisioningserver.drivers.power.ipmi import IPMIPowerDriver17from provisioningserver.drivers.power.ipmi import IPMIPowerDriver
17from provisioningserver.drivers.power.manual import ManualPowerDriver18from provisioningserver.drivers.power.manual import ManualPowerDriver
18from provisioningserver.drivers.power.moonshot import MoonshotIPMIPowerDriver19from provisioningserver.drivers.power.moonshot import MoonshotIPMIPowerDriver
@@ -55,6 +56,7 @@ power_drivers = [
55 DLIPowerDriver(),56 DLIPowerDriver(),
56 EatonPowerDriver(),57 EatonPowerDriver(),
57 HMCPowerDriver(),58 HMCPowerDriver(),
59 HMCZPowerDriver(),
58 IPMIPowerDriver(),60 IPMIPowerDriver(),
59 ManualPowerDriver(),61 ManualPowerDriver(),
60 MoonshotIPMIPowerDriver(),62 MoonshotIPMIPowerDriver(),
diff --git a/src/provisioningserver/drivers/power/tests/test_hmcz.py b/src/provisioningserver/drivers/power/tests/test_hmcz.py
61new file mode 10064463new file mode 100644
index 0000000..abaf15c
--- /dev/null
+++ b/src/provisioningserver/drivers/power/tests/test_hmcz.py
@@ -0,0 +1,255 @@
1# Copyright 2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for `provisioningserver.drivers.power.hmcz`."""
5
6from twisted.internet.defer import inlineCallbacks
7from zhmcclient_mock import FakedSession
8
9from maastesting.factory import factory
10from maastesting.matchers import MockCalledOnce, MockCalledOnceWith
11from maastesting.testcase import MAASTestCase, MAASTwistedRunTest
12from provisioningserver.drivers.power import hmcz as hmcz_module
13from provisioningserver.drivers.power import PowerActionError
14
15
16class TestHMCZPowerDriver(MAASTestCase):
17
18 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
19
20 def setUp(self):
21 super().setUp()
22 self.power_address = factory.make_ip_address()
23 self.fake_session = FakedSession(
24 self.power_address,
25 factory.make_name("hmc_name"),
26 # The version and API version below were taken from
27 # the test environment given by IBM.
28 "2.14.1",
29 "2.40",
30 )
31 self.patch(hmcz_module, "Session").return_value = self.fake_session
32 self.hmcz = hmcz_module.HMCZPowerDriver()
33
34 def make_context(self, power_partition_name=None):
35 if power_partition_name is None:
36 power_partition_name = factory.make_name("power_partition_name")
37 return {
38 "power_address": self.power_address,
39 "power_user": factory.make_name("power_user"),
40 "power_pass": factory.make_name("power_pass"),
41 "power_partition_name": power_partition_name,
42 }
43
44 def test_detect_missing_packages(self):
45 hmcz_module.no_zhmcclient = False
46 self.assertEqual([], self.hmcz.detect_missing_packages())
47
48 def test_detect_missing_packages_missing(self):
49 hmcz_module.no_zhmcclient = True
50 self.assertEqual(
51 ["python3-zhmcclient"], self.hmcz.detect_missing_packages()
52 )
53
54 def test_get_partition(self):
55 power_partition_name = factory.make_name("power_partition_name")
56 cpc = self.fake_session.hmc.cpcs.add(
57 {
58 "name": factory.make_name("cpc"),
59 "dpm-enabled": True,
60 }
61 )
62 cpc.partitions.add({"name": power_partition_name})
63
64 self.assertEqual(
65 power_partition_name,
66 self.hmcz._get_partition(
67 self.make_context(power_partition_name)
68 ).get_property("name"),
69 )
70
71 def test_get_partition_ignores_cpcs_with_no_dpm(self):
72 mock_logger = self.patch(hmcz_module.maaslog, "warning")
73 power_partition_name = factory.make_name("power_partition_name")
74 self.fake_session.hmc.cpcs.add(
75 {
76 "name": factory.make_name("cpc"),
77 "dpm-enabled": False,
78 }
79 )
80 cpc = self.fake_session.hmc.cpcs.add({"dpm-enabled": True})
81 cpc.partitions.add({"name": power_partition_name})
82
83 self.assertEqual(
84 power_partition_name,
85 self.hmcz._get_partition(
86 self.make_context(power_partition_name)
87 ).get_property("name"),
88 )
89 self.assertThat(mock_logger, MockCalledOnce())
90
91 def test_get_partition_doesnt_find_partition(self):
92 cpc = self.fake_session.hmc.cpcs.add(
93 {
94 "name": factory.make_name("cpc"),
95 "dpm-enabled": True,
96 }
97 )
98 cpc.partitions.add({"name": factory.make_name("power_partition_name")})
99
100 self.assertRaises(
101 PowerActionError, self.hmcz._get_partition, self.make_context()
102 )
103
104 # zhmcclient_mock doesn't currently support async so MagicMock
105 # must be used for power on/off
106
107 @inlineCallbacks
108 def test_power_on(self):
109 mock_get_partition = self.patch(self.hmcz, "_get_partition")
110 yield self.hmcz.power_on(None, self.make_context())
111 self.assertThat(
112 mock_get_partition.return_value.start,
113 MockCalledOnceWith(wait_for_completion=False),
114 )
115
116 @inlineCallbacks
117 def test_power_off(self):
118 mock_get_partition = self.patch(self.hmcz, "_get_partition")
119 yield self.hmcz.power_off(None, self.make_context())
120 self.assertThat(
121 mock_get_partition.return_value.stop,
122 MockCalledOnceWith(wait_for_completion=False),
123 )
124
125 @inlineCallbacks
126 def test_power_query_starting(self):
127 power_partition_name = factory.make_name("power_partition_name")
128 cpc = self.fake_session.hmc.cpcs.add(
129 {
130 "name": factory.make_name("cpc"),
131 "dpm-enabled": True,
132 }
133 )
134 cpc.partitions.add(
135 {
136 "name": power_partition_name,
137 "status": "starting",
138 }
139 )
140
141 status = yield self.hmcz.power_query(
142 None, self.make_context(power_partition_name)
143 )
144
145 self.assertEqual("on", status)
146
147 @inlineCallbacks
148 def test_power_query_active(self):
149 power_partition_name = factory.make_name("power_partition_name")
150 cpc = self.fake_session.hmc.cpcs.add(
151 {
152 "name": factory.make_name("cpc"),
153 "dpm-enabled": True,
154 }
155 )
156 cpc.partitions.add(
157 {
158 "name": power_partition_name,
159 "status": "active",
160 }
161 )
162
163 status = yield self.hmcz.power_query(
164 None, self.make_context(power_partition_name)
165 )
166
167 self.assertEqual("on", status)
168
169 @inlineCallbacks
170 def test_power_query_stopping(self):
171 power_partition_name = factory.make_name("power_partition_name")
172 cpc = self.fake_session.hmc.cpcs.add(
173 {
174 "name": factory.make_name("cpc"),
175 "dpm-enabled": True,
176 }
177 )
178 cpc.partitions.add(
179 {
180 "name": power_partition_name,
181 "status": "stopping",
182 }
183 )
184
185 status = yield self.hmcz.power_query(
186 None, self.make_context(power_partition_name)
187 )
188
189 self.assertEqual("off", status)
190
191 @inlineCallbacks
192 def test_power_query_stopped(self):
193 power_partition_name = factory.make_name("power_partition_name")
194 cpc = self.fake_session.hmc.cpcs.add(
195 {
196 "name": factory.make_name("cpc"),
197 "dpm-enabled": True,
198 }
199 )
200 cpc.partitions.add(
201 {
202 "name": power_partition_name,
203 "status": "stopped",
204 }
205 )
206
207 status = yield self.hmcz.power_query(
208 None, self.make_context(power_partition_name)
209 )
210
211 self.assertEqual("off", status)
212
213 @inlineCallbacks
214 def test_power_query_paused(self):
215 power_partition_name = factory.make_name("power_partition_name")
216 cpc = self.fake_session.hmc.cpcs.add(
217 {
218 "name": factory.make_name("cpc"),
219 "dpm-enabled": True,
220 }
221 )
222 cpc.partitions.add(
223 {
224 "name": power_partition_name,
225 "status": "paused",
226 }
227 )
228
229 status = yield self.hmcz.power_query(
230 None, self.make_context(power_partition_name)
231 )
232
233 self.assertEqual("unknown", status)
234
235 @inlineCallbacks
236 def test_power_query_other(self):
237 power_partition_name = factory.make_name("power_partition_name")
238 cpc = self.fake_session.hmc.cpcs.add(
239 {
240 "name": factory.make_name("cpc"),
241 "dpm-enabled": True,
242 }
243 )
244 cpc.partitions.add(
245 {
246 "name": power_partition_name,
247 "status": factory.make_name("status"),
248 }
249 )
250
251 status = yield self.hmcz.power_query(
252 None, self.make_context(power_partition_name)
253 )
254
255 self.assertEqual("unknown", status)
diff --git a/utilities/check-imports b/utilities/check-imports
index ac9a3a7..dc7b496 100755
--- a/utilities/check-imports
+++ b/utilities/check-imports
@@ -164,6 +164,7 @@ TestingLibraries = Pattern(
164 "testresources|testresources.**",164 "testresources|testresources.**",
165 "testscenarios|testscenarios.**",165 "testscenarios|testscenarios.**",
166 "testtools|testtools.**",166 "testtools|testtools.**",
167 "zhmcclient_mock.**",
167)168)
168169
169170
@@ -235,6 +236,7 @@ RackControllerRule = Rule(
235 Allow("urllib3|urllib3.**"),236 Allow("urllib3|urllib3.**"),
236 Allow("uvloop"),237 Allow("uvloop"),
237 Allow("yaml"),238 Allow("yaml"),
239 Allow("zhmcclient.**"),
238 Allow("zope.interface|zope.interface.**"),240 Allow("zope.interface|zope.interface.**"),
239 Allow(StandardLibraries),241 Allow(StandardLibraries),
240)242)

Subscribers

People subscribed via source and target branches