Merge lp:~jtv/maas/extract-pxeconfig into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 2736
Proposed branch: lp:~jtv/maas/extract-pxeconfig
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 554 lines (+251/-213)
4 files modified
src/maasserver/api/api.py (+0/-209)
src/maasserver/api/pxeconfig.py (+247/-0)
src/maasserver/api/tests/test_pxeconfig.py (+3/-3)
src/maasserver/urls_api.py (+1/-1)
To merge this branch: bzr merge lp:~jtv/maas/extract-pxeconfig
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+231111@code.launchpad.net

Commit message

Extract API handler: pxeconfig.

Description of the change

For self-approval after cursory inspection.

Jeroen

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Looks OK. Self-approving.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/api.py'
2--- src/maasserver/api/api.py 2014-08-17 01:43:43 +0000
3+++ src/maasserver/api/api.py 2014-08-17 02:07:38 +0000
4@@ -69,7 +69,6 @@
5 "NodeMacHandler",
6 "NodeMacsHandler",
7 "NodesHandler",
8- "pxeconfig",
9 "render_api_docs",
10 "store_node_power_parameters",
11 ]
12@@ -127,7 +126,6 @@
13 NODE_PERMISSION,
14 NODE_STATUS,
15 NODEGROUP_STATUS,
16- PRESEED_TYPE,
17 )
18 from maasserver.exceptions import (
19 MAASAPIBadRequest,
20@@ -156,7 +154,6 @@
21 validate_config_name,
22 )
23 from maasserver.models import (
24- BootImage,
25 Config,
26 DHCPLease,
27 MACAddress,
28@@ -172,18 +169,10 @@
29 )
30 from maasserver.node_action import Commission
31 from maasserver.node_constraint_filter_forms import AcquireNodeForm
32-from maasserver.preseed import (
33- compose_enlistment_preseed_url,
34- compose_preseed_url,
35- get_preseed_type_for,
36- )
37-from maasserver.server_address import get_maas_facing_server_address
38-from maasserver.third_party_drivers import get_third_party_driver
39 from maasserver.utils import (
40 build_absolute_uri,
41 find_nodegroup,
42 get_local_cluster_UUID,
43- strip_domain,
44 )
45 from maasserver.utils.orm import (
46 get_first,
47@@ -192,7 +181,6 @@
48 from metadataserver.models import NodeResult
49 import netaddr
50 from piston.utils import rc
51-from provisioningserver.kernel_opts import KernelParameters
52 from provisioningserver.logger import get_maas_logger
53 from provisioningserver.power_schema import UNKNOWN_POWER_TYPE
54 import simplejson as json
55@@ -1910,203 +1898,6 @@
56 context_instance=RequestContext(request))
57
58
59-def get_boot_purpose(node):
60- """Return a suitable "purpose" for this boot, e.g. "install"."""
61- # XXX: allenap bug=1031406 2012-07-31: The boot purpose is still in
62- # flux. It may be that there will just be an "ephemeral" environment and
63- # an "install" environment, and the differing behaviour between, say,
64- # enlistment and commissioning - both of which will use the "ephemeral"
65- # environment - will be governed by varying the preseed or PXE
66- # configuration.
67- if node is None:
68- # This node is enlisting, for which we use a commissioning image.
69- return "commissioning"
70- elif node.status == NODE_STATUS.COMMISSIONING:
71- # It is commissioning.
72- return "commissioning"
73- elif node.status == NODE_STATUS.ALLOCATED:
74- # Install the node if netboot is enabled, otherwise boot locally.
75- if node.netboot:
76- preseed_type = get_preseed_type_for(node)
77- if preseed_type == PRESEED_TYPE.CURTIN:
78- return "xinstall"
79- else:
80- return "install"
81- else:
82- return "local" # TODO: Investigate.
83- else:
84- # Just poweroff? TODO: Investigate. Perhaps even send an IPMI signal
85- # to turn off power.
86- return "poweroff"
87-
88-
89-def get_node_from_mac_string(mac_string):
90- """Get a Node object from a MAC address string.
91-
92- Returns a Node object or None if no node with the given MAC address exists.
93-
94- :param mac_string: MAC address string in the form "12-34-56-78-9a-bc"
95- :return: Node object or None
96- """
97- if mac_string is None:
98- return None
99- macaddress = get_one(MACAddress.objects.filter(mac_address=mac_string))
100- return macaddress.node if macaddress else None
101-
102-
103-def find_nodegroup_for_pxeconfig_request(request):
104- """Find the nodegroup responsible for a `pxeconfig` request.
105-
106- Looks for the `cluster_uuid` parameter in the request. If there is
107- none, figures it out based on the requesting IP as a compatibility
108- measure. In that case, the result may be incorrect.
109- """
110- uuid = request.GET.get('cluster_uuid', None)
111- if uuid is None:
112- return find_nodegroup(request)
113- else:
114- return NodeGroup.objects.get(uuid=uuid)
115-
116-
117-def pxeconfig(request):
118- """Get the PXE configuration given a node's details.
119-
120- Returns a JSON object corresponding to a
121- :class:`provisioningserver.kernel_opts.KernelParameters` instance.
122-
123- This is now fairly decoupled from pxelinux's TFTP filename encoding
124- mechanism, with one notable exception. Call this function with (mac, arch,
125- subarch) and it will do the right thing. If details it needs are missing
126- (ie. arch/subarch missing when the MAC is supplied but unknown), then it
127- will as an exception return an HTTP NO_CONTENT (204) in the expectation
128- that this will be translated to a TFTP file not found and pxelinux (or an
129- emulator) will fall back to default-<arch>-<subarch> (in the case of an
130- alternate architecture emulator) or just straight to default (in the case
131- of native pxelinux on i386 or amd64). See bug 1041092 for details and
132- discussion.
133-
134- :param mac: MAC address to produce a boot configuration for.
135- :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not
136- 'armhf').
137- :param subarch: Subarchitecture name (in the pxelinux namespace).
138- :param local: The IP address of the cluster controller.
139- :param remote: The IP address of the booting node.
140- :param cluster_uuid: UUID of the cluster responsible for this node.
141- If omitted, the call will attempt to figure it out based on the
142- requesting IP address, for compatibility. Passing `cluster_uuid`
143- is preferred.
144- """
145- node = get_node_from_mac_string(request.GET.get('mac', None))
146-
147- if node is None or node.status == NODE_STATUS.COMMISSIONING:
148- osystem = Config.objects.get_config('commissioning_osystem')
149- series = Config.objects.get_config('commissioning_distro_series')
150- else:
151- osystem = node.get_osystem()
152- series = node.get_distro_series()
153-
154- if node:
155- arch, subarch = node.architecture.split('/')
156- preseed_url = compose_preseed_url(node)
157- # The node's hostname may include a domain, but we ignore that
158- # and use the one from the nodegroup instead.
159- hostname = strip_domain(node.hostname)
160- nodegroup = node.nodegroup
161- domain = nodegroup.name
162- else:
163- nodegroup = find_nodegroup_for_pxeconfig_request(request)
164- preseed_url = compose_enlistment_preseed_url(nodegroup=nodegroup)
165- hostname = 'maas-enlist'
166- domain = Config.objects.get_config('enlistment_domain')
167-
168- arch = get_optional_param(request.GET, 'arch')
169- if arch is None:
170- if 'mac' in request.GET:
171- # Request was pxelinux.cfg/01-<mac>, so attempt fall back
172- # to pxelinux.cfg/default-<arch>-<subarch> for arch detection.
173- return HttpResponse(status=httplib.NO_CONTENT)
174- else:
175- # Look in BootImage for an image that actually exists for the
176- # current series. If nothing is found, fall back to i386 like
177- # we used to. LP #1181334
178- image = BootImage.objects.get_default_arch_image_in_nodegroup(
179- nodegroup, osystem, series, purpose='commissioning')
180- if image is None:
181- arch = 'i386'
182- else:
183- arch = image.architecture
184-
185- subarch = get_optional_param(request.GET, 'subarch', 'generic')
186-
187- # If we are booting with "xinstall", then we should always return the
188- # commissioning operating system and distro_series.
189- purpose = get_boot_purpose(node)
190- if purpose == "xinstall":
191- osystem = Config.objects.get_config('commissioning_osystem')
192- series = Config.objects.get_config('commissioning_distro_series')
193-
194- # We use as our default label the label of the most recent image for
195- # the criteria we've assembled above. If there is no latest image
196- # (which should never happen in reality but may happen in tests), we
197- # fall back to using 'no-such-image' as our default.
198- latest_image = BootImage.objects.get_latest_image(
199- nodegroup, osystem, arch, subarch, series, purpose)
200- if latest_image is None:
201- # XXX 2014-03-18 gmb bug=1294131:
202- # We really ought to raise an exception here so that client
203- # and server can handle it according to their needs. At the
204- # moment, though, that breaks too many tests in awkward
205- # ways.
206- latest_label = 'no-such-image'
207- else:
208- latest_label = latest_image.label
209- # subarch may be different from the request because newer images
210- # support older hardware enablement, e.g. trusty/generic
211- # supports trusty/hwe-s. We must override the subarch to the one
212- # on the image otherwise the config path will be wrong if
213- # get_latest_image() returned an image with a different subarch.
214- subarch = latest_image.subarchitecture
215- label = get_optional_param(request.GET, 'label', latest_label)
216-
217- if node is not None:
218- # We don't care if the kernel opts is from the global setting or a tag,
219- # just get the options
220- _, effective_kernel_opts = node.get_effective_kernel_options()
221-
222- # Add any extra options from a third party driver.
223- use_driver = Config.objects.get_config('enable_third_party_drivers')
224- if use_driver:
225- driver = get_third_party_driver(node)
226- driver_kernel_opts = driver.get('kernel_opts', '')
227-
228- combined_opts = ('%s %s' % (
229- '' if effective_kernel_opts is None else effective_kernel_opts,
230- driver_kernel_opts)).strip()
231- if len(combined_opts):
232- extra_kernel_opts = combined_opts
233- else:
234- extra_kernel_opts = None
235- else:
236- extra_kernel_opts = effective_kernel_opts
237- else:
238- # If there's no node defined then we must be enlisting here, but
239- # we still need to return the global kernel options.
240- extra_kernel_opts = Config.objects.get_config("kernel_opts")
241-
242- server_address = get_maas_facing_server_address(nodegroup=nodegroup)
243- cluster_address = get_mandatory_param(request.GET, "local")
244-
245- params = KernelParameters(
246- osystem=osystem, arch=arch, subarch=subarch, release=series,
247- label=label, purpose=purpose, hostname=hostname, domain=domain,
248- preseed_url=preseed_url, log_host=server_address,
249- fs_host=cluster_address, extra_opts=extra_kernel_opts)
250-
251- return HttpResponse(
252- json.dumps(params._asdict()),
253- content_type="application/json")
254-
255-
256 class CommissioningResultsHandler(OperationsHandler):
257 """Read the collection of NodeResult in the MAAS."""
258 api_doc_section_name = "Commissioning results"
259
260=== added file 'src/maasserver/api/pxeconfig.py'
261--- src/maasserver/api/pxeconfig.py 1970-01-01 00:00:00 +0000
262+++ src/maasserver/api/pxeconfig.py 2014-08-17 02:07:38 +0000
263@@ -0,0 +1,247 @@
264+# Copyright 2014 Canonical Ltd. This software is licensed under the
265+# GNU Affero General Public License version 3 (see the file LICENSE).
266+
267+"""API handler: `pxeconfig`."""
268+
269+from __future__ import (
270+ absolute_import,
271+ print_function,
272+ unicode_literals,
273+ )
274+
275+str = None
276+
277+__metaclass__ = type
278+__all__ = [
279+ 'pxeconfig',
280+ ]
281+
282+
283+import httplib
284+
285+from django.http import HttpResponse
286+from maasserver.api.utils import (
287+ get_mandatory_param,
288+ get_optional_param,
289+ )
290+from maasserver.enum import (
291+ NODE_STATUS,
292+ PRESEED_TYPE,
293+ )
294+from maasserver.models import (
295+ BootImage,
296+ Config,
297+ MACAddress,
298+ NodeGroup,
299+ )
300+from maasserver.preseed import (
301+ compose_enlistment_preseed_url,
302+ compose_preseed_url,
303+ get_preseed_type_for,
304+ )
305+from maasserver.server_address import get_maas_facing_server_address
306+from maasserver.third_party_drivers import get_third_party_driver
307+from maasserver.utils import (
308+ find_nodegroup,
309+ strip_domain,
310+ )
311+from maasserver.utils.orm import get_one
312+from provisioningserver.kernel_opts import KernelParameters
313+import simplejson as json
314+
315+
316+def find_nodegroup_for_pxeconfig_request(request):
317+ """Find the nodegroup responsible for a `pxeconfig` request.
318+
319+ Looks for the `cluster_uuid` parameter in the request. If there is
320+ none, figures it out based on the requesting IP as a compatibility
321+ measure. In that case, the result may be incorrect.
322+ """
323+ uuid = request.GET.get('cluster_uuid', None)
324+ if uuid is None:
325+ return find_nodegroup(request)
326+ else:
327+ return NodeGroup.objects.get(uuid=uuid)
328+
329+
330+def get_node_from_mac_string(mac_string):
331+ """Get a Node object from a MAC address string.
332+
333+ Returns a Node object or None if no node with the given MAC address exists.
334+
335+ :param mac_string: MAC address string in the form "12-34-56-78-9a-bc"
336+ :return: Node object or None
337+ """
338+ if mac_string is None:
339+ return None
340+ macaddress = get_one(MACAddress.objects.filter(mac_address=mac_string))
341+ return macaddress.node if macaddress else None
342+
343+
344+def get_boot_purpose(node):
345+ """Return a suitable "purpose" for this boot, e.g. "install"."""
346+ # XXX: allenap bug=1031406 2012-07-31: The boot purpose is still in
347+ # flux. It may be that there will just be an "ephemeral" environment and
348+ # an "install" environment, and the differing behaviour between, say,
349+ # enlistment and commissioning - both of which will use the "ephemeral"
350+ # environment - will be governed by varying the preseed or PXE
351+ # configuration.
352+ if node is None:
353+ # This node is enlisting, for which we use a commissioning image.
354+ return "commissioning"
355+ elif node.status == NODE_STATUS.COMMISSIONING:
356+ # It is commissioning.
357+ return "commissioning"
358+ elif node.status == NODE_STATUS.ALLOCATED:
359+ # Install the node if netboot is enabled, otherwise boot locally.
360+ if node.netboot:
361+ preseed_type = get_preseed_type_for(node)
362+ if preseed_type == PRESEED_TYPE.CURTIN:
363+ return "xinstall"
364+ else:
365+ return "install"
366+ else:
367+ return "local" # TODO: Investigate.
368+ else:
369+ # Just poweroff? TODO: Investigate. Perhaps even send an IPMI signal
370+ # to turn off power.
371+ return "poweroff"
372+
373+
374+def pxeconfig(request):
375+ """Get the PXE configuration given a node's details.
376+
377+ Returns a JSON object corresponding to a
378+ :class:`provisioningserver.kernel_opts.KernelParameters` instance.
379+
380+ This is now fairly decoupled from pxelinux's TFTP filename encoding
381+ mechanism, with one notable exception. Call this function with (mac, arch,
382+ subarch) and it will do the right thing. If details it needs are missing
383+ (ie. arch/subarch missing when the MAC is supplied but unknown), then it
384+ will as an exception return an HTTP NO_CONTENT (204) in the expectation
385+ that this will be translated to a TFTP file not found and pxelinux (or an
386+ emulator) will fall back to default-<arch>-<subarch> (in the case of an
387+ alternate architecture emulator) or just straight to default (in the case
388+ of native pxelinux on i386 or amd64). See bug 1041092 for details and
389+ discussion.
390+
391+ :param mac: MAC address to produce a boot configuration for.
392+ :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not
393+ 'armhf').
394+ :param subarch: Subarchitecture name (in the pxelinux namespace).
395+ :param local: The IP address of the cluster controller.
396+ :param remote: The IP address of the booting node.
397+ :param cluster_uuid: UUID of the cluster responsible for this node.
398+ If omitted, the call will attempt to figure it out based on the
399+ requesting IP address, for compatibility. Passing `cluster_uuid`
400+ is preferred.
401+ """
402+ node = get_node_from_mac_string(request.GET.get('mac', None))
403+
404+ if node is None or node.status == NODE_STATUS.COMMISSIONING:
405+ osystem = Config.objects.get_config('commissioning_osystem')
406+ series = Config.objects.get_config('commissioning_distro_series')
407+ else:
408+ osystem = node.get_osystem()
409+ series = node.get_distro_series()
410+
411+ if node:
412+ arch, subarch = node.architecture.split('/')
413+ preseed_url = compose_preseed_url(node)
414+ # The node's hostname may include a domain, but we ignore that
415+ # and use the one from the nodegroup instead.
416+ hostname = strip_domain(node.hostname)
417+ nodegroup = node.nodegroup
418+ domain = nodegroup.name
419+ else:
420+ nodegroup = find_nodegroup_for_pxeconfig_request(request)
421+ preseed_url = compose_enlistment_preseed_url(nodegroup=nodegroup)
422+ hostname = 'maas-enlist'
423+ domain = Config.objects.get_config('enlistment_domain')
424+
425+ arch = get_optional_param(request.GET, 'arch')
426+ if arch is None:
427+ if 'mac' in request.GET:
428+ # Request was pxelinux.cfg/01-<mac>, so attempt fall back
429+ # to pxelinux.cfg/default-<arch>-<subarch> for arch detection.
430+ return HttpResponse(status=httplib.NO_CONTENT)
431+ else:
432+ # Look in BootImage for an image that actually exists for the
433+ # current series. If nothing is found, fall back to i386 like
434+ # we used to. LP #1181334
435+ image = BootImage.objects.get_default_arch_image_in_nodegroup(
436+ nodegroup, osystem, series, purpose='commissioning')
437+ if image is None:
438+ arch = 'i386'
439+ else:
440+ arch = image.architecture
441+
442+ subarch = get_optional_param(request.GET, 'subarch', 'generic')
443+
444+ # If we are booting with "xinstall", then we should always return the
445+ # commissioning operating system and distro_series.
446+ purpose = get_boot_purpose(node)
447+ if purpose == "xinstall":
448+ osystem = Config.objects.get_config('commissioning_osystem')
449+ series = Config.objects.get_config('commissioning_distro_series')
450+
451+ # We use as our default label the label of the most recent image for
452+ # the criteria we've assembled above. If there is no latest image
453+ # (which should never happen in reality but may happen in tests), we
454+ # fall back to using 'no-such-image' as our default.
455+ latest_image = BootImage.objects.get_latest_image(
456+ nodegroup, osystem, arch, subarch, series, purpose)
457+ if latest_image is None:
458+ # XXX 2014-03-18 gmb bug=1294131:
459+ # We really ought to raise an exception here so that client
460+ # and server can handle it according to their needs. At the
461+ # moment, though, that breaks too many tests in awkward
462+ # ways.
463+ latest_label = 'no-such-image'
464+ else:
465+ latest_label = latest_image.label
466+ # subarch may be different from the request because newer images
467+ # support older hardware enablement, e.g. trusty/generic
468+ # supports trusty/hwe-s. We must override the subarch to the one
469+ # on the image otherwise the config path will be wrong if
470+ # get_latest_image() returned an image with a different subarch.
471+ subarch = latest_image.subarchitecture
472+ label = get_optional_param(request.GET, 'label', latest_label)
473+
474+ if node is not None:
475+ # We don't care if the kernel opts is from the global setting or a tag,
476+ # just get the options
477+ _, effective_kernel_opts = node.get_effective_kernel_options()
478+
479+ # Add any extra options from a third party driver.
480+ use_driver = Config.objects.get_config('enable_third_party_drivers')
481+ if use_driver:
482+ driver = get_third_party_driver(node)
483+ driver_kernel_opts = driver.get('kernel_opts', '')
484+
485+ combined_opts = ('%s %s' % (
486+ '' if effective_kernel_opts is None else effective_kernel_opts,
487+ driver_kernel_opts)).strip()
488+ if len(combined_opts):
489+ extra_kernel_opts = combined_opts
490+ else:
491+ extra_kernel_opts = None
492+ else:
493+ extra_kernel_opts = effective_kernel_opts
494+ else:
495+ # If there's no node defined then we must be enlisting here, but
496+ # we still need to return the global kernel options.
497+ extra_kernel_opts = Config.objects.get_config("kernel_opts")
498+
499+ server_address = get_maas_facing_server_address(nodegroup=nodegroup)
500+ cluster_address = get_mandatory_param(request.GET, "local")
501+
502+ params = KernelParameters(
503+ osystem=osystem, arch=arch, subarch=subarch, release=series,
504+ label=label, purpose=purpose, hostname=hostname, domain=domain,
505+ preseed_url=preseed_url, log_host=server_address,
506+ fs_host=cluster_address, extra_opts=extra_kernel_opts)
507+
508+ return HttpResponse(
509+ json.dumps(params._asdict()),
510+ content_type="application/json")
511
512=== modified file 'src/maasserver/api/tests/test_pxeconfig.py'
513--- src/maasserver/api/tests/test_pxeconfig.py 2014-08-16 05:43:33 +0000
514+++ src/maasserver/api/tests/test_pxeconfig.py 2014-08-17 02:07:38 +0000
515@@ -20,8 +20,8 @@
516 from django.core.urlresolvers import reverse
517 from django.test.client import RequestFactory
518 from maasserver import server_address
519-from maasserver.api import api as api_module
520-from maasserver.api.api import (
521+from maasserver.api import pxeconfig as pxeconfig_module
522+from maasserver.api.pxeconfig import (
523 find_nodegroup_for_pxeconfig_request,
524 get_boot_purpose,
525 )
526@@ -344,7 +344,7 @@
527 def test_pxeconfig_uses_boot_purpose(self):
528 fake_boot_purpose = factory.make_name("purpose")
529 self.patch(
530- api_module, "get_boot_purpose"
531+ pxeconfig_module, "get_boot_purpose"
532 ).return_value = fake_boot_purpose
533 response = self.client.get(reverse('pxeconfig'),
534 self.get_default_params())
535
536=== modified file 'src/maasserver/urls_api.py'
537--- src/maasserver/urls_api.py 2014-08-17 01:43:43 +0000
538+++ src/maasserver/urls_api.py 2014-08-17 02:07:38 +0000
539@@ -31,7 +31,6 @@
540 NodeMacHandler,
541 NodeMacsHandler,
542 NodesHandler,
543- pxeconfig,
544 VersionHandler,
545 )
546 from maasserver.api.auth import api_auth
547@@ -67,6 +66,7 @@
548 NodeGroupInterfaceHandler,
549 NodeGroupInterfacesHandler,
550 )
551+from maasserver.api.pxeconfig import pxeconfig
552 from maasserver.api.ssh_keys import (
553 SSHKeyHandler,
554 SSHKeysHandler,