Merge ~andreserl/maas:ipmi_boot_type into maas:master

Proposed by Andres Rodriguez
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)
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/commissioning

To post a comment you must log in.
Revision history for this message
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://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/1993/console
COMMIT: 0bb7de05ed77b2182abae5ee69e13bab0a0ec073

review: Needs Fixing
Revision history for this message
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://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/1997/console
COMMIT: 8877e0c834ee58bc43e9dc2a3fca09fc72d2c18b

review: Needs Fixing
Revision history for this message
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.

review: Approve
Revision history for this message
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://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/1999/console
COMMIT: ff60bc9411fb84e1ea79c058a1364a319cd248f9

review: Needs Fixing
Revision history for this message
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://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/2001/console
COMMIT: 8fc8ae253d4782ee28acf0852ad7efddd9782253

review: Needs Fixing
Revision history for this message
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://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/2002/console
COMMIT: f83ab51731b04997ace525fa43ff0f249b764ca3

review: Needs Fixing
Revision history for this message
Newell Jensen (newell-jensen) wrote :

Inline comment below and it looks like there are unit tests for maas_ipmi_autodetect but I don't see any unit test additions for the changes you have introduced. Was there a reason not to add unit tests for them?

review: Needs Information
Revision history for this message
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://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/2003/console
COMMIT: e9ffd0688e6255063b54d79731adde5aa02ffa2b

review: Needs Fixing
Revision history for this message
Andres Rodriguez (andreserl) wrote :

The unittests for the BMC are targeted to test the BMC related functions. This change is not BMC related.

Revision history for this message
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: 8a1cb40c712671dc1ca7dffe7168d1093d2e9fc1

review: Approve
Revision history for this message
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: f5180d0018413904736453dbfe4a7f81d65f98bc

review: Approve
Revision history for this message
Newell Jensen (newell-jensen) wrote :

+1 based on your comment about unit tests.

review: Approve
Revision history for this message
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://code.launchpad.net/~andreserl/maas/+git/maas/+merge/341740
> You are reviewing the proposed merge of ~andreserl/maas:ipmi_boot_type
> into maas:master.
>

--
Andres Rodriguez
Engineering Manager, MAAS
Canonical USA, Inc.

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNABLE TO START LANDING

STATUS: MISSING COMMIT MESSAGE

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..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)
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..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()
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..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)
118diff --git a/src/provisioningserver/drivers/power/ipmi.py b/src/provisioningserver/drivers/power/ipmi.py
119index 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':
206diff --git a/src/provisioningserver/drivers/power/tests/test_ipmi.py b/src/provisioningserver/drivers/power/tests/test_ipmi.py
207index 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))
289diff --git a/src/provisioningserver/refresh/maas_api_helper.py b/src/provisioningserver/refresh/maas_api_helper.py
290index 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(
314diff --git a/src/provisioningserver/refresh/tests/test_maas_api_helper.py b/src/provisioningserver/refresh/tests/test_maas_api_helper.py
315index 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.

Subscribers

People subscribed via source and target branches