Merge ~andreserl/maas:2.3_ipmi_boot_type into maas:2.3

Proposed by Andres Rodriguez
Status: Merged
Approved by: Andres Rodriguez
Approved revision: 9dffe2fa32bba1b1558f7c45b96bfcc80bc16781
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~andreserl/maas:2.3_ipmi_boot_type
Merge into: maas:2.3
Diff against target: 336 lines (+155/-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 (+44/-4)
src/provisioningserver/drivers/power/tests/test_ipmi.py (+61/-1)
src/provisioningserver/refresh/maas_api_helper.py (+6/-1)
src/provisioningserver/refresh/tests/test_maas_api_helper.py (+1/-0)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+342668@code.launchpad.net

Commit message

Backport c70ed068a and 84a9fd27e

LP: #1750622 - Add ability to force the BIOS boot method, and auto discover it during enlistment/commissioning

To post a comment you must log in.
~andreserl/maas:2.3_ipmi_boot_type updated
9dffe2f... by Andres Rodriguez

Revert change thatcannot be backported

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

Looks like a clean backport to me.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py
2index db951af..74e6218 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'] = 'auto'
10 self.assertEqual(http.client.OK, response.status_code)
11 [machine] = Machine.objects.filter(hostname=hostname)
12 self.assertEqual(power_parameters, machine.power_parameters)
13diff --git a/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py b/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py
14index 49a9424..e2c3ce5 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@@ -320,6 +322,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 'auto'
48+
49+
50 def main():
51 import argparse
52
53@@ -378,12 +387,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()
73diff --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
74index e79740b..9d5fc50 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@@ -722,3 +724,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 = 'auto'
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)
118diff --git a/src/provisioningserver/drivers/power/ipmi.py b/src/provisioningserver/drivers/power/ipmi.py
119index 9ef5ba4..99b3e6e 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,26 @@ IPMI_DRIVER_CHOICES = [
138 ]
139
140
141+class IPMI_BOOT_TYPE:
142+ # DEFAULT used to provide backwards compatibility
143+ DEFAULT = 'auto'
144+ LEGACY = 'legacy'
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+IPMI_BOOT_TYPE_MAPPING = {
156+ IPMI_BOOT_TYPE.EFI: 'EFI',
157+ IPMI_BOOT_TYPE.LEGACY: 'PC-COMPATIBLE',
158+ }
159+
160+
161 class IPMIPowerDriver(PowerDriver):
162
163 name = 'ipmi'
164@@ -168,6 +196,11 @@ class IPMIPowerDriver(PowerDriver):
165 'power_driver', "Power driver", field_type='choice',
166 choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0,
167 required=True),
168+ make_setting_field(
169+ 'power_boot_type', "Power boot type", field_type='choice',
170+ choices=IPMI_BOOT_TYPE_CHOICES, default=IPMI_BOOT_TYPE.DEFAULT,
171+ required=False
172+ ),
173 make_setting_field('power_address', "IP address", required=True),
174 make_setting_field('power_user', "Power user"),
175 make_setting_field(
176@@ -185,11 +218,17 @@ class IPMIPowerDriver(PowerDriver):
177
178 @staticmethod
179 def _issue_ipmi_chassis_config_command(
180- command, power_change, power_address):
181+ command, power_change, power_address, power_boot_type=None):
182 env = shell.select_c_utf8_locale()
183 with NamedTemporaryFile("w+", encoding="utf-8") as tmp_config:
184 # Write out the chassis configuration.
185- tmp_config.write(IPMI_CONFIG)
186+ if (power_boot_type is None or
187+ power_boot_type == IPMI_BOOT_TYPE.DEFAULT):
188+ tmp_config.write(IPMI_CONFIG)
189+ else:
190+ tmp_config.write(
191+ IPMI_CONFIG_WITH_BOOT_TYPE %
192+ IPMI_BOOT_TYPE_MAPPING[power_boot_type])
193 tmp_config.flush()
194 # Use it when running the chassis config command.
195 # XXX: Not using call_and_check here because we
196@@ -234,7 +273,7 @@ class IPMIPowerDriver(PowerDriver):
197 def _issue_ipmi_command(
198 self, power_change, power_address=None, power_user=None,
199 power_pass=None, power_driver=None, power_off_mode=None,
200- mac_address=None, **extra):
201+ mac_address=None, power_boot_type=None, **extra):
202 """Issue command to ipmipower, for the given system."""
203 # This script deliberately does not check the current power state
204 # before issuing the requested power command. See bug 1171418 for an
205@@ -271,7 +310,8 @@ class IPMIPowerDriver(PowerDriver):
206 # Before changing state run the chassis config command.
207 if power_change in ("on", "off"):
208 self._issue_ipmi_chassis_config_command(
209- ipmi_chassis_config_command, power_change, power_address)
210+ ipmi_chassis_config_command, power_change, power_address,
211+ power_boot_type)
212
213 # Additional arguments for the power command.
214 if power_change == 'on':
215diff --git a/src/provisioningserver/drivers/power/tests/test_ipmi.py b/src/provisioningserver/drivers/power/tests/test_ipmi.py
216index bc5d3fb..2172704 100644
217--- a/src/provisioningserver/drivers/power/tests/test_ipmi.py
218+++ b/src/provisioningserver/drivers/power/tests/test_ipmi.py
219@@ -25,7 +25,10 @@ from provisioningserver.drivers.power import (
220 PowerError,
221 )
222 from provisioningserver.drivers.power.ipmi import (
223+ IPMI_BOOT_TYPE,
224+ IPMI_BOOT_TYPE_MAPPING,
225 IPMI_CONFIG,
226+ IPMI_CONFIG_WITH_BOOT_TYPE,
227 IPMI_ERRORS,
228 IPMIPowerDriver,
229 )
230@@ -107,7 +110,9 @@ class TestIPMIPowerDriver(MAASTestCase):
231 # The IP address is passed to _issue_ipmi_chassis_config_command.
232 self.assertThat(
233 driver._issue_ipmi_chassis_config_command,
234- MockCalledOnceWith(ANY, power_change, ip_address))
235+ MockCalledOnceWith(
236+ ANY, power_change, ip_address,
237+ power_boot_type=None))
238 # The IP address is also within the command passed to
239 # _issue_ipmi_chassis_config_command.
240 self.assertThat(
241@@ -324,3 +329,58 @@ class TestIPMIPowerDriver(MAASTestCase):
242
243 self.assertThat(
244 _issue_ipmi_command_mock, MockCalledOnceWith('query', **context))
245+
246+ def test__issue_ipmi_chassis_config_with_power_boot_type(self):
247+ context = make_context()
248+ driver = IPMIPowerDriver()
249+ ip_address = factory.make_ipv4_address()
250+ find_ip_via_arp = self.patch(ipmi_module, 'find_ip_via_arp')
251+ find_ip_via_arp.return_value = ip_address
252+ power_change = random.choice(("on", "off"))
253+
254+ context['mac_address'] = factory.make_mac_address()
255+ context['power_address'] = random.choice((None, "", " "))
256+ context['power_boot_type'] = IPMI_BOOT_TYPE.EFI
257+
258+ self.patch_autospec(driver, "_issue_ipmi_chassis_config_command")
259+ self.patch_autospec(driver, "_issue_ipmipower_command")
260+ driver._issue_ipmi_command(power_change, **context)
261+
262+ # The IP address is passed to _issue_ipmi_chassis_config_command.
263+ self.assertThat(
264+ driver._issue_ipmi_chassis_config_command,
265+ MockCalledOnceWith(
266+ ANY, power_change, ip_address,
267+ power_boot_type=IPMI_BOOT_TYPE.EFI))
268+ # The IP address is also within the command passed to
269+ # _issue_ipmi_chassis_config_command.
270+ self.assertThat(
271+ driver._issue_ipmi_chassis_config_command.call_args[0],
272+ Contains(ip_address))
273+ # The IP address is passed to _issue_ipmipower_command.
274+ self.assertThat(
275+ driver._issue_ipmipower_command,
276+ MockCalledOnceWith(ANY, power_change, ip_address))
277+
278+ def test__chassis_config_written_to_temporary_file_with_boot_type(self):
279+ boot_type = self.patch(ipmi_module, "power_boot_type")
280+ boot_type.return_value = IPMI_BOOT_TYPE.EFI
281+ NamedTemporaryFile = self.patch(ipmi_module, "NamedTemporaryFile")
282+ tmpfile = NamedTemporaryFile.return_value
283+ tmpfile.__enter__.return_value = tmpfile
284+ tmpfile.name = factory.make_name("filename")
285+
286+ IPMIPowerDriver._issue_ipmi_chassis_config_command(
287+ ["true"], sentinel.change, sentinel.addr,
288+ power_boot_type=IPMI_BOOT_TYPE.EFI)
289+
290+ self.assertThat(
291+ NamedTemporaryFile, MockCalledOnceWith("w+", encoding="utf-8"))
292+ self.assertThat(tmpfile.__enter__, MockCalledOnceWith())
293+ self.assertThat(
294+ tmpfile.write,
295+ MockCalledOnceWith(
296+ IPMI_CONFIG_WITH_BOOT_TYPE % IPMI_BOOT_TYPE_MAPPING[
297+ IPMI_BOOT_TYPE.EFI]))
298+ self.assertThat(tmpfile.flush, MockCalledOnceWith())
299+ self.assertThat(tmpfile.__exit__, MockCalledOnceWith(None, None, None))
300diff --git a/src/provisioningserver/refresh/maas_api_helper.py b/src/provisioningserver/refresh/maas_api_helper.py
301index cfcd6a4..94f9cf0 100644
302--- a/src/provisioningserver/refresh/maas_api_helper.py
303+++ b/src/provisioningserver/refresh/maas_api_helper.py
304@@ -234,7 +234,11 @@ def signal(
305
306 if None not in (power_type, power_params):
307 params[b'power_type'] = power_type.encode('utf-8')
308- user, power_pass, power_address, driver = power_params.split(",")
309+ if power_type == 'moonshot':
310+ user, power_pass, power_address, driver = power_params.split(",")
311+ else:
312+ (user, power_pass, power_address,
313+ driver, boot_type) = power_params.split(",")
314 # OrderedDict is used to make testing easier.
315 power_params = OrderedDict([
316 ('power_user', user),
317@@ -245,6 +249,7 @@ def signal(
318 power_params['power_hwaddress'] = driver
319 else:
320 power_params['power_driver'] = driver
321+ power_params['power_boot_type'] = boot_type
322 params[b'power_parameters'] = json.dumps(power_params).encode()
323
324 data, headers = encode_multipart_data(
325diff --git a/src/provisioningserver/refresh/tests/test_maas_api_helper.py b/src/provisioningserver/refresh/tests/test_maas_api_helper.py
326index 72f6341..cdaaa8f 100644
327--- a/src/provisioningserver/refresh/tests/test_maas_api_helper.py
328+++ b/src/provisioningserver/refresh/tests/test_maas_api_helper.py
329@@ -334,6 +334,7 @@ class TestSignal(MAASTestCase):
330 ('power_pass', factory.make_name('power_pass')),
331 ('power_address', factory.make_url()),
332 ('power_driver', factory.make_name('power_driver')),
333+ ('power_boot_type', factory.make_name('power_boot_type')),
334 ])
335
336 # None used for url and creds as we're not actually sending data.

Subscribers

People subscribed via source and target branches