Merge ~ltrager/maas:s390x-power-driver into maas:master

Proposed by Lee Trager
Status: Rejected
Rejected by: Blake Rouse
Proposed branch: ~ltrager/maas:s390x-power-driver
Merge into: maas:master
Prerequisite: ~ltrager/maas:s390x-dpm-boot
Diff against target: 551 lines (+356/-4)
14 files modified
snap/snapcraft.yaml (+2/-0)
src/maasserver/clusterrpc/driver_parameters.py (+2/-0)
src/maasserver/clusterrpc/tests/test_driver_parameters.py (+21/-1)
src/maasserver/forms/__init__.py (+23/-2)
src/maasserver/forms/tests/test_machine.py (+17/-0)
src/maasserver/forms/tests/test_machinewithmacaddresses.py (+11/-0)
src/provisioningserver/drivers/__init__.py (+2/-1)
src/provisioningserver/drivers/hardware/s390x.py (+61/-0)
src/provisioningserver/drivers/hardware/tests/test_s390x.py (+83/-0)
src/provisioningserver/drivers/power/registry.py (+2/-0)
src/provisioningserver/drivers/power/s390x.py (+70/-0)
src/provisioningserver/drivers/power/tests/test_s390x.py (+60/-0)
src/provisioningserver/drivers/tests/test_base.py (+1/-0)
utilities/check-imports (+1/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Needs Fixing
Mike Pontillo (community) Approve
Review via email: mp+361231@code.launchpad.net

Commit message

Add S390X power driver.

LPARs on the S390X are identified by both the boot and power driver using the
same UUID. The power form mixin has been modified to allow power fields to be
copied into node fields on form save. As MAC addresses are not consistent and
the S390X power driver can only be used on the S390X architecture neither
field is required when adding a machine.

Description of the change

TODO: Add power driver dependencies - pending LP:1805367

Known: The HMC takes awhile to respond. It can take ~3minutes to perform a power action.

When a Linux host shuts itself down the LPAR goes into a paused state. A paused LPAR is not running but it cannot be started until its stopped first. When an LPAR starts it goes Stopped -> Starting -> Paused -> Active. Stopping a paused LPAR is the solution but checking, stopping, then starting will be a long delay due to the HMC.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b s390x-power-driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/4812/console
COMMIT: dce33319b74388df36175880b9bce2e9cbd8f9e8

review: Needs Fixing
~ltrager/maas:s390x-power-driver updated
de19895... by Lee Trager

Merge branch 'master' into node-uuid

7a47669... by Lee Trager

Merge branch 'node-uuid' into s390x-dpm-boot

ea1e980... by Lee Trager

Fix lint

bbb853a... by Lee Trager

Filter out ip= on the kernel command line for s390x

86b4d6c... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

89490c3... by Lee Trager

Fix missing import

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b s390x-power-driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/4828/console
COMMIT: 89490c377b249c10249ba39ce1bc1d39c253d733

review: Needs Fixing
Revision history for this message
Mike Pontillo (mpontillo) wrote :

You might plan to do this in a later branch, but I feel like if `zhmcclient` isn't installed, we should do something like what we do the VMware power driver.

See src/provisioningserver/drivers/hardware/vmware.py - try_pyvmomi_import().

The most confusing aspect of this branch is the usage of node_field; it isn't immediately clear what it means. (Some comments on that below; I feel this could be mitigated by improving the comments.)

What is the expected user experience for this change? Should the user be able to edit the UUID in both the power parameters /and/ the node model? It looks like the user is expected to input the UUID at machine creation time. Subsequently, can the user edit the hardware_uuid on the Machine? I don't see where we ever get the value of the node_field except when the form is submitted.

When you setattr() and the hardware_uuid field gets set on the Node model, will that always cause the Node to be saved, both in the case where a new Node is created, /and/ if the UUID is edited? (The setattr() seems to implicitly imply a save().)

review: Needs Information
~ltrager/maas:s390x-power-driver updated
83eda29... by Lee Trager

Merge branch 'master' into node-uuid

e453feb... by Lee Trager

Merge branch 'node-uuid' into s390x-dpm-boot

760bf04... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

ec3a671... by Lee Trager

mpontillo fixes

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

Thanks for the review. I've added support for detecting if zhmcclient is missing from the system and gracefully ignoring it.

My node-uuid branch[1] adds support to MAAS to detect the hardware_uuid in lshw output during commissioning and allows booting using the hardware_uuid as an identifier. A user can't edit this value it must be discovered during commissioning. On S390X the hardware_uuid is used to identify which LPAR to power control and is the only way to identify a system while booting. Because MAAS needs to be able to identify the system node_field allows the user defined hardware_uuid from the power driver to be automatically copied to the node model. This allows us to avoid having to do an extra database query into the BMC model when systems are booting.

I did this generically using the name node_field as I could see this being useful for other power drivers in the future. For example virsh hosts normally use the same name for the VM ID and hostname.

[1] https://code.launchpad.net/~ltrager/maas/+git/maas/+merge/360110

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b s390x-power-driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/4860/console
COMMIT: ec3a6713fc35afb204cd699cc4212586392790bf

review: Needs Fixing
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks for the updates; I'm happy with this branch now. I think Newell should also take a look, since he's got a lot of experience with the power drivers.

My one nit is "what happens if the client can't be imported" - I see you have a try/except in there, but I wonder what the user experience will be if the import doesn't succeed. (Maybe some logging should be added?)

I suggest you work with Newell to ensure this is consistent with other power drivers before landing.

review: Approve
~ltrager/maas:s390x-power-driver updated
620073f... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

c1a87c0... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b s390x-power-driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/4946/console
COMMIT: c1a87c09768d53bc55bd2f9d6fa3846cd9f96d5c

review: Needs Fixing
~ltrager/maas:s390x-power-driver updated
dbc1cd5... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

a8702ba... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b s390x-power-driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: a8702baa04c0be9b7711f770ef390185dab46409

review: Approve
~ltrager/maas:s390x-power-driver updated
58651ef... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

698cefe... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b s390x-power-driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 698cefe90d09aea61c7c4437dbf9c6f35458faa1

review: Approve
~ltrager/maas:s390x-power-driver updated
c7af01e... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

ed525e9... by Lee Trager

Fix merge mistakes

5e0a738... by Lee Trager

Track transfer time for s390x DPM boot configuration files.

ce9269d... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

616d0b5... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

b64c34b... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

77306d0... by Lee Trager

Fix broken tests

166980c... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

af170f4... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

107f430... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

1e2a32c... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

f395212... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

9dbaef8... by Lee Trager

Stop partition before starting it.

01a0d47... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

3377f4e... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

9a3e330... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

0cad56b... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b s390x-power-driver lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/6129/console
COMMIT: 0cad56b1a682511ef3509fa4d8648d9a377041f2

review: Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Rejecting due to inactivity.

Unmerged commits

0cad56b... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

9a3e330... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

3377f4e... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

01a0d47... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

9dbaef8... by Lee Trager

Stop partition before starting it.

f395212... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

1e2a32c... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

107f430... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

af170f4... by Lee Trager

Merge branch 'master' into s390x-dpm-boot

166980c... by Lee Trager

Merge branch 's390x-dpm-boot' into s390x-power-driver

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
2index e1a40bb..1355808 100644
3--- a/snap/snapcraft.yaml
4+++ b/snap/snapcraft.yaml
5@@ -96,6 +96,8 @@ parts:
6 - squid
7 - tcpdump
8 - ubuntu-keyring
9+ python-packages:
10+ - zhmcclient
11 organize:
12 lib/python3.*/site-packages/etc/*: etc
13 lib/python3.*/site-packages/usr/bin/*: usr/bin
14diff --git a/src/maasserver/clusterrpc/driver_parameters.py b/src/maasserver/clusterrpc/driver_parameters.py
15index 3502cd3..dc1746e 100644
16--- a/src/maasserver/clusterrpc/driver_parameters.py
17+++ b/src/maasserver/clusterrpc/driver_parameters.py
18@@ -83,6 +83,8 @@ def make_form_field(json_field):
19 form_field = field_class(
20 label=json_field['label'], required=json_field['required'],
21 **extra_parameters)
22+ form_field.name = json_field['name']
23+ form_field.node_field = json_field.get('node_field')
24 return form_field
25
26
27diff --git a/src/maasserver/clusterrpc/tests/test_driver_parameters.py b/src/maasserver/clusterrpc/tests/test_driver_parameters.py
28index f03c20d..467831e 100644
29--- a/src/maasserver/clusterrpc/tests/test_driver_parameters.py
30+++ b/src/maasserver/clusterrpc/tests/test_driver_parameters.py
31@@ -1,4 +1,4 @@
32-# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
33+# Copyright 2012-2018 Canonical Ltd. This software is licensed under the
34 # GNU Affero General Public License version 3 (see the file LICENSE).
35
36 """Tests for power parameters."""
37@@ -191,6 +191,23 @@ class TestMakeFormField(MAASServerTestCase):
38 django_field = make_form_field(json_field)
39 self.assertEquals(json_field['default'], django_field.initial)
40
41+ def test__sets_node_field(self):
42+ # Tests that if a power drive specifies a field should be mapped to
43+ # the node model the copy happens. Currently only the s390x power
44+ # driver uses this for the hardware_uuid field.
45+ name = factory.make_name('name')
46+ node_field = factory.make_name('node_field')
47+ json_field = {
48+ 'name': name,
49+ 'label': 'Some Field',
50+ 'field_type': 'string',
51+ 'required': False,
52+ 'node_field': node_field,
53+ }
54+ django_field = make_form_field(json_field)
55+ self.assertEquals(json_field['name'], django_field.name)
56+ self.assertEquals(json_field['node_field'], django_field.node_field)
57+
58
59 class TestMakeSettingField(MAASServerTestCase):
60 """Test that make_setting_field() creates JSON-verifiable fields."""
61@@ -209,6 +226,7 @@ class TestMakeSettingField(MAASServerTestCase):
62 'choices': [],
63 'default': '',
64 'scope': 'bmc',
65+ 'node_field': None,
66 }
67 self.assertEqual(expected_field, json_field)
68
69@@ -224,6 +242,7 @@ class TestMakeSettingField(MAASServerTestCase):
70 ],
71 'default': 'spam',
72 'scope': 'bmc',
73+ 'node_field': None,
74 }
75 json_field = make_setting_field(**expected_field)
76 self.assertEqual(expected_field, json_field)
77@@ -244,6 +263,7 @@ class TestMakeSettingField(MAASServerTestCase):
78 'choices': [],
79 'default': '',
80 'scope': 'bmc',
81+ 'node_field': None,
82 }
83 self.assertEqual(expected_field, json_field)
84
85diff --git a/src/maasserver/forms/__init__.py b/src/maasserver/forms/__init__.py
86index 021acdd..0347ab1 100644
87--- a/src/maasserver/forms/__init__.py
88+++ b/src/maasserver/forms/__init__.py
89@@ -379,6 +379,17 @@ class WithPowerTypeMixin:
90 if type_changed or len(initial_parameters) > 0:
91 machine.power_parameters = form.cleaned_data.get(
92 params_field_name)
93+ # Set the specified node fields to BMC field values. This allows for
94+ # a power driver field to be copied onto a node model field. s390x
95+ # uses the hardware_uuid field to identify the LPAR to the power driver
96+ # and boots by hardware_uuid, not MAC. This allows a user to set both
97+ # using the power field as a user normally can't set the hardware_uuid
98+ # field.
99+ for field in form.fields['power_parameters'].fields:
100+ if getattr(field, 'node_field', None):
101+ setattr(
102+ machine, field.node_field,
103+ form.cleaned_data['power_parameters'][field.name])
104
105 def clean(self):
106 cleaned_data = super(WithPowerTypeMixin, self).clean()
107@@ -738,8 +749,11 @@ class MachineForm(NodeForm):
108 invalid_arch_message = compose_invalid_choice_text(
109 'architecture', choices)
110 power_type = self.data.get('power_type', None)
111+ if power_type == 's390x':
112+ default_arch = 's390x/generic'
113 self.fields['architecture'] = forms.ChoiceField(
114- choices=choices, required=(power_type != 'ipmi' or requires_arch),
115+ choices=choices, required=(
116+ power_type not in ['ipmi', 's390x'] or requires_arch),
117 initial=default_arch, error_messages={
118 'invalid_choice': invalid_arch_message})
119
120@@ -801,6 +815,12 @@ class MachineForm(NodeForm):
121 distro_series)
122 except ValidationError as e:
123 set_form_error(self, 'hwe_kernel', e.message)
124+
125+ # The s390x power driver can only be used on s390x.
126+ if (not cleaned_data.get('architecture') and
127+ self.data.get('power_type') == 's390x'):
128+ cleaned_data['architecture'] = 's390x/generic'
129+
130 return cleaned_data
131
132 def is_valid(self):
133@@ -1286,7 +1306,8 @@ class WithMACAddressesMixin:
134 def set_up_mac_addresses_field(self):
135 macs = [mac for mac in self.data.getlist('mac_addresses') if mac]
136 self.fields['mac_addresses'] = MultipleMACAddressField(
137- len(macs), required=(self.data.get('power_type') != 'ipmi'))
138+ len(macs), required=(
139+ self.data.get('power_type') not in ['ipmi', 's390x']))
140 self.data = self.data.copy()
141 self.data['mac_addresses'] = macs
142
143diff --git a/src/maasserver/forms/tests/test_machine.py b/src/maasserver/forms/tests/test_machine.py
144index 9a79406..664f0de 100644
145--- a/src/maasserver/forms/tests/test_machine.py
146+++ b/src/maasserver/forms/tests/test_machine.py
147@@ -461,3 +461,20 @@ class TestAdminMachineForm(MAASServerTestCase):
148 instance=node)
149 node = form.save()
150 self.assertEqual(power_type, node.power_type)
151+
152+ def test_AdminMachineForm_updates_node_fields(self):
153+ arch = make_usable_architecture(self)
154+ uuid = factory.make_UUID()
155+ form = AdminMachineForm(data={
156+ 'architecture': arch,
157+ 'power_type': 's390x',
158+ 'power_parameters': {
159+ 'power_address': factory.make_ip_address(),
160+ 'power_user': factory.make_name('user'),
161+ 'power_pass': factory.make_name('pass'),
162+ 'power_uuid': uuid,
163+ },
164+ })
165+ self.assertTrue(form.is_valid(), form.errors)
166+ node = form.save()
167+ self.assertEqual(uuid, node.hardware_uuid)
168diff --git a/src/maasserver/forms/tests/test_machinewithmacaddresses.py b/src/maasserver/forms/tests/test_machinewithmacaddresses.py
169index 8bc5f98..0b51072 100644
170--- a/src/maasserver/forms/tests/test_machinewithmacaddresses.py
171+++ b/src/maasserver/forms/tests/test_machinewithmacaddresses.py
172@@ -159,6 +159,17 @@ class MachineWithMACAddressesFormTest(MAASServerTestCase):
173 form = MachineWithMACAddressesForm(data=params)
174 self.assertTrue(form.is_valid())
175
176+ def test__no_architecture_or_mac_addresses_is_ok_for_s390x(self):
177+ # S390X hosts are identified by hardware_uuid
178+ params = self.make_params(
179+ mac_addresses=[], architecture='s390x/generic')
180+ params['architecture'] = None
181+ params['power_type'] = 's390x'
182+ form = MachineWithMACAddressesForm(data=params)
183+ self.assertTrue(form.is_valid(), form.errors)
184+ node = form.save()
185+ self.assertEquals('s390x/generic', node.architecture)
186+
187 def test__save(self):
188 macs = ['aa:bb:cc:dd:ee:ff', '9a:bb:c3:33:e5:7f']
189 form = MachineWithMACAddressesForm(
190diff --git a/src/provisioningserver/drivers/__init__.py b/src/provisioningserver/drivers/__init__.py
191index 2770870..3fde00a 100644
192--- a/src/provisioningserver/drivers/__init__.py
193+++ b/src/provisioningserver/drivers/__init__.py
194@@ -126,7 +126,7 @@ class SETTING_SCOPE:
195
196 def make_setting_field(
197 name, label, field_type=None, choices=None, default=None,
198- required=False, scope=SETTING_SCOPE.BMC):
199+ required=False, scope=SETTING_SCOPE.BMC, node_field=None):
200 """Helper function for building a JSON setting parameters field.
201
202 :param name: The name of the field.
203@@ -165,6 +165,7 @@ def make_setting_field(
204 'choices': choices,
205 'default': default,
206 'scope': scope,
207+ 'node_field': node_field,
208 }
209
210
211diff --git a/src/provisioningserver/drivers/hardware/s390x.py b/src/provisioningserver/drivers/hardware/s390x.py
212new file mode 100644
213index 0000000..3677f65
214--- /dev/null
215+++ b/src/provisioningserver/drivers/hardware/s390x.py
216@@ -0,0 +1,61 @@
217+# Copyright 2019 Canonical Ltd. This software is licensed under the
218+# GNU Affero General Public License version 3 (see the file LICENSE).
219+
220+from provisioningserver.logger import get_maas_logger
221+
222+
223+maaslog = get_maas_logger('drivers.s390x')
224+
225+
226+class S390XHMCClient:
227+
228+ def __init__(self, host, user, password):
229+ # Attempt to import zhmcclient
230+ try:
231+ import zhmcclient
232+ except ImportError:
233+ self.session = None
234+ self.client = None
235+ else:
236+ self.session = zhmcclient.Session(host, user, password)
237+ self.client = zhmcclient.Client(self.session)
238+
239+ def get_power_state(self, uuid):
240+ maaslog.info('Checking power state for {}'.format(uuid))
241+ partition = self._get_partition(uuid)
242+ if partition:
243+ return partition.properties['status']
244+ else:
245+ return 'unknown'
246+
247+ def start_partition(self, uuid):
248+ partition = self._get_partition(uuid)
249+ if partition:
250+ if partition.properties['status'] == 'active':
251+ # Partition is already running, nothing to do.
252+ return
253+ elif partition.properties['status'] != 'stopped':
254+ # Partition cannot be started unless it is in a 'stopped'
255+ # state. LPAR's which are shutdown from within Linux go
256+ # into a 'paused' state which cannot be started without
257+ # being stopped first.
258+ partition.stop(wait_for_completion=True)
259+ partition.start(wait_for_completion=False)
260+
261+ def stop_partition(self, uuid):
262+ partition = self._get_partition(uuid)
263+ if partition:
264+ partition.stop(wait_for_completion=False)
265+
266+ def _get_partition(self, uuid):
267+ if None in (self.session, self.client):
268+ return None
269+ for cpc in self.client.cpcs.list():
270+ if not cpc.dpm_enabled:
271+ maaslog.info('DPM is not enabled')
272+ continue
273+ partitions = cpc.partitions.list(
274+ full_properties=True, filter_args={'object-id': uuid})
275+ if partitions:
276+ maaslog.info('Found partition')
277+ return partitions[0]
278diff --git a/src/provisioningserver/drivers/hardware/tests/test_s390x.py b/src/provisioningserver/drivers/hardware/tests/test_s390x.py
279new file mode 100644
280index 0000000..327f1ae
281--- /dev/null
282+++ b/src/provisioningserver/drivers/hardware/tests/test_s390x.py
283@@ -0,0 +1,83 @@
284+# Copyright 2019 Canonical Ltd. This software is licensed under the
285+# GNU Affero General Public License version 3 (see the file LICENSE).
286+
287+"""Tests for `provisioningserver.drivers.hardware.s390x`.
288+"""
289+
290+__all__ = []
291+
292+from unittest.mock import MagicMock
293+
294+from maastesting.factory import factory
295+from maastesting.matchers import (
296+ MockCalledOnceWith,
297+ MockNotCalled,
298+)
299+from maastesting.testcase import MAASTestCase
300+from provisioningserver.drivers.hardware import s390x
301+
302+
303+class TestS390XHMCClient(MAASTestCase):
304+ """Tests for `S390XHMCClient`."""
305+
306+ def test_get_power_state(self):
307+ client = s390x.S390XHMCClient('foo', 'bar', 'baz')
308+ mock_get_partition = self.patch(client, '_get_partition')
309+ part = MagicMock()
310+ mock_get_partition.return_value = part
311+ self.assertEquals(
312+ part.properties['status'],
313+ client.get_power_state(factory.make_UUID()))
314+
315+ def test_get_power_state_unknown_with_no_driver(self):
316+ client = s390x.S390XHMCClient('foo', 'bar', 'baz')
317+ client.session = client.client = None
318+ self.assertEquals(
319+ 'unknown', client.get_power_state(factory.make_UUID()))
320+
321+ def test_start_partition(self):
322+ client = s390x.S390XHMCClient('foo', 'bar', 'baz')
323+ uuid = factory.make_UUID()
324+ mock_get_partition = self.patch(client, '_get_partition')
325+ mock_get_partition.return_value = MagicMock()
326+ client.start_partition(uuid)
327+ self.assertThat(mock_get_partition, MockCalledOnceWith(uuid))
328+ self.assertThat(
329+ mock_get_partition.return_value.start,
330+ MockCalledOnceWith(wait_for_completion=False))
331+
332+ def test_start_partition_does_nothing_if_on(self):
333+ client = s390x.S390XHMCClient('foo', 'bar', 'baz')
334+ uuid = factory.make_UUID()
335+ partition = MagicMock()
336+ partition.properties = {'status': 'active'}
337+ mock_get_partition = self.patch(client, '_get_partition')
338+ mock_get_partition.return_value = partition
339+ client.start_partition(uuid)
340+ self.assertThat(mock_get_partition, MockCalledOnceWith(uuid))
341+ self.assertThat(partition.start, MockNotCalled())
342+
343+ def test_start_partition_stops_if_needed(self):
344+ client = s390x.S390XHMCClient('foo', 'bar', 'baz')
345+ uuid = factory.make_UUID()
346+ partition = MagicMock()
347+ partition.properties = {'status': factory.make_name('status')}
348+ mock_get_partition = self.patch(client, '_get_partition')
349+ mock_get_partition.return_value = partition
350+ client.start_partition(uuid)
351+ self.assertThat(mock_get_partition, MockCalledOnceWith(uuid))
352+ self.assertThat(
353+ partition.stop, MockCalledOnceWith(wait_for_completion=True))
354+ self.assertThat(
355+ partition.start, MockCalledOnceWith(wait_for_completion=False))
356+
357+ def test_stop_partition(self):
358+ client = s390x.S390XHMCClient('foo', 'bar', 'baz')
359+ uuid = factory.make_UUID()
360+ mock_get_partition = self.patch(client, '_get_partition')
361+ mock_get_partition.return_value = MagicMock()
362+ client.stop_partition(uuid)
363+ self.assertThat(mock_get_partition, MockCalledOnceWith(uuid))
364+ self.assertThat(
365+ mock_get_partition.return_value.stop,
366+ MockCalledOnceWith(wait_for_completion=False))
367diff --git a/src/provisioningserver/drivers/power/registry.py b/src/provisioningserver/drivers/power/registry.py
368index ce228bd..98dcc74 100644
369--- a/src/provisioningserver/drivers/power/registry.py
370+++ b/src/provisioningserver/drivers/power/registry.py
371@@ -24,6 +24,7 @@ from provisioningserver.drivers.power.nova import NovaPowerDriver
372 from provisioningserver.drivers.power.openbmc import OpenBMCPowerDriver
373 from provisioningserver.drivers.power.recs import RECSPowerDriver
374 from provisioningserver.drivers.power.redfish import RedfishPowerDriver
375+from provisioningserver.drivers.power.s390x import S390XPowerDriver
376 from provisioningserver.drivers.power.seamicro import SeaMicroPowerDriver
377 from provisioningserver.drivers.power.ucsm import UCSMPowerDriver
378 from provisioningserver.drivers.power.virsh import VirshPowerDriver
379@@ -65,6 +66,7 @@ power_drivers = [
380 OpenBMCPowerDriver(),
381 RECSPowerDriver(),
382 RedfishPowerDriver(),
383+ S390XPowerDriver(),
384 SeaMicroPowerDriver(),
385 UCSMPowerDriver(),
386 VirshPowerDriver(),
387diff --git a/src/provisioningserver/drivers/power/s390x.py b/src/provisioningserver/drivers/power/s390x.py
388new file mode 100644
389index 0000000..287d6af
390--- /dev/null
391+++ b/src/provisioningserver/drivers/power/s390x.py
392@@ -0,0 +1,70 @@
393+# Copyright 2018 Canonical Ltd. This software is licensed under the
394+# GNU Affero General Public License version 3 (see the file LICENSE).
395+
396+"""s390x Power Driver."""
397+
398+__all__ = []
399+
400+from importlib import import_module
401+
402+from provisioningserver.drivers import (
403+ make_ip_extractor,
404+ make_setting_field,
405+ SETTING_SCOPE,
406+)
407+from provisioningserver.drivers.hardware.s390x import S390XHMCClient
408+from provisioningserver.drivers.power import PowerDriver
409+
410+
411+def get_client(context):
412+ return S390XHMCClient(
413+ host=context['power_address'], user=context['power_user'],
414+ password=context['power_pass'])
415+
416+
417+class S390XPowerDriver(PowerDriver):
418+
419+ name = 's390x'
420+ chassis = False
421+ description = 'IBM Z (s390x)'
422+ settings = [
423+ make_setting_field('power_address', 'HMC host', required=True),
424+ make_setting_field('power_user', 'HMC user', required=True),
425+ make_setting_field(
426+ 'power_pass', 'HMC password',
427+ required=True, field_type='password'),
428+ make_setting_field(
429+ 'power_uuid', 'Partition UUID', scope=SETTING_SCOPE.NODE,
430+ required=True, node_field='hardware_uuid'),
431+ ]
432+
433+ ip_extractor = make_ip_extractor('power_address')
434+
435+ def detect_missing_packages(self):
436+ try:
437+ import_module('zhmcclient')
438+ except ImportError:
439+ return ['python3-zhmcclient']
440+ else:
441+ return []
442+
443+ def power_on(self, system_id, context):
444+ """Power on S390X partition."""
445+ client = get_client(context)
446+ client.start_partition(context['power_uuid'])
447+
448+ def power_off(self, system_id, context):
449+ """Power off Virsh node."""
450+ client = get_client(context)
451+ client.stop_partition(context['power_uuid'])
452+
453+ def power_query(self, system_id, context):
454+ """Power query Virsh node."""
455+ client = get_client(context)
456+ state = client.get_power_state(context['power_uuid'])
457+ if state in ['starting', 'active', 'stopping', 'degraded']:
458+ return 'on'
459+ elif state == 'unknown':
460+ return 'unknown'
461+ else:
462+ return 'off'
463diff --git a/src/provisioningserver/drivers/power/tests/test_s390x.py b/src/provisioningserver/drivers/power/tests/test_s390x.py
464new file mode 100644
465index 0000000..3fccceb
466--- /dev/null
467+++ b/src/provisioningserver/drivers/power/tests/test_s390x.py
468@@ -0,0 +1,60 @@
469+# Copyright 2018 Canonical Ltd. This software is licensed under the
470+# GNU Affero General Public License version 3 (see the file LICENSE).
471+
472+"""Tests for `provisioningserver.drivers.power.s390x`."""
473+
474+__all__ = []
475+
476+import random
477+from unittest.mock import MagicMock
478+
479+from maastesting.factory import factory
480+from maastesting.matchers import MockCalledOnceWith
481+from maastesting.testcase import MAASTestCase
482+from provisioningserver.drivers.power import s390x as s390x_module
483+from provisioningserver.drivers.power.s390x import S390XPowerDriver
484+
485+
486+class TestS390XPowerDriver(MAASTestCase):
487+
488+ def setUp(self):
489+ super().setUp()
490+ self.driver = S390XPowerDriver()
491+ self.mock_get_client = self.patch(s390x_module, 'get_client')
492+ self.system_id = factory.make_name('system_id')
493+ self.context = {
494+ 'power_uuid': factory.make_UUID(),
495+ }
496+
497+ def test_power_on(self):
498+ self.driver.power_on(self.system_id, self.context)
499+ self.assertThat(
500+ self.mock_get_client.return_value.start_partition,
501+ MockCalledOnceWith(self.context['power_uuid']))
502+
503+ def test_power_off(self):
504+ self.driver.power_off(self.system_id, self.context)
505+ self.assertThat(
506+ self.mock_get_client.return_value.stop_partition,
507+ MockCalledOnceWith(self.context['power_uuid']))
508+
509+ def test_power_query_on(self):
510+ mock_client = MagicMock()
511+ self.mock_get_client.return_value = mock_client
512+ mock_client.get_power_state.return_value = random.choice([
513+ 'starting', 'active', 'stopping', 'degraded'])
514+ self.assertEquals(
515+ 'on', self.driver.power_query(self.system_id, self.context))
516+ self.assertThat(
517+ mock_client.get_power_state,
518+ MockCalledOnceWith(self.context['power_uuid']))
519+
520+ def test_power_query_off(self):
521+ mock_client = MagicMock()
522+ self.mock_get_client.return_value = mock_client
523+ mock_client.get_power_state.return_value = 'off'
524+ self.assertEquals(
525+ 'off', self.driver.power_query(self.system_id, self.context))
526+ self.assertThat(
527+ mock_client.get_power_state,
528+ MockCalledOnceWith(self.context['power_uuid']))
529diff --git a/src/provisioningserver/drivers/tests/test_base.py b/src/provisioningserver/drivers/tests/test_base.py
530index 250fa09..726b727 100644
531--- a/src/provisioningserver/drivers/tests/test_base.py
532+++ b/src/provisioningserver/drivers/tests/test_base.py
533@@ -229,6 +229,7 @@ class TestMakeSettingField(MAASTestCase):
534 'default': default,
535 'required': True,
536 'scope': SETTING_SCOPE.NODE,
537+ 'node_field': None,
538 }, setting)
539
540
541diff --git a/utilities/check-imports b/utilities/check-imports
542index 6344767..d75e720 100755
543--- a/utilities/check-imports
544+++ b/utilities/check-imports
545@@ -232,6 +232,7 @@ RackControllerRule = Rule(
546 Allow("uvloop"),
547 Allow("yaml"),
548 Allow("zope.interface|zope.interface.**"),
549+ Allow("zhmcclient|zhmcclient.**|zhmcclient_mock.**"),
550 Allow(StandardLibraries),
551 )
552

Subscribers

People subscribed via source and target branches