Merge ~newell-jensen/maas:2.5-lp1814623 into maas:2.5
- Git
- lp:~newell-jensen/maas
- 2.5-lp1814623
- Merge into 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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Newell Jensen (community) | Approve | ||
Review via email: mp+362933@code.launchpad.net |
Commit message
backport 225eed04a3b40bd
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/src/provisioningserver/drivers/power/redfish.py b/src/provisioningserver/drivers/power/redfish.py | |||
2 | index 5b47e21..bca30b5 100644 | |||
3 | --- a/src/provisioningserver/drivers/power/redfish.py | |||
4 | +++ b/src/provisioningserver/drivers/power/redfish.py | |||
5 | @@ -1,4 +1,4 @@ | |||
7 | 1 | # Copyright 2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2018-2019 Canonical Ltd. This software is licensed under the |
8 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
9 | 3 | 3 | ||
10 | 4 | """Redfish Power Driver.""" | 4 | """Redfish Power Driver.""" |
11 | @@ -11,7 +11,10 @@ from base64 import b64encode | |||
12 | 11 | from http import HTTPStatus | 11 | from http import HTTPStatus |
13 | 12 | from io import BytesIO | 12 | from io import BytesIO |
14 | 13 | import json | 13 | import json |
16 | 14 | from os.path import join | 14 | from os.path import ( |
17 | 15 | basename, | ||
18 | 16 | join, | ||
19 | 17 | ) | ||
20 | 15 | 18 | ||
21 | 16 | from provisioningserver.drivers import ( | 19 | from provisioningserver.drivers import ( |
22 | 17 | make_ip_extractor, | 20 | make_ip_extractor, |
23 | @@ -42,7 +45,7 @@ from twisted.web.http_headers import Headers | |||
24 | 42 | REDFISH_POWER_CONTROL_ENDPOINT = ( | 45 | REDFISH_POWER_CONTROL_ENDPOINT = ( |
25 | 43 | b"redfish/v1/Systems/%s/Actions/ComputerSystem.Reset/") | 46 | b"redfish/v1/Systems/%s/Actions/ComputerSystem.Reset/") |
26 | 44 | 47 | ||
28 | 45 | REDFISH_SYSTEMS_ENDPOINT = b"redfish/v1/Systems/%s/" | 48 | REDFISH_SYSTEMS_ENDPOINT = b"redfish/v1/Systems/" |
29 | 46 | 49 | ||
30 | 47 | 50 | ||
31 | 48 | class WebClientContextFactory(BrowserLikePolicyForHTTPS): | 51 | class WebClientContextFactory(BrowserLikePolicyForHTTPS): |
32 | @@ -78,13 +81,6 @@ class RedfishPowerDriverBase(PowerDriver): | |||
33 | 78 | } | 81 | } |
34 | 79 | ) | 82 | ) |
35 | 80 | 83 | ||
36 | 81 | def process_redfish_context(self, context): | ||
37 | 82 | """Process Redfish power driver context.""" | ||
38 | 83 | url = self.get_url(context) | ||
39 | 84 | node_id = context.get('node_id').encode('utf-8') | ||
40 | 85 | headers = self.make_auth_headers(**context) | ||
41 | 86 | return url, node_id, headers | ||
42 | 87 | |||
43 | 88 | @asynchronous | 84 | @asynchronous |
44 | 89 | def redfish_request(self, method, uri, headers=None, bodyProducer=None): | 85 | def redfish_request(self, method, uri, headers=None, bodyProducer=None): |
45 | 90 | """Send the redfish request and return the response.""" | 86 | """Send the redfish request and return the response.""" |
46 | @@ -144,8 +140,7 @@ class RedfishPowerDriver(RedfishPowerDriverBase): | |||
47 | 144 | 'power_pass', "Redfish password", | 140 | 'power_pass', "Redfish password", |
48 | 145 | field_type='password', required=True), | 141 | field_type='password', required=True), |
49 | 146 | make_setting_field( | 142 | make_setting_field( |
52 | 147 | 'node_id', "Node ID", | 143 | 'node_id', "Node ID", scope=SETTING_SCOPE.NODE), |
51 | 148 | scope=SETTING_SCOPE.NODE, required=True), | ||
53 | 149 | ] | 144 | ] |
54 | 150 | ip_extractor = make_ip_extractor('power_address') | 145 | ip_extractor = make_ip_extractor('power_address') |
55 | 151 | 146 | ||
56 | @@ -154,9 +149,38 @@ class RedfishPowerDriver(RedfishPowerDriverBase): | |||
57 | 154 | return [] | 149 | return [] |
58 | 155 | 150 | ||
59 | 156 | @inlineCallbacks | 151 | @inlineCallbacks |
60 | 152 | def process_redfish_context(self, context): | ||
61 | 153 | """Process Redfish power driver context. | ||
62 | 154 | |||
63 | 155 | Returns the basename of the first member found | ||
64 | 156 | in the Redfish Systems: | ||
65 | 157 | |||
66 | 158 | "Members": [ | ||
67 | 159 | { | ||
68 | 160 | "@odata.id": "/redfish/v1/Systems/1" | ||
69 | 161 | } | ||
70 | 162 | """ | ||
71 | 163 | url = self.get_url(context) | ||
72 | 164 | headers = self.make_auth_headers(**context) | ||
73 | 165 | node_id = context.get('node_id') | ||
74 | 166 | if node_id: | ||
75 | 167 | node_id = node_id.encode('utf-8') | ||
76 | 168 | else: | ||
77 | 169 | node_id = yield self.get_node_id(url, headers) | ||
78 | 170 | return url, node_id, headers | ||
79 | 171 | |||
80 | 172 | @inlineCallbacks | ||
81 | 173 | def get_node_id(self, url, headers): | ||
82 | 174 | uri = join(url, REDFISH_SYSTEMS_ENDPOINT) | ||
83 | 175 | systems, _ = yield self.redfish_request(b"GET", uri, headers) | ||
84 | 176 | members = systems.get('Members') | ||
85 | 177 | member = members[0].get('@odata.id') | ||
86 | 178 | return basename(member).encode('utf-8') | ||
87 | 179 | |||
88 | 180 | @inlineCallbacks | ||
89 | 157 | def set_pxe_boot(self, url, node_id, headers): | 181 | def set_pxe_boot(self, url, node_id, headers): |
90 | 158 | """Set the machine with node_id to PXE boot.""" | 182 | """Set the machine with node_id to PXE boot.""" |
92 | 159 | endpoint = REDFISH_SYSTEMS_ENDPOINT % node_id | 183 | endpoint = REDFISH_SYSTEMS_ENDPOINT + b'%s/' % node_id |
93 | 160 | payload = FileBodyProducer( | 184 | payload = FileBodyProducer( |
94 | 161 | BytesIO( | 185 | BytesIO( |
95 | 162 | json.dumps( | 186 | json.dumps( |
96 | @@ -185,10 +209,10 @@ class RedfishPowerDriver(RedfishPowerDriverBase): | |||
97 | 185 | 209 | ||
98 | 186 | @asynchronous | 210 | @asynchronous |
99 | 187 | @inlineCallbacks | 211 | @inlineCallbacks |
101 | 188 | def power_on(self, system_id, context): | 212 | def power_on(self, node_id, context): |
102 | 189 | """Power on machine.""" | 213 | """Power on machine.""" |
105 | 190 | url, node_id, headers = self.process_redfish_context(context) | 214 | url, node_id, headers = yield self.process_redfish_context(context) |
106 | 191 | power_state = yield self.power_query(system_id, context) | 215 | power_state = yield self.power_query(node_id, context) |
107 | 192 | # Power off the machine if currently on. | 216 | # Power off the machine if currently on. |
108 | 193 | if power_state == 'on': | 217 | if power_state == 'on': |
109 | 194 | yield self.power("ForceOff", url, node_id, headers) | 218 | yield self.power("ForceOff", url, node_id, headers) |
110 | @@ -199,9 +223,9 @@ class RedfishPowerDriver(RedfishPowerDriverBase): | |||
111 | 199 | 223 | ||
112 | 200 | @asynchronous | 224 | @asynchronous |
113 | 201 | @inlineCallbacks | 225 | @inlineCallbacks |
115 | 202 | def power_off(self, system_id, context): | 226 | def power_off(self, node_id, context): |
116 | 203 | """Power off machine.""" | 227 | """Power off machine.""" |
118 | 204 | url, node_id, headers = self.process_redfish_context(context) | 228 | url, node_id, headers = yield self.process_redfish_context(context) |
119 | 205 | # Set to PXE boot. | 229 | # Set to PXE boot. |
120 | 206 | yield self.set_pxe_boot(url, node_id, headers) | 230 | yield self.set_pxe_boot(url, node_id, headers) |
121 | 207 | # Power off the machine. | 231 | # Power off the machine. |
122 | @@ -209,9 +233,9 @@ class RedfishPowerDriver(RedfishPowerDriverBase): | |||
123 | 209 | 233 | ||
124 | 210 | @asynchronous | 234 | @asynchronous |
125 | 211 | @inlineCallbacks | 235 | @inlineCallbacks |
127 | 212 | def power_query(self, system_id, context): | 236 | def power_query(self, node_id, context): |
128 | 213 | """Power query machine.""" | 237 | """Power query machine.""" |
131 | 214 | url, node_id, headers = self.process_redfish_context(context) | 238 | url, node_id, headers = yield self.process_redfish_context(context) |
132 | 215 | uri = join(url, REDFISH_SYSTEMS_ENDPOINT % node_id) | 239 | uri = join(url, REDFISH_SYSTEMS_ENDPOINT + b'%s/' % node_id) |
133 | 216 | node_data, _ = yield self.redfish_request(b"GET", uri, headers) | 240 | node_data, _ = yield self.redfish_request(b"GET", uri, headers) |
134 | 217 | return node_data.get('PowerState').lower() | 241 | return node_data.get('PowerState').lower() |
135 | diff --git a/src/provisioningserver/drivers/power/tests/test_redfish.py b/src/provisioningserver/drivers/power/tests/test_redfish.py | |||
136 | index f6db6e3..ca2a771 100644 | |||
137 | --- a/src/provisioningserver/drivers/power/tests/test_redfish.py | |||
138 | +++ b/src/provisioningserver/drivers/power/tests/test_redfish.py | |||
139 | @@ -1,4 +1,4 @@ | |||
141 | 1 | # Copyright 2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2018-2019 Canonical Ltd. This software is licensed under the |
142 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
143 | 3 | 3 | ||
144 | 4 | """Tests for `provisioningserver.drivers.power.redfish`.""" | 4 | """Tests for `provisioningserver.drivers.power.redfish`.""" |
145 | @@ -168,7 +168,6 @@ def make_context(): | |||
146 | 168 | 'power_address': factory.make_ipv4_address(), | 168 | 'power_address': factory.make_ipv4_address(), |
147 | 169 | 'power_user': factory.make_name('power_user'), | 169 | 'power_user': factory.make_name('power_user'), |
148 | 170 | 'power_pass': factory.make_name('power_pass'), | 170 | 'power_pass': factory.make_name('power_pass'), |
149 | 171 | 'node_id': factory.make_name('node_id'), | ||
150 | 172 | } | 171 | } |
151 | 173 | 172 | ||
152 | 174 | 173 | ||
153 | @@ -332,7 +331,7 @@ class TestRedfishPowerDriver(MAASTestCase): | |||
154 | 332 | power_change = factory.make_name('power_change') | 331 | power_change = factory.make_name('power_change') |
155 | 333 | url = driver.get_url(context) | 332 | url = driver.get_url(context) |
156 | 334 | headers = driver.make_auth_headers(**context) | 333 | headers = driver.make_auth_headers(**context) |
158 | 335 | node_id = context.get('node_id').encode('utf-8') | 334 | node_id = b'1' |
159 | 336 | mock_file_body_producer = self.patch( | 335 | mock_file_body_producer = self.patch( |
160 | 337 | redfish_module, 'FileBodyProducer') | 336 | redfish_module, 'FileBodyProducer') |
161 | 338 | payload = FileBodyProducer( | 337 | payload = FileBodyProducer( |
162 | @@ -354,7 +353,7 @@ class TestRedfishPowerDriver(MAASTestCase): | |||
163 | 354 | driver = RedfishPowerDriver() | 353 | driver = RedfishPowerDriver() |
164 | 355 | context = make_context() | 354 | context = make_context() |
165 | 356 | url = driver.get_url(context) | 355 | url = driver.get_url(context) |
167 | 357 | node_id = context.get('node_id').encode('utf-8') | 356 | node_id = b'1' |
168 | 358 | headers = driver.make_auth_headers(**context) | 357 | headers = driver.make_auth_headers(**context) |
169 | 359 | mock_file_body_producer = self.patch( | 358 | mock_file_body_producer = self.patch( |
170 | 360 | redfish_module, 'FileBodyProducer') | 359 | redfish_module, 'FileBodyProducer') |
171 | @@ -378,21 +377,23 @@ class TestRedfishPowerDriver(MAASTestCase): | |||
172 | 378 | @inlineCallbacks | 377 | @inlineCallbacks |
173 | 379 | def test__power_on(self): | 378 | def test__power_on(self): |
174 | 380 | driver = RedfishPowerDriver() | 379 | driver = RedfishPowerDriver() |
175 | 381 | system_id = factory.make_name('system_id') | ||
176 | 382 | context = make_context() | 380 | context = make_context() |
177 | 383 | url = driver.get_url(context) | 381 | url = driver.get_url(context) |
178 | 384 | headers = driver.make_auth_headers(**context) | 382 | headers = driver.make_auth_headers(**context) |
180 | 385 | node_id = context.get('node_id').encode('utf-8') | 383 | node_id = b'1' |
181 | 384 | mock_redfish_request = self.patch(driver, 'redfish_request') | ||
182 | 385 | mock_redfish_request.return_value = ( | ||
183 | 386 | SAMPLE_JSON_SYSTEMS, None) | ||
184 | 386 | mock_set_pxe_boot = self.patch(driver, 'set_pxe_boot') | 387 | mock_set_pxe_boot = self.patch(driver, 'set_pxe_boot') |
185 | 387 | mock_power_query = self.patch(driver, 'power_query') | 388 | mock_power_query = self.patch(driver, 'power_query') |
186 | 388 | mock_power_query.return_value = "on" | 389 | mock_power_query.return_value = "on" |
187 | 389 | mock_power = self.patch(driver, 'power') | 390 | mock_power = self.patch(driver, 'power') |
188 | 390 | 391 | ||
190 | 391 | yield driver.power_on(system_id, context) | 392 | yield driver.power_on(node_id, context) |
191 | 392 | self.assertThat(mock_set_pxe_boot, MockCalledOnceWith( | 393 | self.assertThat(mock_set_pxe_boot, MockCalledOnceWith( |
192 | 393 | url, node_id, headers)) | 394 | url, node_id, headers)) |
193 | 394 | self.assertThat(mock_power_query, MockCalledOnceWith( | 395 | self.assertThat(mock_power_query, MockCalledOnceWith( |
195 | 395 | system_id, context)) | 396 | node_id, context)) |
196 | 396 | self.assertThat(mock_power, MockCallsMatch( | 397 | self.assertThat(mock_power, MockCallsMatch( |
197 | 397 | call("ForceOff", url, node_id, headers), | 398 | call("ForceOff", url, node_id, headers), |
198 | 398 | call("On", url, node_id, headers))) | 399 | call("On", url, node_id, headers))) |
199 | @@ -400,15 +401,17 @@ class TestRedfishPowerDriver(MAASTestCase): | |||
200 | 400 | @inlineCallbacks | 401 | @inlineCallbacks |
201 | 401 | def test__power_off(self): | 402 | def test__power_off(self): |
202 | 402 | driver = RedfishPowerDriver() | 403 | driver = RedfishPowerDriver() |
203 | 403 | system_id = factory.make_name('system_id') | ||
204 | 404 | context = make_context() | 404 | context = make_context() |
205 | 405 | url = driver.get_url(context) | 405 | url = driver.get_url(context) |
206 | 406 | headers = driver.make_auth_headers(**context) | 406 | headers = driver.make_auth_headers(**context) |
208 | 407 | node_id = context.get('node_id').encode('utf-8') | 407 | node_id = b'1' |
209 | 408 | mock_redfish_request = self.patch(driver, 'redfish_request') | ||
210 | 409 | mock_redfish_request.return_value = ( | ||
211 | 410 | SAMPLE_JSON_SYSTEMS, None) | ||
212 | 408 | mock_set_pxe_boot = self.patch(driver, 'set_pxe_boot') | 411 | mock_set_pxe_boot = self.patch(driver, 'set_pxe_boot') |
213 | 409 | mock_power = self.patch(driver, 'power') | 412 | mock_power = self.patch(driver, 'power') |
214 | 410 | 413 | ||
216 | 411 | yield driver.power_off(system_id, context) | 414 | yield driver.power_off(node_id, context) |
217 | 412 | self.assertThat(mock_set_pxe_boot, MockCalledOnceWith( | 415 | self.assertThat(mock_set_pxe_boot, MockCalledOnceWith( |
218 | 413 | url, node_id, headers)) | 416 | url, node_id, headers)) |
219 | 414 | self.assertThat(mock_power, MockCalledOnceWith( | 417 | self.assertThat(mock_power, MockCalledOnceWith( |
220 | @@ -423,7 +426,10 @@ class TestRedfishPowerDriver(MAASTestCase): | |||
221 | 423 | mock_redfish_request = self.patch(driver, 'redfish_request') | 426 | mock_redfish_request = self.patch(driver, 'redfish_request') |
222 | 424 | NODE_POWERED_ON = deepcopy(SAMPLE_JSON_SYSTEM) | 427 | NODE_POWERED_ON = deepcopy(SAMPLE_JSON_SYSTEM) |
223 | 425 | NODE_POWERED_ON['PowerState'] = "On" | 428 | NODE_POWERED_ON['PowerState'] = "On" |
225 | 426 | mock_redfish_request.return_value = (NODE_POWERED_ON, None) | 429 | mock_redfish_request.side_effect = [ |
226 | 430 | (SAMPLE_JSON_SYSTEMS, None), | ||
227 | 431 | (NODE_POWERED_ON, None), | ||
228 | 432 | ] | ||
229 | 427 | power_state = yield driver.power_query(system_id, context) | 433 | power_state = yield driver.power_query(system_id, context) |
230 | 428 | self.assertEquals(power_state, power_change.lower()) | 434 | self.assertEquals(power_state, power_change.lower()) |
231 | 429 | 435 | ||
232 | @@ -434,6 +440,9 @@ class TestRedfishPowerDriver(MAASTestCase): | |||
233 | 434 | system_id = factory.make_name('system_id') | 440 | system_id = factory.make_name('system_id') |
234 | 435 | context = make_context() | 441 | context = make_context() |
235 | 436 | mock_redfish_request = self.patch(driver, 'redfish_request') | 442 | mock_redfish_request = self.patch(driver, 'redfish_request') |
237 | 437 | mock_redfish_request.return_value = (SAMPLE_JSON_SYSTEM, None) | 443 | mock_redfish_request.side_effect = [ |
238 | 444 | (SAMPLE_JSON_SYSTEMS, None), | ||
239 | 445 | (SAMPLE_JSON_SYSTEM, None), | ||
240 | 446 | ] | ||
241 | 438 | power_state = yield driver.power_query(system_id, context) | 447 | power_state = yield driver.power_query(system_id, context) |
242 | 439 | self.assertEquals(power_state, power_change.lower()) | 448 | self.assertEquals(power_state, power_change.lower()) |
Self approved backport.