Merge ~andreserl/maas:ipmi_boot_type_2 into maas:master

Proposed by Andres Rodriguez
Status: Superseded
Proposed branch: ~andreserl/maas:ipmi_boot_type_2
Merge into: maas:master
Diff against target: 335 lines (+154/-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 (+43/-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
MAAS Maintainers Pending
Review via email: mp+342118@code.launchpad.net

This proposal has been superseded by a proposal from 2018-03-26.

To post a comment you must log in.

Unmerged commits

7d9a04f... by Andres Rodriguez

Change the options for power_boot_type so that API users can specify them by using auto|legacy|efi instead of freeipmi-tools specific <null>|EFI|PC-COMPATIBLE

f5180d0... by Andres Rodriguez

Fix lint

87fc9dd... by Andres Rodriguez

Add missing tests

8a1cb40... by Andres Rodriguez

Fix tests

e9ffd06... by Andres Rodriguez

Fix tests

bf35c2f... by Andres Rodriguez

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

2b2a439... by Andres Rodriguez

Fix lint

07d5287... by Andres Rodriguez

Fix review comments

36cdc4b... by Andres Rodriguez

Fix test

9e45488... by Andres Rodriguez

Add/fix tests

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

Subscribers

People subscribed via source and target branches