Merge ~newell-jensen/maas:2.5-lp1814623 into maas:2.5

Proposed by Newell Jensen
Status: Merged
Approved by: Newell Jensen
Approved revision: ce19827232ef9f86df01ec94cf0e9741b82c4302
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~newell-jensen/maas:2.5-lp1814623
Merge into: maas:2.5
Diff against target: 242 lines (+67/-34)
2 files modified
src/provisioningserver/drivers/power/redfish.py (+45/-21)
src/provisioningserver/drivers/power/tests/test_redfish.py (+22/-13)
Reviewer Review Type Date Requested Status
Newell Jensen (community) Approve
Review via email: mp+362933@code.launchpad.net

Commit message

backport 225eed04a3b40bd2b2d2b777bf70030452ecf394 - LP: #1814623 -- Don't require node_id power parameter for Redfish. If not supplied by the user, search for the first available system's id.

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

Self approved backport.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/provisioningserver/drivers/power/redfish.py b/src/provisioningserver/drivers/power/redfish.py
index 5b47e21..bca30b5 100644
--- a/src/provisioningserver/drivers/power/redfish.py
+++ b/src/provisioningserver/drivers/power/redfish.py
@@ -1,4 +1,4 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the1# Copyright 2018-2019 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"""Redfish Power Driver."""4"""Redfish Power Driver."""
@@ -11,7 +11,10 @@ from base64 import b64encode
11from http import HTTPStatus11from http import HTTPStatus
12from io import BytesIO12from io import BytesIO
13import json13import json
14from os.path import join14from os.path import (
15 basename,
16 join,
17)
1518
16from provisioningserver.drivers import (19from provisioningserver.drivers import (
17 make_ip_extractor,20 make_ip_extractor,
@@ -42,7 +45,7 @@ from twisted.web.http_headers import Headers
42REDFISH_POWER_CONTROL_ENDPOINT = (45REDFISH_POWER_CONTROL_ENDPOINT = (
43 b"redfish/v1/Systems/%s/Actions/ComputerSystem.Reset/")46 b"redfish/v1/Systems/%s/Actions/ComputerSystem.Reset/")
4447
45REDFISH_SYSTEMS_ENDPOINT = b"redfish/v1/Systems/%s/"48REDFISH_SYSTEMS_ENDPOINT = b"redfish/v1/Systems/"
4649
4750
48class WebClientContextFactory(BrowserLikePolicyForHTTPS):51class WebClientContextFactory(BrowserLikePolicyForHTTPS):
@@ -78,13 +81,6 @@ class RedfishPowerDriverBase(PowerDriver):
78 }81 }
79 )82 )
8083
81 def process_redfish_context(self, context):
82 """Process Redfish power driver context."""
83 url = self.get_url(context)
84 node_id = context.get('node_id').encode('utf-8')
85 headers = self.make_auth_headers(**context)
86 return url, node_id, headers
87
88 @asynchronous84 @asynchronous
89 def redfish_request(self, method, uri, headers=None, bodyProducer=None):85 def redfish_request(self, method, uri, headers=None, bodyProducer=None):
90 """Send the redfish request and return the response."""86 """Send the redfish request and return the response."""
@@ -144,8 +140,7 @@ class RedfishPowerDriver(RedfishPowerDriverBase):
144 'power_pass', "Redfish password",140 'power_pass', "Redfish password",
145 field_type='password', required=True),141 field_type='password', required=True),
146 make_setting_field(142 make_setting_field(
147 'node_id', "Node ID",143 'node_id', "Node ID", scope=SETTING_SCOPE.NODE),
148 scope=SETTING_SCOPE.NODE, required=True),
149 ]144 ]
150 ip_extractor = make_ip_extractor('power_address')145 ip_extractor = make_ip_extractor('power_address')
151146
@@ -154,9 +149,38 @@ class RedfishPowerDriver(RedfishPowerDriverBase):
154 return []149 return []
155150
156 @inlineCallbacks151 @inlineCallbacks
152 def process_redfish_context(self, context):
153 """Process Redfish power driver context.
154
155 Returns the basename of the first member found
156 in the Redfish Systems:
157
158 "Members": [
159 {
160 "@odata.id": "/redfish/v1/Systems/1"
161 }
162 """
163 url = self.get_url(context)
164 headers = self.make_auth_headers(**context)
165 node_id = context.get('node_id')
166 if node_id:
167 node_id = node_id.encode('utf-8')
168 else:
169 node_id = yield self.get_node_id(url, headers)
170 return url, node_id, headers
171
172 @inlineCallbacks
173 def get_node_id(self, url, headers):
174 uri = join(url, REDFISH_SYSTEMS_ENDPOINT)
175 systems, _ = yield self.redfish_request(b"GET", uri, headers)
176 members = systems.get('Members')
177 member = members[0].get('@odata.id')
178 return basename(member).encode('utf-8')
179
180 @inlineCallbacks
157 def set_pxe_boot(self, url, node_id, headers):181 def set_pxe_boot(self, url, node_id, headers):
158 """Set the machine with node_id to PXE boot."""182 """Set the machine with node_id to PXE boot."""
159 endpoint = REDFISH_SYSTEMS_ENDPOINT % node_id183 endpoint = REDFISH_SYSTEMS_ENDPOINT + b'%s/' % node_id
160 payload = FileBodyProducer(184 payload = FileBodyProducer(
161 BytesIO(185 BytesIO(
162 json.dumps(186 json.dumps(
@@ -185,10 +209,10 @@ class RedfishPowerDriver(RedfishPowerDriverBase):
185209
186 @asynchronous210 @asynchronous
187 @inlineCallbacks211 @inlineCallbacks
188 def power_on(self, system_id, context):212 def power_on(self, node_id, context):
189 """Power on machine."""213 """Power on machine."""
190 url, node_id, headers = self.process_redfish_context(context)214 url, node_id, headers = yield self.process_redfish_context(context)
191 power_state = yield self.power_query(system_id, context)215 power_state = yield self.power_query(node_id, context)
192 # Power off the machine if currently on.216 # Power off the machine if currently on.
193 if power_state == 'on':217 if power_state == 'on':
194 yield self.power("ForceOff", url, node_id, headers)218 yield self.power("ForceOff", url, node_id, headers)
@@ -199,9 +223,9 @@ class RedfishPowerDriver(RedfishPowerDriverBase):
199223
200 @asynchronous224 @asynchronous
201 @inlineCallbacks225 @inlineCallbacks
202 def power_off(self, system_id, context):226 def power_off(self, node_id, context):
203 """Power off machine."""227 """Power off machine."""
204 url, node_id, headers = self.process_redfish_context(context)228 url, node_id, headers = yield self.process_redfish_context(context)
205 # Set to PXE boot.229 # Set to PXE boot.
206 yield self.set_pxe_boot(url, node_id, headers)230 yield self.set_pxe_boot(url, node_id, headers)
207 # Power off the machine.231 # Power off the machine.
@@ -209,9 +233,9 @@ class RedfishPowerDriver(RedfishPowerDriverBase):
209233
210 @asynchronous234 @asynchronous
211 @inlineCallbacks235 @inlineCallbacks
212 def power_query(self, system_id, context):236 def power_query(self, node_id, context):
213 """Power query machine."""237 """Power query machine."""
214 url, node_id, headers = self.process_redfish_context(context)238 url, node_id, headers = yield self.process_redfish_context(context)
215 uri = join(url, REDFISH_SYSTEMS_ENDPOINT % node_id)239 uri = join(url, REDFISH_SYSTEMS_ENDPOINT + b'%s/' % node_id)
216 node_data, _ = yield self.redfish_request(b"GET", uri, headers)240 node_data, _ = yield self.redfish_request(b"GET", uri, headers)
217 return node_data.get('PowerState').lower()241 return node_data.get('PowerState').lower()
diff --git a/src/provisioningserver/drivers/power/tests/test_redfish.py b/src/provisioningserver/drivers/power/tests/test_redfish.py
index f6db6e3..ca2a771 100644
--- a/src/provisioningserver/drivers/power/tests/test_redfish.py
+++ b/src/provisioningserver/drivers/power/tests/test_redfish.py
@@ -1,4 +1,4 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the1# Copyright 2018-2019 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"""Tests for `provisioningserver.drivers.power.redfish`."""4"""Tests for `provisioningserver.drivers.power.redfish`."""
@@ -168,7 +168,6 @@ def make_context():
168 'power_address': factory.make_ipv4_address(),168 'power_address': factory.make_ipv4_address(),
169 'power_user': factory.make_name('power_user'),169 'power_user': factory.make_name('power_user'),
170 'power_pass': factory.make_name('power_pass'),170 'power_pass': factory.make_name('power_pass'),
171 'node_id': factory.make_name('node_id'),
172 }171 }
173172
174173
@@ -332,7 +331,7 @@ class TestRedfishPowerDriver(MAASTestCase):
332 power_change = factory.make_name('power_change')331 power_change = factory.make_name('power_change')
333 url = driver.get_url(context)332 url = driver.get_url(context)
334 headers = driver.make_auth_headers(**context)333 headers = driver.make_auth_headers(**context)
335 node_id = context.get('node_id').encode('utf-8')334 node_id = b'1'
336 mock_file_body_producer = self.patch(335 mock_file_body_producer = self.patch(
337 redfish_module, 'FileBodyProducer')336 redfish_module, 'FileBodyProducer')
338 payload = FileBodyProducer(337 payload = FileBodyProducer(
@@ -354,7 +353,7 @@ class TestRedfishPowerDriver(MAASTestCase):
354 driver = RedfishPowerDriver()353 driver = RedfishPowerDriver()
355 context = make_context()354 context = make_context()
356 url = driver.get_url(context)355 url = driver.get_url(context)
357 node_id = context.get('node_id').encode('utf-8')356 node_id = b'1'
358 headers = driver.make_auth_headers(**context)357 headers = driver.make_auth_headers(**context)
359 mock_file_body_producer = self.patch(358 mock_file_body_producer = self.patch(
360 redfish_module, 'FileBodyProducer')359 redfish_module, 'FileBodyProducer')
@@ -378,21 +377,23 @@ class TestRedfishPowerDriver(MAASTestCase):
378 @inlineCallbacks377 @inlineCallbacks
379 def test__power_on(self):378 def test__power_on(self):
380 driver = RedfishPowerDriver()379 driver = RedfishPowerDriver()
381 system_id = factory.make_name('system_id')
382 context = make_context()380 context = make_context()
383 url = driver.get_url(context)381 url = driver.get_url(context)
384 headers = driver.make_auth_headers(**context)382 headers = driver.make_auth_headers(**context)
385 node_id = context.get('node_id').encode('utf-8')383 node_id = b'1'
384 mock_redfish_request = self.patch(driver, 'redfish_request')
385 mock_redfish_request.return_value = (
386 SAMPLE_JSON_SYSTEMS, None)
386 mock_set_pxe_boot = self.patch(driver, 'set_pxe_boot')387 mock_set_pxe_boot = self.patch(driver, 'set_pxe_boot')
387 mock_power_query = self.patch(driver, 'power_query')388 mock_power_query = self.patch(driver, 'power_query')
388 mock_power_query.return_value = "on"389 mock_power_query.return_value = "on"
389 mock_power = self.patch(driver, 'power')390 mock_power = self.patch(driver, 'power')
390391
391 yield driver.power_on(system_id, context)392 yield driver.power_on(node_id, context)
392 self.assertThat(mock_set_pxe_boot, MockCalledOnceWith(393 self.assertThat(mock_set_pxe_boot, MockCalledOnceWith(
393 url, node_id, headers))394 url, node_id, headers))
394 self.assertThat(mock_power_query, MockCalledOnceWith(395 self.assertThat(mock_power_query, MockCalledOnceWith(
395 system_id, context))396 node_id, context))
396 self.assertThat(mock_power, MockCallsMatch(397 self.assertThat(mock_power, MockCallsMatch(
397 call("ForceOff", url, node_id, headers),398 call("ForceOff", url, node_id, headers),
398 call("On", url, node_id, headers)))399 call("On", url, node_id, headers)))
@@ -400,15 +401,17 @@ class TestRedfishPowerDriver(MAASTestCase):
400 @inlineCallbacks401 @inlineCallbacks
401 def test__power_off(self):402 def test__power_off(self):
402 driver = RedfishPowerDriver()403 driver = RedfishPowerDriver()
403 system_id = factory.make_name('system_id')
404 context = make_context()404 context = make_context()
405 url = driver.get_url(context)405 url = driver.get_url(context)
406 headers = driver.make_auth_headers(**context)406 headers = driver.make_auth_headers(**context)
407 node_id = context.get('node_id').encode('utf-8')407 node_id = b'1'
408 mock_redfish_request = self.patch(driver, 'redfish_request')
409 mock_redfish_request.return_value = (
410 SAMPLE_JSON_SYSTEMS, None)
408 mock_set_pxe_boot = self.patch(driver, 'set_pxe_boot')411 mock_set_pxe_boot = self.patch(driver, 'set_pxe_boot')
409 mock_power = self.patch(driver, 'power')412 mock_power = self.patch(driver, 'power')
410413
411 yield driver.power_off(system_id, context)414 yield driver.power_off(node_id, context)
412 self.assertThat(mock_set_pxe_boot, MockCalledOnceWith(415 self.assertThat(mock_set_pxe_boot, MockCalledOnceWith(
413 url, node_id, headers))416 url, node_id, headers))
414 self.assertThat(mock_power, MockCalledOnceWith(417 self.assertThat(mock_power, MockCalledOnceWith(
@@ -423,7 +426,10 @@ class TestRedfishPowerDriver(MAASTestCase):
423 mock_redfish_request = self.patch(driver, 'redfish_request')426 mock_redfish_request = self.patch(driver, 'redfish_request')
424 NODE_POWERED_ON = deepcopy(SAMPLE_JSON_SYSTEM)427 NODE_POWERED_ON = deepcopy(SAMPLE_JSON_SYSTEM)
425 NODE_POWERED_ON['PowerState'] = "On"428 NODE_POWERED_ON['PowerState'] = "On"
426 mock_redfish_request.return_value = (NODE_POWERED_ON, None)429 mock_redfish_request.side_effect = [
430 (SAMPLE_JSON_SYSTEMS, None),
431 (NODE_POWERED_ON, None),
432 ]
427 power_state = yield driver.power_query(system_id, context)433 power_state = yield driver.power_query(system_id, context)
428 self.assertEquals(power_state, power_change.lower())434 self.assertEquals(power_state, power_change.lower())
429435
@@ -434,6 +440,9 @@ class TestRedfishPowerDriver(MAASTestCase):
434 system_id = factory.make_name('system_id')440 system_id = factory.make_name('system_id')
435 context = make_context()441 context = make_context()
436 mock_redfish_request = self.patch(driver, 'redfish_request')442 mock_redfish_request = self.patch(driver, 'redfish_request')
437 mock_redfish_request.return_value = (SAMPLE_JSON_SYSTEM, None)443 mock_redfish_request.side_effect = [
444 (SAMPLE_JSON_SYSTEMS, None),
445 (SAMPLE_JSON_SYSTEM, None),
446 ]
438 power_state = yield driver.power_query(system_id, context)447 power_state = yield driver.power_query(system_id, context)
439 self.assertEquals(power_state, power_change.lower())448 self.assertEquals(power_state, power_change.lower())

Subscribers

People subscribed via source and target branches