Merge ~wpk/maas:IPXE-Boot into maas:master

Proposed by Witold Krecicki
Status: Rejected
Rejected by: Andres Rodriguez
Proposed branch: ~wpk/maas:IPXE-Boot
Merge into: maas:master
Diff against target: 363 lines (+136/-7)
18 files modified
src/maasserver/models/node.py (+1/-1)
src/provisioningserver/boot/__init__.py (+15/-0)
src/provisioningserver/boot/ipxe.py (+83/-0)
src/provisioningserver/boot/open_firmware_ppc64el.py (+1/-0)
src/provisioningserver/boot/powernv.py (+1/-0)
src/provisioningserver/boot/pxe.py (+1/-0)
src/provisioningserver/boot/uefi_amd64.py (+1/-0)
src/provisioningserver/boot/uefi_arm64.py (+1/-0)
src/provisioningserver/boot/windows.py (+1/-0)
src/provisioningserver/dhcp/config.py (+15/-4)
src/provisioningserver/templates/dhcp/dhcpd6.conf.template (+1/-0)
src/provisioningserver/templates/ipxe/config.commissioning.template (+1/-0)
src/provisioningserver/templates/ipxe/config.enlist.template (+1/-0)
src/provisioningserver/templates/ipxe/config.install.template (+5/-0)
src/provisioningserver/templates/ipxe/config.local.template (+2/-0)
src/provisioningserver/templates/ipxe/config.poweroff.template (+3/-0)
src/provisioningserver/templates/ipxe/config.xinstall.template (+1/-0)
src/provisioningserver/utils/registry.py (+2/-2)
Reviewer Review Type Date Requested Status
MAAS Lander Needs Fixing
Blake Rouse (community) Needs Fixing
Review via email: mp+332552@code.launchpad.net

Description of the change

This change introduces iPXE boot method - iPXE (used for example by virsh pods, but it can also be burned into ROM and used in servers) can load kernel and initrd directly without any intermediate bootloader as pxelinux.0. It also, unlike pxelinux.0, works on IPv6.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

These changes look great, I like the idea of support iPXE directly instead of chainloading pxelinux.0.

You are missing unit tests. You need to add unit tests for your new boot method, the changes to the boot method generation code for dhcpd configurations.

Also have a couple of questions.

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

Olease do not land this in MAAS 2.3, as it is now closed for development.
This will need to wait until 2.4 is open!

On Mon, Oct 23, 2017 at 9:27 AM Blake Rouse <email address hidden>
wrote:

> Review: Needs Fixing
>
> These changes look great, I like the idea of support iPXE directly instead
> of chainloading pxelinux.0.
>
> You are missing unit tests. You need to add unit tests for your new boot
> method, the changes to the boot method generation code for dhcpd
> configurations.
>
> Also have a couple of questions.
>
> Diff comments:
>
> > diff --git a/src/provisioningserver/dhcp/config.py
> b/src/provisioningserver/dhcp/config.py
> > index 160d30d..9af33e3 100644
> > --- a/src/provisioningserver/dhcp/config.py
> > +++ b/src/provisioningserver/dhcp/config.py
> > @@ -83,12 +84,21 @@ else {
> > class DHCPConfigError(Exception):
> > """Exception raised for errors processing the DHCP config."""
> >
> > +def user_class_to_hex(value):
> > + """isc-dhcp does not understand dhcpv6 User Class option as string,
> > + so we need to give it to it as a series of octets."""
> > + if value is None or value == "" or len(value) > 65535:
> > + return None
> > + rv = "%.2x:%.2x:" % (len(value)//256, len(value)%256)
>
> I beleive this should fail lint, as operations should be seperated by
> spaces.
>
> rv = "%.2x:%.2x:" % (len(value) // 256, len(value) % 256)
>
> > + rv += ":".join(map(lambda x: "%.2x" % ord(x), value))
> > + return rv
> >
> > def compose_conditional_bootloader(ipv6, rack_ip=None):
> > output = ""
> > behaviour = chain(["if"], repeat("elsif"))
> > for name, method in BootMethodRegistry:
> > - if method.arch_octet is not None:
> > + if method.arch_octet is not None or method.user_class is not
> None:
> > + hexified_user_class = user_class_to_hex(method.user_class)
> > url = ('tftp://[%s]/' if ipv6 else 'tftp://%s/') % rack_ip
> > if method.path_prefix:
> > url += method.path_prefix
> > diff --git
> a/src/provisioningserver/templates/ipxe/config.install.template
> b/src/provisioningserver/templates/ipxe/config.install.template
> > new file mode 100644
> > index 0000000..bf87001
> > --- /dev/null
> > +++ b/src/provisioningserver/templates/ipxe/config.install.template
> > @@ -0,0 +1,5 @@
> > +#!ipxe
> > +kernel {{kernel_params | kernel_path }}
> > +imgargs {{kernel_params | kernel_name }} {{kernel_params |
> kernel_command}} BOOTIF=01-${net_default_mac}
>
> ${net_default_mac} is a grub thing? Is it the same for iPXE?
>
> > +initrd {{kernel_params | initrd_path }}
> > +boot
> > \ No newline at end of file
>
>
> --
> https://code.launchpad.net/~wpk/maas/+git/maas/+merge/332552
> Your team MAAS Committers is subscribed to branch maas:master.
>
--
Andres Rodriguez
Engineering Manager, MAAS
Canonical USA, Inc.

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

UNIT TESTS
-b IPXE-Boot lp:~wpk/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/539/console
COMMIT: b0150ac402c00d328f92b7932beb63bca618dc0b

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

UNIT TESTS
-b IPXE-Boot lp:~wpk/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/539/console
COMMIT: b0150ac402c00d328f92b7932beb63bca618dc0b

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

UNIT TESTS
-b IPXE-Boot lp:~wpk/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/539/console
COMMIT: b0150ac402c00d328f92b7932beb63bca618dc0b

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

UNIT TESTS
-b IPXE-Boot lp:~wpk/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/539/console
COMMIT: b0150ac402c00d328f92b7932beb63bca618dc0b

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

Marking this as rejected as this has now been fix in MAAS 2.6.

Unmerged commits

b0150ac... by Witold Krecicki

iPXE boot method

d310824... by Witold Krecicki

Merge branch 'master' of git+ssh://git.launchpad.net/maas

95a3f7e... by Witold Krecicki

Merge branch 'master' of git+ssh://git.launchpad.net/maas

6e06021... by Witold Krecicki

Merge branch 'master' of git+ssh://git.launchpad.net/maas

5648a31... by Witold Krecicki

Fix maas_enlist with IPv6 addresses in server_url LP: #1717287

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
2index a397404..9ea811f 100644
3--- a/src/maasserver/models/node.py
4+++ b/src/maasserver/models/node.py
5@@ -234,7 +234,7 @@ maaslog = get_maas_logger("node")
6
7 # Holds the known `bios_boot_methods`. If `bios_boot_method` is not in this
8 # list then it will fallback to `DEFAULT_BIOS_BOOT_METHOD`.
9-KNOWN_BIOS_BOOT_METHODS = ["pxe", "uefi", "powernv", "powerkvm"]
10+KNOWN_BIOS_BOOT_METHODS = ["pxe", "uefi", "powernv", "powerkvm", "ipxe"]
11
12 # Default `bios_boot_method`. See `KNOWN_BIOS_BOOT_METHOD` above for usage.
13 DEFAULT_BIOS_BOOT_METHOD = "pxe"
14diff --git a/src/provisioningserver/boot/__init__.py b/src/provisioningserver/boot/__init__.py
15index 5f32111..bd6698f 100644
16--- a/src/provisioningserver/boot/__init__.py
17+++ b/src/provisioningserver/boot/__init__.py
18@@ -172,6 +172,11 @@ class BootMethod(metaclass=ABCMeta):
19 dhcpv6-parameters.xhtml#processor-architecture
20 """
21
22+ @abstractproperty
23+ def user_class(self):
24+ """ User class that supports this method. Used for the
25+ dhcpd.conf file that is generated."""
26+
27 def match_path(self, backend, path):
28 """Checks path for a file the boot method needs to handle.
29
30@@ -282,6 +287,7 @@ class BootMethod(metaclass=ABCMeta):
31 assert isinstance(self.bootloader_files, list) and all(
32 isinstance(element, str) for element in self.bootloader_files)
33 assert isinstance(self.arch_octet, str) or self.arch_octet is None
34+ assert isinstance(self.user_class, str) or self.user_class is None
35
36 def get_template_dir(self):
37 """Gets the template directory for the boot method."""
38@@ -339,6 +345,12 @@ class BootMethod(metaclass=ABCMeta):
39 initrd = 'boot-initrd'
40 return "%s/%s" % (image_dir(params), initrd)
41
42+ def kernel_name(params):
43+ if params.kernel is not None:
44+ return params.kernel
45+ else:
46+ return 'boot-kernel'
47+
48 def kernel_path(params):
49 # Normally the kernel filename is the SimpleStream filetype. If
50 # no filetype is given try the filetype.
51@@ -368,6 +380,7 @@ class BootMethod(metaclass=ABCMeta):
52 "kernel_command": kernel_command,
53 "kernel_params": kernel_params,
54 "kernel_path": kernel_path,
55+ "kernel_name": kernel_name,
56 "dtb_path": dtb_path,
57 }
58
59@@ -379,6 +392,7 @@ class BootMethodRegistry(Registry):
60
61
62 # Import the supported boot methods after defining BootMethod.
63+from provisioningserver.boot.ipxe import IPXEBootMethod
64 from provisioningserver.boot.pxe import PXEBootMethod
65 from provisioningserver.boot.uefi_amd64 import UEFIAMD64BootMethod
66 from provisioningserver.boot.uefi_arm64 import UEFIARM64BootMethod
67@@ -390,6 +404,7 @@ from provisioningserver.boot.windows import WindowsPXEBootMethod
68
69
70 builtin_boot_methods = [
71+ IPXEBootMethod(),
72 PXEBootMethod(),
73 UEFIAMD64BootMethod(),
74 UEFIARM64BootMethod(),
75diff --git a/src/provisioningserver/boot/ipxe.py b/src/provisioningserver/boot/ipxe.py
76new file mode 100644
77index 0000000..bc75f38
78--- /dev/null
79+++ b/src/provisioningserver/boot/ipxe.py
80@@ -0,0 +1,83 @@
81+# Copyright 2017 Canonical Ltd. This software is licensed under the
82+# GNU Affero General Public License version 3 (see the file LICENSE).
83+
84+"""IPXE Boot Method"""
85+
86+__all__ = [
87+ 'IPXEBootMethod',
88+ ]
89+
90+from itertools import repeat
91+import os
92+import re
93+import shutil
94+
95+from provisioningserver.boot import (
96+ BootMethod,
97+ BytesReader,
98+ get_parameters,
99+ get_remote_mac,
100+)
101+from provisioningserver.events import (
102+ EVENT_TYPES,
103+ try_send_rack_event,
104+)
105+from provisioningserver.logger import (
106+ get_maas_logger,
107+ LegacyLogger,
108+)
109+from provisioningserver.utils.fs import (
110+ atomic_copy,
111+ atomic_symlink,
112+)
113+
114+
115+maaslog = get_maas_logger('ipxe')
116+log = LegacyLogger()
117+
118+
119+re_ipxe_file = r'''^/*maas-ipxe[.]cfg$'''
120+
121+re_ipxe_file = re_ipxe_file.encode("ascii")
122+re_ipxe_file = re.compile(re_ipxe_file, re.VERBOSE)
123+
124+
125+class IPXEBootMethod(BootMethod):
126+ name = 'ipxe'
127+ bios_boot_method = 'ipxe'
128+ template_subdir = 'ipxe'
129+ bootloader_arches = ['i386', 'amd64']
130+ bootloader_path = 'maas-ipxe.cfg'
131+ bootloader_files = [ 'maas-ipxe.cfg' ]
132+ arch_octet = None
133+ user_class = 'iPXE'
134+
135+ def match_path(self, backend, path):
136+ """Checks path for the configuration file that needs to be
137+ generated.
138+
139+ :param backend: requesting backend
140+ :param path: requested path
141+ :return: dict of match params from path, None if no match
142+ """
143+ match = re_ipxe_file.match(path)
144+ if match is None:
145+ return None
146+ else:
147+ return {'mac': get_remote_mac()}
148+
149+ def get_reader(self, backend, kernel_params, **extra):
150+ """Render a configuration file as a unicode string.
151+
152+ :param backend: requesting backend
153+ :param kernel_params: An instance of `KernelParameters`.
154+ :param extra: Allow for other arguments. This is a safety valve;
155+ parameters generated in another component (for example, see
156+ `TFTPBackend.get_boot_method_reader`) won't cause this to break.
157+ """
158+ template = self.get_template(
159+ kernel_params.purpose, kernel_params.arch,
160+ kernel_params.subarch)
161+ namespace = self.compose_template_namespace(kernel_params)
162+ return BytesReader(template.substitute(namespace).encode("utf-8"))
163+
164diff --git a/src/provisioningserver/boot/open_firmware_ppc64el.py b/src/provisioningserver/boot/open_firmware_ppc64el.py
165index 574bbbc..4ad4da7 100644
166--- a/src/provisioningserver/boot/open_firmware_ppc64el.py
167+++ b/src/provisioningserver/boot/open_firmware_ppc64el.py
168@@ -19,3 +19,4 @@ class OpenFirmwarePPC64ELBootMethod(BootMethod):
169 bootloader_path = 'bootppc64.bin'
170 bootloader_files = ['bootppc64.bin']
171 arch_octet = '00:0C'
172+ user_class = None
173diff --git a/src/provisioningserver/boot/powernv.py b/src/provisioningserver/boot/powernv.py
174index eac0d9a..4535f2b 100644
175--- a/src/provisioningserver/boot/powernv.py
176+++ b/src/provisioningserver/boot/powernv.py
177@@ -79,6 +79,7 @@ class PowerNVBootMethod(BootMethod):
178 template_subdir = "pxe"
179 bootloader_path = "pxelinux.0"
180 arch_octet = "00:0E"
181+ user_class = None
182 path_prefix = "ppc64el/"
183
184 def get_params(self, backend, path):
185diff --git a/src/provisioningserver/boot/pxe.py b/src/provisioningserver/boot/pxe.py
186index 9884054..e8b6d8a 100644
187--- a/src/provisioningserver/boot/pxe.py
188+++ b/src/provisioningserver/boot/pxe.py
189@@ -88,6 +88,7 @@ class PXEBootMethod(BootMethod):
190 'libutil.c32',
191 ]
192 arch_octet = '00:00'
193+ user_class = None
194
195 def match_path(self, backend, path):
196 """Checks path for the configuration file that needs to be
197diff --git a/src/provisioningserver/boot/uefi_amd64.py b/src/provisioningserver/boot/uefi_amd64.py
198index 430268f..b5b78f9 100644
199--- a/src/provisioningserver/boot/uefi_amd64.py
200+++ b/src/provisioningserver/boot/uefi_amd64.py
201@@ -81,6 +81,7 @@ class UEFIAMD64BootMethod(BootMethod):
202 bootloader_path = 'bootx64.efi'
203 bootloader_files = ['bootx64.efi', 'grubx64.efi']
204 arch_octet = '00:07'
205+ user_class = None
206
207 def match_path(self, backend, path):
208 """Checks path for the configuration file that needs to be
209diff --git a/src/provisioningserver/boot/uefi_arm64.py b/src/provisioningserver/boot/uefi_arm64.py
210index 086dbc1..50fba08 100644
211--- a/src/provisioningserver/boot/uefi_arm64.py
212+++ b/src/provisioningserver/boot/uefi_arm64.py
213@@ -19,3 +19,4 @@ class UEFIARM64BootMethod(BootMethod):
214 bootloader_path = 'grubaa64.efi'
215 bootloader_files = ['grubaa64.efi']
216 arch_octet = '00:0B'
217+ user_class = None
218diff --git a/src/provisioningserver/boot/windows.py b/src/provisioningserver/boot/windows.py
219index f89e92b..9846c41 100644
220--- a/src/provisioningserver/boot/windows.py
221+++ b/src/provisioningserver/boot/windows.py
222@@ -197,6 +197,7 @@ class WindowsPXEBootMethod(BootMethod):
223 template_subdir = "windows"
224 bootloader_path = "pxeboot.0"
225 arch_octet = None
226+ user_class = None
227
228 @deferred
229 def get_node_info(self):
230diff --git a/src/provisioningserver/dhcp/config.py b/src/provisioningserver/dhcp/config.py
231index 160d30d..9af33e3 100644
232--- a/src/provisioningserver/dhcp/config.py
233+++ b/src/provisioningserver/dhcp/config.py
234@@ -47,12 +47,13 @@ logger = logging.getLogger(__name__)
235 # Used to generate the conditional bootloader behaviour
236 CONDITIONAL_BOOTLOADER = ("""
237 {{if ipv6}}
238- {{behaviour}} exists dhcp6.client-arch-type and
239- option dhcp6.client-arch-type = {{arch_octet}} {
240+ {{behaviour}} {{if arch_octet}} exists dhcp6.client-arch-type and option dhcp6.client-arch-type = {{arch_octet}} {{if user_class}} and {{endif}}{{endif}} \
241+ {{if user_class}} exists dhcp6.user-class and option dhcp6.user-class = {{user_class}} {{endif}} {
242 option dhcp6.bootfile-url \"{{url}}\";
243 }
244 {{else}}
245-{{behaviour}} option arch = {{arch_octet}} {
246+{{behaviour}} {{if arch_octet}} exists arch and option arch = {{arch_octet}} {{if user_class}} and {{endif}}{{endif}} \
247+ {{if user_class}} exists user-class and option user-class == {{user_class}} {{endif}} {
248 # {{name}}
249 filename \"{{bootloader}}\";
250 {{if path_prefix}}
251@@ -83,12 +84,21 @@ else {
252 class DHCPConfigError(Exception):
253 """Exception raised for errors processing the DHCP config."""
254
255+def user_class_to_hex(value):
256+ """isc-dhcp does not understand dhcpv6 User Class option as string,
257+ so we need to give it to it as a series of octets."""
258+ if value is None or value == "" or len(value) > 65535:
259+ return None
260+ rv = "%.2x:%.2x:" % (len(value)//256, len(value)%256)
261+ rv += ":".join(map(lambda x: "%.2x" % ord(x), value))
262+ return rv
263
264 def compose_conditional_bootloader(ipv6, rack_ip=None):
265 output = ""
266 behaviour = chain(["if"], repeat("elsif"))
267 for name, method in BootMethodRegistry:
268- if method.arch_octet is not None:
269+ if method.arch_octet is not None or method.user_class is not None:
270+ hexified_user_class = user_class_to_hex(method.user_class)
271 url = ('tftp://[%s]/' if ipv6 else 'tftp://%s/') % rack_ip
272 if method.path_prefix:
273 url += method.path_prefix
274@@ -98,6 +108,7 @@ def compose_conditional_bootloader(ipv6, rack_ip=None):
275 ipv6=ipv6, rack_ip=rack_ip, url=url,
276 behaviour=next(behaviour),
277 arch_octet=method.arch_octet,
278+ user_class=hexified_user_class,
279 bootloader=method.bootloader_path,
280 path_prefix=method.path_prefix,
281 name=method.name,
282diff --git a/src/provisioningserver/templates/dhcp/dhcpd6.conf.template b/src/provisioningserver/templates/dhcp/dhcpd6.conf.template
283index e7b2394..7c0a004 100644
284--- a/src/provisioningserver/templates/dhcp/dhcpd6.conf.template
285+++ b/src/provisioningserver/templates/dhcp/dhcpd6.conf.template
286@@ -2,6 +2,7 @@
287 # overwrite any changes made there. Instead, you can modify dhcpd6.conf by
288 # using DHCP snippets over the API or through the web interface.
289
290+option dhcp6.user-class code 15 = string;
291 option dhcp6.client-arch-type code 61 = array of unsigned integer 16; # RFC5970
292 option path-prefix code 210 = text; #RFC5071
293
294diff --git a/src/provisioningserver/templates/ipxe/config.commissioning.template b/src/provisioningserver/templates/ipxe/config.commissioning.template
295new file mode 120000
296index 0000000..44389e4
297--- /dev/null
298+++ b/src/provisioningserver/templates/ipxe/config.commissioning.template
299@@ -0,0 +1 @@
300+config.install.template
301\ No newline at end of file
302diff --git a/src/provisioningserver/templates/ipxe/config.enlist.template b/src/provisioningserver/templates/ipxe/config.enlist.template
303new file mode 120000
304index 0000000..44389e4
305--- /dev/null
306+++ b/src/provisioningserver/templates/ipxe/config.enlist.template
307@@ -0,0 +1 @@
308+config.install.template
309\ No newline at end of file
310diff --git a/src/provisioningserver/templates/ipxe/config.install.template b/src/provisioningserver/templates/ipxe/config.install.template
311new file mode 100644
312index 0000000..bf87001
313--- /dev/null
314+++ b/src/provisioningserver/templates/ipxe/config.install.template
315@@ -0,0 +1,5 @@
316+#!ipxe
317+kernel {{kernel_params | kernel_path }}
318+imgargs {{kernel_params | kernel_name }} {{kernel_params | kernel_command}} BOOTIF=01-${net_default_mac}
319+initrd {{kernel_params | initrd_path }}
320+boot
321\ No newline at end of file
322diff --git a/src/provisioningserver/templates/ipxe/config.local.template b/src/provisioningserver/templates/ipxe/config.local.template
323new file mode 100644
324index 0000000..4fe0777
325--- /dev/null
326+++ b/src/provisioningserver/templates/ipxe/config.local.template
327@@ -0,0 +1,2 @@
328+#!ipxe
329+exit
330diff --git a/src/provisioningserver/templates/ipxe/config.poweroff.template b/src/provisioningserver/templates/ipxe/config.poweroff.template
331new file mode 100644
332index 0000000..9fa5030
333--- /dev/null
334+++ b/src/provisioningserver/templates/ipxe/config.poweroff.template
335@@ -0,0 +1,3 @@
336+#!ipxe
337+poweroff
338+
339diff --git a/src/provisioningserver/templates/ipxe/config.xinstall.template b/src/provisioningserver/templates/ipxe/config.xinstall.template
340new file mode 120000
341index 0000000..44389e4
342--- /dev/null
343+++ b/src/provisioningserver/templates/ipxe/config.xinstall.template
344@@ -0,0 +1 @@
345+config.install.template
346\ No newline at end of file
347diff --git a/src/provisioningserver/utils/registry.py b/src/provisioningserver/utils/registry.py
348index c816368..f9d3ee6 100644
349--- a/src/provisioningserver/utils/registry.py
350+++ b/src/provisioningserver/utils/registry.py
351@@ -7,10 +7,10 @@ __all__ = [
352 "Registry",
353 ]
354
355-from collections import defaultdict
356+from collections import defaultdict, OrderedDict
357
358
359-_registry = defaultdict(dict)
360+_registry = defaultdict(OrderedDict)
361
362
363 class RegistryType(type):

Subscribers

People subscribed via source and target branches