Merge lp:~ltrager/maas/hwe_backend into lp:~maas-committers/maas/trunk

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: no longer in the source branch.
Merged at revision: 4160
Proposed branch: lp:~ltrager/maas/hwe_backend
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 580 lines (+436/-2)
8 files modified
src/maasserver/api/pxeconfig.py (+11/-0)
src/maasserver/api/tests/test_node.py (+165/-0)
src/maasserver/forms.py (+91/-0)
src/maasserver/models/bootresource.py (+40/-0)
src/maasserver/models/tests/test_bootresource.py (+67/-0)
src/maasserver/preseed.py (+21/-1)
src/maasserver/tests/test_forms_node.py (+14/-0)
src/maasserver/tests/test_preseed.py (+27/-1)
To merge this branch: bzr merge lp:~ltrager/maas/hwe_backend
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Lee Trager (community) Needs Resubmitting
Mike Pontillo (community) Abstain
Jason Hobbs Pending
Review via email: mp+266163@code.launchpad.net

Commit message

Enable deploying a specific kernel to a node with validation

If hwe_kernel is specified validate that its newer than min_hwe_kernel and newer than the selected Ubuntu release. If just min_hwe_kernel is specified and the release is older MAAS will automatically select a newer kernel.

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

I think this is intended to fix bug 1473167 ?

If so, when you commit you can:
 bzr commit --fixes lp:1473167

then when you push, launchpad will automagically link the bug to this review.

Revision history for this message
Christian Reis (kiko) wrote :

On Wed, Jul 29, 2015 at 12:17:30AM -0000, Lee Trager wrote:
> + print(response.content)

^^^

> + def get_usable_kernels(self, architecture, name):
> + """Return the set of usable kernels for architecture and release."""
> + kernels = set()
> + for resource in self.filter(
> + architecture__startswith=architecture, name=name):
> + resource_set = resource.get_latest_set()
> + if(resource_set is not None and
> + resource_set.commissionable and
> + resource_set.installable):

Would some continues make this ladder less tall?
--
Christian Robottom Reis | [+1] 612 888 4935 | http://launchpad.net/~kiko
Canonical VP Hyperscale | [+55 16] 9 9112 6430

Revision history for this message
Lee Trager (ltrager) wrote :

ah sorry wasn't ready to set this for review. I'm still finishing this up. This code will fix 1473167 by checking that the specified kernel is available during deployment. This will also check that the specified kernel is newer than the min_hwe_kernel.

Blake helped me with get_usable_kernels I'll take another look to see if I can break it down some more.

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

Got lots of comments. This is missing lots of tests and I have some questions and some suggestions that need to be addressed.

review: Needs Fixing
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks like Blake has got this one well-covered. I'll leave it to him.

review: Abstain
Revision history for this message
Lee Trager (ltrager) :
Revision history for this message
Lee Trager (ltrager) wrote :

I've made the corrections Blake has asked and added a bunch of tests.

Christian, I looked over get_usable_architectures, is this what your looking for?

arches = set()
for resource in self.all():
    resource_set = resource.get_latest_set()
    if (not resource_set or
       not resource_set.commissionable or
       not resource_set.installable):
        continue
    arches.add(resource.architecture)
    if 'subarches' in resource.extra:
        arch, _ = resource.split_arch()
        for subarch in resource.extra['subarches'].split(','):
            arches.add('%s/%s' % (arch, subarch.strip()))
return arches

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

This is looking very very close. Only a couple fixes that are still required.

As for the testing of the Form, you are doing it all at the API level. This is okay as I don't want you to have to go and change it all. Normally what we do is test in at the Form level first. Its easier to setup for testing and any exceptions will be raise in the test. By testing at the API level the exceptions will not be raised you have to check the HTTP code to make sure no errors happened. You also need to test at the API level, but just based stuff like: did it use the form, did it raise a validation error, did it return a result. All the other complex combinations of validation should be tested at the form level.

Also the "Resubmit" is to be used by a review to say, please redo the entire branch and resubmit as something is really wrong. Like its targeted to the wrong branch or something. If you need another review from the reviewer just comment on the MP or message them on IRC.

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

This looks really good. Not going to block you on the one comment, but I do believe it needs fixing before this branch lands.

Also have you been doing deployments with the branch and the HWE kernel to make sure this is getting you the expected resulted on a deployed node?

review: Approve
Revision history for this message
Lee Trager (ltrager) wrote :

I've expanded the get_usable_kernels test, let me know if you think it needs more.

I've been running a full make test as well as deploying the proposed bits into a virtual environment to ensure everything works as it should in the real world. Right now most of my testing has been while using the CLI instead of UI. My next patchset will be focused on the UI

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/pxeconfig.py'
2--- src/maasserver/api/pxeconfig.py 2015-07-30 18:14:27 +0000
3+++ src/maasserver/api/pxeconfig.py 2015-08-05 20:09:04 +0000
4@@ -203,6 +203,17 @@
5 hostname = strip_domain(node.hostname)
6 nodegroup = node.nodegroup
7 domain = nodegroup.name
8+
9+ # Pre MAAS-1.9 the subarchitecture defined any kernel the node needed
10+ # to be able to boot. This could be a hardware enablement kernel(e.g
11+ # hwe-t) or something like highbank. With MAAS-1.9 any hardware
12+ # enablement kernel must be specifed in the hwe_kernel field, any other
13+ # kernel, such as highbank, is still specifed as a
14+ # subarchitecture. Since Ubuntu does not support architecture specific
15+ # hardware enablement kernels(i.e a highbank hwe-t kernel on precise)
16+ # we give precedence to any kernel defined in the subarchitecture field
17+ if subarch == "generic" and node.hwe_kernel:
18+ subarch = node.hwe_kernel
19 else:
20 nodegroup = find_nodegroup_for_pxeconfig_request(request)
21 preseed_url = compose_enlistment_preseed_url(nodegroup=nodegroup)
22
23=== modified file 'src/maasserver/api/tests/test_node.py'
24--- src/maasserver/api/tests/test_node.py 2015-07-27 20:23:17 +0000
25+++ src/maasserver/api/tests/test_node.py 2015-08-05 20:09:04 +0000
26@@ -37,6 +37,7 @@
27 MAC_ERROR_MSG,
28 )
29 from maasserver.models import (
30+ BootResource,
31 MACAddress,
32 Node,
33 node as node_module,
34@@ -524,7 +525,29 @@
35 self.assertEqual(httplib.OK, response.status_code)
36 self.assertEqual(user_data, NodeUserData.objects.get_user_data(node))
37
38+ def test_POST_start_sets_default_hwe_kernel(self):
39+ self.patch(
40+ BootResource.objects,
41+ 'get_usable_kernels').return_value = ('hwe-t', 'hwe-u')
42+ osystem = make_usable_osystem(self, 'ubuntu', ['trusty'])
43+ distro_series = osystem['default_release']
44+ node = factory.make_Node(
45+ owner=self.logged_in_user, mac=True, power_type='ether_wake',
46+ architecture=make_usable_architecture(self))
47+ response = self.client.post(
48+ self.get_node_uri(node), {
49+ 'op': 'start',
50+ 'distro_series': distro_series,
51+ 'osystem': osystem['name'],
52+ })
53+ self.assertEqual(httplib.OK, response.status_code)
54+ self.assertEqual(
55+ 'hwe-' + distro_series[0], reload_object(node).hwe_kernel)
56+
57 def test_POST_start_sets_hwe_kernel(self):
58+ self.patch(
59+ BootResource.objects,
60+ 'get_usable_kernels').return_value = ('hwe-t', 'hwe-v')
61 node = factory.make_Node(
62 owner=self.logged_in_user, mac=True, power_type='ether_wake',
63 architecture=make_usable_architecture(self))
64@@ -536,6 +559,148 @@
65 self.assertEqual(httplib.OK, response.status_code)
66 self.assertEqual('hwe-v', reload_object(node).hwe_kernel)
67
68+ def test_POST_start_fails_with_nongeneric_arch_and_hwe_kernel(self):
69+ osystem = make_usable_osystem(self, 'ubuntu', ['trusty'])
70+ distro_series = osystem['default_release']
71+ node = factory.make_Node(
72+ owner=self.logged_in_user, mac=True, power_type='ether_wake',
73+ architecture=make_usable_architecture(self))
74+ response = self.client.post(
75+ self.get_node_uri(node), {
76+ 'op': 'start',
77+ 'hwe_kernel': 'hwe-v',
78+ 'distro_series': distro_series,
79+ 'osystem': osystem['name'],
80+ })
81+ self.assertEqual(httplib.BAD_REQUEST, response.status_code,
82+ response.content)
83+ self.assertEqual(
84+ {'hwe_kernel':
85+ ["Subarchitecture(%s) must be generic when setting hwe_kernel." %
86+ node.split_arch()[1]]},
87+ json.loads(response.content))
88+
89+ def test_POST_start_fails_with_missing_hwe_kernel(self):
90+ self.patch(
91+ BootResource.objects,
92+ 'get_usable_kernels').return_value = ('hwe-t', 'hwe-u')
93+ osystem = make_usable_osystem(self, 'ubuntu', ['trusty'])
94+ distro_series = osystem['default_release']
95+ node = factory.make_Node(
96+ owner=self.logged_in_user, mac=True, power_type='ether_wake',
97+ architecture=make_usable_architecture(
98+ self, subarch_name='generic'))
99+ response = self.client.post(
100+ self.get_node_uri(node), {
101+ 'op': 'start',
102+ 'hwe_kernel': 'hwe-v',
103+ 'distro_series': distro_series,
104+ 'osystem': osystem['name'],
105+ })
106+ self.assertEqual(httplib.BAD_REQUEST, response.status_code,
107+ response.content)
108+ self.assertEqual(
109+ {'hwe_kernel':
110+ ["hwe-v is not avaliable for %s/%s on %s." %
111+ (osystem['name'], distro_series, node.architecture)]},
112+ json.loads(response.content))
113+
114+ def test_POST_start_fails_with_old_kernel_and_newer_release(self):
115+ self.patch(
116+ BootResource.objects,
117+ 'get_usable_kernels').return_value = ('hwe-t', 'hwe-v')
118+ osystem = make_usable_osystem(self, 'ubuntu', ['vivid'])
119+ distro_series = osystem['default_release']
120+ node = factory.make_Node(
121+ owner=self.logged_in_user, mac=True, power_type='ether_wake',
122+ architecture=make_usable_architecture(
123+ self, subarch_name='generic'))
124+ response = self.client.post(
125+ self.get_node_uri(node), {
126+ 'op': 'start',
127+ 'distro_series': distro_series,
128+ 'osystem': osystem['name'],
129+ 'hwe_kernel': 'hwe-t',
130+ })
131+ self.assertEqual(httplib.BAD_REQUEST, response.status_code,
132+ response.content)
133+ self.assertEqual(
134+ {'hwe_kernel':
135+ ["hwe-t is too old to use on ubuntu/vivid."]},
136+ json.loads(response.content))
137+
138+ def test_POST_start_fails_with_old_kernel_and_newer_min_hwe_kernel(self):
139+ self.patch(
140+ BootResource.objects,
141+ 'get_usable_kernels').return_value = ('hwe-t', 'hwe-v')
142+ osystem = make_usable_osystem(self, 'ubuntu', ['precise'])
143+ distro_series = osystem['default_release']
144+ node = factory.make_Node(
145+ owner=self.logged_in_user, mac=True, power_type='ether_wake',
146+ architecture=make_usable_architecture(
147+ self, subarch_name='generic'),
148+ min_hwe_kernel='hwe-v')
149+ response = self.client.post(
150+ self.get_node_uri(node), {
151+ 'op': 'start',
152+ 'distro_series': distro_series,
153+ 'osystem': osystem['name'],
154+ 'hwe_kernel': 'hwe-t',
155+ })
156+ self.assertEqual(httplib.BAD_REQUEST, response.status_code,
157+ response.content)
158+ self.assertEqual(
159+ {'hwe_kernel':
160+ ["hwe_kernel(hwe-t) is older than min_hwe_kernel(hwe-v)."]},
161+ json.loads(response.content))
162+
163+ def test_POST_start_fails_with_no_avalible_kernels(self):
164+ self.patch(
165+ BootResource.objects,
166+ 'get_usable_kernels').return_value = ('hwe-t', 'hwe-v')
167+ osystem = make_usable_osystem(self, 'ubuntu', ['precise'])
168+ distro_series = osystem['default_release']
169+ node = factory.make_Node(
170+ owner=self.logged_in_user, mac=True, power_type='ether_wake',
171+ architecture=make_usable_architecture(
172+ self, subarch_name='generic'),
173+ min_hwe_kernel='hwe-v')
174+ response = self.client.post(
175+ self.get_node_uri(node), {
176+ 'op': 'start',
177+ 'distro_series': distro_series,
178+ 'osystem': osystem['name'],
179+ 'hwe_kernel': 'hwe-t',
180+ })
181+ self.assertEqual(httplib.BAD_REQUEST, response.status_code,
182+ response.content)
183+ self.assertEqual(
184+ {'hwe_kernel':
185+ ["hwe_kernel(hwe-t) is older than min_hwe_kernel(hwe-v)."]},
186+ json.loads(response.content))
187+
188+ def test_POST_start_with_old_release_and_newer_min_hwe_kernel(self):
189+ osystem = make_usable_osystem(self, 'ubuntu', ['trusty'])
190+ distro_series = osystem['default_release']
191+ node = factory.make_Node(
192+ owner=self.logged_in_user, mac=True, power_type='ether_wake',
193+ architecture=make_usable_architecture(
194+ self, subarch_name='generic'),
195+ min_hwe_kernel='hwe-v')
196+ response = self.client.post(
197+ self.get_node_uri(node), {
198+ 'op': 'start',
199+ 'distro_series': distro_series,
200+ 'osystem': osystem['name'],
201+ })
202+ self.assertEqual(httplib.BAD_REQUEST, response.status_code,
203+ response.content)
204+ self.assertEqual(
205+ {'hwe_kernel':
206+ ["trusty has no kernels availible which meet " +
207+ "min_hwe_kernel(hwe-v)."]},
208+ json.loads(response.content))
209+
210 def test_POST_release_releases_owned_node(self):
211 self.patch(node_module, 'power_off_node')
212 self.patch(node_module.Node, 'start_transition_monitor')
213
214=== modified file 'src/maasserver/forms.py'
215--- src/maasserver/forms.py 2015-07-31 23:12:15 +0000
216+++ src/maasserver/forms.py 2015-08-05 20:09:04 +0000
217@@ -388,6 +388,96 @@
218
219 class NodeForm(MAASModelForm):
220
221+ def _release_a_newer_than_b(self, a, b):
222+ """ Compare two Ubuntu releases and return true if a >= b
223+
224+ The release names can be the full release name(e.g Precise, Trusty), or
225+ a hardware enablement(e.g hwe-p, hwe-t). The function wraps around the
226+ letter 'p' as Precise was the first version of Ubuntu MAAS supported
227+ """
228+ def get_release_num(release):
229+ release = release.lower()
230+ if 'hwe-' in release:
231+ release = release.lstrip('hwe-')
232+ return ord(release[0])
233+
234+ # Compare release versions based off of the first letter of their
235+ # release name or the letter in hwe-<letter>. Wrap around the letter
236+ # 'p' as that is the first version of Ubuntu MAAS supported.
237+ num_a = get_release_num(a)
238+ num_b = get_release_num(b)
239+ num_wrap = ord('p')
240+
241+ if((num_a >= num_wrap and num_b >= num_wrap and num_a >= num_b) or
242+ (num_a < num_wrap and num_b >= num_wrap and num_a < num_b) or
243+ (num_a < num_wrap and num_b < num_wrap and num_a >= num_b)):
244+ return True
245+ else:
246+ return False
247+
248+ def _clean_hwe_kernel(self):
249+ hwe_kernel = self.cleaned_data.get('hwe_kernel')
250+ min_hwe_kernel = self.cleaned_data.get('min_hwe_kernel')
251+ architecture = self.cleaned_data.get('architecture')
252+ osystem = self.cleaned_data.get('osystem')
253+ distro_series = self.cleaned_data.get('distro_series')
254+
255+ # The hwe_kernel feature is only supported on Ubuntu
256+ if((osystem and "ubuntu" not in osystem.lower()) or
257+ (not architecture or architecture == '') or
258+ (not distro_series or distro_series == '')):
259+ return hwe_kernel
260+
261+ arch, subarch = architecture.split('/')
262+
263+ if (subarch != 'generic' and
264+ (hwe_kernel.startswith('hwe-') or
265+ min_hwe_kernel.startswith('hwe-'))):
266+ set_form_error(
267+ self, 'hwe_kernel',
268+ 'Subarchitecture(%s) must be generic when setting hwe_kernel.'
269+ % subarch)
270+ return
271+
272+ os_release = osystem + '/' + distro_series
273+ usable_kernels = BootResource.objects.get_usable_kernels(
274+ os_release, arch)
275+
276+ if hwe_kernel.startswith('hwe-'):
277+ if hwe_kernel not in usable_kernels:
278+ set_form_error(
279+ self, 'hwe_kernel',
280+ '%s is not avaliable for %s on %s.' %
281+ (hwe_kernel, os_release, architecture))
282+ return
283+ if not self._release_a_newer_than_b(hwe_kernel, distro_series):
284+ set_form_error(
285+ self, 'hwe_kernel',
286+ '%s is too old to use on %s.' % (hwe_kernel, os_release))
287+ return
288+
289+ if(hwe_kernel.startswith('hwe-') and
290+ min_hwe_kernel.startswith('hwe-') and
291+ not self._release_a_newer_than_b(hwe_kernel, min_hwe_kernel)):
292+ set_form_error(
293+ self, 'hwe_kernel',
294+ 'hwe_kernel(%s) is older than min_hwe_kernel(%s).' %
295+ (hwe_kernel, min_hwe_kernel))
296+ return
297+ elif(min_hwe_kernel.startswith('hwe-')):
298+ for i in usable_kernels:
299+ if self._release_a_newer_than_b(i, min_hwe_kernel):
300+ return i
301+ set_form_error(
302+ self, 'hwe_kernel',
303+ '%s has no kernels availible which meet min_hwe_kernel(%s).' %
304+ (distro_series, min_hwe_kernel))
305+ return
306+ elif hwe_kernel.strip() == '':
307+ return 'hwe-' + distro_series[0]
308+
309+ return hwe_kernel
310+
311 def __init__(self, request=None, *args, **kwargs):
312 super(NodeForm, self).__init__(*args, **kwargs)
313 # Even though it doesn't need it and doesn't use it, this form accepts
314@@ -574,6 +664,7 @@
315 # Take the default value from the node's cluster.
316 nodegroup = cleaned_data['nodegroup']
317 cleaned_data['disable_ipv4'] = nodegroup.default_disable_ipv4
318+ cleaned_data['hwe_kernel'] = self._clean_hwe_kernel()
319 return cleaned_data
320
321 def is_valid(self):
322
323=== modified file 'src/maasserver/models/bootresource.py'
324--- src/maasserver/models/bootresource.py 2015-06-02 08:46:21 +0000
325+++ src/maasserver/models/bootresource.py 2015-08-05 20:09:04 +0000
326@@ -24,6 +24,7 @@
327 )
328 from maasserver import DefaultMeta
329 from maasserver.enum import (
330+ BOOT_RESOURCE_FILE_TYPE,
331 BOOT_RESOURCE_TYPE,
332 BOOT_RESOURCE_TYPE_CHOICES,
333 BOOT_RESOURCE_TYPE_CHOICES_DICT,
334@@ -223,6 +224,45 @@
335 return False
336 return True
337
338+ def get_usable_kernels(self, name, architecture):
339+ """Return the set of usable kernels for architecture and release."""
340+ kernels = set()
341+ for resource in self.filter(
342+ architecture__startswith=architecture, name=name):
343+ resource_set = resource.get_latest_set()
344+ if(resource_set is None or
345+ not resource_set.commissionable or
346+ not resource_set.installable):
347+ continue
348+ subarch = resource.split_arch()[1]
349+ if subarch.startswith("hwe-"):
350+ kernels.add(subarch)
351+ if "subarches" in resource.extra:
352+ for subarch in resource.extra["subarches"].split(","):
353+ if subarch.startswith("hwe-"):
354+ kernels.add(subarch)
355+ return kernels
356+
357+ def get_kpackage_for_node(self, node):
358+ """Return the kernel package name for the kernel specified."""
359+ if not node.hwe_kernel:
360+ return None
361+ arch = node.split_arch()[0]
362+ os_release = node.get_osystem() + '/' + node.get_distro_series()
363+ # Before hwe_kernel was introduced the subarchitecture was the
364+ # hwe_kernel simple stream still uses this convention
365+ hwe_arch = arch + '/' + node.hwe_kernel
366+
367+ resource = self.filter(name=os_release, architecture=hwe_arch).first()
368+ if resource:
369+ latest_set = resource.get_latest_set()
370+ if latest_set:
371+ kernel = latest_set.files.filter(
372+ filetype=BOOT_RESOURCE_FILE_TYPE.BOOT_KERNEL).first()
373+ if kernel and 'kpackage' in kernel.extra:
374+ return kernel.extra['kpackage']
375+ return None
376+
377
378 def validate_architecture(value):
379 """Validates that architecture value contains a subarchitecture."""
380
381=== modified file 'src/maasserver/models/tests/test_bootresource.py'
382--- src/maasserver/models/tests/test_bootresource.py 2015-05-07 18:14:38 +0000
383+++ src/maasserver/models/tests/test_bootresource.py 2015-08-05 20:09:04 +0000
384@@ -510,6 +510,73 @@
385 self.assertTrue(BootResource.objects.boot_images_are_in_sync([image]))
386
387
388+class TestGetUsableKernels(MAASServerTestCase):
389+ """Tests for `get_usable_kernels`."""
390+
391+ scenarios = (
392+ ("ubuntu/trusty", {
393+ "name": "ubuntu/trusty",
394+ "arch": "amd64",
395+ "subarch": "generic",
396+ "kernels": ["hwe-t", "hwe-u", "hwe-v"],
397+ }),
398+ ("ubuntu/vivid", {
399+ "name": "ubuntu/vivid",
400+ "arch": "i386",
401+ "subarch": "generic",
402+ "kernels": ["hwe-v"],
403+ }),
404+ ("ubuntu/precise", {
405+ "name": "ubuntu/precise",
406+ "arch": "armfh",
407+ "subarch": "generic",
408+ "kernels": ["hwe-p", "hwe-t", "hwe-v"],
409+ }),
410+ ("ubuntu/wily", {
411+ "name": "ubuntu/wily",
412+ "arch": "armfh",
413+ "subarch": "hardbank",
414+ "kernels": [],
415+ }))
416+
417+ def test__returns_usable_kernels(self):
418+ if self.subarch == "generic":
419+ for i in self.kernels:
420+ factory.make_usable_boot_resource(
421+ name=self.name, rtype=BOOT_RESOURCE_TYPE.SYNCED,
422+ architecture="%s/%s" % (self.arch, i))
423+ else:
424+ factory.make_usable_boot_resource(
425+ name=self.name, rtype=BOOT_RESOURCE_TYPE.SYNCED,
426+ architecture="%s/%s" % (self.arch, self.subarch))
427+ self.assertEqual(
428+ set(self.kernels),
429+ BootResource.objects.get_usable_kernels(
430+ self.name, self.arch),
431+ "%s should return %s as its usable kernel" % (
432+ self.name, set(self.kernels)))
433+
434+
435+class TestGetKpackageForNode(MAASServerTestCase):
436+ """Tests for `get_kpackage_for_node`."""
437+
438+ def test__returns_kpackage(self):
439+ resource = factory.make_BootResource(
440+ name="ubuntu/trusty", architecture="amd64/hwe-t",
441+ rtype=BOOT_RESOURCE_TYPE.SYNCED)
442+ resource_set = factory.make_BootResourceSet(resource)
443+ factory.make_boot_resource_file_with_content(
444+ resource_set, filename="boot-kernel", filetype="boot-kernel",
445+ extra={"kpackage": "linux-image-generic-lts-trusty"})
446+ node = factory.make_Node(
447+ mac=True, power_type='ether_wake', osystem='ubuntu',
448+ distro_series='trusty', architecture='amd64/generic',
449+ hwe_kernel='hwe-t')
450+ self.assertEqual(
451+ "linux-image-generic-lts-trusty",
452+ BootResource.objects.get_kpackage_for_node(node))
453+
454+
455 class TestBootResource(MAASServerTestCase):
456 """Tests for the `BootResource` model."""
457
458
459=== modified file 'src/maasserver/preseed.py'
460--- src/maasserver/preseed.py 2015-07-30 20:15:47 +0000
461+++ src/maasserver/preseed.py 2015-08-05 20:09:04 +0000
462@@ -50,6 +50,7 @@
463 PreseedError,
464 )
465 from maasserver.models import (
466+ BootResource,
467 Config,
468 DHCPLease,
469 )
470@@ -225,6 +226,24 @@
471 return []
472
473
474+def compose_curtin_kernel_preseed(node):
475+ """Return the curtin preseed for installing a kernel other than default.
476+
477+ The BootResourceFile table contains a mapping between hwe kernels and
478+ Ubuntu package names. If this mapping is missing we fall back to letting
479+ Curtin figure out which kernel should be installed"""
480+ kpackage = BootResource.objects.get_kpackage_for_node(node)
481+ if kpackage:
482+ kernel_config = {
483+ 'kernel': {
484+ 'package': kpackage,
485+ 'mapping': {},
486+ },
487+ }
488+ return [yaml.safe_dump(kernel_config)]
489+ return []
490+
491+
492 def get_curtin_userdata(node):
493 """Return the curtin user-data.
494
495@@ -237,6 +256,7 @@
496 reporter_config = compose_curtin_maas_reporter(node)
497 network_config = compose_curtin_network_preseed_for(node)
498 swap_config = compose_curtin_swap_preseed(node)
499+ kernel_config = compose_curtin_kernel_preseed(node)
500
501 # Get the storage configration if curtin supports custom storage.
502 storage_config = compose_curtin_storage_config(node)
503@@ -251,7 +271,7 @@
504 return pack_install(
505 configs=(
506 [main_config] + reporter_config + storage_config +
507- network_config + swap_config),
508+ network_config + swap_config + kernel_config),
509 args=[installer_url])
510
511
512
513=== modified file 'src/maasserver/tests/test_forms_node.py'
514--- src/maasserver/tests/test_forms_node.py 2015-07-25 04:02:23 +0000
515+++ src/maasserver/tests/test_forms_node.py 2015-08-05 20:09:04 +0000
516@@ -578,3 +578,17 @@
517 AdminNodeForm(data={'nodegroup': new_nodegroup}, instance=node).save()
518 # The form saved without error, but the nodegroup change was ignored.
519 self.assertEqual(old_nodegroup, node.nodegroup)
520+
521+ def test__release_a_newer_than_b(self):
522+ form = AdminNodeForm()
523+ # Since we wrap around 'p' we want to use 'p' as our starting point
524+ alphabet = ([chr(i) for i in xrange(ord('p'), ord('z') + 1)] +
525+ [chr(i) for i in xrange(ord('a'), ord('p'))])
526+ previous_true = 0
527+ for i in alphabet:
528+ true_count = 0
529+ for j in alphabet:
530+ if form._release_a_newer_than_b(i, j):
531+ true_count += 1
532+ previous_true += 1
533+ self.assertEqual(previous_true, true_count)
534
535=== modified file 'src/maasserver/tests/test_preseed.py'
536--- src/maasserver/tests/test_preseed.py 2015-07-29 14:15:21 +0000
537+++ src/maasserver/tests/test_preseed.py 2015-08-05 20:09:04 +0000
538@@ -35,8 +35,12 @@
539 MissingBootImage,
540 PreseedError,
541 )
542-from maasserver.models import Config
543+from maasserver.models import (
544+ BootResource,
545+ Config,
546+)
547 from maasserver.preseed import (
548+ compose_curtin_kernel_preseed,
549 compose_curtin_maas_reporter,
550 compose_curtin_network_preseed,
551 compose_curtin_swap_preseed,
552@@ -837,6 +841,28 @@
553 self.assertEqual(swap_preseed, ['swap: {size: 10000000000B}\n'])
554
555
556+class TestComposeCurtinKernel(MAASServerTestCase):
557+
558+ def test__returns_null_kernel(self):
559+ node = factory.make_Node()
560+ self.assertEqual(node.hwe_kernel, None)
561+ kernel_preseed = compose_curtin_kernel_preseed(node)
562+ self.assertEqual(kernel_preseed, [])
563+
564+ def test__returns_set_kernel(self):
565+ self.patch(
566+ BootResource.objects, 'get_kpackage_for_node').return_value = (
567+ 'linux-image-generic-lts-vivid')
568+ node = factory.make_Node(hwe_kernel='hwe-v')
569+ self.assertEqual(node.hwe_kernel, 'hwe-v')
570+ kernel_preseed = compose_curtin_kernel_preseed(node)
571+ self.assertEqual(kernel_preseed,
572+ ['kernel:\n' +
573+ ' mapping: {}\n' +
574+ ' package: linux-image-generic-lts-vivid\n'
575+ ])
576+
577+
578 class TestComposeCurtinNetworkPreseed(MAASServerTestCase):
579
580 def test__returns_list_of_yaml_strings(self):