Merge lp:~blake-rouse/maas/remove-pxeconfig into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 4799
Proposed branch: lp:~blake-rouse/maas/remove-pxeconfig
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~blake-rouse/maas/tftp-rpc-boot-config
Diff against target: 1140 lines (+1/-1080)
6 files modified
src/maasserver/api/pxeconfig.py (+0/-340)
src/maasserver/api/tests/test_pxeconfig.py (+0/-712)
src/maasserver/models/node.py (+1/-1)
src/maasserver/urls_api.py (+0/-2)
src/provisioningserver/config.py (+0/-5)
src/provisioningserver/tests/test_config.py (+0/-20)
To merge this branch: bzr merge lp:~blake-rouse/maas/remove-pxeconfig
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+289277@code.launchpad.net

Commit message

Remove pxeconfig API as its not longer used. Has been replaced by the GetBootConfig RPC call.

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

All red!!!!

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

lgtm!

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

I count two lines of green ;)

Nice stuff!

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Now its all red. ;-)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'src/maasserver/api/pxeconfig.py'
2--- src/maasserver/api/pxeconfig.py 2016-03-09 06:54:33 +0000
3+++ src/maasserver/api/pxeconfig.py 1970-01-01 00:00:00 +0000
4@@ -1,340 +0,0 @@
5-# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
6-# GNU Affero General Public License version 3 (see the file LICENSE).
7-
8-"""API handler: `pxeconfig`."""
9-
10-__all__ = [
11- 'pxeconfig',
12- ]
13-
14-import http.client
15-import json
16-
17-from crochet import TimeoutError
18-from django.http import HttpResponse
19-from maasserver import logger
20-from maasserver.api.utils import (
21- get_mandatory_param,
22- get_optional_param,
23-)
24-from maasserver.clusterrpc.boot_images import get_boot_images_for
25-from maasserver.enum import INTERFACE_TYPE
26-from maasserver.models import (
27- BootResource,
28- Config,
29- Event,
30- RackController,
31-)
32-from maasserver.models.interface import (
33- Interface,
34- PhysicalInterface,
35-)
36-from maasserver.preseed import (
37- compose_enlistment_preseed_url,
38- compose_preseed_url,
39-)
40-from maasserver.server_address import get_maas_facing_server_address
41-from maasserver.third_party_drivers import get_third_party_driver
42-from maasserver.utils import find_rack_controller
43-from maasserver.utils.orm import get_one
44-from provisioningserver.events import EVENT_TYPES
45-from provisioningserver.kernel_opts import KernelParameters
46-from provisioningserver.rpc.exceptions import NoConnectionsAvailable
47-
48-
49-def find_rack_controller_for_pxeconfig_request(request):
50- """Find the rack controller responsible for a `pxeconfig` request.
51-
52- Looks for the `rackcontroller_id` parameter in the request. If there is
53- none, figures it out based on the requesting IP as a compatibility
54- measure. In that case, the result may be incorrect.
55- """
56- rackcontroller_id = request.GET.get('rackcontroller_id', None)
57- if rackcontroller_id is None:
58- return find_rack_controller(request)
59- else:
60- return RackController.objects.get(system_id=rackcontroller_id)
61-
62-
63-def get_node_from_mac_string(mac_string):
64- """Get a Node object from a MAC address string.
65-
66- Returns a Node object or None if no node with the given MAC address exists.
67-
68- :param mac_string: MAC address string in the form "12-34-56-78-9a-bc"
69- :return: Node object or None
70- """
71- if mac_string is None:
72- return None
73- interface = get_one(
74- Interface.objects.filter(
75- type=INTERFACE_TYPE.PHYSICAL, mac_address=mac_string))
76- return interface.node if interface else None
77-
78-
79-def get_boot_image(
80- rack_controller, osystem, architecture, subarchitecture, series,
81- purpose):
82- """Obtain the first available boot image for this rack controller for the
83- given osystem, architecture, subarchitecute, series, and purpose."""
84- # When local booting a node we put it through a PXE cycle. In
85- # this case it requests a purpose of "local" when looking for
86- # boot images. To avoid unnecessary work, we can shortcut that
87- # here and just return None right away.
88- if purpose == "local":
89- return None
90-
91- try:
92- images = get_boot_images_for(
93- rack_controller, osystem, architecture, subarchitecture, series)
94- except (NoConnectionsAvailable, TimeoutError):
95- logger.error(
96- "Unable to identify boot image for (%s/%s/%s/%s/%s): "
97- "no RPC connection to rack controller '%s'",
98- osystem, architecture, subarchitecture, series, purpose,
99- rack_controller.hostname)
100- return None
101- for image in images:
102- # get_boot_images_for returns all images that match the subarchitecure
103- # and its supporting subarches. Only want to return the image with
104- # the exact subarchitecture.
105- if (image['subarchitecture'] == subarchitecture and
106- image['purpose'] == purpose):
107- return image
108- logger.error(
109- "Unable to identify boot image for (%s/%s/%s/%s/%s): "
110- "rack controller '%s' does not have matching boot image.",
111- osystem, architecture, subarchitecture, series, purpose,
112- rack_controller.hostname)
113- return None
114-
115-
116-# XXX newell 2014-10-01 bug=1376489: Currently logging the pxe
117-# request for the given boot purpose here. It would be better to
118-# someday fix this to create the log entries when the file
119-# transfer completes, rather than when we receive the first
120-# (and potentially duplicate) packet of the request.
121-def event_log_pxe_request(node, purpose):
122- """Log PXE request to node's event log."""
123- options = {
124- 'commissioning': "commissioning",
125- 'xinstall': "curtin install",
126- 'install': "d-i install",
127- 'local': "local boot",
128- 'poweroff': "power off",
129- }
130- Event.objects.create_node_event(
131- system_id=node.system_id, event_type=EVENT_TYPES.NODE_PXE_REQUEST,
132- event_description=options[purpose])
133-
134-
135-DEFAULT_ARCH = 'i386'
136-
137-
138-def pxeconfig(request):
139- """Get the PXE configuration given a node's details.
140-
141- Returns a JSON object corresponding to a
142- :class:`provisioningserver.kernel_opts.KernelParameters` instance.
143-
144- This is now fairly decoupled from pxelinux's TFTP filename encoding
145- mechanism, with one notable exception. Call this function with (mac, arch,
146- subarch) and it will do the right thing. If details it needs are missing
147- (ie. arch/subarch missing when the MAC is supplied but unknown), then it
148- will as an exception return an HTTP NO_CONTENT (204) in the expectation
149- that this will be translated to a TFTP file not found and pxelinux (or an
150- emulator) will fall back to default-<arch>-<subarch> (in the case of an
151- alternate architecture emulator) or just straight to default (in the case
152- of native pxelinux on i386 or amd64). See bug 1041092 for details and
153- discussion.
154-
155- :param mac: MAC address to produce a boot configuration for.
156- :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not
157- 'armhf').
158- :param subarch: Subarchitecture name (in the pxelinux namespace).
159- :param local: The IP address of the cluster controller.
160- :param remote: The IP address of the booting node.
161- :param rackcontroller_id: system_id of the rackcontroller responsible for
162- this node. If omitted, the call will attempt to figure it out based on
163- the requesting IP address, for compatibility. Passing
164- `rackcontroller_id` is preferred.
165- """
166- request_mac = request.GET.get('mac', None)
167- cluster_ip = get_mandatory_param(request.GET, "local")
168- bios_boot_method = request.GET.get('bios_boot_method', None)
169- node = get_node_from_mac_string(request_mac)
170-
171- if node is not None:
172- node_needs_saving = False
173-
174- # Only update the booting interface for the node if it has
175- # changed.
176- if (node.boot_interface is None or
177- node.boot_interface.mac_address != request_mac):
178- node.boot_interface = PhysicalInterface.objects.get(
179- mac_address=request_mac)
180- node_needs_saving = True
181-
182- # Update the last IP address the cluster booted from.
183- if (node.boot_cluster_ip is None or
184- node.boot_cluster_ip != cluster_ip):
185- node.boot_cluster_ip = cluster_ip
186- node_needs_saving = True
187-
188- # Only update the bios boot method if its changed.
189- if node.bios_boot_method != bios_boot_method:
190- node.bios_boot_method = bios_boot_method
191- node_needs_saving = True
192-
193- if node_needs_saving:
194- node.save()
195-
196- if node is None or node.get_boot_purpose() == "commissioning":
197- osystem = Config.objects.get_config('commissioning_osystem')
198- series = Config.objects.get_config('commissioning_distro_series')
199- else:
200- osystem = node.get_osystem()
201- series = node.get_distro_series()
202-
203- rack_controller = find_rack_controller_for_pxeconfig_request(request)
204- if node:
205- arch, subarch = node.architecture.split('/')
206- preseed_url = compose_preseed_url(node, rack_controller)
207- hostname = node.hostname
208- domain = node.domain.name
209-
210- # Pre MAAS-1.9 the subarchitecture defined any kernel the node needed
211- # to be able to boot. This could be a hardware enablement kernel(e.g
212- # hwe-t) or something like highbank. With MAAS-1.9 any hardware
213- # enablement kernel must be specifed in the hwe_kernel field, any other
214- # kernel, such as highbank, is still specifed as a
215- # subarchitecture. Since Ubuntu does not support architecture specific
216- # hardware enablement kernels(i.e a highbank hwe-t kernel on precise)
217- # we give precedence to any kernel defined in the subarchitecture field
218- if subarch == "generic" and node.hwe_kernel:
219- subarch = node.hwe_kernel
220- elif(subarch == "generic" and
221- node.get_boot_purpose() == "commissioning" and
222- node.min_hwe_kernel):
223- subarch = node.min_hwe_kernel
224- else:
225- preseed_url = compose_enlistment_preseed_url(
226- rack_controller=rack_controller)
227- hostname = 'maas-enlist'
228- domain = 'local'
229-
230- arch = get_optional_param(request.GET, 'arch')
231- if arch is None:
232- if 'mac' in request.GET:
233- # Request was pxelinux.cfg/01-<mac>, so attempt fall back
234- # to pxelinux.cfg/default-<arch>-<subarch> for arch detection.
235- return HttpResponse(status=int(http.client.NO_CONTENT))
236- else:
237- # Look in BootResource for an resource that actually exists for
238- # the current series. If nothing is found, fall back to i386
239- # like we used to. LP #1181334
240- resource = (
241- BootResource.objects.get_default_commissioning_resource(
242- osystem, series))
243- if resource is None:
244- arch = DEFAULT_ARCH
245- else:
246- arch, _ = resource.split_arch()
247-
248- default_min_hwe_kernel = Config.objects.get_config(
249- 'default_min_hwe_kernel')
250- if default_min_hwe_kernel:
251- subarch = get_optional_param(
252- request.GET, 'subarch', default_min_hwe_kernel)
253- else:
254- subarch = get_optional_param(
255- request.GET, 'subarch', 'generic')
256-
257- # If we are booting with "xinstall", then we should always return the
258- # commissioning operating system and distro_series.
259- if node is None:
260- purpose = "commissioning" # enlistment
261- else:
262- purpose = node.get_boot_purpose()
263- event_log_pxe_request(node, purpose)
264-
265- # Use only the commissioning osystem and series, for operating systems
266- # other than Ubuntu. As Ubuntu supports HWE kernels, and needs to use
267- # that kernel to perform the installation.
268- if purpose == "xinstall" and osystem != 'ubuntu':
269- osystem = Config.objects.get_config('commissioning_osystem')
270- series = Config.objects.get_config('commissioning_distro_series')
271-
272- if purpose == 'poweroff':
273- # In order to power the node off, we need to get it booted in the
274- # commissioning environment and issue a `poweroff` command.
275- boot_purpose = 'commissioning'
276- else:
277- boot_purpose = purpose
278-
279- # We use as our default label the label of the most recent image for
280- # the criteria we've assembled above. If there is no latest image
281- # (which should never happen in reality but may happen in tests), we
282- # fall back to using 'no-such-image' as our default.
283- latest_image = get_boot_image(
284- rack_controller, osystem, arch, subarch, series, boot_purpose)
285- if latest_image is None:
286- # XXX 2014-03-18 gmb bug=1294131:
287- # We really ought to raise an exception here so that client
288- # and server can handle it according to their needs. At the
289- # moment, though, that breaks too many tests in awkward
290- # ways.
291- latest_label = 'no-such-image'
292- else:
293- latest_label = latest_image['label']
294- # subarch may be different from the request because newer images
295- # support older hardware enablement, e.g. trusty/generic
296- # supports trusty/hwe-s. We must override the subarch to the one
297- # on the image otherwise the config path will be wrong if
298- # get_latest_image() returned an image with a different subarch.
299- subarch = latest_image['subarchitecture']
300- label = get_optional_param(request.GET, 'label', latest_label)
301-
302- if node is not None:
303- # We don't care if the kernel opts is from the global setting or a tag,
304- # just get the options
305- _, effective_kernel_opts = node.get_effective_kernel_options()
306-
307- # Add any extra options from a third party driver.
308- use_driver = Config.objects.get_config('enable_third_party_drivers')
309- if use_driver:
310- driver = get_third_party_driver(node)
311- driver_kernel_opts = driver.get('kernel_opts', '')
312-
313- combined_opts = ('%s %s' % (
314- '' if effective_kernel_opts is None else effective_kernel_opts,
315- driver_kernel_opts)).strip()
316- if len(combined_opts):
317- extra_kernel_opts = combined_opts
318- else:
319- extra_kernel_opts = None
320- else:
321- extra_kernel_opts = effective_kernel_opts
322- else:
323- # If there's no node defined then we must be enlisting here, but
324- # we still need to return the global kernel options.
325- extra_kernel_opts = Config.objects.get_config("kernel_opts")
326-
327- server_address = get_maas_facing_server_address(
328- rack_controller=rack_controller)
329-
330- # If the node is enlisting and the arch is the default arch (i386),
331- # use the dedicated enlistment template which performs architecture
332- # detection.
333- if node is None and arch == DEFAULT_ARCH:
334- boot_purpose = "enlist"
335-
336- params = KernelParameters(
337- osystem=osystem, arch=arch, subarch=subarch, release=series,
338- label=label, purpose=boot_purpose, hostname=hostname, domain=domain,
339- preseed_url=preseed_url, log_host=server_address,
340- fs_host=cluster_ip, extra_opts=extra_kernel_opts)
341-
342- return HttpResponse(
343- json.dumps(params._asdict()),
344- content_type="application/json")
345
346=== removed file 'src/maasserver/api/tests/test_pxeconfig.py'
347--- src/maasserver/api/tests/test_pxeconfig.py 2016-02-29 19:21:03 +0000
348+++ src/maasserver/api/tests/test_pxeconfig.py 1970-01-01 00:00:00 +0000
349@@ -1,712 +0,0 @@
350-# Copyright 2013-2016 Canonical Ltd. This software is licensed under the
351-# GNU Affero General Public License version 3 (see the file LICENSE).
352-
353-"""Tests for PXE configuration retrieval from the API."""
354-
355-__all__ = []
356-
357-import http.client
358-import json
359-from unittest import skip
360-
361-from crochet import TimeoutError
362-from django.conf import settings
363-from django.core.urlresolvers import reverse
364-from django.test.client import RequestFactory
365-from maasserver import (
366- preseed as preseed_module,
367- server_address,
368-)
369-from maasserver.api import pxeconfig as pxeconfig_module
370-from maasserver.api.pxeconfig import (
371- event_log_pxe_request,
372- find_rack_controller_for_pxeconfig_request,
373- get_boot_image,
374-)
375-from maasserver.clusterrpc.testing.boot_images import make_rpc_boot_image
376-from maasserver.enum import (
377- BOOT_RESOURCE_TYPE,
378- INTERFACE_TYPE,
379- NODE_STATUS,
380-)
381-from maasserver.models import (
382- Config,
383- Event,
384- Interface,
385- Node,
386-)
387-from maasserver.preseed import (
388- compose_enlistment_preseed_url,
389- compose_preseed_url,
390-)
391-from maasserver.testing.architecture import make_usable_architecture
392-from maasserver.testing.config import RegionConfigurationFixture
393-from maasserver.testing.factory import factory
394-from maasserver.testing.testcase import MAASServerTestCase
395-from maasserver.utils.orm import reload_object
396-from maastesting.fakemethod import FakeMethod
397-from maastesting.matchers import (
398- MockCalledOnceWith,
399- MockNotCalled,
400-)
401-from mock import sentinel
402-from netaddr import IPNetwork
403-from provisioningserver import kernel_opts
404-from provisioningserver.kernel_opts import KernelParameters
405-from provisioningserver.rpc.exceptions import NoConnectionsAvailable
406-from testtools.matchers import (
407- Contains,
408- ContainsAll,
409- Equals,
410- Is,
411- MatchesListwise,
412- StartsWith,
413-)
414-
415-
416-class TestGetBootImage(MAASServerTestCase):
417-
418- def setUp(self):
419- super().setUp()
420- self.rackcontroller = factory.make_RackController()
421-
422- def test__returns_None_when_connection_unavailable(self):
423- self.patch(
424- pxeconfig_module,
425- 'get_boot_images_for').side_effect = NoConnectionsAvailable
426- self.assertEqual(
427- None,
428- get_boot_image(
429- self.rackcontroller, sentinel.osystem,
430- sentinel.architecture, sentinel.subarchitecture,
431- sentinel.series, sentinel.purpose))
432-
433- def test__returns_None_when_timeout_error(self):
434- self.patch(
435- pxeconfig_module,
436- 'get_boot_images_for').side_effect = TimeoutError
437- self.assertEqual(
438- None,
439- get_boot_image(
440- self.rackcontroller, sentinel.osystem,
441- sentinel.architecture, sentinel.subarchitecture,
442- sentinel.series, sentinel.purpose))
443-
444- def test__returns_matching_image(self):
445- subarch = factory.make_name('subarch')
446- purpose = factory.make_name('purpose')
447- boot_image = make_rpc_boot_image(
448- subarchitecture=subarch, purpose=purpose)
449- other_images = [make_rpc_boot_image() for _ in range(3)]
450- self.patch(
451- pxeconfig_module,
452- 'get_boot_images_for').return_value = other_images + [boot_image]
453- self.assertEqual(
454- boot_image,
455- get_boot_image(
456- self.rackcontroller, sentinel.osystem,
457- sentinel.architecture, subarch,
458- sentinel.series, purpose))
459-
460- def test__returns_None_on_no_matching_image(self):
461- subarch = factory.make_name('subarch')
462- purpose = factory.make_name('purpose')
463- other_images = [make_rpc_boot_image() for _ in range(3)]
464- self.patch(
465- pxeconfig_module,
466- 'get_boot_images_for').return_value = other_images
467- self.assertEqual(
468- None,
469- get_boot_image(
470- self.rackcontroller, sentinel.osystem,
471- sentinel.architecture, subarch,
472- sentinel.series, purpose))
473-
474- def test__returns_None_immediately_if_purpose_is_local(self):
475- self.patch(pxeconfig_module, 'get_boot_images_for')
476- self.expectThat(
477- get_boot_image(
478- self.rackcontroller, sentinel.osystem,
479- sentinel.architecture, sentinel.subarchitecture,
480- sentinel.series, "local"),
481- Is(None))
482- self.expectThat(pxeconfig_module.get_boot_images_for, MockNotCalled())
483-
484-
485-class TestPXEConfigAPI(MAASServerTestCase):
486- def setUp(self):
487- super(TestPXEConfigAPI, self).setUp()
488- self.useFixture(RegionConfigurationFixture())
489-
490- def get_default_params(self, rack_controller=None):
491- if rack_controller is None:
492- rack_controller = factory.make_RackController()
493- return {
494- "local": factory.make_ipv4_address(),
495- "remote": factory.make_ipv4_address(),
496- "rackcontroller_id": rack_controller.system_id,
497- }
498-
499- def get_mac_params(self):
500- node = factory.make_Node_with_Interface_on_Subnet(
501- status=NODE_STATUS.DEPLOYING)
502- arch, subarch = node.split_arch()
503- image = make_rpc_boot_image(
504- osystem=node.get_osystem(), release=node.get_distro_series(),
505- architecture=arch, subarchitecture=subarch,
506- purpose='install')
507- self.patch(
508- preseed_module,
509- 'get_boot_images_for').return_value = [image]
510- params = self.get_default_params()
511- params['mac'] = node.get_boot_interface().mac_address
512- return params
513-
514- def get_pxeconfig(self, params=None):
515- """Make a request to `pxeconfig`, and return its response dict."""
516- if params is None:
517- params = self.get_default_params()
518- response = self.client.get(reverse('pxeconfig'), params)
519- return json.loads(response.content.decode(settings.DEFAULT_CHARSET))
520-
521- @skip("XXX: GavinPanella 2016-02-24 bug=1549206: Fails spuriously.")
522- def test_pxeconfig_returns_json(self):
523- params = self.get_default_params()
524- response = self.client.get(
525- reverse('pxeconfig'), params)
526- self.assertThat(
527- (
528- response.status_code,
529- response['Content-Type'],
530- response.content,
531- response.content.decode(settings.DEFAULT_CHARSET),
532- ),
533- MatchesListwise(
534- (
535- Equals(http.client.OK),
536- Equals("application/json"),
537- StartsWith(b'{'),
538- Contains('arch'),
539- )),
540- response)
541-
542- def test_pxeconfig_returns_all_kernel_parameters(self):
543- params = self.get_default_params()
544- self.assertThat(
545- self.get_pxeconfig(params),
546- ContainsAll(KernelParameters._fields))
547-
548- def test_pxeconfig_returns_success_for_known_node(self):
549- params = self.get_mac_params()
550- response = self.client.get(reverse('pxeconfig'), params)
551- self.assertEqual(http.client.OK, response.status_code)
552-
553- def test_pxeconfig_returns_no_content_for_unknown_node(self):
554- params = dict(
555- mac=factory.make_mac_address(delimiter='-'),
556- local=factory.make_ipv4_address())
557- response = self.client.get(reverse('pxeconfig'), params)
558- self.assertEqual(http.client.NO_CONTENT, response.status_code)
559-
560- def test_pxeconfig_returns_success_for_detailed_but_unknown_node(self):
561- architecture = make_usable_architecture(self)
562- arch, subarch = architecture.split('/')
563- rack = factory.make_RackController()
564- params = dict(
565- self.get_default_params(),
566- mac=factory.make_mac_address(delimiter='-'),
567- arch=arch,
568- subarch=subarch,
569- rackcontroller_id=rack.system_id)
570- response = self.client.get(reverse('pxeconfig'), params)
571- self.assertEqual(http.client.OK, response.status_code)
572-
573- def test_pxeconfig_returns_global_kernel_params_for_enlisting_node(self):
574- # An 'enlisting' node means it looks like a node with details but we
575- # don't know about it yet. It should still receive the global
576- # kernel options.
577- value = factory.make_string()
578- Config.objects.set_config("kernel_opts", value)
579- architecture = make_usable_architecture(self)
580- arch, subarch = architecture.split('/')
581- rack = factory.make_RackController()
582- params = dict(
583- self.get_default_params(),
584- mac=factory.make_mac_address(delimiter='-'),
585- arch=arch,
586- subarch=subarch,
587- rackcontroller_id=rack.system_id)
588- response = self.client.get(reverse('pxeconfig'), params)
589- response_dict = json.loads(
590- response.content.decode(settings.DEFAULT_CHARSET))
591- self.assertEqual(value, response_dict['extra_opts'])
592-
593- def test_pxeconfig_uses_present_boot_image(self):
594- osystem = Config.objects.get_config('commissioning_osystem')
595- release = Config.objects.get_config('commissioning_distro_series')
596- resource_name = '%s/%s' % (osystem, release)
597- factory.make_usable_boot_resource(
598- rtype=BOOT_RESOURCE_TYPE.SYNCED,
599- name=resource_name, architecture='amd64/generic')
600- params = self.get_default_params()
601- params_out = self.get_pxeconfig(params)
602- self.assertEqual("amd64", params_out["arch"])
603-
604- def test_pxeconfig_defaults_to_i386_for_default(self):
605- # As a lowest-common-denominator, i386 is chosen when the node is not
606- # yet known to MAAS.
607- expected_arch = tuple(
608- make_usable_architecture(
609- self, arch_name="i386", subarch_name="generic").split("/"))
610- params = self.get_default_params()
611- params_out = self.get_pxeconfig(params)
612- observed_arch = params_out["arch"], params_out["subarch"]
613- self.assertEqual(expected_arch, observed_arch)
614-
615- def test_pxeconfig_uses_fixed_hostname_for_enlisting_node(self):
616- params = self.get_default_params()
617- self.assertEqual(
618- 'maas-enlist', self.get_pxeconfig(params).get('hostname'))
619-
620- def test_pxeconfig_uses_local_domain_for_enlisting_node(self):
621- params = self.get_default_params()
622- self.assertEqual(
623- "local",
624- self.get_pxeconfig(params).get('domain'))
625-
626- def test_pxeconfig_splits_domain_from_node_hostname(self):
627- host = factory.make_name('host')
628- domainname = factory.make_name('domain')
629- domain = factory.make_Domain(name=domainname)
630- full_hostname = '.'.join([host, domainname])
631- node = factory.make_Node_with_Interface_on_Subnet(
632- hostname=full_hostname, domain=domain)
633- interface = node.get_boot_interface()
634- params = self.get_default_params()
635- params['mac'] = interface.mac_address
636- pxe_config = self.get_pxeconfig(params)
637- self.assertEqual(host, pxe_config.get('hostname'))
638- self.assertNotIn(domain, pxe_config.values())
639-
640- def test_pxeconfig_uses_domain_for_node(self):
641- node = factory.make_Node_with_Interface_on_Subnet()
642- interface = node.get_boot_interface()
643- params = self.get_default_params()
644- params['mac'] = interface.mac_address
645- self.assertEqual(
646- node.domain.name,
647- self.get_pxeconfig(params).get('domain'))
648-
649- def get_without_param(self, param):
650- """Request a `pxeconfig()` response, but omit `param` from request."""
651- params = self.get_params()
652- del params[param]
653- return self.client.get(reverse('pxeconfig'), params)
654-
655- def silence_get_ephemeral_name(self):
656- # Silence `get_ephemeral_name` to avoid having to fetch the
657- # ephemeral name from the filesystem.
658- self.patch(
659- kernel_opts, 'get_ephemeral_name',
660- FakeMethod(result=factory.make_string()))
661-
662- def test_pxeconfig_has_enlistment_preseed_url_for_default(self):
663- self.silence_get_ephemeral_name()
664- params = self.get_default_params()
665- response = self.client.get(reverse('pxeconfig'), params)
666- self.assertEqual(
667- compose_enlistment_preseed_url(),
668- json.loads(response.content.decode(
669- settings.DEFAULT_CHARSET))["preseed_url"])
670-
671- def test_pxeconfig_enlistment_preseed_url_detects_request_origin(self):
672- self.silence_get_ephemeral_name()
673- hostname = factory.make_hostname()
674- rack_url = 'http://%s' % hostname
675- network = IPNetwork("10.1.1/24")
676- ip = factory.pick_ip_in_network(network)
677- self.patch(server_address, 'resolve_hostname').return_value = {ip}
678- rack_controller = factory.make_RackController(url=rack_url)
679- subnet = factory.make_Subnet(cidr=str(network.cidr), dhcp_on=True)
680- subnet.vlan.primary_rack = rack_controller
681- subnet.vlan.save()
682- params = self.get_default_params(rack_controller)
683- del params['rackcontroller_id']
684-
685- # Simulate that the request originates from ip by setting
686- # 'REMOTE_ADDR'.
687- response = self.client.get(
688- reverse('pxeconfig'), params, REMOTE_ADDR=ip)
689- self.assertThat(
690- json.loads(
691- response.content.decode(
692- settings.DEFAULT_CHARSET))["preseed_url"],
693- StartsWith(rack_url))
694-
695- def test_pxeconfig_enlistment_log_host_url_detects_request_origin(self):
696- self.silence_get_ephemeral_name()
697- hostname = factory.make_hostname()
698- rack_url = 'http://%s' % hostname
699- network = IPNetwork("10.1.1/24")
700- ip = factory.pick_ip_in_network(network)
701- mock = self.patch(server_address, 'resolve_hostname')
702- mock.return_value = {ip}
703- rack_controller = factory.make_RackController(url=rack_url)
704- subnet = factory.make_Subnet(cidr=str(network.cidr), dhcp_on=True)
705- subnet.vlan.primary_rack = rack_controller
706- subnet.vlan.save()
707- params = self.get_default_params(rack_controller)
708- del params['rackcontroller_id']
709-
710- # Simulate that the request originates from ip by setting
711- # 'REMOTE_ADDR'.
712- response = self.client.get(
713- reverse('pxeconfig'), params, REMOTE_ADDR=ip)
714- self.assertEqual(
715- (ip, hostname),
716- (json.loads(
717- response.content.decode(
718- settings.DEFAULT_CHARSET))["log_host"],
719- mock.call_args[0][0]))
720-
721- def test_pxeconfig_enlistment_checks_default_min_hwe_kernel(self):
722- params = self.get_default_params()
723- params['arch'] = 'armhf'
724- Config.objects.set_config('default_min_hwe_kernel', 'hwe-v')
725- response = self.client.get(reverse('pxeconfig'), params)
726- self.assertEqual(
727- "hwe-v",
728- json.loads(
729- response.content.decode(settings.DEFAULT_CHARSET))["subarch"])
730-
731- def test_pxeconfig_has_preseed_url_for_known_node(self):
732- rack_controller = factory.make_RackController()
733- params = self.get_mac_params()
734- params["rackcontroller_id"] = rack_controller.system_id
735- node = Interface.objects.get(mac_address=params['mac']).node
736- response = self.client.get(reverse('pxeconfig'), params)
737- self.assertEqual(
738- compose_preseed_url(node, rack_controller),
739- json.loads(
740- response.content.decode(
741- settings.DEFAULT_CHARSET))["preseed_url"])
742-
743- def test_find_rack_for_pxeconfig_request_uses_rack_system_id(self):
744- params = self.get_mac_params()
745- rack = factory.make_RackController()
746- params['rackcontroller_id'] = rack.system_id
747- request = RequestFactory().get(reverse('pxeconfig'), params)
748- self.assertEqual(
749- rack,
750- find_rack_controller_for_pxeconfig_request(request))
751-
752- def test_preseed_url_for_known_node_uses_rack_url(self):
753- rack_url = 'http://%s' % factory.make_name('host')
754- network = IPNetwork("10.1.1/24")
755- ip = factory.pick_ip_in_network(network)
756- self.patch(server_address, 'resolve_hostname').return_value = {ip}
757- rack_controller = factory.make_RackController(url=rack_url)
758- node = factory.make_Node_with_Interface_on_Subnet(
759- primary_rack=rack_controller)
760- params = self.get_default_params()
761- params['mac'] = str(node.get_boot_interface().mac_address)
762- params['rackcontroller_id'] = rack_controller.system_id
763-
764- # Simulate that the request originates from ip by setting
765- # 'REMOTE_ADDR'.
766- response = self.client.get(
767- reverse('pxeconfig'), params, REMOTE_ADDR=ip)
768- self.assertThat(
769- json.loads(
770- response.content.decode(
771- settings.DEFAULT_CHARSET))["preseed_url"],
772- StartsWith(rack_url))
773-
774- def test_pxeconfig_uses_boot_purpose_enlistment(self):
775- # test that purpose is set to "commissioning" for
776- # enlistment (when node is None).
777- params = self.get_default_params()
778- params['arch'] = 'armhf'
779- response = self.client.get(reverse('pxeconfig'), params)
780- self.assertEqual(
781- "commissioning",
782- json.loads(
783- response.content.decode(settings.DEFAULT_CHARSET))["purpose"])
784-
785- def test_pxeconfig_returns_enlist_config_if_no_architecture_provided(self):
786- params = self.get_default_params()
787- pxe_config = self.get_pxeconfig(params)
788- self.assertEqual('enlist', pxe_config['purpose'])
789-
790- def test_pxeconfig_returns_fs_host_as_cluster_controller(self):
791- # The kernel parameter `fs_host` points to the cluster controller
792- # address, which is passed over within the `local` parameter.
793- params = self.get_default_params()
794- kernel_params = KernelParameters(**self.get_pxeconfig(params))
795- self.assertEqual(params["local"], kernel_params.fs_host)
796-
797- def test_pxeconfig_returns_extra_kernel_options(self):
798- extra_kernel_opts = factory.make_string()
799- Config.objects.set_config('kernel_opts', extra_kernel_opts)
800- params = self.get_mac_params()
801- pxe_config = self.get_pxeconfig(params)
802- self.assertEqual(extra_kernel_opts, pxe_config['extra_opts'])
803-
804- def test_pxeconfig_returns_None_for_extra_kernel_opts(self):
805- params = self.get_mac_params()
806- pxe_config = self.get_pxeconfig(params)
807- self.assertEqual(None, pxe_config['extra_opts'])
808-
809- def test_pxeconfig_returns_commissioning_for_insane_state(self):
810- node = factory.make_Node_with_Interface_on_Subnet(
811- status=NODE_STATUS.BROKEN)
812- nic = node.get_boot_interface()
813- params = self.get_default_params()
814- params['mac'] = nic.mac_address
815- pxe_config = self.get_pxeconfig(params)
816- # The 'purpose' of the PXE config is 'commissioning' here
817- # even if the 'purpose' returned by node.get_boot_purpose
818- # is 'poweroff' because MAAS needs to bring the machine
819- # up in a commissioning environment in order to power
820- # the machine down.
821- self.assertEqual('commissioning', pxe_config['purpose'])
822-
823- def test_pxeconfig_returns_commissioning_for_ready_node(self):
824- node = factory.make_Node_with_Interface_on_Subnet(
825- status=NODE_STATUS.READY)
826- nic = node.get_boot_interface()
827- params = self.get_default_params()
828- params['mac'] = nic.mac_address
829- pxe_config = self.get_pxeconfig(params)
830- self.assertEqual('commissioning', pxe_config['purpose'])
831-
832- def test_pxeconfig_returns_image_subarch_not_node_subarch(self):
833- # In the scenario such as deploying trusty on an hwe-s subarch
834- # node, the code will have fallen back to using trusty's generic
835- # image as per the supported_subarches on the image. However,
836- # pxeconfig needs to make sure the image path refers to the
837- # subarch from the image, rather than the requested one.
838- osystem = 'ubuntu'
839- release = Config.objects.get_config('default_distro_series')
840- rack = factory.make_RackController()
841- generic_image = make_rpc_boot_image(
842- osystem=osystem, release=release,
843- architecture="amd64", subarchitecture="generic",
844- purpose='install')
845- hwe_s_image = make_rpc_boot_image(
846- osystem=osystem, release=release,
847- architecture="amd64", subarchitecture="hwe-s",
848- purpose='install')
849- self.patch(
850- preseed_module,
851- 'get_boot_images_for').return_value = [generic_image, hwe_s_image]
852- self.patch(
853- pxeconfig_module,
854- 'get_boot_images_for').return_value = [generic_image, hwe_s_image]
855- node = factory.make_Node_with_Interface_on_Subnet(
856- status=NODE_STATUS.DEPLOYING,
857- architecture="amd64/hwe-s",
858- primary_rack=rack)
859- params = self.get_default_params()
860- params['rackcontroller_id'] = rack.system_id
861- params['mac'] = node.get_boot_interface().mac_address
862- params['arch'] = "amd64"
863- params['subarch'] = "hwe-s"
864-
865- params_out = self.get_pxeconfig(params)
866- self.assertEqual("hwe-s", params_out["subarch"])
867-
868- def test_pxeconfig_calls_event_log_pxe_request(self):
869- node = factory.make_Node_with_Interface_on_Subnet()
870- nic = node.get_boot_interface()
871- params = self.get_default_params()
872- params['mac'] = nic.mac_address
873- event_log_pxe_request = self.patch_autospec(
874- pxeconfig_module, 'event_log_pxe_request')
875- self.client.get(reverse('pxeconfig'), params)
876- self.assertThat(
877- event_log_pxe_request,
878- MockCalledOnceWith(node, node.get_boot_purpose()))
879-
880- def test_event_log_pxe_request_for_known_boot_purpose(self):
881- purposes = [
882- ("commissioning", "commissioning"),
883- ("install", "d-i install"),
884- ("xinstall", "curtin install"),
885- ("local", "local boot"),
886- ("poweroff", "power off")]
887- for purpose, description in purposes:
888- node = factory.make_Node()
889- event_log_pxe_request(node, purpose)
890- self.assertEqual(
891- description,
892- Event.objects.get(node=node).description)
893-
894- def test_pxeconfig_sets_boot_interface_when_empty(self):
895- node = factory.make_Node_with_Interface_on_Subnet()
896- nic = node.get_boot_interface()
897- node.boot_interface = None
898- node.save()
899- params = self.get_default_params()
900- params['mac'] = nic.mac_address
901- self.client.get(reverse('pxeconfig'), params)
902- node = reload_object(node)
903- self.assertEqual(nic, node.boot_interface)
904-
905- def test_pxeconfig_updates_boot_interface_when_changed(self):
906- node = factory.make_Node_with_Interface_on_Subnet()
907- node.boot_interface = node.get_boot_interface()
908- node.save()
909- nic = factory.make_Interface(
910- INTERFACE_TYPE.PHYSICAL, node=node, vlan=node.boot_interface.vlan)
911- params = self.get_default_params()
912- params['mac'] = nic.mac_address
913- self.client.get(reverse('pxeconfig'), params)
914- node = reload_object(node)
915- self.assertEqual(nic, node.boot_interface)
916-
917- def test_pxeconfig_doesnt_update_boot_interface_when_same(self):
918- node = factory.make_Node_with_Interface_on_Subnet()
919- node.boot_interface = node.get_boot_interface()
920- node.save()
921- params = self.get_default_params()
922- params['mac'] = node.boot_interface.mac_address
923- node.boot_cluster_ip = params['local']
924- node.save()
925- mock_save = self.patch(Node, 'save')
926- self.client.get(reverse('pxeconfig'), params)
927- self.assertThat(mock_save, MockNotCalled())
928-
929- def test_pxeconfig_sets_boot_cluster_ip_when_empty(self):
930- node = factory.make_Node_with_Interface_on_Subnet()
931- params = self.get_default_params()
932- params['mac'] = node.get_boot_interface().mac_address
933- self.client.get(reverse('pxeconfig'), params)
934- node = reload_object(node)
935- self.assertEqual(params['local'], node.boot_cluster_ip)
936-
937- def test_pxeconfig_updates_boot_cluster_ip_when_changed(self):
938- node = factory.make_Node_with_Interface_on_Subnet()
939- node.boot_cluster_ip = factory.make_ipv4_address()
940- node.save()
941- params = self.get_default_params()
942- params['mac'] = node.get_boot_interface().mac_address
943- self.client.get(reverse('pxeconfig'), params)
944- node = reload_object(node)
945- self.assertEqual(params['local'], node.boot_cluster_ip)
946-
947- def test_pxeconfig_doesnt_update_boot_cluster_ip_when_same(self):
948- node = factory.make_Node_with_Interface_on_Subnet()
949- node.boot_interface = node.get_boot_interface()
950- params = self.get_default_params()
951- params['mac'] = node.boot_interface.mac_address
952- node.boot_cluster_ip = params['local']
953- node.save()
954- mock_save = self.patch(Node, 'save')
955- self.client.get(reverse('pxeconfig'), params)
956- self.assertThat(mock_save, MockNotCalled())
957-
958- def test_pxeconfig_updates_bios_boot_method(self):
959- node = factory.make_Node_with_Interface_on_Subnet()
960- nic = node.get_boot_interface()
961- params = self.get_default_params()
962- params['mac'] = nic.mac_address
963- params['bios_boot_method'] = 'pxe'
964- self.client.get(reverse('pxeconfig'), params)
965- node = reload_object(node)
966- self.assertEqual('pxe', node.bios_boot_method)
967-
968- def test_pxeconfig_doesnt_update_bios_boot_method_when_same(self):
969- node = factory.make_Node_with_Interface_on_Subnet(
970- bios_boot_method='uefi')
971- nic = node.get_boot_interface()
972- params = self.get_default_params()
973- params['mac'] = nic.mac_address
974- params['bios_boot_method'] = 'uefi'
975- node.boot_interface = nic
976- node.boot_cluster_ip = params['local']
977- node.save()
978- mock_save = self.patch(Node, 'save')
979- self.client.get(reverse('pxeconfig'), params)
980- self.assertThat(mock_save, MockNotCalled())
981-
982- def test_pxeconfig_returns_commissioning_os_series_for_other_oses(self):
983- osystem = Config.objects.get_config('default_osystem')
984- release = Config.objects.get_config('default_distro_series')
985- os_image = make_rpc_boot_image(purpose='xinstall')
986- architecture = '%s/%s' % (
987- os_image['architecture'], os_image['subarchitecture'])
988- self.patch(
989- preseed_module,
990- 'get_boot_images_for').return_value = [os_image]
991- self.patch(
992- pxeconfig_module,
993- 'get_boot_images_for').return_value = [os_image]
994- rack_controller = factory.make_RackController()
995- node = factory.make_Node_with_Interface_on_Subnet(
996- status=NODE_STATUS.DEPLOYING,
997- osystem=os_image['osystem'],
998- distro_series=os_image['release'],
999- architecture=architecture,
1000- primary_rack=rack_controller)
1001- params = self.get_default_params()
1002- params['rackcontroller_id'] = rack_controller.system_id
1003- params['mac'] = node.get_boot_interface().mac_address
1004- params_out = self.get_pxeconfig(params)
1005- self.assertEqual(osystem, params_out["osystem"])
1006- self.assertEqual(release, params_out["release"])
1007-
1008- def test_pxeconfig_commissioning_node_uses_min_hwe_kernel(self):
1009- node = factory.make_Node_with_Interface_on_Subnet(
1010- min_hwe_kernel="hwe-v")
1011- nic = node.get_boot_interface()
1012- self.patch(Node, 'get_boot_purpose').return_value = "commissioning"
1013- params = self.get_default_params()
1014- params['mac'] = nic.mac_address
1015- response = self.client.get(reverse('pxeconfig'), params)
1016- self.assertEqual(
1017- "hwe-v",
1018- json.loads(
1019- response.content.decode(settings.DEFAULT_CHARSET))["subarch"])
1020-
1021- def test_pxeconfig_returns_ubuntu_os_series_for_ubuntu_xinstall(self):
1022- ubuntu_image = make_rpc_boot_image(
1023- osystem='ubuntu', purpose='xinstall')
1024- architecture = '%s/%s' % (
1025- ubuntu_image['architecture'], ubuntu_image['subarchitecture'])
1026- self.patch(
1027- preseed_module,
1028- 'get_boot_images_for').return_value = [ubuntu_image]
1029- self.patch(
1030- pxeconfig_module,
1031- 'get_boot_images_for').return_value = [ubuntu_image]
1032- rack_controller = factory.make_RackController()
1033- node = factory.make_Node_with_Interface_on_Subnet(
1034- status=NODE_STATUS.DEPLOYING, osystem='ubuntu',
1035- distro_series=ubuntu_image['release'], architecture=architecture,
1036- primary_rack=rack_controller)
1037- params = self.get_default_params()
1038- params['rackcontroller_id'] = rack_controller.system_id
1039- params['mac'] = node.get_boot_interface().mac_address
1040- params_out = self.get_pxeconfig(params)
1041- self.assertEqual(ubuntu_image['release'], params_out["release"])
1042-
1043- def test_pxeconfig_returns_commissioning_os_when_erasing_disks(self):
1044- commissioning_osystem = factory.make_name("os")
1045- Config.objects.set_config(
1046- "commissioning_osystem", commissioning_osystem)
1047- commissioning_series = factory.make_name("series")
1048- Config.objects.set_config(
1049- "commissioning_distro_series", commissioning_series)
1050- rack_controller = factory.make_RackController()
1051- node = factory.make_Node_with_Interface_on_Subnet(
1052- status=NODE_STATUS.DISK_ERASING,
1053- osystem=factory.make_name("centos"),
1054- distro_series=factory.make_name("release"),
1055- primary_rack=rack_controller)
1056- params = self.get_default_params()
1057- params['rackcontroller_id'] = rack_controller.system_id
1058- params['mac'] = node.get_boot_interface().mac_address
1059- params_out = self.get_pxeconfig(params)
1060- self.assertEqual(commissioning_osystem, params_out['osystem'])
1061- self.assertEqual(commissioning_series, params_out['release'])
1062
1063=== modified file 'src/maasserver/models/node.py'
1064--- src/maasserver/models/node.py 2016-03-16 07:19:41 +0000
1065+++ src/maasserver/models/node.py 2016-03-17 13:36:34 +0000
1066@@ -2585,7 +2585,7 @@
1067 """Get the boot interface this node is expected to boot from.
1068
1069 Normally, this will be the boot interface last used in a
1070- pxeconfig() API request for the node, as recorded in the
1071+ GetBootConfig RPC request for the node, as recorded in the
1072 'boot_interface' property. However, if the node hasn't booted since
1073 the 'boot_interface' property was added to the Node model, this will
1074 return the node's first interface instead.
1075
1076=== modified file 'src/maasserver/urls_api.py'
1077--- src/maasserver/urls_api.py 2016-02-03 14:34:13 +0000
1078+++ src/maasserver/urls_api.py 2016-03-17 13:36:34 +0000
1079@@ -111,7 +111,6 @@
1080 PartitionHandler,
1081 PartitionsHandler,
1082 )
1083-from maasserver.api.pxeconfig import pxeconfig
1084 from maasserver.api.rackcontrollers import (
1085 RackControllerHandler,
1086 RackControllersHandler,
1087@@ -292,7 +291,6 @@
1088 '',
1089 url(r'doc/$', api_doc, name='api-doc'),
1090 url(r'describe/$', describe, name='describe'),
1091- url(r'pxeconfig/$', pxeconfig, name='pxeconfig'),
1092 url(r'version/$', version_handler, name='version_handler'),
1093 )
1094
1095
1096=== modified file 'src/provisioningserver/config.py'
1097--- src/provisioningserver/config.py 2016-02-10 20:24:35 +0000
1098+++ src/provisioningserver/config.py 2016-03-17 13:36:34 +0000
1099@@ -767,11 +767,6 @@
1100 accept_python=True, if_missing=get_tentative_path(
1101 "/var/lib/maas/boot-resources/current")))
1102
1103- @property
1104- def tftp_generator_url(self):
1105- """The URL at which to obtain the TFTP options for a node."""
1106- return "%s/api/2.0/pxeconfig/" % self.maas_url.rstrip("/")
1107-
1108 # GRUB options.
1109
1110 @property
1111
1112=== modified file 'src/provisioningserver/tests/test_config.py'
1113--- src/provisioningserver/tests/test_config.py 2016-02-10 20:24:35 +0000
1114+++ src/provisioningserver/tests/test_config.py 2016-03-17 13:36:34 +0000
1115@@ -667,26 +667,6 @@
1116 self.assertEqual({"cluster_uuid": str(example_uuid)}, config.store)
1117
1118
1119-class TestClusterConfigurationTFTPGeneratorURL(MAASTestCase):
1120- """Tests for `ClusterConfiguration.tftp_generator_url`."""
1121-
1122- def test__is_relative_to_maas_url(self):
1123- random_url = factory.make_simple_http_url()
1124- self.useFixture(ClusterConfigurationFixture(maas_url=random_url))
1125- with ClusterConfiguration.open() as configuration:
1126- self.assertEqual(
1127- random_url + "/api/2.0/pxeconfig/",
1128- configuration.tftp_generator_url)
1129-
1130- def test__strips_trailing_slashes_from_maas_url(self):
1131- random_url = factory.make_simple_http_url(path="foobar/")
1132- self.useFixture(ClusterConfigurationFixture(maas_url=random_url))
1133- with ClusterConfiguration.open() as configuration:
1134- self.assertEqual(
1135- random_url.rstrip("/") + "/api/2.0/pxeconfig/",
1136- configuration.tftp_generator_url)
1137-
1138-
1139 class TestClusterConfigurationGRUBRoot(MAASTestCase):
1140 """Tests for `ClusterConfiguration.grub_root`."""
1141