Merge lp:~jtv/maas/extract-pxeconfig into lp:~maas-committers/maas/trunk
- extract-pxeconfig
- Merge into 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 |
Related bugs: |
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.
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 | 69 | "NodeMacHandler", | 69 | "NodeMacHandler", |
6 | 70 | "NodeMacsHandler", | 70 | "NodeMacsHandler", |
7 | 71 | "NodesHandler", | 71 | "NodesHandler", |
8 | 72 | "pxeconfig", | ||
9 | 73 | "render_api_docs", | 72 | "render_api_docs", |
10 | 74 | "store_node_power_parameters", | 73 | "store_node_power_parameters", |
11 | 75 | ] | 74 | ] |
12 | @@ -127,7 +126,6 @@ | |||
13 | 127 | NODE_PERMISSION, | 126 | NODE_PERMISSION, |
14 | 128 | NODE_STATUS, | 127 | NODE_STATUS, |
15 | 129 | NODEGROUP_STATUS, | 128 | NODEGROUP_STATUS, |
16 | 130 | PRESEED_TYPE, | ||
17 | 131 | ) | 129 | ) |
18 | 132 | from maasserver.exceptions import ( | 130 | from maasserver.exceptions import ( |
19 | 133 | MAASAPIBadRequest, | 131 | MAASAPIBadRequest, |
20 | @@ -156,7 +154,6 @@ | |||
21 | 156 | validate_config_name, | 154 | validate_config_name, |
22 | 157 | ) | 155 | ) |
23 | 158 | from maasserver.models import ( | 156 | from maasserver.models import ( |
24 | 159 | BootImage, | ||
25 | 160 | Config, | 157 | Config, |
26 | 161 | DHCPLease, | 158 | DHCPLease, |
27 | 162 | MACAddress, | 159 | MACAddress, |
28 | @@ -172,18 +169,10 @@ | |||
29 | 172 | ) | 169 | ) |
30 | 173 | from maasserver.node_action import Commission | 170 | from maasserver.node_action import Commission |
31 | 174 | from maasserver.node_constraint_filter_forms import AcquireNodeForm | 171 | from maasserver.node_constraint_filter_forms import AcquireNodeForm |
32 | 175 | from maasserver.preseed import ( | ||
33 | 176 | compose_enlistment_preseed_url, | ||
34 | 177 | compose_preseed_url, | ||
35 | 178 | get_preseed_type_for, | ||
36 | 179 | ) | ||
37 | 180 | from maasserver.server_address import get_maas_facing_server_address | ||
38 | 181 | from maasserver.third_party_drivers import get_third_party_driver | ||
39 | 182 | from maasserver.utils import ( | 172 | from maasserver.utils import ( |
40 | 183 | build_absolute_uri, | 173 | build_absolute_uri, |
41 | 184 | find_nodegroup, | 174 | find_nodegroup, |
42 | 185 | get_local_cluster_UUID, | 175 | get_local_cluster_UUID, |
43 | 186 | strip_domain, | ||
44 | 187 | ) | 176 | ) |
45 | 188 | from maasserver.utils.orm import ( | 177 | from maasserver.utils.orm import ( |
46 | 189 | get_first, | 178 | get_first, |
47 | @@ -192,7 +181,6 @@ | |||
48 | 192 | from metadataserver.models import NodeResult | 181 | from metadataserver.models import NodeResult |
49 | 193 | import netaddr | 182 | import netaddr |
50 | 194 | from piston.utils import rc | 183 | from piston.utils import rc |
51 | 195 | from provisioningserver.kernel_opts import KernelParameters | ||
52 | 196 | from provisioningserver.logger import get_maas_logger | 184 | from provisioningserver.logger import get_maas_logger |
53 | 197 | from provisioningserver.power_schema import UNKNOWN_POWER_TYPE | 185 | from provisioningserver.power_schema import UNKNOWN_POWER_TYPE |
54 | 198 | import simplejson as json | 186 | import simplejson as json |
55 | @@ -1910,203 +1898,6 @@ | |||
56 | 1910 | context_instance=RequestContext(request)) | 1898 | context_instance=RequestContext(request)) |
57 | 1911 | 1899 | ||
58 | 1912 | 1900 | ||
59 | 1913 | def get_boot_purpose(node): | ||
60 | 1914 | """Return a suitable "purpose" for this boot, e.g. "install".""" | ||
61 | 1915 | # XXX: allenap bug=1031406 2012-07-31: The boot purpose is still in | ||
62 | 1916 | # flux. It may be that there will just be an "ephemeral" environment and | ||
63 | 1917 | # an "install" environment, and the differing behaviour between, say, | ||
64 | 1918 | # enlistment and commissioning - both of which will use the "ephemeral" | ||
65 | 1919 | # environment - will be governed by varying the preseed or PXE | ||
66 | 1920 | # configuration. | ||
67 | 1921 | if node is None: | ||
68 | 1922 | # This node is enlisting, for which we use a commissioning image. | ||
69 | 1923 | return "commissioning" | ||
70 | 1924 | elif node.status == NODE_STATUS.COMMISSIONING: | ||
71 | 1925 | # It is commissioning. | ||
72 | 1926 | return "commissioning" | ||
73 | 1927 | elif node.status == NODE_STATUS.ALLOCATED: | ||
74 | 1928 | # Install the node if netboot is enabled, otherwise boot locally. | ||
75 | 1929 | if node.netboot: | ||
76 | 1930 | preseed_type = get_preseed_type_for(node) | ||
77 | 1931 | if preseed_type == PRESEED_TYPE.CURTIN: | ||
78 | 1932 | return "xinstall" | ||
79 | 1933 | else: | ||
80 | 1934 | return "install" | ||
81 | 1935 | else: | ||
82 | 1936 | return "local" # TODO: Investigate. | ||
83 | 1937 | else: | ||
84 | 1938 | # Just poweroff? TODO: Investigate. Perhaps even send an IPMI signal | ||
85 | 1939 | # to turn off power. | ||
86 | 1940 | return "poweroff" | ||
87 | 1941 | |||
88 | 1942 | |||
89 | 1943 | def get_node_from_mac_string(mac_string): | ||
90 | 1944 | """Get a Node object from a MAC address string. | ||
91 | 1945 | |||
92 | 1946 | Returns a Node object or None if no node with the given MAC address exists. | ||
93 | 1947 | |||
94 | 1948 | :param mac_string: MAC address string in the form "12-34-56-78-9a-bc" | ||
95 | 1949 | :return: Node object or None | ||
96 | 1950 | """ | ||
97 | 1951 | if mac_string is None: | ||
98 | 1952 | return None | ||
99 | 1953 | macaddress = get_one(MACAddress.objects.filter(mac_address=mac_string)) | ||
100 | 1954 | return macaddress.node if macaddress else None | ||
101 | 1955 | |||
102 | 1956 | |||
103 | 1957 | def find_nodegroup_for_pxeconfig_request(request): | ||
104 | 1958 | """Find the nodegroup responsible for a `pxeconfig` request. | ||
105 | 1959 | |||
106 | 1960 | Looks for the `cluster_uuid` parameter in the request. If there is | ||
107 | 1961 | none, figures it out based on the requesting IP as a compatibility | ||
108 | 1962 | measure. In that case, the result may be incorrect. | ||
109 | 1963 | """ | ||
110 | 1964 | uuid = request.GET.get('cluster_uuid', None) | ||
111 | 1965 | if uuid is None: | ||
112 | 1966 | return find_nodegroup(request) | ||
113 | 1967 | else: | ||
114 | 1968 | return NodeGroup.objects.get(uuid=uuid) | ||
115 | 1969 | |||
116 | 1970 | |||
117 | 1971 | def pxeconfig(request): | ||
118 | 1972 | """Get the PXE configuration given a node's details. | ||
119 | 1973 | |||
120 | 1974 | Returns a JSON object corresponding to a | ||
121 | 1975 | :class:`provisioningserver.kernel_opts.KernelParameters` instance. | ||
122 | 1976 | |||
123 | 1977 | This is now fairly decoupled from pxelinux's TFTP filename encoding | ||
124 | 1978 | mechanism, with one notable exception. Call this function with (mac, arch, | ||
125 | 1979 | subarch) and it will do the right thing. If details it needs are missing | ||
126 | 1980 | (ie. arch/subarch missing when the MAC is supplied but unknown), then it | ||
127 | 1981 | will as an exception return an HTTP NO_CONTENT (204) in the expectation | ||
128 | 1982 | that this will be translated to a TFTP file not found and pxelinux (or an | ||
129 | 1983 | emulator) will fall back to default-<arch>-<subarch> (in the case of an | ||
130 | 1984 | alternate architecture emulator) or just straight to default (in the case | ||
131 | 1985 | of native pxelinux on i386 or amd64). See bug 1041092 for details and | ||
132 | 1986 | discussion. | ||
133 | 1987 | |||
134 | 1988 | :param mac: MAC address to produce a boot configuration for. | ||
135 | 1989 | :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not | ||
136 | 1990 | 'armhf'). | ||
137 | 1991 | :param subarch: Subarchitecture name (in the pxelinux namespace). | ||
138 | 1992 | :param local: The IP address of the cluster controller. | ||
139 | 1993 | :param remote: The IP address of the booting node. | ||
140 | 1994 | :param cluster_uuid: UUID of the cluster responsible for this node. | ||
141 | 1995 | If omitted, the call will attempt to figure it out based on the | ||
142 | 1996 | requesting IP address, for compatibility. Passing `cluster_uuid` | ||
143 | 1997 | is preferred. | ||
144 | 1998 | """ | ||
145 | 1999 | node = get_node_from_mac_string(request.GET.get('mac', None)) | ||
146 | 2000 | |||
147 | 2001 | if node is None or node.status == NODE_STATUS.COMMISSIONING: | ||
148 | 2002 | osystem = Config.objects.get_config('commissioning_osystem') | ||
149 | 2003 | series = Config.objects.get_config('commissioning_distro_series') | ||
150 | 2004 | else: | ||
151 | 2005 | osystem = node.get_osystem() | ||
152 | 2006 | series = node.get_distro_series() | ||
153 | 2007 | |||
154 | 2008 | if node: | ||
155 | 2009 | arch, subarch = node.architecture.split('/') | ||
156 | 2010 | preseed_url = compose_preseed_url(node) | ||
157 | 2011 | # The node's hostname may include a domain, but we ignore that | ||
158 | 2012 | # and use the one from the nodegroup instead. | ||
159 | 2013 | hostname = strip_domain(node.hostname) | ||
160 | 2014 | nodegroup = node.nodegroup | ||
161 | 2015 | domain = nodegroup.name | ||
162 | 2016 | else: | ||
163 | 2017 | nodegroup = find_nodegroup_for_pxeconfig_request(request) | ||
164 | 2018 | preseed_url = compose_enlistment_preseed_url(nodegroup=nodegroup) | ||
165 | 2019 | hostname = 'maas-enlist' | ||
166 | 2020 | domain = Config.objects.get_config('enlistment_domain') | ||
167 | 2021 | |||
168 | 2022 | arch = get_optional_param(request.GET, 'arch') | ||
169 | 2023 | if arch is None: | ||
170 | 2024 | if 'mac' in request.GET: | ||
171 | 2025 | # Request was pxelinux.cfg/01-<mac>, so attempt fall back | ||
172 | 2026 | # to pxelinux.cfg/default-<arch>-<subarch> for arch detection. | ||
173 | 2027 | return HttpResponse(status=httplib.NO_CONTENT) | ||
174 | 2028 | else: | ||
175 | 2029 | # Look in BootImage for an image that actually exists for the | ||
176 | 2030 | # current series. If nothing is found, fall back to i386 like | ||
177 | 2031 | # we used to. LP #1181334 | ||
178 | 2032 | image = BootImage.objects.get_default_arch_image_in_nodegroup( | ||
179 | 2033 | nodegroup, osystem, series, purpose='commissioning') | ||
180 | 2034 | if image is None: | ||
181 | 2035 | arch = 'i386' | ||
182 | 2036 | else: | ||
183 | 2037 | arch = image.architecture | ||
184 | 2038 | |||
185 | 2039 | subarch = get_optional_param(request.GET, 'subarch', 'generic') | ||
186 | 2040 | |||
187 | 2041 | # If we are booting with "xinstall", then we should always return the | ||
188 | 2042 | # commissioning operating system and distro_series. | ||
189 | 2043 | purpose = get_boot_purpose(node) | ||
190 | 2044 | if purpose == "xinstall": | ||
191 | 2045 | osystem = Config.objects.get_config('commissioning_osystem') | ||
192 | 2046 | series = Config.objects.get_config('commissioning_distro_series') | ||
193 | 2047 | |||
194 | 2048 | # We use as our default label the label of the most recent image for | ||
195 | 2049 | # the criteria we've assembled above. If there is no latest image | ||
196 | 2050 | # (which should never happen in reality but may happen in tests), we | ||
197 | 2051 | # fall back to using 'no-such-image' as our default. | ||
198 | 2052 | latest_image = BootImage.objects.get_latest_image( | ||
199 | 2053 | nodegroup, osystem, arch, subarch, series, purpose) | ||
200 | 2054 | if latest_image is None: | ||
201 | 2055 | # XXX 2014-03-18 gmb bug=1294131: | ||
202 | 2056 | # We really ought to raise an exception here so that client | ||
203 | 2057 | # and server can handle it according to their needs. At the | ||
204 | 2058 | # moment, though, that breaks too many tests in awkward | ||
205 | 2059 | # ways. | ||
206 | 2060 | latest_label = 'no-such-image' | ||
207 | 2061 | else: | ||
208 | 2062 | latest_label = latest_image.label | ||
209 | 2063 | # subarch may be different from the request because newer images | ||
210 | 2064 | # support older hardware enablement, e.g. trusty/generic | ||
211 | 2065 | # supports trusty/hwe-s. We must override the subarch to the one | ||
212 | 2066 | # on the image otherwise the config path will be wrong if | ||
213 | 2067 | # get_latest_image() returned an image with a different subarch. | ||
214 | 2068 | subarch = latest_image.subarchitecture | ||
215 | 2069 | label = get_optional_param(request.GET, 'label', latest_label) | ||
216 | 2070 | |||
217 | 2071 | if node is not None: | ||
218 | 2072 | # We don't care if the kernel opts is from the global setting or a tag, | ||
219 | 2073 | # just get the options | ||
220 | 2074 | _, effective_kernel_opts = node.get_effective_kernel_options() | ||
221 | 2075 | |||
222 | 2076 | # Add any extra options from a third party driver. | ||
223 | 2077 | use_driver = Config.objects.get_config('enable_third_party_drivers') | ||
224 | 2078 | if use_driver: | ||
225 | 2079 | driver = get_third_party_driver(node) | ||
226 | 2080 | driver_kernel_opts = driver.get('kernel_opts', '') | ||
227 | 2081 | |||
228 | 2082 | combined_opts = ('%s %s' % ( | ||
229 | 2083 | '' if effective_kernel_opts is None else effective_kernel_opts, | ||
230 | 2084 | driver_kernel_opts)).strip() | ||
231 | 2085 | if len(combined_opts): | ||
232 | 2086 | extra_kernel_opts = combined_opts | ||
233 | 2087 | else: | ||
234 | 2088 | extra_kernel_opts = None | ||
235 | 2089 | else: | ||
236 | 2090 | extra_kernel_opts = effective_kernel_opts | ||
237 | 2091 | else: | ||
238 | 2092 | # If there's no node defined then we must be enlisting here, but | ||
239 | 2093 | # we still need to return the global kernel options. | ||
240 | 2094 | extra_kernel_opts = Config.objects.get_config("kernel_opts") | ||
241 | 2095 | |||
242 | 2096 | server_address = get_maas_facing_server_address(nodegroup=nodegroup) | ||
243 | 2097 | cluster_address = get_mandatory_param(request.GET, "local") | ||
244 | 2098 | |||
245 | 2099 | params = KernelParameters( | ||
246 | 2100 | osystem=osystem, arch=arch, subarch=subarch, release=series, | ||
247 | 2101 | label=label, purpose=purpose, hostname=hostname, domain=domain, | ||
248 | 2102 | preseed_url=preseed_url, log_host=server_address, | ||
249 | 2103 | fs_host=cluster_address, extra_opts=extra_kernel_opts) | ||
250 | 2104 | |||
251 | 2105 | return HttpResponse( | ||
252 | 2106 | json.dumps(params._asdict()), | ||
253 | 2107 | content_type="application/json") | ||
254 | 2108 | |||
255 | 2109 | |||
256 | 2110 | class CommissioningResultsHandler(OperationsHandler): | 1901 | class CommissioningResultsHandler(OperationsHandler): |
257 | 2111 | """Read the collection of NodeResult in the MAAS.""" | 1902 | """Read the collection of NodeResult in the MAAS.""" |
258 | 2112 | api_doc_section_name = "Commissioning results" | 1903 | api_doc_section_name = "Commissioning results" |
259 | 2113 | 1904 | ||
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 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
265 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
266 | 3 | |||
267 | 4 | """API handler: `pxeconfig`.""" | ||
268 | 5 | |||
269 | 6 | from __future__ import ( | ||
270 | 7 | absolute_import, | ||
271 | 8 | print_function, | ||
272 | 9 | unicode_literals, | ||
273 | 10 | ) | ||
274 | 11 | |||
275 | 12 | str = None | ||
276 | 13 | |||
277 | 14 | __metaclass__ = type | ||
278 | 15 | __all__ = [ | ||
279 | 16 | 'pxeconfig', | ||
280 | 17 | ] | ||
281 | 18 | |||
282 | 19 | |||
283 | 20 | import httplib | ||
284 | 21 | |||
285 | 22 | from django.http import HttpResponse | ||
286 | 23 | from maasserver.api.utils import ( | ||
287 | 24 | get_mandatory_param, | ||
288 | 25 | get_optional_param, | ||
289 | 26 | ) | ||
290 | 27 | from maasserver.enum import ( | ||
291 | 28 | NODE_STATUS, | ||
292 | 29 | PRESEED_TYPE, | ||
293 | 30 | ) | ||
294 | 31 | from maasserver.models import ( | ||
295 | 32 | BootImage, | ||
296 | 33 | Config, | ||
297 | 34 | MACAddress, | ||
298 | 35 | NodeGroup, | ||
299 | 36 | ) | ||
300 | 37 | from maasserver.preseed import ( | ||
301 | 38 | compose_enlistment_preseed_url, | ||
302 | 39 | compose_preseed_url, | ||
303 | 40 | get_preseed_type_for, | ||
304 | 41 | ) | ||
305 | 42 | from maasserver.server_address import get_maas_facing_server_address | ||
306 | 43 | from maasserver.third_party_drivers import get_third_party_driver | ||
307 | 44 | from maasserver.utils import ( | ||
308 | 45 | find_nodegroup, | ||
309 | 46 | strip_domain, | ||
310 | 47 | ) | ||
311 | 48 | from maasserver.utils.orm import get_one | ||
312 | 49 | from provisioningserver.kernel_opts import KernelParameters | ||
313 | 50 | import simplejson as json | ||
314 | 51 | |||
315 | 52 | |||
316 | 53 | def find_nodegroup_for_pxeconfig_request(request): | ||
317 | 54 | """Find the nodegroup responsible for a `pxeconfig` request. | ||
318 | 55 | |||
319 | 56 | Looks for the `cluster_uuid` parameter in the request. If there is | ||
320 | 57 | none, figures it out based on the requesting IP as a compatibility | ||
321 | 58 | measure. In that case, the result may be incorrect. | ||
322 | 59 | """ | ||
323 | 60 | uuid = request.GET.get('cluster_uuid', None) | ||
324 | 61 | if uuid is None: | ||
325 | 62 | return find_nodegroup(request) | ||
326 | 63 | else: | ||
327 | 64 | return NodeGroup.objects.get(uuid=uuid) | ||
328 | 65 | |||
329 | 66 | |||
330 | 67 | def get_node_from_mac_string(mac_string): | ||
331 | 68 | """Get a Node object from a MAC address string. | ||
332 | 69 | |||
333 | 70 | Returns a Node object or None if no node with the given MAC address exists. | ||
334 | 71 | |||
335 | 72 | :param mac_string: MAC address string in the form "12-34-56-78-9a-bc" | ||
336 | 73 | :return: Node object or None | ||
337 | 74 | """ | ||
338 | 75 | if mac_string is None: | ||
339 | 76 | return None | ||
340 | 77 | macaddress = get_one(MACAddress.objects.filter(mac_address=mac_string)) | ||
341 | 78 | return macaddress.node if macaddress else None | ||
342 | 79 | |||
343 | 80 | |||
344 | 81 | def get_boot_purpose(node): | ||
345 | 82 | """Return a suitable "purpose" for this boot, e.g. "install".""" | ||
346 | 83 | # XXX: allenap bug=1031406 2012-07-31: The boot purpose is still in | ||
347 | 84 | # flux. It may be that there will just be an "ephemeral" environment and | ||
348 | 85 | # an "install" environment, and the differing behaviour between, say, | ||
349 | 86 | # enlistment and commissioning - both of which will use the "ephemeral" | ||
350 | 87 | # environment - will be governed by varying the preseed or PXE | ||
351 | 88 | # configuration. | ||
352 | 89 | if node is None: | ||
353 | 90 | # This node is enlisting, for which we use a commissioning image. | ||
354 | 91 | return "commissioning" | ||
355 | 92 | elif node.status == NODE_STATUS.COMMISSIONING: | ||
356 | 93 | # It is commissioning. | ||
357 | 94 | return "commissioning" | ||
358 | 95 | elif node.status == NODE_STATUS.ALLOCATED: | ||
359 | 96 | # Install the node if netboot is enabled, otherwise boot locally. | ||
360 | 97 | if node.netboot: | ||
361 | 98 | preseed_type = get_preseed_type_for(node) | ||
362 | 99 | if preseed_type == PRESEED_TYPE.CURTIN: | ||
363 | 100 | return "xinstall" | ||
364 | 101 | else: | ||
365 | 102 | return "install" | ||
366 | 103 | else: | ||
367 | 104 | return "local" # TODO: Investigate. | ||
368 | 105 | else: | ||
369 | 106 | # Just poweroff? TODO: Investigate. Perhaps even send an IPMI signal | ||
370 | 107 | # to turn off power. | ||
371 | 108 | return "poweroff" | ||
372 | 109 | |||
373 | 110 | |||
374 | 111 | def pxeconfig(request): | ||
375 | 112 | """Get the PXE configuration given a node's details. | ||
376 | 113 | |||
377 | 114 | Returns a JSON object corresponding to a | ||
378 | 115 | :class:`provisioningserver.kernel_opts.KernelParameters` instance. | ||
379 | 116 | |||
380 | 117 | This is now fairly decoupled from pxelinux's TFTP filename encoding | ||
381 | 118 | mechanism, with one notable exception. Call this function with (mac, arch, | ||
382 | 119 | subarch) and it will do the right thing. If details it needs are missing | ||
383 | 120 | (ie. arch/subarch missing when the MAC is supplied but unknown), then it | ||
384 | 121 | will as an exception return an HTTP NO_CONTENT (204) in the expectation | ||
385 | 122 | that this will be translated to a TFTP file not found and pxelinux (or an | ||
386 | 123 | emulator) will fall back to default-<arch>-<subarch> (in the case of an | ||
387 | 124 | alternate architecture emulator) or just straight to default (in the case | ||
388 | 125 | of native pxelinux on i386 or amd64). See bug 1041092 for details and | ||
389 | 126 | discussion. | ||
390 | 127 | |||
391 | 128 | :param mac: MAC address to produce a boot configuration for. | ||
392 | 129 | :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not | ||
393 | 130 | 'armhf'). | ||
394 | 131 | :param subarch: Subarchitecture name (in the pxelinux namespace). | ||
395 | 132 | :param local: The IP address of the cluster controller. | ||
396 | 133 | :param remote: The IP address of the booting node. | ||
397 | 134 | :param cluster_uuid: UUID of the cluster responsible for this node. | ||
398 | 135 | If omitted, the call will attempt to figure it out based on the | ||
399 | 136 | requesting IP address, for compatibility. Passing `cluster_uuid` | ||
400 | 137 | is preferred. | ||
401 | 138 | """ | ||
402 | 139 | node = get_node_from_mac_string(request.GET.get('mac', None)) | ||
403 | 140 | |||
404 | 141 | if node is None or node.status == NODE_STATUS.COMMISSIONING: | ||
405 | 142 | osystem = Config.objects.get_config('commissioning_osystem') | ||
406 | 143 | series = Config.objects.get_config('commissioning_distro_series') | ||
407 | 144 | else: | ||
408 | 145 | osystem = node.get_osystem() | ||
409 | 146 | series = node.get_distro_series() | ||
410 | 147 | |||
411 | 148 | if node: | ||
412 | 149 | arch, subarch = node.architecture.split('/') | ||
413 | 150 | preseed_url = compose_preseed_url(node) | ||
414 | 151 | # The node's hostname may include a domain, but we ignore that | ||
415 | 152 | # and use the one from the nodegroup instead. | ||
416 | 153 | hostname = strip_domain(node.hostname) | ||
417 | 154 | nodegroup = node.nodegroup | ||
418 | 155 | domain = nodegroup.name | ||
419 | 156 | else: | ||
420 | 157 | nodegroup = find_nodegroup_for_pxeconfig_request(request) | ||
421 | 158 | preseed_url = compose_enlistment_preseed_url(nodegroup=nodegroup) | ||
422 | 159 | hostname = 'maas-enlist' | ||
423 | 160 | domain = Config.objects.get_config('enlistment_domain') | ||
424 | 161 | |||
425 | 162 | arch = get_optional_param(request.GET, 'arch') | ||
426 | 163 | if arch is None: | ||
427 | 164 | if 'mac' in request.GET: | ||
428 | 165 | # Request was pxelinux.cfg/01-<mac>, so attempt fall back | ||
429 | 166 | # to pxelinux.cfg/default-<arch>-<subarch> for arch detection. | ||
430 | 167 | return HttpResponse(status=httplib.NO_CONTENT) | ||
431 | 168 | else: | ||
432 | 169 | # Look in BootImage for an image that actually exists for the | ||
433 | 170 | # current series. If nothing is found, fall back to i386 like | ||
434 | 171 | # we used to. LP #1181334 | ||
435 | 172 | image = BootImage.objects.get_default_arch_image_in_nodegroup( | ||
436 | 173 | nodegroup, osystem, series, purpose='commissioning') | ||
437 | 174 | if image is None: | ||
438 | 175 | arch = 'i386' | ||
439 | 176 | else: | ||
440 | 177 | arch = image.architecture | ||
441 | 178 | |||
442 | 179 | subarch = get_optional_param(request.GET, 'subarch', 'generic') | ||
443 | 180 | |||
444 | 181 | # If we are booting with "xinstall", then we should always return the | ||
445 | 182 | # commissioning operating system and distro_series. | ||
446 | 183 | purpose = get_boot_purpose(node) | ||
447 | 184 | if purpose == "xinstall": | ||
448 | 185 | osystem = Config.objects.get_config('commissioning_osystem') | ||
449 | 186 | series = Config.objects.get_config('commissioning_distro_series') | ||
450 | 187 | |||
451 | 188 | # We use as our default label the label of the most recent image for | ||
452 | 189 | # the criteria we've assembled above. If there is no latest image | ||
453 | 190 | # (which should never happen in reality but may happen in tests), we | ||
454 | 191 | # fall back to using 'no-such-image' as our default. | ||
455 | 192 | latest_image = BootImage.objects.get_latest_image( | ||
456 | 193 | nodegroup, osystem, arch, subarch, series, purpose) | ||
457 | 194 | if latest_image is None: | ||
458 | 195 | # XXX 2014-03-18 gmb bug=1294131: | ||
459 | 196 | # We really ought to raise an exception here so that client | ||
460 | 197 | # and server can handle it according to their needs. At the | ||
461 | 198 | # moment, though, that breaks too many tests in awkward | ||
462 | 199 | # ways. | ||
463 | 200 | latest_label = 'no-such-image' | ||
464 | 201 | else: | ||
465 | 202 | latest_label = latest_image.label | ||
466 | 203 | # subarch may be different from the request because newer images | ||
467 | 204 | # support older hardware enablement, e.g. trusty/generic | ||
468 | 205 | # supports trusty/hwe-s. We must override the subarch to the one | ||
469 | 206 | # on the image otherwise the config path will be wrong if | ||
470 | 207 | # get_latest_image() returned an image with a different subarch. | ||
471 | 208 | subarch = latest_image.subarchitecture | ||
472 | 209 | label = get_optional_param(request.GET, 'label', latest_label) | ||
473 | 210 | |||
474 | 211 | if node is not None: | ||
475 | 212 | # We don't care if the kernel opts is from the global setting or a tag, | ||
476 | 213 | # just get the options | ||
477 | 214 | _, effective_kernel_opts = node.get_effective_kernel_options() | ||
478 | 215 | |||
479 | 216 | # Add any extra options from a third party driver. | ||
480 | 217 | use_driver = Config.objects.get_config('enable_third_party_drivers') | ||
481 | 218 | if use_driver: | ||
482 | 219 | driver = get_third_party_driver(node) | ||
483 | 220 | driver_kernel_opts = driver.get('kernel_opts', '') | ||
484 | 221 | |||
485 | 222 | combined_opts = ('%s %s' % ( | ||
486 | 223 | '' if effective_kernel_opts is None else effective_kernel_opts, | ||
487 | 224 | driver_kernel_opts)).strip() | ||
488 | 225 | if len(combined_opts): | ||
489 | 226 | extra_kernel_opts = combined_opts | ||
490 | 227 | else: | ||
491 | 228 | extra_kernel_opts = None | ||
492 | 229 | else: | ||
493 | 230 | extra_kernel_opts = effective_kernel_opts | ||
494 | 231 | else: | ||
495 | 232 | # If there's no node defined then we must be enlisting here, but | ||
496 | 233 | # we still need to return the global kernel options. | ||
497 | 234 | extra_kernel_opts = Config.objects.get_config("kernel_opts") | ||
498 | 235 | |||
499 | 236 | server_address = get_maas_facing_server_address(nodegroup=nodegroup) | ||
500 | 237 | cluster_address = get_mandatory_param(request.GET, "local") | ||
501 | 238 | |||
502 | 239 | params = KernelParameters( | ||
503 | 240 | osystem=osystem, arch=arch, subarch=subarch, release=series, | ||
504 | 241 | label=label, purpose=purpose, hostname=hostname, domain=domain, | ||
505 | 242 | preseed_url=preseed_url, log_host=server_address, | ||
506 | 243 | fs_host=cluster_address, extra_opts=extra_kernel_opts) | ||
507 | 244 | |||
508 | 245 | return HttpResponse( | ||
509 | 246 | json.dumps(params._asdict()), | ||
510 | 247 | content_type="application/json") | ||
511 | 0 | 248 | ||
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 | 20 | from django.core.urlresolvers import reverse | 20 | from django.core.urlresolvers import reverse |
517 | 21 | from django.test.client import RequestFactory | 21 | from django.test.client import RequestFactory |
518 | 22 | from maasserver import server_address | 22 | from maasserver import server_address |
521 | 23 | from maasserver.api import api as api_module | 23 | from maasserver.api import pxeconfig as pxeconfig_module |
522 | 24 | from maasserver.api.api import ( | 24 | from maasserver.api.pxeconfig import ( |
523 | 25 | find_nodegroup_for_pxeconfig_request, | 25 | find_nodegroup_for_pxeconfig_request, |
524 | 26 | get_boot_purpose, | 26 | get_boot_purpose, |
525 | 27 | ) | 27 | ) |
526 | @@ -344,7 +344,7 @@ | |||
527 | 344 | def test_pxeconfig_uses_boot_purpose(self): | 344 | def test_pxeconfig_uses_boot_purpose(self): |
528 | 345 | fake_boot_purpose = factory.make_name("purpose") | 345 | fake_boot_purpose = factory.make_name("purpose") |
529 | 346 | self.patch( | 346 | self.patch( |
531 | 347 | api_module, "get_boot_purpose" | 347 | pxeconfig_module, "get_boot_purpose" |
532 | 348 | ).return_value = fake_boot_purpose | 348 | ).return_value = fake_boot_purpose |
533 | 349 | response = self.client.get(reverse('pxeconfig'), | 349 | response = self.client.get(reverse('pxeconfig'), |
534 | 350 | self.get_default_params()) | 350 | self.get_default_params()) |
535 | 351 | 351 | ||
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 | 31 | NodeMacHandler, | 31 | NodeMacHandler, |
541 | 32 | NodeMacsHandler, | 32 | NodeMacsHandler, |
542 | 33 | NodesHandler, | 33 | NodesHandler, |
543 | 34 | pxeconfig, | ||
544 | 35 | VersionHandler, | 34 | VersionHandler, |
545 | 36 | ) | 35 | ) |
546 | 37 | from maasserver.api.auth import api_auth | 36 | from maasserver.api.auth import api_auth |
547 | @@ -67,6 +66,7 @@ | |||
548 | 67 | NodeGroupInterfaceHandler, | 66 | NodeGroupInterfaceHandler, |
549 | 68 | NodeGroupInterfacesHandler, | 67 | NodeGroupInterfacesHandler, |
550 | 69 | ) | 68 | ) |
551 | 69 | from maasserver.api.pxeconfig import pxeconfig | ||
552 | 70 | from maasserver.api.ssh_keys import ( | 70 | from maasserver.api.ssh_keys import ( |
553 | 71 | SSHKeyHandler, | 71 | SSHKeyHandler, |
554 | 72 | SSHKeysHandler, | 72 | SSHKeysHandler, |
Looks OK. Self-approving.