Merge ~andreserl/maas:ipmi_boot_type into maas:master
- Git
- lp:~andreserl/maas
- ipmi_boot_type
- Merge into master
Status: | Merged |
---|---|
Approved by: | Andres Rodriguez |
Approved revision: | f5180d0018413904736453dbfe4a7f81d65f98bc |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~andreserl/maas:ipmi_boot_type |
Merge into: | maas:master |
Diff against target: |
325 lines (+144/-11) 7 files modified
src/maasserver/api/tests/test_enlistment.py (+1/-0) src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py (+19/-5) src/metadataserver/user_data/templates/snippets/tests/test_maas_ipmi_autodetect.py (+23/-0) src/provisioningserver/drivers/power/ipmi.py (+35/-4) src/provisioningserver/drivers/power/tests/test_ipmi.py (+59/-1) src/provisioningserver/refresh/maas_api_helper.py (+6/-1) src/provisioningserver/refresh/tests/test_maas_api_helper.py (+1/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Newell Jensen (community) | Approve | ||
MAAS Lander | Approve | ||
Review via email: mp+341740@code.launchpad.net |
Commit message
LP: #1750622 - Add ability to force the BIOS boot method, and auto discover it during enlistment/
Description of the change
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b ipmi_boot_type lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 8877e0c834ee58b
Newell Jensen (newell-jensen) wrote : | # |
Need to set a commit message. See inline comments as well. I won't block you on these small changes but they should be pushed before landing.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b ipmi_boot_type lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: ff60bc9411fb84e
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b ipmi_boot_type lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 8fc8ae253d4782e
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b ipmi_boot_type lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: f83ab51731b0499
Newell Jensen (newell-jensen) wrote : | # |
Inline comment below and it looks like there are unit tests for maas_ipmi_
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b ipmi_boot_type lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: e9ffd0688e62550
Andres Rodriguez (andreserl) wrote : | # |
The unittests for the BMC are targeted to test the BMC related functions. This change is not BMC related.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b ipmi_boot_type lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS
COMMIT: 8a1cb40c712671d
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b ipmi_boot_type lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS
COMMIT: f5180d001841390
Newell Jensen (newell-jensen) wrote : | # |
+1 based on your comment about unit tests.
Andres Rodriguez (andreserl) wrote : | # |
I've added a unittest anyway :). Thanks for the review.
On Wed, Mar 21, 2018 at 2:25 PM, Newell Jensen <email address hidden>
wrote:
> Review: Approve
>
> +1 based on your comment about unit tests.
> --
> https:/
> You are reviewing the proposed merge of ~andreserl/
> into maas:master.
>
--
Andres Rodriguez
Engineering Manager, MAAS
Canonical USA, Inc.
MAAS Lander (maas-lander) wrote : | # |
UNABLE TO START LANDING
STATUS: MISSING COMMIT MESSAGE
Preview Diff
1 | diff --git a/src/maasserver/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py |
2 | index 370cb1b..5b636fb 100644 |
3 | --- a/src/maasserver/api/tests/test_enlistment.py |
4 | +++ b/src/maasserver/api/tests/test_enlistment.py |
5 | @@ -115,6 +115,7 @@ class EnlistmentAPITest(APITestCase.ForAnonymousAndUserAndAdmin): |
6 | # Add the default values. |
7 | power_parameters['power_driver'] = 'LAN_2_0' |
8 | power_parameters['mac_address'] = '' |
9 | + power_parameters['power_boot_type'] = '' |
10 | self.assertEqual(http.client.OK, response.status_code) |
11 | [machine] = Machine.objects.filter(hostname=hostname) |
12 | self.assertEqual(power_parameters, machine.power_parameters) |
13 | diff --git a/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py b/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py |
14 | index cd53215..a2caa8e 100644 |
15 | --- a/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py |
16 | +++ b/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py |
17 | @@ -263,16 +263,18 @@ def commit_ipmi_settings(config): |
18 | run_command(('bmc-config', '--commit', '--filename', config)) |
19 | |
20 | |
21 | -def get_maas_power_settings(user, password, ipaddress, version): |
22 | - return "%s,%s,%s,%s" % (user, password, ipaddress, version) |
23 | +def get_maas_power_settings(user, password, ipaddress, version, boot_type): |
24 | + return "%s,%s,%s,%s,%s" % (user, password, ipaddress, version, boot_type) |
25 | |
26 | |
27 | -def get_maas_power_settings_json(user, password, ipaddress, version): |
28 | +def get_maas_power_settings_json( |
29 | + user, password, ipaddress, version, boot_type): |
30 | power_params = { |
31 | "power_address": ipaddress, |
32 | "power_pass": password, |
33 | "power_user": user, |
34 | "power_driver": version, |
35 | + "power_boot_type": boot_type, |
36 | } |
37 | return json.dumps(power_params) |
38 | |
39 | @@ -316,6 +318,13 @@ def bmc_supports_lan2_0(): |
40 | return False |
41 | |
42 | |
43 | +def get_system_boot_type(): |
44 | + """Detect if the system has boot EFI.""" |
45 | + if os.path.isdir('/sys/firmware/efi'): |
46 | + return 'EFI' |
47 | + return '' |
48 | + |
49 | + |
50 | def main(): |
51 | import argparse |
52 | |
53 | @@ -374,12 +383,17 @@ def main(): |
54 | IPMI_VERSION = "LAN_2_0" |
55 | else: |
56 | IPMI_VERSION = "LAN" |
57 | + |
58 | + IPMI_BOOT_TYPE = get_system_boot_type() |
59 | + |
60 | if args.commission_creds: |
61 | print(get_maas_power_settings_json( |
62 | - IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS, IPMI_VERSION)) |
63 | + IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS, |
64 | + IPMI_VERSION, IPMI_BOOT_TYPE)) |
65 | else: |
66 | print(get_maas_power_settings( |
67 | - IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS, IPMI_VERSION)) |
68 | + IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS, |
69 | + IPMI_VERSION, IPMI_BOOT_TYPE)) |
70 | |
71 | if __name__ == '__main__': |
72 | main() |
73 | diff --git a/src/metadataserver/user_data/templates/snippets/tests/test_maas_ipmi_autodetect.py b/src/metadataserver/user_data/templates/snippets/tests/test_maas_ipmi_autodetect.py |
74 | index 31ef8c3..37f52b6 100644 |
75 | --- a/src/metadataserver/user_data/templates/snippets/tests/test_maas_ipmi_autodetect.py |
76 | +++ b/src/metadataserver/user_data/templates/snippets/tests/test_maas_ipmi_autodetect.py |
77 | @@ -6,6 +6,7 @@ |
78 | __all__ = [] |
79 | |
80 | from collections import OrderedDict |
81 | +import os.path |
82 | import platform |
83 | import re |
84 | import subprocess |
85 | @@ -31,6 +32,7 @@ from snippets.maas_ipmi_autodetect import ( |
86 | format_user_key, |
87 | generate_random_password, |
88 | get_ipmi_ip_address, |
89 | + get_system_boot_type, |
90 | IPMIError, |
91 | list_user_numbers, |
92 | make_ipmi_user_settings, |
93 | @@ -718,3 +720,24 @@ class TestEnableLanChannel(MAASTestCase): |
94 | subprocess.CalledProcessError, bmc_set_mock) |
95 | self.assertThat( |
96 | bmc_set_mock.call_args_list, HasLength(1)) |
97 | + |
98 | + |
99 | +class TestGetSystemBootType(MAASTestCase): |
100 | + |
101 | + def test_get_system_boot_type_efi(self): |
102 | + """Test that returns .""" |
103 | + boot_type = 'EFI' |
104 | + # path os.path.isdir to return True to simulate |
105 | + # that /sys/firmware/efi exists. |
106 | + self.patch(os.path, "isdir").return_value = True |
107 | + actual_boot_type = get_system_boot_type() |
108 | + self.assertEqual(boot_type, actual_boot_type) |
109 | + |
110 | + def test_get_system_boot_type_non_efi(self): |
111 | + """Test """ |
112 | + boot_type = '' |
113 | + # path os.path.isdir to return False to simulate |
114 | + # that /sys/firmware/efi doesn't exist. |
115 | + self.patch(os.path, "isdir").return_value = False |
116 | + actual_boot_type = get_system_boot_type() |
117 | + self.assertEqual(boot_type, actual_boot_type) |
118 | diff --git a/src/provisioningserver/drivers/power/ipmi.py b/src/provisioningserver/drivers/power/ipmi.py |
119 | index ed1e1ab..84835e1 100644 |
120 | --- a/src/provisioningserver/drivers/power/ipmi.py |
121 | +++ b/src/provisioningserver/drivers/power/ipmi.py |
122 | @@ -39,6 +39,14 @@ EndSection |
123 | """ |
124 | |
125 | |
126 | +IPMI_CONFIG_WITH_BOOT_TYPE = """\ |
127 | +Section Chassis_Boot_Flags |
128 | + Boot_Flags_Persistent No |
129 | + BIOS_Boot_Type %s |
130 | + Boot_Device PXE |
131 | +EndSection |
132 | +""" |
133 | + |
134 | IPMI_ERRORS = { |
135 | 'username invalid': { |
136 | 'message': ( |
137 | @@ -159,6 +167,20 @@ IPMI_DRIVER_CHOICES = [ |
138 | ] |
139 | |
140 | |
141 | +class IPMI_BOOT_TYPE: |
142 | + # DEFAULT used to provide backwards compatibility |
143 | + DEFAULT = '' |
144 | + LEGACY = 'PC-COMPATIBLE' |
145 | + EFI = 'EFI' |
146 | + |
147 | + |
148 | +IPMI_BOOT_TYPE_CHOICES = [ |
149 | + [IPMI_BOOT_TYPE.DEFAULT, "Automatic"], |
150 | + [IPMI_BOOT_TYPE.LEGACY, "Legacy boot"], |
151 | + [IPMI_BOOT_TYPE.EFI, "EFI boot"] |
152 | + ] |
153 | + |
154 | + |
155 | class IPMIPowerDriver(PowerDriver): |
156 | |
157 | name = 'ipmi' |
158 | @@ -168,6 +190,11 @@ class IPMIPowerDriver(PowerDriver): |
159 | 'power_driver', "Power driver", field_type='choice', |
160 | choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0, |
161 | required=True), |
162 | + make_setting_field( |
163 | + 'power_boot_type', "Power boot type", field_type='choice', |
164 | + choices=IPMI_BOOT_TYPE_CHOICES, default=IPMI_BOOT_TYPE.DEFAULT, |
165 | + required=False |
166 | + ), |
167 | make_setting_field('power_address', "IP address", required=True), |
168 | make_setting_field('power_user', "Power user"), |
169 | make_setting_field( |
170 | @@ -185,11 +212,14 @@ class IPMIPowerDriver(PowerDriver): |
171 | |
172 | @staticmethod |
173 | def _issue_ipmi_chassis_config_command( |
174 | - command, power_change, power_address): |
175 | + command, power_change, power_address, power_boot_type=None): |
176 | env = shell.get_env_with_locale() |
177 | with NamedTemporaryFile("w+", encoding="utf-8") as tmp_config: |
178 | # Write out the chassis configuration. |
179 | - tmp_config.write(IPMI_CONFIG) |
180 | + if power_boot_type is None or power_boot_type == '': |
181 | + tmp_config.write(IPMI_CONFIG) |
182 | + else: |
183 | + tmp_config.write(IPMI_CONFIG_WITH_BOOT_TYPE % power_boot_type) |
184 | tmp_config.flush() |
185 | # Use it when running the chassis config command. |
186 | # XXX: Not using call_and_check here because we |
187 | @@ -234,7 +264,7 @@ class IPMIPowerDriver(PowerDriver): |
188 | def _issue_ipmi_command( |
189 | self, power_change, power_address=None, power_user=None, |
190 | power_pass=None, power_driver=None, power_off_mode=None, |
191 | - mac_address=None, **extra): |
192 | + mac_address=None, power_boot_type=None, **extra): |
193 | """Issue command to ipmipower, for the given system.""" |
194 | # This script deliberately does not check the current power state |
195 | # before issuing the requested power command. See bug 1171418 for an |
196 | @@ -271,7 +301,8 @@ class IPMIPowerDriver(PowerDriver): |
197 | # Before changing state run the chassis config command. |
198 | if power_change in ("on", "off"): |
199 | self._issue_ipmi_chassis_config_command( |
200 | - ipmi_chassis_config_command, power_change, power_address) |
201 | + ipmi_chassis_config_command, power_change, power_address, |
202 | + power_boot_type) |
203 | |
204 | # Additional arguments for the power command. |
205 | if power_change == 'on': |
206 | diff --git a/src/provisioningserver/drivers/power/tests/test_ipmi.py b/src/provisioningserver/drivers/power/tests/test_ipmi.py |
207 | index 816d52e..6e7a4b9 100644 |
208 | --- a/src/provisioningserver/drivers/power/tests/test_ipmi.py |
209 | +++ b/src/provisioningserver/drivers/power/tests/test_ipmi.py |
210 | @@ -25,7 +25,9 @@ from provisioningserver.drivers.power import ( |
211 | PowerError, |
212 | ) |
213 | from provisioningserver.drivers.power.ipmi import ( |
214 | + IPMI_BOOT_TYPE, |
215 | IPMI_CONFIG, |
216 | + IPMI_CONFIG_WITH_BOOT_TYPE, |
217 | IPMI_ERRORS, |
218 | IPMIPowerDriver, |
219 | ) |
220 | @@ -107,7 +109,9 @@ class TestIPMIPowerDriver(MAASTestCase): |
221 | # The IP address is passed to _issue_ipmi_chassis_config_command. |
222 | self.assertThat( |
223 | driver._issue_ipmi_chassis_config_command, |
224 | - MockCalledOnceWith(ANY, power_change, ip_address)) |
225 | + MockCalledOnceWith( |
226 | + ANY, power_change, ip_address, |
227 | + power_boot_type=None)) |
228 | # The IP address is also within the command passed to |
229 | # _issue_ipmi_chassis_config_command. |
230 | self.assertThat( |
231 | @@ -324,3 +328,57 @@ class TestIPMIPowerDriver(MAASTestCase): |
232 | |
233 | self.assertThat( |
234 | _issue_ipmi_command_mock, MockCalledOnceWith('query', **context)) |
235 | + |
236 | + def test__issue_ipmi_chassis_config_with_power_boot_type(self): |
237 | + context = make_context() |
238 | + driver = IPMIPowerDriver() |
239 | + ip_address = factory.make_ipv4_address() |
240 | + find_ip_via_arp = self.patch(ipmi_module, 'find_ip_via_arp') |
241 | + find_ip_via_arp.return_value = ip_address |
242 | + power_change = random.choice(("on", "off")) |
243 | + |
244 | + context['mac_address'] = factory.make_mac_address() |
245 | + context['power_address'] = random.choice((None, "", " ")) |
246 | + context['power_boot_type'] = IPMI_BOOT_TYPE.EFI |
247 | + |
248 | + self.patch_autospec(driver, "_issue_ipmi_chassis_config_command") |
249 | + self.patch_autospec(driver, "_issue_ipmipower_command") |
250 | + driver._issue_ipmi_command(power_change, **context) |
251 | + |
252 | + # The IP address is passed to _issue_ipmi_chassis_config_command. |
253 | + self.assertThat( |
254 | + driver._issue_ipmi_chassis_config_command, |
255 | + MockCalledOnceWith( |
256 | + ANY, power_change, ip_address, |
257 | + power_boot_type=IPMI_BOOT_TYPE.EFI)) |
258 | + # The IP address is also within the command passed to |
259 | + # _issue_ipmi_chassis_config_command. |
260 | + self.assertThat( |
261 | + driver._issue_ipmi_chassis_config_command.call_args[0], |
262 | + Contains(ip_address)) |
263 | + # The IP address is passed to _issue_ipmipower_command. |
264 | + self.assertThat( |
265 | + driver._issue_ipmipower_command, |
266 | + MockCalledOnceWith(ANY, power_change, ip_address)) |
267 | + |
268 | + def test__chassis_config_written_to_temporary_file_with_boot_type(self): |
269 | + boot_type = self.patch(ipmi_module, "power_boot_type") |
270 | + boot_type.return_value = IPMI_BOOT_TYPE.EFI |
271 | + NamedTemporaryFile = self.patch(ipmi_module, "NamedTemporaryFile") |
272 | + tmpfile = NamedTemporaryFile.return_value |
273 | + tmpfile.__enter__.return_value = tmpfile |
274 | + tmpfile.name = factory.make_name("filename") |
275 | + |
276 | + IPMIPowerDriver._issue_ipmi_chassis_config_command( |
277 | + ["true"], sentinel.change, sentinel.addr, |
278 | + power_boot_type=IPMI_BOOT_TYPE.EFI) |
279 | + |
280 | + self.assertThat( |
281 | + NamedTemporaryFile, MockCalledOnceWith("w+", encoding="utf-8")) |
282 | + self.assertThat(tmpfile.__enter__, MockCalledOnceWith()) |
283 | + self.assertThat( |
284 | + tmpfile.write, |
285 | + MockCalledOnceWith( |
286 | + IPMI_CONFIG_WITH_BOOT_TYPE % IPMI_BOOT_TYPE.EFI)) |
287 | + self.assertThat(tmpfile.flush, MockCalledOnceWith()) |
288 | + self.assertThat(tmpfile.__exit__, MockCalledOnceWith(None, None, None)) |
289 | diff --git a/src/provisioningserver/refresh/maas_api_helper.py b/src/provisioningserver/refresh/maas_api_helper.py |
290 | index bb03e31..8977210 100644 |
291 | --- a/src/provisioningserver/refresh/maas_api_helper.py |
292 | +++ b/src/provisioningserver/refresh/maas_api_helper.py |
293 | @@ -234,7 +234,11 @@ def signal( |
294 | |
295 | if None not in (power_type, power_params): |
296 | params[b'power_type'] = power_type.encode('utf-8') |
297 | - user, power_pass, power_address, driver = power_params.split(",") |
298 | + if power_type == 'moonshot': |
299 | + user, power_pass, power_address, driver = power_params.split(",") |
300 | + else: |
301 | + (user, power_pass, power_address, |
302 | + driver, boot_type) = power_params.split(",") |
303 | # OrderedDict is used to make testing easier. |
304 | power_params = OrderedDict([ |
305 | ('power_user', user), |
306 | @@ -245,6 +249,7 @@ def signal( |
307 | power_params['power_hwaddress'] = driver |
308 | else: |
309 | power_params['power_driver'] = driver |
310 | + power_params['power_boot_type'] = boot_type |
311 | params[b'power_parameters'] = json.dumps(power_params).encode() |
312 | |
313 | data, headers = encode_multipart_data( |
314 | diff --git a/src/provisioningserver/refresh/tests/test_maas_api_helper.py b/src/provisioningserver/refresh/tests/test_maas_api_helper.py |
315 | index ae1017c..e5ce942 100644 |
316 | --- a/src/provisioningserver/refresh/tests/test_maas_api_helper.py |
317 | +++ b/src/provisioningserver/refresh/tests/test_maas_api_helper.py |
318 | @@ -354,6 +354,7 @@ class TestSignal(MAASTestCase): |
319 | ('power_pass', factory.make_name('power_pass')), |
320 | ('power_address', factory.make_url()), |
321 | ('power_driver', factory.make_name('power_driver')), |
322 | + ('power_boot_type', factory.make_name('power_boot_type')), |
323 | ]) |
324 | |
325 | # None used for url and creds as we're not actually sending data. |
UNIT TESTS
-b ipmi_boot_type lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED maas-ci- jenkins. internal: 8080/job/ maas/job/ branch- tester/ 1993/console 82abae5ee69e13b ab0a0ec073
LOG: http://
COMMIT: 0bb7de05ed77b21