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
diff --git a/src/maasserver/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py
index 370cb1b..2f03848 100644
--- a/src/maasserver/api/tests/test_enlistment.py
+++ b/src/maasserver/api/tests/test_enlistment.py
@@ -115,6 +115,7 @@ class EnlistmentAPITest(APITestCase.ForAnonymousAndUserAndAdmin):
115 # Add the default values.115 # Add the default values.
116 power_parameters['power_driver'] = 'LAN_2_0'116 power_parameters['power_driver'] = 'LAN_2_0'
117 power_parameters['mac_address'] = ''117 power_parameters['mac_address'] = ''
118 power_parameters['power_boot_type'] = 'auto'
118 self.assertEqual(http.client.OK, response.status_code)119 self.assertEqual(http.client.OK, response.status_code)
119 [machine] = Machine.objects.filter(hostname=hostname)120 [machine] = Machine.objects.filter(hostname=hostname)
120 self.assertEqual(power_parameters, machine.power_parameters)121 self.assertEqual(power_parameters, machine.power_parameters)
diff --git a/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py b/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py
index cd53215..72e2496 100644
--- a/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py
+++ b/src/metadataserver/user_data/templates/snippets/maas_ipmi_autodetect.py
@@ -263,16 +263,18 @@ def commit_ipmi_settings(config):
263 run_command(('bmc-config', '--commit', '--filename', config))263 run_command(('bmc-config', '--commit', '--filename', config))
264264
265265
266def get_maas_power_settings(user, password, ipaddress, version):266def get_maas_power_settings(user, password, ipaddress, version, boot_type):
267 return "%s,%s,%s,%s" % (user, password, ipaddress, version)267 return "%s,%s,%s,%s,%s" % (user, password, ipaddress, version, boot_type)
268268
269269
270def get_maas_power_settings_json(user, password, ipaddress, version):270def get_maas_power_settings_json(
271 user, password, ipaddress, version, boot_type):
271 power_params = {272 power_params = {
272 "power_address": ipaddress,273 "power_address": ipaddress,
273 "power_pass": password,274 "power_pass": password,
274 "power_user": user,275 "power_user": user,
275 "power_driver": version,276 "power_driver": version,
277 "power_boot_type": boot_type,
276 }278 }
277 return json.dumps(power_params)279 return json.dumps(power_params)
278280
@@ -316,6 +318,13 @@ def bmc_supports_lan2_0():
316 return False318 return False
317319
318320
321def get_system_boot_type():
322 """Detect if the system has boot EFI."""
323 if os.path.isdir('/sys/firmware/efi'):
324 return 'efi'
325 return 'auto'
326
327
319def main():328def main():
320 import argparse329 import argparse
321330
@@ -374,12 +383,17 @@ def main():
374 IPMI_VERSION = "LAN_2_0"383 IPMI_VERSION = "LAN_2_0"
375 else:384 else:
376 IPMI_VERSION = "LAN"385 IPMI_VERSION = "LAN"
386
387 IPMI_BOOT_TYPE = get_system_boot_type()
388
377 if args.commission_creds:389 if args.commission_creds:
378 print(get_maas_power_settings_json(390 print(get_maas_power_settings_json(
379 IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS, IPMI_VERSION))391 IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS,
392 IPMI_VERSION, IPMI_BOOT_TYPE))
380 else:393 else:
381 print(get_maas_power_settings(394 print(get_maas_power_settings(
382 IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS, IPMI_VERSION))395 IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS,
396 IPMI_VERSION, IPMI_BOOT_TYPE))
383397
384if __name__ == '__main__':398if __name__ == '__main__':
385 main()399 main()
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
index 31ef8c3..86803eb 100644
--- 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
@@ -6,6 +6,7 @@
6__all__ = []6__all__ = []
77
8from collections import OrderedDict8from collections import OrderedDict
9import os.path
9import platform10import platform
10import re11import re
11import subprocess12import subprocess
@@ -31,6 +32,7 @@ from snippets.maas_ipmi_autodetect import (
31 format_user_key,32 format_user_key,
32 generate_random_password,33 generate_random_password,
33 get_ipmi_ip_address,34 get_ipmi_ip_address,
35 get_system_boot_type,
34 IPMIError,36 IPMIError,
35 list_user_numbers,37 list_user_numbers,
36 make_ipmi_user_settings,38 make_ipmi_user_settings,
@@ -718,3 +720,24 @@ class TestEnableLanChannel(MAASTestCase):
718 subprocess.CalledProcessError, bmc_set_mock)720 subprocess.CalledProcessError, bmc_set_mock)
719 self.assertThat(721 self.assertThat(
720 bmc_set_mock.call_args_list, HasLength(1))722 bmc_set_mock.call_args_list, HasLength(1))
723
724
725class TestGetSystemBootType(MAASTestCase):
726
727 def test_get_system_boot_type_efi(self):
728 """Test that returns ."""
729 boot_type = 'efi'
730 # path os.path.isdir to return True to simulate
731 # that /sys/firmware/efi exists.
732 self.patch(os.path, "isdir").return_value = True
733 actual_boot_type = get_system_boot_type()
734 self.assertEqual(boot_type, actual_boot_type)
735
736 def test_get_system_boot_type_non_efi(self):
737 """Test """
738 boot_type = 'auto'
739 # path os.path.isdir to return False to simulate
740 # that /sys/firmware/efi doesn't exist.
741 self.patch(os.path, "isdir").return_value = False
742 actual_boot_type = get_system_boot_type()
743 self.assertEqual(boot_type, actual_boot_type)
diff --git a/src/provisioningserver/drivers/power/ipmi.py b/src/provisioningserver/drivers/power/ipmi.py
index ed1e1ab..ee5b413 100644
--- a/src/provisioningserver/drivers/power/ipmi.py
+++ b/src/provisioningserver/drivers/power/ipmi.py
@@ -39,6 +39,14 @@ EndSection
39"""39"""
4040
4141
42IPMI_CONFIG_WITH_BOOT_TYPE = """\
43Section Chassis_Boot_Flags
44 Boot_Flags_Persistent No
45 BIOS_Boot_Type %s
46 Boot_Device PXE
47EndSection
48"""
49
42IPMI_ERRORS = {50IPMI_ERRORS = {
43 'username invalid': {51 'username invalid': {
44 'message': (52 'message': (
@@ -159,6 +167,25 @@ IPMI_DRIVER_CHOICES = [
159 ]167 ]
160168
161169
170class IPMI_BOOT_TYPE:
171 # DEFAULT used to provide backwards compatibility
172 DEFAULT = 'auto'
173 LEGACY = 'legacy'
174 EFI = 'efi'
175
176
177IPMI_BOOT_TYPE_CHOICES = [
178 [IPMI_BOOT_TYPE.DEFAULT, "Automatic"],
179 [IPMI_BOOT_TYPE.LEGACY, "Legacy boot"],
180 [IPMI_BOOT_TYPE.EFI, "EFI boot"]
181 ]
182
183IPMI_BOOT_TYPE_MAPPING = {
184 IPMI_BOOT_TYPE.EFI: 'EFI',
185 IPMI_BOOT_TYPE.LEGACY: 'PC-COMPATIBLE',
186 }
187
188
162class IPMIPowerDriver(PowerDriver):189class IPMIPowerDriver(PowerDriver):
163190
164 name = 'ipmi'191 name = 'ipmi'
@@ -168,6 +195,11 @@ class IPMIPowerDriver(PowerDriver):
168 'power_driver', "Power driver", field_type='choice',195 'power_driver', "Power driver", field_type='choice',
169 choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0,196 choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0,
170 required=True),197 required=True),
198 make_setting_field(
199 'power_boot_type', "Power boot type", field_type='choice',
200 choices=IPMI_BOOT_TYPE_CHOICES, default=IPMI_BOOT_TYPE.DEFAULT,
201 required=False
202 ),
171 make_setting_field('power_address', "IP address", required=True),203 make_setting_field('power_address', "IP address", required=True),
172 make_setting_field('power_user', "Power user"),204 make_setting_field('power_user', "Power user"),
173 make_setting_field(205 make_setting_field(
@@ -185,11 +217,17 @@ class IPMIPowerDriver(PowerDriver):
185217
186 @staticmethod218 @staticmethod
187 def _issue_ipmi_chassis_config_command(219 def _issue_ipmi_chassis_config_command(
188 command, power_change, power_address):220 command, power_change, power_address, power_boot_type=None):
189 env = shell.get_env_with_locale()221 env = shell.get_env_with_locale()
190 with NamedTemporaryFile("w+", encoding="utf-8") as tmp_config:222 with NamedTemporaryFile("w+", encoding="utf-8") as tmp_config:
191 # Write out the chassis configuration.223 # Write out the chassis configuration.
192 tmp_config.write(IPMI_CONFIG)224 if (power_boot_type is None
225 or power_boot_type == IPMI_BOOT_TYPE.DEFAULT):
226 tmp_config.write(IPMI_CONFIG)
227 else:
228 tmp_config.write(
229 IPMI_CONFIG_WITH_BOOT_TYPE %
230 IPMI_BOOT_TYPE_MAPPING[power_boot_type])
193 tmp_config.flush()231 tmp_config.flush()
194 # Use it when running the chassis config command.232 # Use it when running the chassis config command.
195 # XXX: Not using call_and_check here because we233 # XXX: Not using call_and_check here because we
@@ -234,7 +272,7 @@ class IPMIPowerDriver(PowerDriver):
234 def _issue_ipmi_command(272 def _issue_ipmi_command(
235 self, power_change, power_address=None, power_user=None,273 self, power_change, power_address=None, power_user=None,
236 power_pass=None, power_driver=None, power_off_mode=None,274 power_pass=None, power_driver=None, power_off_mode=None,
237 mac_address=None, **extra):275 mac_address=None, power_boot_type=None, **extra):
238 """Issue command to ipmipower, for the given system."""276 """Issue command to ipmipower, for the given system."""
239 # This script deliberately does not check the current power state277 # This script deliberately does not check the current power state
240 # before issuing the requested power command. See bug 1171418 for an278 # before issuing the requested power command. See bug 1171418 for an
@@ -271,7 +309,8 @@ class IPMIPowerDriver(PowerDriver):
271 # Before changing state run the chassis config command.309 # Before changing state run the chassis config command.
272 if power_change in ("on", "off"):310 if power_change in ("on", "off"):
273 self._issue_ipmi_chassis_config_command(311 self._issue_ipmi_chassis_config_command(
274 ipmi_chassis_config_command, power_change, power_address)312 ipmi_chassis_config_command, power_change, power_address,
313 power_boot_type)
275314
276 # Additional arguments for the power command.315 # Additional arguments for the power command.
277 if power_change == 'on':316 if power_change == 'on':
diff --git a/src/provisioningserver/drivers/power/tests/test_ipmi.py b/src/provisioningserver/drivers/power/tests/test_ipmi.py
index 816d52e..548846e 100644
--- a/src/provisioningserver/drivers/power/tests/test_ipmi.py
+++ b/src/provisioningserver/drivers/power/tests/test_ipmi.py
@@ -25,7 +25,10 @@ from provisioningserver.drivers.power import (
25 PowerError,25 PowerError,
26)26)
27from provisioningserver.drivers.power.ipmi import (27from provisioningserver.drivers.power.ipmi import (
28 IPMI_BOOT_TYPE,
29 IPMI_BOOT_TYPE_MAPPING,
28 IPMI_CONFIG,30 IPMI_CONFIG,
31 IPMI_CONFIG_WITH_BOOT_TYPE,
29 IPMI_ERRORS,32 IPMI_ERRORS,
30 IPMIPowerDriver,33 IPMIPowerDriver,
31)34)
@@ -107,7 +110,9 @@ class TestIPMIPowerDriver(MAASTestCase):
107 # The IP address is passed to _issue_ipmi_chassis_config_command.110 # The IP address is passed to _issue_ipmi_chassis_config_command.
108 self.assertThat(111 self.assertThat(
109 driver._issue_ipmi_chassis_config_command,112 driver._issue_ipmi_chassis_config_command,
110 MockCalledOnceWith(ANY, power_change, ip_address))113 MockCalledOnceWith(
114 ANY, power_change, ip_address,
115 power_boot_type=None))
111 # The IP address is also within the command passed to116 # The IP address is also within the command passed to
112 # _issue_ipmi_chassis_config_command.117 # _issue_ipmi_chassis_config_command.
113 self.assertThat(118 self.assertThat(
@@ -324,3 +329,58 @@ class TestIPMIPowerDriver(MAASTestCase):
324329
325 self.assertThat(330 self.assertThat(
326 _issue_ipmi_command_mock, MockCalledOnceWith('query', **context))331 _issue_ipmi_command_mock, MockCalledOnceWith('query', **context))
332
333 def test__issue_ipmi_chassis_config_with_power_boot_type(self):
334 context = make_context()
335 driver = IPMIPowerDriver()
336 ip_address = factory.make_ipv4_address()
337 find_ip_via_arp = self.patch(ipmi_module, 'find_ip_via_arp')
338 find_ip_via_arp.return_value = ip_address
339 power_change = random.choice(("on", "off"))
340
341 context['mac_address'] = factory.make_mac_address()
342 context['power_address'] = random.choice((None, "", " "))
343 context['power_boot_type'] = IPMI_BOOT_TYPE.EFI
344
345 self.patch_autospec(driver, "_issue_ipmi_chassis_config_command")
346 self.patch_autospec(driver, "_issue_ipmipower_command")
347 driver._issue_ipmi_command(power_change, **context)
348
349 # The IP address is passed to _issue_ipmi_chassis_config_command.
350 self.assertThat(
351 driver._issue_ipmi_chassis_config_command,
352 MockCalledOnceWith(
353 ANY, power_change, ip_address,
354 power_boot_type=IPMI_BOOT_TYPE.EFI))
355 # The IP address is also within the command passed to
356 # _issue_ipmi_chassis_config_command.
357 self.assertThat(
358 driver._issue_ipmi_chassis_config_command.call_args[0],
359 Contains(ip_address))
360 # The IP address is passed to _issue_ipmipower_command.
361 self.assertThat(
362 driver._issue_ipmipower_command,
363 MockCalledOnceWith(ANY, power_change, ip_address))
364
365 def test__chassis_config_written_to_temporary_file_with_boot_type(self):
366 boot_type = self.patch(ipmi_module, "power_boot_type")
367 boot_type.return_value = IPMI_BOOT_TYPE.EFI
368 NamedTemporaryFile = self.patch(ipmi_module, "NamedTemporaryFile")
369 tmpfile = NamedTemporaryFile.return_value
370 tmpfile.__enter__.return_value = tmpfile
371 tmpfile.name = factory.make_name("filename")
372
373 IPMIPowerDriver._issue_ipmi_chassis_config_command(
374 ["true"], sentinel.change, sentinel.addr,
375 power_boot_type=IPMI_BOOT_TYPE.EFI)
376
377 self.assertThat(
378 NamedTemporaryFile, MockCalledOnceWith("w+", encoding="utf-8"))
379 self.assertThat(tmpfile.__enter__, MockCalledOnceWith())
380 self.assertThat(
381 tmpfile.write,
382 MockCalledOnceWith(
383 IPMI_CONFIG_WITH_BOOT_TYPE % IPMI_BOOT_TYPE_MAPPING[
384 IPMI_BOOT_TYPE.EFI]))
385 self.assertThat(tmpfile.flush, MockCalledOnceWith())
386 self.assertThat(tmpfile.__exit__, MockCalledOnceWith(None, None, None))
diff --git a/src/provisioningserver/refresh/maas_api_helper.py b/src/provisioningserver/refresh/maas_api_helper.py
index bb03e31..8977210 100644
--- a/src/provisioningserver/refresh/maas_api_helper.py
+++ b/src/provisioningserver/refresh/maas_api_helper.py
@@ -234,7 +234,11 @@ def signal(
234234
235 if None not in (power_type, power_params):235 if None not in (power_type, power_params):
236 params[b'power_type'] = power_type.encode('utf-8')236 params[b'power_type'] = power_type.encode('utf-8')
237 user, power_pass, power_address, driver = power_params.split(",")237 if power_type == 'moonshot':
238 user, power_pass, power_address, driver = power_params.split(",")
239 else:
240 (user, power_pass, power_address,
241 driver, boot_type) = power_params.split(",")
238 # OrderedDict is used to make testing easier.242 # OrderedDict is used to make testing easier.
239 power_params = OrderedDict([243 power_params = OrderedDict([
240 ('power_user', user),244 ('power_user', user),
@@ -245,6 +249,7 @@ def signal(
245 power_params['power_hwaddress'] = driver249 power_params['power_hwaddress'] = driver
246 else:250 else:
247 power_params['power_driver'] = driver251 power_params['power_driver'] = driver
252 power_params['power_boot_type'] = boot_type
248 params[b'power_parameters'] = json.dumps(power_params).encode()253 params[b'power_parameters'] = json.dumps(power_params).encode()
249254
250 data, headers = encode_multipart_data(255 data, headers = encode_multipart_data(
diff --git a/src/provisioningserver/refresh/tests/test_maas_api_helper.py b/src/provisioningserver/refresh/tests/test_maas_api_helper.py
index ae1017c..e5ce942 100644
--- a/src/provisioningserver/refresh/tests/test_maas_api_helper.py
+++ b/src/provisioningserver/refresh/tests/test_maas_api_helper.py
@@ -354,6 +354,7 @@ class TestSignal(MAASTestCase):
354 ('power_pass', factory.make_name('power_pass')),354 ('power_pass', factory.make_name('power_pass')),
355 ('power_address', factory.make_url()),355 ('power_address', factory.make_url()),
356 ('power_driver', factory.make_name('power_driver')),356 ('power_driver', factory.make_name('power_driver')),
357 ('power_boot_type', factory.make_name('power_boot_type')),
357 ])358 ])
358359
359 # None used for url and creds as we're not actually sending data.360 # None used for url and creds as we're not actually sending data.

Subscribers

People subscribed via source and target branches