Merge lp:~blake-rouse/maas/osystem-preseed-cleanup into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Superseded
Proposed branch: lp:~blake-rouse/maas/osystem-preseed-cleanup
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 3859 lines (+2093/-239)
51 files modified
src/maasserver/api.py (+64/-25)
src/maasserver/compose_preseed.py (+7/-0)
src/maasserver/context_processors.py (+1/-0)
src/maasserver/enum.py (+0/-30)
src/maasserver/forms.py (+125/-14)
src/maasserver/forms_settings.py (+53/-15)
src/maasserver/migrations/0075_add_osystem_to_bootimage.py (+254/-0)
src/maasserver/migrations/0076_add_osystem_to_node.py (+243/-0)
src/maasserver/models/bootimage.py (+41/-20)
src/maasserver/models/config.py (+9/-3)
src/maasserver/models/node.py (+63/-6)
src/maasserver/models/tests/test_bootimage.py (+100/-20)
src/maasserver/models/tests/test_node.py (+29/-6)
src/maasserver/preseed.py (+5/-2)
src/maasserver/static/js/node_add.js (+19/-1)
src/maasserver/static/js/os_distro_select.js (+131/-0)
src/maasserver/static/js/tests/test_os_distro_select.html (+38/-0)
src/maasserver/static/js/tests/test_os_distro_select.js (+106/-0)
src/maasserver/templates/maasserver/bootimage-list.html (+2/-0)
src/maasserver/templates/maasserver/node_edit.html (+9/-1)
src/maasserver/templates/maasserver/settings.html (+32/-0)
src/maasserver/templates/maasserver/snippets.html (+4/-0)
src/maasserver/testing/factory.py (+22/-3)
src/maasserver/testing/osystems.py (+94/-0)
src/maasserver/tests/test_api_boot_images.py (+4/-1)
src/maasserver/tests/test_api_node.py (+82/-9)
src/maasserver/tests/test_api_pxeconfig.py (+12/-3)
src/maasserver/tests/test_compose_preseed.py (+14/-0)
src/maasserver/tests/test_forms.py (+65/-0)
src/maasserver/tests/test_preseed.py (+16/-12)
src/maasserver/views/clusters.py (+11/-2)
src/maasserver/views/settings.py (+9/-0)
src/maasserver/views/tests/test_boot_image_list.py (+9/-3)
src/maasserver/views/tests/test_clusters.py (+8/-2)
src/maasserver/views/tests/test_settings.py (+32/-12)
src/metadataserver/tests/test_api.py (+1/-0)
src/provisioningserver/boot/__init__.py (+1/-1)
src/provisioningserver/boot/tests/test_pxe.py (+6/-3)
src/provisioningserver/boot/tests/test_tftppath.py (+71/-4)
src/provisioningserver/boot/tests/test_uefi.py (+1/-1)
src/provisioningserver/boot/tftppath.py (+24/-18)
src/provisioningserver/import_images/boot_resources.py (+18/-7)
src/provisioningserver/import_images/tests/test_boot_resources.py (+5/-3)
src/provisioningserver/kernel_opts.py (+12/-4)
src/provisioningserver/osystems/__init__.py (+103/-0)
src/provisioningserver/osystems/tests/test_ubuntu.py (+31/-0)
src/provisioningserver/osystems/ubuntu.py (+89/-0)
src/provisioningserver/rpc/cluster.py (+2/-1)
src/provisioningserver/rpc/tests/test_clusterservice.py (+7/-3)
src/provisioningserver/testing/boot_images.py (+5/-2)
src/provisioningserver/tests/test_kernel_opts.py (+4/-2)
To merge this branch: bzr merge lp:~blake-rouse/maas/osystem-preseed-cleanup
Reviewer Review Type Date Requested Status
MAAS Maintainers Pending
Review via email: mp+217098@code.launchpad.net

This proposal has been superseded by a proposal from 2014-04-24.

Commit message

Removed the DISTRO_SERIES enums, allow each operating system to compose its own preseed.

Description of the change

This is the final change in the series of changes to allow MAAS to deploy other operating systems. As we have Windows and CentOS support coming soon this is needed to easily add new operating systems.

Removed the DISTRO_SERIES enums as they are no longer needed, all information comes from the OperatingSystemRegistry.

Added the ability for an operating system to compose its own preseed. This will be used for Windows, CentOS and other operating systems.

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.py'
2--- src/maasserver/api.py 2014-04-24 14:57:25 +0000
3+++ src/maasserver/api.py 2014-04-24 17:04:47 +0000
4@@ -246,6 +246,10 @@
5 from piston.handler import typemapper
6 from piston.utils import rc
7 from provisioningserver.kernel_opts import KernelParameters
8+from provisioningserver.osystems import (
9+ BOOT_IMAGE_PURPOSE,
10+ OperatingSystemRegistry,
11+ )
12 from provisioningserver.power_schema import UNKNOWN_POWER_TYPE
13 import simplejson as json
14
15@@ -402,8 +406,11 @@
16 :param user_data: If present, this blob of user-data to be made
17 available to the nodes through the metadata service.
18 :type user_data: base64-encoded unicode
19+ :param osystem: If present, this parameter specifies the
20+ operating system the node will use.
21+ :type osystem: unicode
22 :param distro_series: If present, this parameter specifies the
23- Ubuntu Release the node will use.
24+ os relase the node will use.
25 :type distro_series: unicode
26
27 Ideally we'd have MIME multipart and content-transfer-encoding etc.
28@@ -412,14 +419,25 @@
29 encoding instead.
30 """
31 user_data = request.POST.get('user_data', None)
32+ osystem = request.POST.get('os', request.POST.get('osystem', None))
33 series = request.POST.get('distro_series', None)
34 if user_data is not None:
35 user_data = b64decode(user_data)
36- if series is not None:
37+
38+ if osystem is not None or series is not None:
39+ # If series is set and not osystem this means that we
40+ # should use the default operating system
41+ if osystem is None and series is not None:
42+ osystem = Config.objects.get_config('default_osystem')
43+ # If osystem is set and not series, then we need to set
44+ # the series to be default for the osystem.
45+ elif osystem is not None and series is None:
46+ series = ''
47+
48 node = Node.objects.get_node_or_404(
49 system_id=system_id, user=request.user,
50 perm=NODE_PERMISSION.EDIT)
51- node.set_distro_series(series=series)
52+ node.set_osystem_and_distro_series(osystem, series)
53 nodes = Node.objects.start_nodes(
54 [system_id], request.user, user_data=user_data)
55 if len(nodes) == 0:
56@@ -432,7 +450,7 @@
57 """Release a node. Opposite of `NodesHandler.acquire`."""
58 node = Node.objects.get_node_or_404(
59 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
60- node.set_distro_series(series='')
61+ node.set_osystem_and_distro_series(osystem='', series='')
62 if node.status == NODE_STATUS.READY:
63 # Nothing to do. This may be a redundant retry, and the
64 # postcondition is achieved, so call this success.
65@@ -2321,7 +2339,7 @@
66 context_instance=RequestContext(request))
67
68
69-def get_boot_purpose(node):
70+def get_boot_purpose(node, osystem, arch, subarch, series, label):
71 """Return a suitable "purpose" for this boot, e.g. "install"."""
72 # XXX: allenap bug=1031406 2012-07-31: The boot purpose is still in
73 # flux. It may be that there will just be an "ephemeral" environment and
74@@ -2341,7 +2359,15 @@
75 if node.should_use_traditional_installer():
76 return "install"
77 else:
78- return "xinstall"
79+ # Check that the booting operating system, actually supports
80+ # fast-path installation. If it does not then, we need to
81+ # return normal install.
82+ osystem_obj = OperatingSystemRegistry[osystem]
83+ purposes = osystem_obj.get_boot_image_purposes(
84+ arch, subarch, series, label)
85+ if BOOT_IMAGE_PURPOSE.XINSTALL in purposes:
86+ return "xinstall"
87+ return "install"
88 else:
89 return "local" # TODO: Investigate.
90 else:
91@@ -2409,12 +2435,12 @@
92 node = get_node_from_mac_string(request.GET.get('mac', None))
93
94 if node is None or node.status == NODE_STATUS.COMMISSIONING:
95+ osystem = Config.objects.get_config('commissioning_osystem')
96 series = Config.objects.get_config('commissioning_distro_series')
97 else:
98+ osystem = node.get_osystem()
99 series = node.get_distro_series()
100
101- purpose = get_boot_purpose(node)
102-
103 if node:
104 arch, subarch = node.architecture.split('/')
105 preseed_url = compose_preseed_url(node)
106@@ -2440,7 +2466,7 @@
107 # current series. If nothing is found, fall back to i386 like
108 # we used to. LP #1181334
109 image = BootImage.objects.get_default_arch_image_in_nodegroup(
110- nodegroup, series, purpose=purpose)
111+ nodegroup, osystem, series, purpose='commissioning')
112 if image is None:
113 arch = 'i386'
114 else:
115@@ -2448,12 +2474,17 @@
116
117 subarch = get_optional_param(request.GET, 'subarch', 'generic')
118
119+ # Get the purpose, checking that if node is using xinstall, the operating
120+ # system actaully supports that mode. We pass None for the label, here
121+ # because it is unknown which label will be used yet.
122+ purpose = get_boot_purpose(node, osystem, arch, subarch, series, None)
123+
124 # We use as our default label the label of the most recent image for
125 # the criteria we've assembled above. If there is no latest image
126 # (which should never happen in reality but may happen in tests), we
127 # fall back to using 'no-such-image' as our default.
128 latest_image = BootImage.objects.get_latest_image(
129- nodegroup, arch, subarch, series, purpose)
130+ nodegroup, osystem, arch, subarch, series, purpose)
131 if latest_image is None:
132 # XXX 2014-03-18 gmb bug=1294131:
133 # We really ought to raise an exception here so that client
134@@ -2465,6 +2496,10 @@
135 latest_label = latest_image.label
136 label = get_optional_param(request.GET, 'label', latest_label)
137
138+ # Now that we have the correct label, lets check the boot purpose again
139+ # to make sure that the boot image with that label, is the correct purpose.
140+ purpose = get_boot_purpose(node, osystem, arch, subarch, series, label)
141+
142 if node is not None:
143 # We don't care if the kernel opts is from the global setting or a tag,
144 # just get the options
145@@ -2494,8 +2529,8 @@
146 cluster_address = get_mandatory_param(request.GET, "local")
147
148 params = KernelParameters(
149- arch=arch, subarch=subarch, release=series, label=label,
150- purpose=purpose, hostname=hostname, domain=domain,
151+ osystem=osystem, arch=arch, subarch=subarch, release=series,
152+ label=label, purpose=purpose, hostname=hostname, domain=domain,
153 preseed_url=preseed_url, log_host=server_address,
154 fs_host=cluster_address, extra_opts=extra_kernel_opts)
155
156@@ -2536,10 +2571,11 @@
157 This function has a counterpart, `summarise_boot_image_dict`. The two
158 return the same value for the same boot image.
159
160- :return: A tuple of the image's architecture, subarchitecture, release,
161- label, and purpose.
162+ :return: A tuple of the image's osystem, architecture, subarchitecture,
163+ release, label, and purpose.
164 """
165 return (
166+ image_object.osystem,
167 image_object.architecture,
168 image_object.subarchitecture,
169 image_object.release,
170@@ -2554,10 +2590,11 @@
171 This is the counterpart to `summarise_boot_image_object`. The two return
172 the same value for the same boot image.
173
174- :return: A tuple of the image's architecture, subarchitecture, release,
175- label, and purpose.
176+ :return: A tuple of the image's osystem, architecture, subarchitecture,
177+ release, label, and purpose.
178 """
179 return (
180+ image_dict['osystem'],
181 image_dict['architecture'],
182 image_dict.get('subarchitecture', 'generic'),
183 image_dict['release'],
184@@ -2600,10 +2637,11 @@
185 `summarise_stored_images`.
186 """
187 new_images = reported_images - stored_images
188- for arch, subarch, release, label, purpose in new_images:
189+ for osystem, arch, subarch, release, label, purpose in new_images:
190 BootImage.objects.register_image(
191- nodegroup=nodegroup, architecture=arch, subarchitecture=subarch,
192- release=release, purpose=purpose, label=label)
193+ nodegroup=nodegroup, osystem=osystem, architecture=arch,
194+ subarchitecture=subarch, release=release, purpose=purpose,
195+ label=label)
196
197
198 def prune_boot_images(nodegroup, reported_images, stored_images):
199@@ -2620,15 +2658,16 @@
200 `summarise_stored_images`.
201 """
202 removed_images = stored_images - reported_images
203- for arch, subarch, release, label, purpose in removed_images:
204+ for osystem, arch, subarch, release, label, purpose in removed_images:
205 db_images = BootImage.objects.filter(
206- architecture=arch, subarchitecture=subarch,
207+ osystem=osystem, architecture=arch, subarchitecture=subarch,
208 release=release, label=label, purpose=purpose)
209 db_images.delete()
210
211
212 DISPLAYED_BOOTIMAGE_FIELDS = (
213 'id',
214+ 'osystem',
215 'release',
216 'architecture',
217 'subarchitecture',
218@@ -2694,10 +2733,10 @@
219 :param uuid: The UUID of the cluster for which the images are
220 being reported.
221 :param images: A list of dicts, each describing a boot image with
222- these properties: `architecture`, `subarchitecture`, `release`,
223- `purpose`, and optionally, `label` (which defaults to "release").
224- These should match the code that determines TFTP paths for these
225- images.
226+ these properties: `os`, `architecture`, `subarchitecture`,
227+ `release`, `purpose`, and optionally, `label` (which defaults
228+ to "release"). These should match the code that determines TFTP
229+ paths for these images.
230 """
231 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
232 check_nodegroup_access(request, nodegroup)
233
234=== modified file 'src/maasserver/compose_preseed.py'
235--- src/maasserver/compose_preseed.py 2013-10-18 09:54:17 +0000
236+++ src/maasserver/compose_preseed.py 2014-04-24 17:04:47 +0000
237@@ -20,6 +20,7 @@
238
239 from maasserver.enum import NODE_STATUS
240 from maasserver.utils import absolute_reverse
241+from provisioningserver.osystems import OperatingSystemRegistry
242 import yaml
243
244
245@@ -104,6 +105,12 @@
246 if node.status == NODE_STATUS.COMMISSIONING:
247 return compose_commissioning_preseed(token, base_url)
248 else:
249+ # Operating system might support a custom preseed generation
250+ osystem = OperatingSystemRegistry.get_item(node.get_osystem())
251+ if osystem is not None and hasattr(osystem, 'compose_preseed'):
252+ metadata_url = absolute_reverse('metadata', base_url=base_url)
253+ return osystem.compose_preseed(node, token, metadata_url)
254+
255 if node.should_use_traditional_installer():
256 return compose_cloud_init_preseed(token, base_url)
257 else:
258
259=== modified file 'src/maasserver/context_processors.py'
260--- src/maasserver/context_processors.py 2014-04-01 06:54:45 +0000
261+++ src/maasserver/context_processors.py 2014-04-24 17:04:47 +0000
262@@ -63,6 +63,7 @@
263 'js/node_views.js',
264 'js/longpoll.js',
265 'js/enums.js',
266+ 'js/os_distro_select.js',
267 'js/power_parameters.js',
268 'js/nodes_chart.js',
269 'js/reveal.js',
270
271=== modified file 'src/maasserver/enum.py'
272--- src/maasserver/enum.py 2014-03-28 16:36:32 +0000
273+++ src/maasserver/enum.py 2014-04-24 17:04:47 +0000
274@@ -86,36 +86,6 @@
275 NODE_STATUS_CHOICES_DICT = OrderedDict(NODE_STATUS_CHOICES)
276
277
278-class DISTRO_SERIES:
279- """List of supported ubuntu releases."""
280- #:
281- default = ''
282- #:
283- precise = 'precise'
284- #:
285- quantal = 'quantal'
286- #:
287- raring = 'raring'
288- #:
289- saucy = 'saucy'
290- #:
291- trusty = 'trusty'
292-
293-DISTRO_SERIES_CHOICES = (
294- (DISTRO_SERIES.default, 'Default Ubuntu Release'),
295- (DISTRO_SERIES.precise, 'Ubuntu 12.04 LTS "Precise Pangolin"'),
296- (DISTRO_SERIES.quantal, 'Ubuntu 12.10 "Quantal Quetzal"'),
297- (DISTRO_SERIES.raring, 'Ubuntu 13.04 "Raring Ringtail"'),
298- (DISTRO_SERIES.saucy, 'Ubuntu 13.10 "Saucy Salamander"'),
299- (DISTRO_SERIES.trusty, 'Ubuntu 14.04 LTS "Trusty Tahr"'),
300-)
301-
302-
303-COMMISSIONING_DISTRO_SERIES_CHOICES = (
304- (DISTRO_SERIES.trusty, dict(DISTRO_SERIES_CHOICES)[DISTRO_SERIES.trusty]),
305-)
306-
307-
308 class NODE_PERMISSION:
309 """Permissions relating to nodes."""
310 VIEW = 'view_node'
311
312=== modified file 'src/maasserver/forms.py'
313--- src/maasserver/forms.py 2014-04-22 09:08:02 +0000
314+++ src/maasserver/forms.py 2014-04-24 17:04:47 +0000
315@@ -71,9 +71,6 @@
316 )
317 from maasserver.config_forms import SKIP_CHECK_NAME
318 from maasserver.enum import (
319- COMMISSIONING_DISTRO_SERIES_CHOICES,
320- DISTRO_SERIES,
321- DISTRO_SERIES_CHOICES,
322 NODE_STATUS,
323 NODEGROUPINTERFACE_MANAGEMENT,
324 NODEGROUPINTERFACE_MANAGEMENT_CHOICES,
325@@ -86,7 +83,7 @@
326 from maasserver.forms_settings import (
327 CONFIG_ITEMS_KEYS,
328 get_config_field,
329- INVALID_DISTRO_SERIES_MESSAGE,
330+ list_commisioning_choices,
331 INVALID_SETTING_MSG_TEMPLATE,
332 )
333 from maasserver.models import (
334@@ -113,6 +110,7 @@
335 from maasserver.utils.network import make_network
336 from metadataserver.fields import Bin
337 from metadataserver.models import CommissioningScript
338+from provisioningserver.osystems import OperatingSystemRegistry
339
340 # A reusable null-option for choice fields.
341 BLANK_CHOICE = ('', '-------')
342@@ -202,6 +200,81 @@
343 return all_architectures[0]
344
345
346+def list_all_usable_osystems():
347+ """Return all operating systems that can be used for nodes.
348+
349+ These are the operating systems for which any nodegroup has the boot images
350+ required to boot the node.
351+ """
352+ # The Node edit form offers all usable operating systems as options for the
353+ # osystem field. Not all of these may be available in the node's
354+ # nodegroup, but to represent that accurately in the UI would depend on
355+ # the currently selected nodegroup. Narrowing the options down further
356+ # would have to happen browser-side.
357+ osystems = set()
358+ for nodegroup in NodeGroup.objects.all():
359+ osystems = osystems.union(
360+ BootImage.objects.get_usable_osystems(nodegroup))
361+ osystems = [OperatingSystemRegistry[osystem] for osystem in osystems]
362+ return sorted(osystems, key=lambda osystem: osystem.title)
363+
364+
365+def list_osystem_choices(osystems):
366+ """Return Django "choices" list for `osystem`."""
367+ choices = [('', 'Default OS')]
368+ choices += [
369+ (osystem.name, osystem.title)
370+ for osystem in osystems
371+ ]
372+ return choices
373+
374+
375+def list_all_usable_releases(osystems):
376+ """Return dictionary of usable `releases` for each opertaing system."""
377+ distro_series = {}
378+ nodegroups = list(NodeGroup.objects.all())
379+ for osystem in osystems:
380+ releases = set()
381+ for nodegroup in nodegroups:
382+ releases = releases.union(
383+ BootImage.objects.get_usable_releases(nodegroup, osystem.name))
384+ distro_series[osystem.name] = sorted(releases)
385+ return distro_series
386+
387+
388+def list_release_choices(releases):
389+ """Return Django "choices" list for `releases`."""
390+ choices = [('', 'Default OS Release')]
391+ for key, value in releases.items():
392+ osystem = OperatingSystemRegistry[key]
393+ options = osystem.format_release_choices(value)
394+ choices += [(
395+ '%s/' % osystem.name,
396+ 'Latest %s Release' % osystem.title
397+ )]
398+ choices += [
399+ ('%s/%s' % (osystem.name, name), title)
400+ for name, title in options
401+ ]
402+ choices += options
403+ return choices
404+
405+
406+def clean_distro_series_field(form, field, os_field):
407+ # distro_series field can be supplied the value os/release, that is the
408+ # way the web UI provides the value.
409+ new_distro_series = form.cleaned_data.get(field)
410+ if new_distro_series is not None and '/' in new_distro_series:
411+ os, release = new_distro_series.split('/', 1)
412+ if 'os' in form.cleaned_data:
413+ new_os = form.cleaned_data[os_field]
414+ if os != new_os:
415+ raise ValidationError(
416+ "%s option does not match osystem option." % field)
417+ return release
418+ return new_distro_series
419+
420+
421 class NodeForm(ModelForm):
422
423 def __init__(self, request=None, *args, **kwargs):
424@@ -214,6 +287,7 @@
425 self.fields['nodegroup'] = NodeGroupFormField(
426 required=False, empty_label="Default (master)")
427 self.set_up_architecture_field()
428+ self.set_up_osystem_and_distro_series_fields()
429
430 def set_up_architecture_field(self):
431 """Create the `architecture` field.
432@@ -233,6 +307,27 @@
433 choices=choices, required=True, initial=default_arch,
434 error_messages={'invalid_choice': invalid_arch_message})
435
436+ def set_up_osystem_and_distro_series_fields(self):
437+ """Create the `osystem` and `distro_series` fields.
438+
439+ This needs to be done on the fly so that we can pass a dynamic list of
440+ usable operating systems and distro_series.
441+ """
442+ osystems = list_all_usable_osystems()
443+ releases = list_all_usable_releases(osystems)
444+ choices = list_osystem_choices(osystems)
445+ distro_choices = list_release_choices(releases)
446+ invalid_osystem_message = compose_invalid_choice_text(
447+ 'osystem', choices)
448+ invalid_distro_series_message = compose_invalid_choice_text(
449+ 'distro_series', distro_choices)
450+ self.fields['osystem'] = forms.ChoiceField(
451+ choices=choices, required=False, initial='',
452+ error_messages={'invalid_choice': invalid_osystem_message})
453+ self.fields['distro_series'] = forms.ChoiceField(
454+ choices=distro_choices, required=False, initial='',
455+ error_messages={'invalid_choice': invalid_distro_series_message})
456+
457 def clean_hostname(self):
458 # Don't allow the hostname to be changed if the node is
459 # currently allocated. Juju knows the node by its old name, so
460@@ -246,6 +341,9 @@
461
462 return new_hostname
463
464+ def clean_distro_series(self):
465+ return clean_distro_series_field(self, 'distro_series', 'osystem')
466+
467 def is_valid(self):
468 is_valid = super(NodeForm, self).is_valid()
469 if len(list_all_usable_architectures()) == 0:
470@@ -254,12 +352,6 @@
471 is_valid = False
472 return is_valid
473
474- distro_series = forms.ChoiceField(
475- choices=DISTRO_SERIES_CHOICES, required=False,
476- initial=DISTRO_SERIES.default,
477- label="Release",
478- error_messages={'invalid_choice': INVALID_DISTRO_SERIES_MESSAGE})
479-
480 hostname = forms.CharField(
481 label="Host name", required=False, help_text=(
482 "The FQDN (Fully Qualified Domain Name) is derived from the "
483@@ -277,6 +369,7 @@
484 fields = (
485 'hostname',
486 'architecture',
487+ 'osystem',
488 'distro_series',
489 )
490
491@@ -858,16 +951,34 @@
492 """Settings page, Commissioning section."""
493 check_compatibility = get_config_field('check_compatibility')
494 commissioning_distro_series = forms.ChoiceField(
495- choices=COMMISSIONING_DISTRO_SERIES_CHOICES, required=False,
496- label="Default distro series used for commissioning",
497+ choices=list_commisioning_choices(), required=False,
498+ label="Default Ubuntu release used for commissioning",
499 error_messages={'invalid_choice': compose_invalid_choice_text(
500 'commissioning_distro_series',
501- COMMISSIONING_DISTRO_SERIES_CHOICES)})
502+ list_commisioning_choices())})
503+
504+
505+class DeployForm(ConfigForm):
506+ """Settings page, Deploy section."""
507+ default_osystem = get_config_field('default_osystem')
508+ default_distro_series = get_config_field('default_distro_series')
509+
510+ def _load_initials(self):
511+ super(DeployForm, self)._load_initials()
512+ initial_os = self.initial['default_osystem']
513+ initial_series = self.initial['default_distro_series']
514+ self.initial['default_distro_series'] = '%s/%s' % (
515+ initial_os,
516+ initial_series
517+ )
518+
519+ def clean_default_distro_series(self):
520+ return clean_distro_series_field(
521+ self, 'default_distro_series', 'default_osystem')
522
523
524 class UbuntuForm(ConfigForm):
525 """Settings page, Ubuntu section."""
526- default_distro_series = get_config_field('default_distro_series')
527 main_archive = get_config_field('main_archive')
528 ports_archive = get_config_field('ports_archive')
529
530
531=== modified file 'src/maasserver/forms_settings.py'
532--- src/maasserver/forms_settings.py 2014-04-10 20:21:24 +0000
533+++ src/maasserver/forms_settings.py 2014-04-24 17:04:47 +0000
534@@ -23,19 +23,39 @@
535 from socket import gethostname
536
537 from django import forms
538-from maasserver.enum import (
539- COMMISSIONING_DISTRO_SERIES_CHOICES,
540- DISTRO_SERIES,
541- DISTRO_SERIES_CHOICES,
542- )
543+from maasserver.models.config import DEFAULT_OS
544 from maasserver.utils.forms import compose_invalid_choice_text
545+from provisioningserver.osystems import OperatingSystemRegistry
546
547
548 INVALID_URL_MESSAGE = "Enter a valid url (e.g. http://host.example.com)."
549
550
551-INVALID_DISTRO_SERIES_MESSAGE = compose_invalid_choice_text(
552- 'distro_series', DISTRO_SERIES_CHOICES)
553+def list_osystem_choices():
554+ return [
555+ (osystem.name, osystem.title)
556+ for _, osystem in OperatingSystemRegistry
557+ ]
558+
559+
560+def list_release_choices():
561+ osystems = [osystem for _, osystem in OperatingSystemRegistry]
562+ choices = []
563+ for osystem in osystems:
564+ supported = sorted(osystem.get_supported_releases())
565+ options = osystem.format_release_choices(supported)
566+ options = [
567+ ('%s/%s' % (osystem.name, name), title)
568+ for name, title in options
569+ ]
570+ choices += options
571+ return choices
572+
573+
574+def list_commisioning_choices():
575+ releases = DEFAULT_OS.get_supported_commissioning_releases()
576+ options = DEFAULT_OS.format_release_choices(releases)
577+ return [(name, title) for name, title in options]
578
579
580 CONFIG_ITEMS = {
581@@ -139,28 +159,46 @@
582 "e.g. for ntp.ubuntu.com: '91.189.94.4'")
583 }
584 },
585+ 'default_osystem': {
586+ 'default': DEFAULT_OS.name,
587+ 'form': forms.ChoiceField,
588+ 'form_kwargs': {
589+ 'label': "Default operating system used for deployment",
590+ 'choices': list_osystem_choices(),
591+ 'required': False,
592+ 'error_messages': {
593+ 'invalid_choice': compose_invalid_choice_text(
594+ 'osystem',
595+ list_osystem_choices())},
596+ }
597+ },
598 'default_distro_series': {
599- 'default': DISTRO_SERIES.trusty,
600+ 'default': '%s/%s' % (
601+ DEFAULT_OS.name,
602+ DEFAULT_OS.get_default_release()
603+ ),
604 'form': forms.ChoiceField,
605 'form_kwargs': {
606- 'label': "Default distro series used for deployment",
607- 'choices': DISTRO_SERIES_CHOICES,
608+ 'label': "Default OS release used for deployment",
609+ 'choices': list_release_choices(),
610 'required': False,
611 'error_messages': {
612- 'invalid_choice': INVALID_DISTRO_SERIES_MESSAGE},
613+ 'invalid_choice': compose_invalid_choice_text(
614+ 'distro_series',
615+ list_release_choices())},
616 }
617 },
618 'commissioning_distro_series': {
619- 'default': DISTRO_SERIES.trusty,
620+ 'default': DEFAULT_OS.get_default_commissioning_release(),
621 'form': forms.ChoiceField,
622 'form_kwargs': {
623- 'label': "Default distro series used for commissioning",
624- 'choices': COMMISSIONING_DISTRO_SERIES_CHOICES,
625+ 'label': "Default Ubuntu release used for commissioning",
626+ 'choices': list_commisioning_choices(),
627 'required': False,
628 'error_messages': {
629 'invalid_choice': compose_invalid_choice_text(
630 'commissioning_distro_series',
631- COMMISSIONING_DISTRO_SERIES_CHOICES)},
632+ list_commisioning_choices())},
633 }
634 },
635 'enable_third_party_drivers': {
636
637=== added file 'src/maasserver/migrations/0075_add_osystem_to_bootimage.py'
638--- src/maasserver/migrations/0075_add_osystem_to_bootimage.py 1970-01-01 00:00:00 +0000
639+++ src/maasserver/migrations/0075_add_osystem_to_bootimage.py 2014-04-24 17:04:47 +0000
640@@ -0,0 +1,254 @@
641+# -*- coding: utf-8 -*-
642+from south.utils import datetime_utils as datetime
643+from south.db import db
644+from south.v2 import SchemaMigration
645+from django.db import models
646+
647+
648+class Migration(SchemaMigration):
649+
650+ def forwards(self, orm):
651+ # Removing unique constraint on 'BootImage', fields ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
652+ db.delete_unique(u'maasserver_bootimage', ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
653+
654+ # Adding field 'BootImage.osystem'
655+ db.add_column(u'maasserver_bootimage', 'osystem',
656+ self.gf('django.db.models.fields.CharField')(default='ubuntu', max_length=255),
657+ keep_default=False)
658+
659+ # Adding unique constraint on 'BootImage', fields ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
660+ db.create_unique(u'maasserver_bootimage', ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
661+
662+
663+ def backwards(self, orm):
664+ # Removing unique constraint on 'BootImage', fields ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
665+ db.delete_unique(u'maasserver_bootimage', ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
666+
667+ # Deleting field 'BootImage.osystem'
668+ db.delete_column(u'maasserver_bootimage', 'osystem')
669+
670+ # Adding unique constraint on 'BootImage', fields ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
671+ db.create_unique(u'maasserver_bootimage', ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
672+
673+
674+ models = {
675+ u'auth.group': {
676+ 'Meta': {'object_name': 'Group'},
677+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
678+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
679+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
680+ },
681+ u'auth.permission': {
682+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
683+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
684+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
685+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
686+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
687+ },
688+ u'auth.user': {
689+ 'Meta': {'object_name': 'User'},
690+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
691+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
692+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
693+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
694+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
695+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
696+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
697+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
698+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
699+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
700+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
701+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
702+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
703+ },
704+ u'contenttypes.contenttype': {
705+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
706+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
707+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
708+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
709+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
710+ },
711+ u'maasserver.bootimage': {
712+ 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
713+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
714+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
715+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
716+ 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
717+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
718+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
719+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
720+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
721+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
722+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
723+ },
724+ u'maasserver.componenterror': {
725+ 'Meta': {'object_name': 'ComponentError'},
726+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
727+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
728+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
729+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
730+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
731+ },
732+ u'maasserver.config': {
733+ 'Meta': {'object_name': 'Config'},
734+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
735+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
736+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
737+ },
738+ u'maasserver.dhcplease': {
739+ 'Meta': {'object_name': 'DHCPLease'},
740+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
741+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
742+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
743+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
744+ },
745+ u'maasserver.downloadprogress': {
746+ 'Meta': {'object_name': 'DownloadProgress'},
747+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
748+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
749+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
750+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
751+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
752+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
753+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
754+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
755+ },
756+ u'maasserver.filestorage': {
757+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
758+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
759+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
760+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
761+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'26215e0a-cafa-11e3-8554-bcee7b78dc5b'", 'unique': 'True', 'max_length': '36'}),
762+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
763+ },
764+ u'maasserver.macaddress': {
765+ 'Meta': {'object_name': 'MACAddress'},
766+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
767+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
768+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
769+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
770+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
771+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
772+ },
773+ u'maasserver.network': {
774+ 'Meta': {'object_name': 'Network'},
775+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
776+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
777+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
778+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
779+ 'netmask': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
780+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
781+ },
782+ u'maasserver.node': {
783+ 'Meta': {'object_name': 'Node'},
784+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
785+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
786+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
787+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
788+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'null': 'True', 'blank': 'True'}),
789+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
790+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
791+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
792+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
793+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
794+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
795+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
796+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
797+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
798+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
799+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
800+ 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
801+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-26226a84-cafa-11e3-8554-bcee7b78dc5b'", 'unique': 'True', 'max_length': '41'}),
802+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
803+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
804+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
805+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
806+ },
807+ u'maasserver.nodegroup': {
808+ 'Meta': {'object_name': 'NodeGroup'},
809+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
810+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
811+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
812+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
813+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
814+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
815+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
816+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
817+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
818+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
819+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
820+ },
821+ u'maasserver.nodegroupinterface': {
822+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
823+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
824+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
825+ 'foreign_dhcp_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
826+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
827+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
828+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
829+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
830+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
831+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
832+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
833+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
834+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
835+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
836+ },
837+ u'maasserver.sshkey': {
838+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
839+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
840+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
841+ 'key': ('django.db.models.fields.TextField', [], {}),
842+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
843+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
844+ },
845+ u'maasserver.tag': {
846+ 'Meta': {'object_name': 'Tag'},
847+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
848+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
849+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
850+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
851+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
852+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
853+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
854+ },
855+ u'maasserver.userprofile': {
856+ 'Meta': {'object_name': 'UserProfile'},
857+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
858+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
859+ },
860+ u'maasserver.zone': {
861+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
862+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
863+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
864+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
865+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
866+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
867+ },
868+ u'piston.consumer': {
869+ 'Meta': {'object_name': 'Consumer'},
870+ 'description': ('django.db.models.fields.TextField', [], {}),
871+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
872+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
873+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
874+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
875+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
876+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
877+ },
878+ u'piston.token': {
879+ 'Meta': {'object_name': 'Token'},
880+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
881+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
882+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
883+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
884+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
885+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
886+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
887+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1398266138L'}),
888+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
889+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
890+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
891+ }
892+ }
893+
894+ complete_apps = ['maasserver']
895\ No newline at end of file
896
897=== added file 'src/maasserver/migrations/0076_add_osystem_to_node.py'
898--- src/maasserver/migrations/0076_add_osystem_to_node.py 1970-01-01 00:00:00 +0000
899+++ src/maasserver/migrations/0076_add_osystem_to_node.py 2014-04-24 17:04:47 +0000
900@@ -0,0 +1,243 @@
901+# -*- coding: utf-8 -*-
902+from south.utils import datetime_utils as datetime
903+from south.db import db
904+from south.v2 import SchemaMigration
905+from django.db import models
906+
907+
908+class Migration(SchemaMigration):
909+
910+ def forwards(self, orm):
911+ # Adding field 'Node.osystem'
912+ db.add_column(u'maasserver_node', 'osystem',
913+ self.gf('django.db.models.fields.CharField')(default=u'', max_length=20, null=True, blank=True),
914+ keep_default=False)
915+
916+
917+ def backwards(self, orm):
918+ # Deleting field 'Node.osystem'
919+ db.delete_column(u'maasserver_node', 'osystem')
920+
921+
922+ models = {
923+ u'auth.group': {
924+ 'Meta': {'object_name': 'Group'},
925+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
926+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
927+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
928+ },
929+ u'auth.permission': {
930+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
931+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
932+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
933+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
934+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
935+ },
936+ u'auth.user': {
937+ 'Meta': {'object_name': 'User'},
938+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
939+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
940+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
941+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
942+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
943+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
944+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
945+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
946+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
947+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
948+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
949+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
950+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
951+ },
952+ u'contenttypes.contenttype': {
953+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
954+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
955+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
956+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
957+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
958+ },
959+ u'maasserver.bootimage': {
960+ 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
961+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
962+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
963+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
964+ 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
965+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
966+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
967+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
968+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
969+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
970+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
971+ },
972+ u'maasserver.componenterror': {
973+ 'Meta': {'object_name': 'ComponentError'},
974+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
975+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
976+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
977+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
978+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
979+ },
980+ u'maasserver.config': {
981+ 'Meta': {'object_name': 'Config'},
982+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
983+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
984+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
985+ },
986+ u'maasserver.dhcplease': {
987+ 'Meta': {'object_name': 'DHCPLease'},
988+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
989+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
990+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
991+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
992+ },
993+ u'maasserver.downloadprogress': {
994+ 'Meta': {'object_name': 'DownloadProgress'},
995+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
996+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
997+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
998+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
999+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1000+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
1001+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
1002+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
1003+ },
1004+ u'maasserver.filestorage': {
1005+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
1006+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
1007+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
1008+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1009+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'9bbf01e4-cbbd-11e3-afb3-bcee7b78dc5b'", 'unique': 'True', 'max_length': '36'}),
1010+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
1011+ },
1012+ u'maasserver.macaddress': {
1013+ 'Meta': {'object_name': 'MACAddress'},
1014+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
1015+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1016+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
1017+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
1018+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
1019+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
1020+ },
1021+ u'maasserver.network': {
1022+ 'Meta': {'object_name': 'Network'},
1023+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
1024+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1025+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
1026+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
1027+ 'netmask': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
1028+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
1029+ },
1030+ u'maasserver.node': {
1031+ 'Meta': {'object_name': 'Node'},
1032+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
1033+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
1034+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
1035+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
1036+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'null': 'True', 'blank': 'True'}),
1037+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
1038+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
1039+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1040+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
1041+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
1042+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
1043+ 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'null': 'True', 'blank': 'True'}),
1044+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
1045+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
1046+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
1047+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
1048+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
1049+ 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
1050+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-9bbde11a-cbbd-11e3-afb3-bcee7b78dc5b'", 'unique': 'True', 'max_length': '41'}),
1051+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
1052+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
1053+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
1054+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
1055+ },
1056+ u'maasserver.nodegroup': {
1057+ 'Meta': {'object_name': 'NodeGroup'},
1058+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
1059+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
1060+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
1061+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
1062+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
1063+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1064+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
1065+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
1066+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
1067+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
1068+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
1069+ },
1070+ u'maasserver.nodegroupinterface': {
1071+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
1072+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
1073+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
1074+ 'foreign_dhcp_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
1075+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1076+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
1077+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
1078+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
1079+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
1080+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
1081+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
1082+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
1083+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
1084+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
1085+ },
1086+ u'maasserver.sshkey': {
1087+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
1088+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
1089+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1090+ 'key': ('django.db.models.fields.TextField', [], {}),
1091+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
1092+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
1093+ },
1094+ u'maasserver.tag': {
1095+ 'Meta': {'object_name': 'Tag'},
1096+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
1097+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
1098+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
1099+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1100+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
1101+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
1102+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
1103+ },
1104+ u'maasserver.userprofile': {
1105+ 'Meta': {'object_name': 'UserProfile'},
1106+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1107+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
1108+ },
1109+ u'maasserver.zone': {
1110+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
1111+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
1112+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
1113+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1114+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
1115+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
1116+ },
1117+ u'piston.consumer': {
1118+ 'Meta': {'object_name': 'Consumer'},
1119+ 'description': ('django.db.models.fields.TextField', [], {}),
1120+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1121+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
1122+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
1123+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
1124+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
1125+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
1126+ },
1127+ u'piston.token': {
1128+ 'Meta': {'object_name': 'Token'},
1129+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
1130+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
1131+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
1132+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
1133+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
1134+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
1135+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
1136+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1398350097L'}),
1137+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
1138+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
1139+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
1140+ }
1141+ }
1142+
1143+ complete_apps = ['maasserver']
1144\ No newline at end of file
1145
1146=== modified file 'src/maasserver/models/bootimage.py'
1147--- src/maasserver/models/bootimage.py 2014-03-26 16:01:34 +0000
1148+++ src/maasserver/models/bootimage.py 2014-04-24 17:04:47 +0000
1149@@ -34,44 +34,47 @@
1150 Don't import or instantiate this directly; access as `BootImage.objects`.
1151 """
1152
1153- def get_by_natural_key(self, nodegroup, architecture, subarchitecture,
1154- release, purpose, label):
1155+ def get_by_natural_key(self, nodegroup, osystem, architecture,
1156+ subarchitecture, release, purpose, label):
1157 """Look up a specific image."""
1158 return self.get(
1159- nodegroup=nodegroup, architecture=architecture,
1160+ nodegroup=nodegroup, osystem=osystem, architecture=architecture,
1161 subarchitecture=subarchitecture, release=release,
1162 purpose=purpose, label=label)
1163
1164- def register_image(self, nodegroup, architecture, subarchitecture,
1165+ def register_image(self, nodegroup, osystem, architecture, subarchitecture,
1166 release, purpose, label):
1167 """Register an image if it wasn't already registered."""
1168 self.get_or_create(
1169- nodegroup=nodegroup, architecture=architecture,
1170+ nodegroup=nodegroup, osystem=osystem, architecture=architecture,
1171 subarchitecture=subarchitecture, release=release,
1172 purpose=purpose, label=label)
1173
1174- def have_image(self, nodegroup, architecture, subarchitecture, release,
1175- purpose, label=None):
1176+ def have_image(self, nodegroup, osystem, architecture, subarchitecture,
1177+ release, purpose, label=None):
1178 """Is an image for the given kind of boot available?"""
1179 if label is None:
1180 label = "release"
1181 try:
1182 self.get_by_natural_key(
1183- nodegroup=nodegroup, architecture=architecture,
1184- subarchitecture=subarchitecture, release=release,
1185- purpose=purpose, label=label)
1186+ nodegroup=nodegroup, osystem=osystem,
1187+ architecture=architecture, subarchitecture=subarchitecture,
1188+ release=release, purpose=purpose, label=label)
1189 return True
1190 except BootImage.DoesNotExist:
1191 return False
1192
1193- def get_default_arch_image_in_nodegroup(self, nodegroup, series, purpose):
1194- """Return any image for the given nodegroup, series, and purpose.
1195+ def get_default_arch_image_in_nodegroup(self, nodegroup, osystem, series,
1196+ purpose):
1197+ """Return any image for the given nodegroup, osystem, series,
1198+ and purpose.
1199
1200 Prefers `i386` images if available. Returns `None` if no images match
1201 requirements.
1202 """
1203 images = BootImage.objects.filter(
1204- release=series, nodegroup=nodegroup, purpose=purpose)
1205+ osystem=osystem, release=series, nodegroup=nodegroup,
1206+ purpose=purpose)
1207 for image in images:
1208 # Prefer i386, any available subarchitecture (usually just
1209 # "generic"). It will work for most cases where we don't know
1210@@ -106,14 +109,28 @@
1211 nodegroup, 'install')
1212 return arches_commissioning & arches_install
1213
1214- def get_latest_image(self, nodegroup, architecture, subarchitecture,
1215- release, purpose):
1216+ def get_latest_image(self, nodegroup, osystem, architecture,
1217+ subarchitecture, release, purpose):
1218 """Return the latest image for a set of criteria."""
1219 return BootImage.objects.filter(
1220- nodegroup=nodegroup, architecture=architecture,
1221+ nodegroup=nodegroup, osystem=osystem, architecture=architecture,
1222 subarchitecture=subarchitecture, release=release,
1223 purpose=purpose).order_by('id').last()
1224
1225+ def get_usable_osystems(self, nodegroup):
1226+ """Return the list of usable operating systems for a nodegroup.
1227+ """
1228+ query = BootImage.objects.filter(nodegroup=nodegroup)
1229+ return set(query.values_list('osystem', flat=True))
1230+
1231+ def get_usable_releases(self, nodegroup, osystem):
1232+ """Return the list of usable releases for a nodegroup and
1233+ operating system.
1234+ """
1235+ query = BootImage.objects.filter(nodegroup=nodegroup, osystem=osystem)
1236+ releases = query.values_list('release', flat=True)
1237+ return set(releases)
1238+
1239
1240 class BootImage(TimestampedModel):
1241 """Available boot image (i.e. kernel and initrd).
1242@@ -131,8 +148,8 @@
1243
1244 class Meta(DefaultMeta):
1245 unique_together = (
1246- ('nodegroup', 'architecture', 'subarchitecture', 'release',
1247- 'purpose', 'label'),
1248+ ('nodegroup', 'osystem', 'architecture', 'subarchitecture',
1249+ 'release', 'purpose', 'label'),
1250 )
1251
1252 objects = BootImageManager()
1253@@ -140,6 +157,9 @@
1254 # Nodegroup (cluster controller) that has the images.
1255 nodegroup = ForeignKey(NodeGroup, null=False, editable=False, unique=False)
1256
1257+ # Operating system (e.g. "ubuntu") that the image boots.
1258+ osystem = CharField(max_length=255, blank=False, editable=False)
1259+
1260 # System architecture (e.g. "i386") that the image is for.
1261 architecture = CharField(max_length=255, blank=False, editable=False)
1262
1263@@ -148,7 +168,7 @@
1264 # such as i386 and amd64, we use "generic").
1265 subarchitecture = CharField(max_length=255, blank=False, editable=False)
1266
1267- # Ubuntu release (e.g. "precise") that the image boots.
1268+ # OS release (e.g. "precise") that the image boots.
1269 release = CharField(max_length=255, blank=False, editable=False)
1270
1271 # Boot purpose (e.g. "commissioning" or "install") that the image is for.
1272@@ -159,7 +179,8 @@
1273 max_length=255, blank=False, editable=False, default="release")
1274
1275 def __repr__(self):
1276- return "<BootImage %s/%s-%s-%s-%s>" % (
1277+ return "<BootImage %s-%s/%s-%s-%s-%s>" % (
1278+ self.osystem,
1279 self.architecture,
1280 self.subarchitecture,
1281 self.release,
1282
1283=== modified file 'src/maasserver/models/config.py'
1284--- src/maasserver/models/config.py 2014-04-14 21:42:00 +0000
1285+++ src/maasserver/models/config.py 2014-04-24 17:04:47 +0000
1286@@ -28,8 +28,11 @@
1287 )
1288 from django.db.models.signals import post_save
1289 from maasserver import DefaultMeta
1290-from maasserver.enum import DISTRO_SERIES
1291 from maasserver.fields import JSONObjectField
1292+from provisioningserver.osystems.ubuntu import UbuntuOS
1293+
1294+
1295+DEFAULT_OS = UbuntuOS()
1296
1297
1298 def get_default_config():
1299@@ -40,11 +43,14 @@
1300 # Ubuntu section configuration.
1301 'main_archive': 'http://archive.ubuntu.com/ubuntu',
1302 'ports_archive': 'http://ports.ubuntu.com/ubuntu-ports',
1303- 'commissioning_distro_series': DISTRO_SERIES.trusty,
1304+ 'commissioning_osystem': DEFAULT_OS.name,
1305+ 'commissioning_distro_series':
1306+ DEFAULT_OS.get_default_commissioning_release(),
1307 # Network section configuration.
1308 'maas_name': gethostname(),
1309 'enlistment_domain': b'local',
1310- 'default_distro_series': DISTRO_SERIES.trusty,
1311+ 'default_osystem': DEFAULT_OS.name,
1312+ 'default_distro_series': DEFAULT_OS.get_default_release(),
1313 'http_proxy': None,
1314 'upstream_dns': None,
1315 'ntp_server': '91.189.94.4', # ntp.ubuntu.com
1316
1317=== modified file 'src/maasserver/models/node.py'
1318--- src/maasserver/models/node.py 2014-04-21 11:09:09 +0000
1319+++ src/maasserver/models/node.py 2014-04-24 17:04:47 +0000
1320@@ -49,8 +49,6 @@
1321 logger,
1322 )
1323 from maasserver.enum import (
1324- DISTRO_SERIES,
1325- DISTRO_SERIES_CHOICES,
1326 NODE_PERMISSION,
1327 NODE_STATUS,
1328 NODE_STATUS_CHOICES,
1329@@ -72,6 +70,7 @@
1330 strip_domain,
1331 )
1332 from piston.models import Token
1333+from provisioningserver.osystems import OperatingSystemRegistry
1334 from provisioningserver.tasks import (
1335 power_off,
1336 power_on,
1337@@ -190,6 +189,33 @@
1338 "Name contains disallowed characters: %r." % label)
1339
1340
1341+def validate_osystem(name):
1342+ """Validator for operating system.
1343+
1344+ :raise ValidationError: If invalid operating system selected.
1345+ """
1346+ if name is not None and name != '':
1347+ osystem = OperatingSystemRegistry.get_item(name)
1348+ if osystem is None:
1349+ raise ValidationError(
1350+ "Value u'%s' is not a valid choice." % name)
1351+
1352+
1353+def validate_distro_series(name):
1354+ """Validator for distro_series.
1355+
1356+ :raise ValidationError: If invalid distro series selected.
1357+ """
1358+ if name is None or name == '':
1359+ return
1360+ releases = set()
1361+ for _, obj in OperatingSystemRegistry:
1362+ releases = releases.union(obj.get_supported_releases())
1363+ if name not in releases:
1364+ raise ValidationError(
1365+ "Value u'%s' is not a valid choice." % name)
1366+
1367+
1368 class NodeManager(Manager):
1369 """A utility to manage the collection of Nodes."""
1370
1371@@ -477,9 +503,13 @@
1372 owner = ForeignKey(
1373 User, default=None, blank=True, null=True, editable=False)
1374
1375+ osystem = CharField(
1376+ max_length=20, null=True, blank=True, default='',
1377+ validators=[validate_osystem])
1378+
1379 distro_series = CharField(
1380- max_length=20, choices=DISTRO_SERIES_CHOICES, null=True,
1381- blank=True, default='')
1382+ max_length=20, null=True, blank=True, default='',
1383+ validators=[validate_distro_series])
1384
1385 architecture = CharField(max_length=31, blank=False)
1386
1387@@ -774,21 +804,48 @@
1388 """The name of the queue for tasks specific to this node."""
1389 return self.nodegroup.work_queue
1390
1391+ def get_osystem(self):
1392+ """Return the operating system to install that node."""
1393+ use_default_osystem = (
1394+ not self.osystem or
1395+ self.osystem == '')
1396+ if use_default_osystem:
1397+ return Config.objects.get_config('default_osystem')
1398+ else:
1399+ return self.osystem
1400+
1401 def get_distro_series(self):
1402 """Return the distro series to install that node."""
1403+ use_default_osystem = (
1404+ not self.osystem or
1405+ self.osystem == '')
1406 use_default_distro_series = (
1407 not self.distro_series or
1408- self.distro_series == DISTRO_SERIES.default)
1409- if use_default_distro_series:
1410+ self.distro_series == '')
1411+ if use_default_osystem and use_default_distro_series:
1412 return Config.objects.get_config('default_distro_series')
1413+ elif use_default_distro_series:
1414+ osystem = OperatingSystemRegistry[self.osystem]
1415+ return osystem.get_default_release()
1416 else:
1417 return self.distro_series
1418
1419+ def set_osystem(self, osystem=''):
1420+ """Set the operating system to install that node."""
1421+ self.osystem = osystem
1422+ self.save()
1423+
1424 def set_distro_series(self, series=''):
1425 """Set the distro series to install that node."""
1426 self.distro_series = series
1427 self.save()
1428
1429+ def set_osystem_and_distro_series(self, osystem='', series=''):
1430+ """Set the oeprating system to install that node."""
1431+ self.osystem = osystem
1432+ self.distro_series = series
1433+ self.save()
1434+
1435 def get_effective_power_parameters(self):
1436 """Return effective power parameters, including any defaults."""
1437 if self.power_parameters:
1438
1439=== modified file 'src/maasserver/models/tests/test_bootimage.py'
1440--- src/maasserver/models/tests/test_bootimage.py 2014-03-18 14:39:11 +0000
1441+++ src/maasserver/models/tests/test_bootimage.py 2014-04-24 17:04:47 +0000
1442@@ -50,69 +50,79 @@
1443 self.assertTrue(BootImage.objects.have_image(nodegroup, **params))
1444
1445 def test_default_arch_image_returns_None_if_no_images_match(self):
1446+ osystem = Config.objects.get_config('commissioning_osystem')
1447 series = Config.objects.get_config('commissioning_distro_series')
1448 result = BootImage.objects.get_default_arch_image_in_nodegroup(
1449- factory.make_node_group(), series, factory.make_name('purpose'))
1450+ factory.make_node_group(), osystem, series,
1451+ factory.make_name('purpose'))
1452 self.assertIsNone(result)
1453
1454 def test_default_arch_image_returns_only_matching_image(self):
1455 nodegroup = factory.make_node_group()
1456+ osystem = factory.make_name('os')
1457 series = factory.make_name('series')
1458 label = factory.make_name('label')
1459 arch = factory.make_name('arch')
1460 purpose = factory.make_name("purpose")
1461 factory.make_boot_image(
1462- architecture=arch, release=series, label=label,
1463+ osystem=osystem, architecture=arch,
1464+ release=series, label=label,
1465 nodegroup=nodegroup, purpose=purpose)
1466 result = BootImage.objects.get_default_arch_image_in_nodegroup(
1467- nodegroup, series, purpose=purpose)
1468+ nodegroup, osystem, series, purpose=purpose)
1469 self.assertEqual(result.architecture, arch)
1470
1471 def test_default_arch_image_prefers_i386(self):
1472 nodegroup = factory.make_node_group()
1473+ osystem = factory.make_name('os')
1474 series = factory.make_name('series')
1475 label = factory.make_name('label')
1476 purpose = factory.make_name("purpose")
1477 for arch in ['amd64', 'axp', 'i386', 'm88k']:
1478 factory.make_boot_image(
1479- architecture=arch, release=series, nodegroup=nodegroup,
1480+ osystem=osystem, architecture=arch,
1481+ release=series, nodegroup=nodegroup,
1482 purpose=purpose, label=label)
1483 result = BootImage.objects.get_default_arch_image_in_nodegroup(
1484- nodegroup, series, purpose=purpose)
1485+ nodegroup, osystem, series, purpose=purpose)
1486 self.assertEqual(result.architecture, "i386")
1487
1488 def test_default_arch_image_returns_arbitrary_pick_if_all_else_fails(self):
1489 nodegroup = factory.make_node_group()
1490+ osystem = factory.make_name('os')
1491 series = factory.make_name('series')
1492 label = factory.make_name('label')
1493 purpose = factory.make_name("purpose")
1494 images = [
1495 factory.make_boot_image(
1496- architecture=factory.make_name('arch'), release=series,
1497- label=label, nodegroup=nodegroup, purpose=purpose)
1498+ osystem=osystem, architecture=factory.make_name('arch'),
1499+ release=series, label=label, nodegroup=nodegroup,
1500+ purpose=purpose)
1501 for _ in range(3)
1502 ]
1503 self.assertIn(
1504 BootImage.objects.get_default_arch_image_in_nodegroup(
1505- nodegroup, series, purpose=purpose),
1506+ nodegroup, osystem, series, purpose=purpose),
1507 images)
1508
1509 def test_default_arch_image_copes_with_subarches(self):
1510 nodegroup = factory.make_node_group()
1511 arch = 'i386'
1512+ osystem = factory.make_name('os')
1513 series = factory.make_name('series')
1514 label = factory.make_name('label')
1515 purpose = factory.make_name("purpose")
1516 images = [
1517 factory.make_boot_image(
1518- architecture=arch, subarchitecture=factory.make_name('sub'),
1519+ osystem=osystem, architecture=arch,
1520+ subarchitecture=factory.make_name('sub'),
1521 release=series, label=label, nodegroup=nodegroup,
1522 purpose=purpose)
1523 for _ in range(3)
1524 ]
1525 self.assertIn(
1526 BootImage.objects.get_default_arch_image_in_nodegroup(
1527- nodegroup, series, purpose=purpose),
1528+ nodegroup, osystem, series, purpose=purpose),
1529 images)
1530
1531 def test_get_usable_architectures_returns_supported_arches(self):
1532@@ -164,54 +174,62 @@
1533 BootImage.objects.get_usable_architectures(nodegroup))
1534
1535 def test_get_latest_image_returns_latest_image_for_criteria(self):
1536+ osystem = factory.make_name('os')
1537 arch = factory.make_name('arch')
1538 subarch = factory.make_name('sub')
1539 release = factory.make_name('release')
1540 nodegroup = factory.make_node_group()
1541 purpose = factory.make_name("purpose")
1542 boot_image = factory.make_boot_image(
1543- nodegroup=nodegroup, architecture=arch,
1544+ nodegroup=nodegroup, osystem=osystem, architecture=arch,
1545 subarchitecture=subarch, release=release, purpose=purpose,
1546 label=factory.make_name('label'))
1547 self.assertEqual(
1548 boot_image,
1549 BootImage.objects.get_latest_image(
1550- nodegroup, arch, subarch, release, purpose))
1551+ nodegroup, osystem, arch, subarch, release, purpose))
1552
1553 def test_get_latest_image_doesnt_return_images_for_other_purposes(self):
1554+ osystem = factory.make_name('os')
1555 arch = factory.make_name('arch')
1556 subarch = factory.make_name('sub')
1557 release = factory.make_name('release')
1558 nodegroup = factory.make_node_group()
1559 purpose = factory.make_name("purpose")
1560 relevant_image = factory.make_boot_image(
1561- nodegroup=nodegroup, architecture=arch,
1562+ nodegroup=nodegroup, osystem=osystem, architecture=arch,
1563 subarchitecture=subarch, release=release, purpose=purpose,
1564 label=factory.make_name('label'))
1565
1566 # Create a bunch of more recent but irrelevant BootImages..
1567 factory.make_boot_image(
1568- nodegroup=factory.make_node_group(), architecture=arch,
1569- subarchitecture=subarch, release=release,
1570+ nodegroup=factory.make_node_group(), osystem=osystem,
1571+ architecture=arch, subarchitecture=subarch, release=release,
1572 purpose=purpose, label=factory.make_name('label'))
1573 factory.make_boot_image(
1574- nodegroup=nodegroup,
1575+ nodegroup=nodegroup, osystem=osystem,
1576 architecture=factory.make_name('arch'),
1577 subarchitecture=subarch, release=release, purpose=purpose,
1578 label=factory.make_name('label'))
1579 factory.make_boot_image(
1580- nodegroup=nodegroup, architecture=arch,
1581+ nodegroup=nodegroup, osystem=osystem, architecture=arch,
1582 subarchitecture=factory.make_name('subarch'),
1583 release=release, purpose=purpose,
1584 label=factory.make_name('label'))
1585 factory.make_boot_image(
1586- nodegroup=nodegroup,
1587+ nodegroup=nodegroup, osystem=osystem,
1588 architecture=factory.make_name('arch'),
1589 subarchitecture=subarch,
1590 release=factory.make_name('release'), purpose=purpose,
1591 label=factory.make_name('label'))
1592 factory.make_boot_image(
1593- nodegroup=nodegroup,
1594+ nodegroup=nodegroup, osystem=osystem,
1595+ architecture=factory.make_name('arch'),
1596+ subarchitecture=subarch, release=release,
1597+ purpose=factory.make_name('purpose'),
1598+ label=factory.make_name('label'))
1599+ factory.make_boot_image(
1600+ nodegroup=nodegroup, osystem=factory.make_name('os'),
1601 architecture=factory.make_name('arch'),
1602 subarchitecture=subarch, release=release,
1603 purpose=factory.make_name('purpose'),
1604@@ -220,4 +238,66 @@
1605 self.assertEqual(
1606 relevant_image,
1607 BootImage.objects.get_latest_image(
1608- nodegroup, arch, subarch, release, purpose))
1609+ nodegroup, osystem, arch, subarch, release, purpose))
1610+
1611+ def test_get_usable_osystems_returns_supported_osystems(self):
1612+ nodegroup = factory.make_node_group()
1613+ osystems = [
1614+ factory.make_name('os'),
1615+ factory.make_name('os'),
1616+ ]
1617+ for osystem in osystems:
1618+ factory.make_boot_image(
1619+ osystem=osystem,
1620+ nodegroup=nodegroup)
1621+ self.assertItemsEqual(
1622+ osystems,
1623+ BootImage.objects.get_usable_osystems(nodegroup))
1624+
1625+ def test_get_usable_osystems_uses_given_nodegroup(self):
1626+ nodegroup = factory.make_node_group()
1627+ osystem = factory.make_name('os')
1628+ factory.make_boot_image(
1629+ osystem=osystem, nodegroup=nodegroup)
1630+ self.assertItemsEqual(
1631+ [],
1632+ BootImage.objects.get_usable_osystems(
1633+ factory.make_node_group()))
1634+
1635+ def test_get_usable_releases_returns_supported_releases(self):
1636+ nodegroup = factory.make_node_group()
1637+ osystem = factory.make_name('os')
1638+ releases = [
1639+ factory.make_name('release'),
1640+ factory.make_name('release'),
1641+ ]
1642+ for release in releases:
1643+ factory.make_boot_image(
1644+ osystem=osystem,
1645+ release=release,
1646+ nodegroup=nodegroup)
1647+ self.assertItemsEqual(
1648+ releases,
1649+ BootImage.objects.get_usable_releases(nodegroup, osystem))
1650+
1651+ def test_get_usable_releases_uses_given_nodegroup(self):
1652+ nodegroup = factory.make_node_group()
1653+ osystem = factory.make_name('os')
1654+ release = factory.make_name('release')
1655+ factory.make_boot_image(
1656+ osystem=osystem, release=release, nodegroup=nodegroup)
1657+ self.assertItemsEqual(
1658+ [],
1659+ BootImage.objects.get_usable_releases(
1660+ factory.make_node_group(), osystem))
1661+
1662+ def test_get_usable_releases_uses_given_osystem(self):
1663+ nodegroup = factory.make_node_group()
1664+ osystem = factory.make_name('os')
1665+ release = factory.make_name('release')
1666+ factory.make_boot_image(
1667+ osystem=osystem, release=release, nodegroup=nodegroup)
1668+ self.assertItemsEqual(
1669+ [],
1670+ BootImage.objects.get_usable_releases(
1671+ factory.make_node_group(), factory.make_name('os')))
1672
1673=== modified file 'src/maasserver/models/tests/test_node.py'
1674--- src/maasserver/models/tests/test_node.py 2014-04-21 11:07:32 +0000
1675+++ src/maasserver/models/tests/test_node.py 2014-04-24 17:04:47 +0000
1676@@ -20,7 +20,6 @@
1677 from django.core.exceptions import ValidationError
1678 from maasserver.clusterrpc.power_parameters import get_power_types
1679 from maasserver.enum import (
1680- DISTRO_SERIES,
1681 NODE_PERMISSION,
1682 NODE_STATUS,
1683 NODE_STATUS_CHOICES,
1684@@ -270,17 +269,41 @@
1685 offset += timedelta(1)
1686 self.assertEqual(macs[0], node.get_primary_mac().mac_address)
1687
1688- def test_get_distro_series_returns_default_series(self):
1689- node = factory.make_node()
1690- series = Config.objects.get_config('commissioning_distro_series')
1691- self.assertEqual(series, node.get_distro_series())
1692+ def test_get_osystem_returns_default_osystem_and_series(self):
1693+ node = factory.make_node()
1694+ osystem = Config.objects.get_config('default_osystem')
1695+ series = Config.objects.get_config('default_distro_series')
1696+ self.assertEqual(osystem, node.get_osystem())
1697+ self.assertEqual(series, node.get_distro_series())
1698+
1699+ def test_get_series_returns_default_for_osystem(self):
1700+ node = factory.make_node()
1701+ osystem = factory.getRandomOS()
1702+ series = osystem.get_default_release()
1703+ node.set_osystem(osystem.name)
1704+ self.assertEqual(series, node.get_distro_series())
1705+
1706+ def test_set_get_osystem_returns_osystem(self):
1707+ osystem = factory.getRandomOS()
1708+ node = factory.make_node()
1709+ node.set_osystem(osystem.name)
1710+ self.assertEqual(osystem.name, node.get_osystem())
1711
1712 def test_set_get_distro_series_returns_series(self):
1713+ osystem = factory.getRandomOS()
1714+ series = factory.getRandomRelease(osystem)
1715 node = factory.make_node()
1716- series = DISTRO_SERIES.quantal
1717 node.set_distro_series(series)
1718 self.assertEqual(series, node.get_distro_series())
1719
1720+ def test_set_get_osystem_and_distro_series_returns_valid(self):
1721+ osystem = factory.getRandomOS()
1722+ series = factory.getRandomRelease(osystem)
1723+ node = factory.make_node()
1724+ node.set_osystem_and_distro_series(osystem.name, series)
1725+ self.assertEqual(osystem.name, node.get_osystem())
1726+ self.assertEqual(series, node.get_distro_series())
1727+
1728 def test_delete_node_deletes_related_mac(self):
1729 node = factory.make_node()
1730 mac = node.add_mac_address('AA:BB:CC:DD:EE:FF')
1731
1732=== modified file 'src/maasserver/preseed.py'
1733--- src/maasserver/preseed.py 2014-04-10 13:43:33 +0000
1734+++ src/maasserver/preseed.py 2014-04-24 17:04:47 +0000
1735@@ -93,6 +93,7 @@
1736
1737 def get_curtin_installer_url(node):
1738 """Return the URL where curtin on the node can download its installer."""
1739+ osystem = node.get_osystem()
1740 series = node.get_distro_series()
1741 cluster_host = pick_cluster_controller_address(node)
1742 # XXX rvb(?): The path shouldn't be hardcoded like this, but rather synced
1743@@ -100,18 +101,20 @@
1744 arch, subarch = node.architecture.split('/')
1745 purpose = 'xinstall'
1746 image = BootImage.objects.get_latest_image(
1747- node.nodegroup, arch, subarch, series, purpose)
1748+ node.nodegroup, osystem, arch, subarch, series, purpose)
1749 if image is None:
1750 raise MAASAPIException(
1751 "Error generating the URL of curtin's root-tgz file. "
1752 "No image could be found for the given selection: "
1753- "arch=%s, subarch=%s, series=%s, purpose=%s." % (
1754+ "os=%s, arch=%s, subarch=%s, series=%s, purpose=%s." % (
1755+ osystem,
1756 arch,
1757 subarch,
1758 series,
1759 purpose
1760 ))
1761 dyn_uri = '/'.join([
1762+ osystem,
1763 arch,
1764 subarch,
1765 series,
1766
1767=== modified file 'src/maasserver/static/js/node_add.js'
1768--- src/maasserver/static/js/node_add.js 2014-03-03 06:33:34 +0000
1769+++ src/maasserver/static/js/node_add.js 2014-04-24 17:04:47 +0000
1770@@ -228,11 +228,28 @@
1771 var heading = Y.Node.create('<h2 />')
1772 .set('text', "Add node");
1773 this.get('srcNode').append(heading).append(this.createForm());
1774+ this.setUpDistroSeriesField();
1775 this.setUpPowerParameterField();
1776 this.initializeNodes();
1777 },
1778
1779 /**
1780+ * If the 'distro_series' field is present, setup the linked
1781+ * 'distro_series' field.
1782+ *
1783+ * @method setUpDistroSeriesField
1784+ */
1785+ setUpDistroSeriesField: function() {
1786+ if (Y.Lang.isValue(Y.one('#id_distro_series'))) {
1787+ var widget = new Y.maas.os_distro_select.OSReleaseWidget({
1788+ srcNode: '#id_distro_series'
1789+ });
1790+ widget.bindTo(Y.one('#id_osystem'), 'change');
1791+ widget.render();
1792+ }
1793+ },
1794+
1795+ /**
1796 * If the 'power_type' field is present, setup the linked
1797 * 'power_parameter' field.
1798 *
1799@@ -415,5 +432,6 @@
1800 };
1801
1802 }, '0.1', {'requires': ['io', 'node', 'widget', 'event', 'event-custom',
1803- 'maas.morph', 'maas.enums', 'maas.power_parameters']}
1804+ 'maas.morph', 'maas.enums', 'maas.power_parameters',
1805+ 'maas.os_distro_select']}
1806 );
1807
1808=== added file 'src/maasserver/static/js/os_distro_select.js'
1809--- src/maasserver/static/js/os_distro_select.js 1970-01-01 00:00:00 +0000
1810+++ src/maasserver/static/js/os_distro_select.js 2014-04-24 17:04:47 +0000
1811@@ -0,0 +1,131 @@
1812+/* Copyright 2012-2014 Canonical Ltd. This software is licensed under the
1813+ * GNU Affero General Public License version 3 (see the file LICENSE).
1814+ *
1815+ * OS/Release seletion utilities.
1816+ *
1817+ * @module Y.maas.power_parameter
1818+ */
1819+
1820+YUI.add('maas.os_distro_select', function(Y) {
1821+
1822+Y.log('loading maas.os_distro_select');
1823+var module = Y.namespace('maas.os_distro_select');
1824+
1825+// Only used to mockup io in tests.
1826+module._io = new Y.IO();
1827+
1828+var OSReleaseWidget;
1829+
1830+/**
1831+ * A widget class used to have the content of a node's release <select>
1832+ * modified based on the selected operating system.
1833+ *
1834+ */
1835+OSReleaseWidget = function() {
1836+ OSReleaseWidget.superclass.constructor.apply(this, arguments);
1837+};
1838+
1839+OSReleaseWidget.NAME = 'os-release-widget';
1840+
1841+Y.extend(OSReleaseWidget, Y.Widget, {
1842+
1843+ /**
1844+ * Initialize the widget.
1845+ * - cfg.srcNode is the node which will be updated when the selected
1846+ * value of the 'os node' will change.
1847+ * - cfg.osNode is the node containing a 'select' element. When
1848+ * the selected element will change, the srcNode HTML will be
1849+ * updated.
1850+ *
1851+ * @method initializer
1852+ */
1853+ initializer: function(cfg) {
1854+ this.initialSkip = true;
1855+ },
1856+
1857+ /**
1858+ * Bind the widget to events (to name 'event_name') generated by the given
1859+ * 'osNode'.
1860+ *
1861+ * @method bindTo
1862+ */
1863+ bindTo: function(osNode, event_name) {
1864+ var self = this;
1865+ Y.one(osNode).on(event_name, function(e) {
1866+ var osValue = e.currentTarget.get('value');
1867+ self.switchTo(osValue);
1868+ });
1869+ var osValue = Y.one(osNode).get('value');
1870+ self.switchTo(osValue);
1871+ },
1872+
1873+ /**
1874+ * React to a new value of the os node: update the HTML of
1875+ * 'srcNode'.
1876+ *
1877+ * @method switchTo
1878+ */
1879+ switchTo: function(newOSValue) {
1880+ var srcNode = this.get('srcNode');
1881+ var options = srcNode.all('option');
1882+ var selected = false;
1883+ options.each(function(option) {
1884+ var value = option.get('value');
1885+ var split_value = value.split("/");
1886+
1887+ // Only show the default option, if that
1888+ // is the selected os option as well.
1889+ if(newOSValue == '') {
1890+ if(value == '') {
1891+ option.removeClass('hidden');
1892+ option.set('selected', 'selected');
1893+ }
1894+ else {
1895+ option.addClass('hidden');
1896+ }
1897+ }
1898+ else {
1899+ if(split_value[0] == newOSValue) {
1900+ option.removeClass('hidden');
1901+ if(split_value[1] == '') {
1902+ selected = true;
1903+ option.set('selected', 'selected');
1904+ }
1905+ }
1906+ else {
1907+ option.addClass('hidden');
1908+ }
1909+ }
1910+ });
1911+
1912+ // See if this was the inital skip. As the following
1913+ // should only be done, after the first load, as the
1914+ // initial will already be selected correctly.
1915+ if(this.initialSkip == true) {
1916+ this.initialSkip = false;
1917+ return;
1918+ }
1919+
1920+ // See if a selection was made, if not then we need
1921+ // to select the first visible as a default is not
1922+ // present.
1923+ if(!selected) {
1924+ var first_option = null;
1925+ options.each(function(option) {
1926+ if(!option.hasClass('hidden')) {
1927+ if(first_option == null) {
1928+ first_option = option;
1929+ }
1930+ }
1931+ });
1932+ if(first_option != null) {
1933+ first_option.set('selected', 'selected');
1934+ }
1935+ }
1936+ }
1937+});
1938+
1939+module.OSReleaseWidget = OSReleaseWidget;
1940+
1941+}, '0.1', {'requires': ['widget', 'io']}
1942+);
1943
1944=== added file 'src/maasserver/static/js/tests/test_os_distro_select.html'
1945--- src/maasserver/static/js/tests/test_os_distro_select.html 1970-01-01 00:00:00 +0000
1946+++ src/maasserver/static/js/tests/test_os_distro_select.html 2014-04-24 17:04:47 +0000
1947@@ -0,0 +1,38 @@
1948+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1949+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
1950+ <head>
1951+ <title>Test maas.os_distro_select</title>
1952+
1953+ <!-- YUI and test setup -->
1954+ <script type="text/javascript" src="../testing/yui_test_conf.js"></script>
1955+ <script type="text/javascript" src="/usr/share/javascript/yui3/yui/yui.js"></script>
1956+ <script type="text/javascript" src="../testing/testrunner.js"></script>
1957+ <script type="text/javascript" src="../testing/testing.js"></script>
1958+ <!-- The module under test -->
1959+ <script type="text/javascript" src="../os_distro_select.js"></script>
1960+ <!-- The test suite -->
1961+ <script type="text/javascript" src="test_os_distro_select.js"></script>
1962+ </head>
1963+ <body>
1964+ <span id="suite">maas.os_distro_select.tests</span>
1965+ <script type="text/x-template" id="select_node">
1966+ <select id="id_osystem">
1967+ <option value="" selected="selected">Default OS</option>
1968+ <option value="value1">Value1</option>
1969+ <option value="value2">Value2</option>
1970+ </select>
1971+ </script>
1972+ <script type="text/x-template" id="target_node">
1973+ <select id="id_distro_series">
1974+ <option value="" selected="selected">Default Release</option>
1975+ <option value="value1/series1">Value1Series1</option>
1976+ <option value="value1/series2">Value1Series2</option>
1977+ <option value="value1/series3">Value1Series3</option>
1978+ <option value="value2/series1">Value2Series1</option>
1979+ <option value="value2/series2">Value2Series2</option>
1980+ <option value="value2/series3">Value2Series3</option>
1981+ </select>
1982+ </script>
1983+ <div id="placeholder"></div>
1984+ </body>
1985+</html>
1986
1987=== added file 'src/maasserver/static/js/tests/test_os_distro_select.js'
1988--- src/maasserver/static/js/tests/test_os_distro_select.js 1970-01-01 00:00:00 +0000
1989+++ src/maasserver/static/js/tests/test_os_distro_select.js 2014-04-24 17:04:47 +0000
1990@@ -0,0 +1,106 @@
1991+/* Copyright 2012 Canonical Ltd. This software is licensed under the
1992+ * GNU Affero General Public License version 3 (see the file LICENSE).
1993+ */
1994+
1995+YUI({ useBrowserConsole: true }).add(
1996+ 'maas.os_distro_select.tests', function(Y) {
1997+
1998+Y.log('loading maas.os_distro_select.tests');
1999+var namespace = Y.namespace('maas.os_distro_select.tests');
2000+
2001+var module = Y.maas.os_distro_select;
2002+var suite = new Y.Test.Suite("maas.os_distro_select Tests");
2003+
2004+var select_node_template = Y.one('#select_node').getContent();
2005+var target_node_template = Y.one('#target_node').getContent();
2006+
2007+suite.add(new Y.maas.testing.TestCase({
2008+ name: 'test-os_distro_select',
2009+
2010+ setUp: function () {
2011+ Y.one('#placeholder').empty().append(
2012+ Y.Node.create(select_node_template).append(
2013+ Y.Node.create(target_node_template)));
2014+ },
2015+
2016+ testBindToDefaultShowsDefaultRelease: function() {
2017+ var widget = new Y.maas.os_distro_select.OSReleaseWidget({
2018+ srcNode: '#id_distro_series',
2019+ });
2020+ widget.bindTo(Y.one('#id_osystem'), 'change');
2021+
2022+ var options = Y.one('#id_distro_series').all('option');
2023+ options.each(function(option) {
2024+ var value = option.get('value');
2025+ if(value == '') {
2026+ Y.Assert.isFalse(option.hasClass('hidden'));
2027+ }
2028+ else {
2029+ Y.Assert.isTrue(option.hasClass('hidden'));
2030+ }
2031+ });
2032+ },
2033+
2034+ testNonDefaultShowsRelatedReleases: function() {
2035+ var node = Y.one('#id_distro_series');
2036+ node.append('<option value="value1/">Value1Default</option>');
2037+
2038+ var widget = new Y.maas.os_distro_select.OSReleaseWidget({
2039+ srcNode: '#id_distro_series',
2040+ });
2041+ widget.bindTo(Y.one('#id_osystem'), 'change');
2042+
2043+ var newValue = 'value1';
2044+ var select = Y.one('#id_osystem');
2045+ select.set('value', newValue);
2046+ select.simulate('change');
2047+
2048+ var options = Y.one('#id_distro_series').all('option');
2049+ options.each(function(option) {
2050+ var value = option.get('value');
2051+ if(value == '') {
2052+ Y.Assert.isTrue(option.hasClass('hidden'));
2053+ }
2054+ else {
2055+ var split_value = value.split('/');
2056+ if(split_value[0] == newValue) {
2057+ Y.Assert.isFalse(option.hasClass('hidden'));
2058+ }
2059+ else {
2060+ Y.Assert.isTrue(option.hasClass('hidden'));
2061+ }
2062+ }
2063+ });
2064+
2065+ Y.Assert.areEqual(
2066+ newValue + '/', Y.one('#id_distro_series').get('value'))
2067+ },
2068+
2069+ testInitialSkipOnFirstChange: function() {
2070+ var newValue = 'value1/series1';
2071+ var select = Y.one('#id_osystem');
2072+ select.set('value', 'value1');
2073+ select.simulate('change');
2074+ select = Y.one('#id_distro_series');
2075+ select.set('value', newValue);
2076+ select.simulate('change');
2077+
2078+ var widget = new Y.maas.os_distro_select.OSReleaseWidget({
2079+ srcNode: '#id_distro_series',
2080+ });
2081+
2082+ Y.Assert.isTrue(widget.initialSkip);
2083+ widget.bindTo(Y.one('#id_osystem'), 'change');
2084+ Y.Assert.isFalse(widget.initialSkip);
2085+
2086+ Y.Assert.areEqual(
2087+ newValue, Y.one('#id_distro_series').get('value'))
2088+ }
2089+
2090+}));
2091+
2092+namespace.suite = suite;
2093+
2094+}, '0.1', {'requires': [
2095+ 'node-event-simulate', 'test', 'maas.testing', 'maas.os_distro_select']}
2096+);
2097
2098=== modified file 'src/maasserver/templates/maasserver/bootimage-list.html'
2099--- src/maasserver/templates/maasserver/bootimage-list.html 2014-03-27 07:39:38 +0000
2100+++ src/maasserver/templates/maasserver/bootimage-list.html 2014-04-24 17:04:47 +0000
2101@@ -20,6 +20,7 @@
2102 <thead>
2103 <tr>
2104 <th>ID</th>
2105+ <th>OS</th>
2106 <th>Release</th>
2107 <th>Architecture</th>
2108 <th>Subarchitecture</th>
2109@@ -32,6 +33,7 @@
2110 {% for bootimage in bootimage_list %}
2111 <tr class="bootimage {% cycle 'even' 'odd' %}">
2112 <td>{{ bootimage.id }}</td>
2113+ <td>{{ bootimage.osystem_title }}</td>
2114 <td>{{ bootimage.release }}</td>
2115 <td>{{ bootimage.architecture }}</td>
2116 <td>{{ bootimage.subarchitecture }}</td>
2117
2118=== modified file 'src/maasserver/templates/maasserver/node_edit.html'
2119--- src/maasserver/templates/maasserver/node_edit.html 2014-03-03 09:44:25 +0000
2120+++ src/maasserver/templates/maasserver/node_edit.html 2014-04-24 17:04:47 +0000
2121@@ -11,9 +11,17 @@
2122 <script type="text/javascript">
2123 <!--
2124 YUI().use(
2125- 'maas.enums', 'maas.power_parameters',
2126+ 'maas.enums', 'maas.power_parameters', 'maas.os_distro_select',
2127 function (Y) {
2128 Y.on('load', function() {
2129+ // Create OSDistroWidget so that the release field will be
2130+ // updated each time the value of the os field changes.
2131+ var releaseWidget = new Y.maas.os_distro_select.OSReleaseWidget({
2132+ srcNode: '#id_distro_series'
2133+ });
2134+ releaseWidget.bindTo(Y.one('.osystem').one('select'), 'change');
2135+ releaseWidget.render();
2136+
2137 // Create LinkedContentWidget widget so that the power_parameter field
2138 // will be updated each time the value of the power_type field changes.
2139 var power_types = {{ power_types }};
2140
2141=== modified file 'src/maasserver/templates/maasserver/settings.html'
2142--- src/maasserver/templates/maasserver/settings.html 2014-04-10 04:29:20 +0000
2143+++ src/maasserver/templates/maasserver/settings.html 2014-04-24 17:04:47 +0000
2144@@ -6,6 +6,23 @@
2145 {% block page-title %}Settings{% endblock %}
2146
2147 {% block head %}
2148+ <script type="text/javascript">
2149+ <!--
2150+ YUI().use(
2151+ 'maas.os_distro_select',
2152+ function (Y) {
2153+ Y.on('load', function() {
2154+ // Create OSDistroWidget so that the release field will be
2155+ // updated each time the value of the os field changes.
2156+ var releaseWidget = new Y.maas.os_distro_select.OSReleaseWidget({
2157+ srcNode: '#id_deploy-default_distro_series'
2158+ });
2159+ releaseWidget.bindTo(Y.one('#id_deploy-default_osystem'), 'change');
2160+ releaseWidget.render();
2161+ });
2162+ });
2163+ // -->
2164+ </script>
2165 {% endblock %}
2166
2167 {% block content %}
2168@@ -90,6 +107,21 @@
2169 <div class="clear"></div>
2170 </div>
2171 <div class="divider"></div>
2172+ <div id="deploy" class="block size7 first">
2173+ <h2>Deploy</h2>
2174+ <form action="{% url "settings" %}" method="post">
2175+ {% csrf_token %}
2176+ <ul>
2177+ {% for field in deploy_form %}
2178+ {% include "maasserver/form_field.html" %}
2179+ {% endfor %}
2180+ </ul>
2181+ <input type="hidden" name="deploy_submit" value="1" />
2182+ <input type="submit" class="button right" value="Save" />
2183+ </form>
2184+ <div class="clear"></div>
2185+ </div>
2186+ <div class="divider"></div>
2187 <div id="ubuntu" class="block size7 first">
2188 <h2>Ubuntu</h2>
2189 <form action="{% url "settings" %}" method="post">
2190
2191=== modified file 'src/maasserver/templates/maasserver/snippets.html'
2192--- src/maasserver/templates/maasserver/snippets.html 2014-03-25 13:45:52 +0000
2193+++ src/maasserver/templates/maasserver/snippets.html 2014-04-24 17:04:47 +0000
2194@@ -16,6 +16,10 @@
2195 </div>
2196 </p>
2197 <p>
2198+ <label for="id_osystem">OS</label>
2199+ {{ node_form.osystem }}
2200+ </p>
2201+ <p>
2202 <label for="id_distro_series">Release</label>
2203 {{ node_form.distro_series }}
2204 </p>
2205
2206=== modified file 'src/maasserver/testing/factory.py'
2207--- src/maasserver/testing/factory.py 2014-04-04 06:46:05 +0000
2208+++ src/maasserver/testing/factory.py 2014-04-24 17:04:47 +0000
2209@@ -56,6 +56,7 @@
2210 NodeCommissionResult,
2211 )
2212 from netaddr import IPAddress
2213+from provisioningserver.osystems import OperatingSystemRegistry
2214
2215 # We have a limited number of public keys:
2216 # src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub
2217@@ -137,6 +138,21 @@
2218 [choice for choice in list(get_power_types().keys())
2219 if choice not in but_not])
2220
2221+ def getRandomOS(self):
2222+ """Pick a random operating system from the registry."""
2223+ osystems = [obj for _, obj in OperatingSystemRegistry]
2224+ return random.choice(osystems)
2225+
2226+ def getRandomRelease(self, osystem):
2227+ """Pick a random release from operating system."""
2228+ releases = osystem.get_supported_releases()
2229+ return random.choice(releases)
2230+
2231+ def getRandomCommissioningRelease(self, osystem):
2232+ """Pick a random commissioning release from operating system."""
2233+ releases = osystem.get_supported_commissioning_releases()
2234+ return random.choice(releases)
2235+
2236 def _save_node_unchecked(self, node):
2237 """Save a :class:`Node`, but circumvent status transition checks."""
2238 valid_initial_states = NODE_TRANSITIONS[None]
2239@@ -427,9 +443,11 @@
2240 return "OAuth " + ", ".join([
2241 '%s="%s"' % (key, value) for key, value in items.items()])
2242
2243- def make_boot_image(self, architecture=None, subarchitecture=None,
2244- release=None, purpose=None, nodegroup=None,
2245- label=None):
2246+ def make_boot_image(self, osystem=None, architecture=None,
2247+ subarchitecture=None, release=None, purpose=None,
2248+ nodegroup=None, label=None):
2249+ if osystem is None:
2250+ osystem = self.make_name('os')
2251 if architecture is None:
2252 architecture = self.make_name('architecture')
2253 if subarchitecture is None:
2254@@ -444,6 +462,7 @@
2255 label = self.make_name('label')
2256 return BootImage.objects.create(
2257 nodegroup=nodegroup,
2258+ osystem=osystem,
2259 architecture=architecture,
2260 subarchitecture=subarchitecture,
2261 release=release,
2262
2263=== added file 'src/maasserver/testing/osystems.py'
2264--- src/maasserver/testing/osystems.py 1970-01-01 00:00:00 +0000
2265+++ src/maasserver/testing/osystems.py 2014-04-24 17:04:47 +0000
2266@@ -0,0 +1,94 @@
2267+# Copyright 2014 Canonical Ltd. This software is licensed under the
2268+# GNU Affero General Public License version 3 (see the file LICENSE).
2269+
2270+"""Helpers for operating systems in testing."""
2271+
2272+from __future__ import (
2273+ absolute_import,
2274+ print_function,
2275+ unicode_literals,
2276+ )
2277+
2278+str = None
2279+
2280+__metaclass__ = type
2281+__all__ = [
2282+ 'make_usable_osystem',
2283+ 'patch_usable_osystems',
2284+ ]
2285+
2286+from random import randint
2287+
2288+from maasserver import forms
2289+from maasserver.testing.factory import factory
2290+from provisioningserver.osystems import BOOT_IMAGE_PURPOSE
2291+from provisioningserver.boot.tests.test_tftppath import make_osystem
2292+
2293+
2294+def make_osystem_with_releases(testcase, with_releases=True, osystem_name=None,
2295+ releases=None):
2296+ """Generate an arbitrary operating system.
2297+
2298+ :param with_releases: Should the operating system include releases?
2299+ Defaults to `True`.
2300+ """
2301+ if osystem_name is None:
2302+ osystem_name = factory.make_name('os')
2303+ if with_releases:
2304+ if releases is None:
2305+ releases = [factory.make_name('release') for _ in range(3)]
2306+
2307+ osystem = make_osystem(
2308+ osystem_name,
2309+ [BOOT_IMAGE_PURPOSE.INSTALL, BOOT_IMAGE_PURPOSE.XINSTALL],
2310+ testcase)
2311+ osystem.fake_list = releases
2312+ return osystem
2313+
2314+
2315+def patch_usable_osystems(testcase, osystems=None, allow_empty=True):
2316+ """Set a fixed list of usable oeprating systems.
2317+
2318+ A usable operating system is one for which boot images are available.
2319+
2320+ :param testcase: A `TestCase` whose `patch` this function can use.
2321+ :param architectures: Optional list of oprating systems. If omitted,
2322+ defaults to a list (which may be empty) of random operating systems.
2323+ """
2324+ start = 0
2325+ if allow_empty is False:
2326+ start = 1
2327+ if osystems is None:
2328+ osystems = [
2329+ make_osystem_with_releases(testcase)
2330+ for _ in range(randint(start, 2))
2331+ ]
2332+ distro_series = {}
2333+ for osystem in osystems:
2334+ distro_series[osystem.name] = osystem.get_supported_releases()
2335+ patch = testcase.patch(forms, 'list_all_usable_osystems')
2336+ patch.return_value = osystems
2337+ patch = testcase.patch(forms, 'list_all_usable_releases')
2338+ patch.return_value = distro_series
2339+
2340+
2341+def make_usable_osystem(testcase, with_releases=True, osystem_name=None,
2342+ releases=None):
2343+ """Return arbitrary operating system, and make it "usable."
2344+
2345+ A usable operating system is one for which boot images are available.
2346+
2347+ :param testcase: A `TestCase` whose `patch` this function can pass to
2348+ `patch_usable_osystems`.
2349+ :param with_releases: Should the operating system include releases?
2350+ Defaults to `True`.
2351+ :param osystem_name: The operating system name. Useful in cases where
2352+ we need to test that not supplying an os works correctly.
2353+ :param releases: The list of releases name. Useful in cases where
2354+ we need to test that not supplying a release works correctly.
2355+ """
2356+ osystem = make_osystem_with_releases(
2357+ testcase, with_releases=with_releases, osystem_name=osystem_name,
2358+ releases=releases)
2359+ patch_usable_osystems(testcase, [osystem])
2360+ return osystem
2361
2362=== modified file 'src/maasserver/tests/test_api_boot_images.py'
2363--- src/maasserver/tests/test_api_boot_images.py 2014-03-21 19:01:40 +0000
2364+++ src/maasserver/tests/test_api_boot_images.py 2014-04-24 17:04:47 +0000
2365@@ -134,6 +134,7 @@
2366 image = factory.make_boot_image()
2367 self.assertEqual(
2368 (
2369+ image.osystem,
2370 image.architecture,
2371 image.subarchitecture,
2372 image.release,
2373@@ -146,6 +147,7 @@
2374 image = make_boot_image_params()
2375 self.assertEqual(
2376 (
2377+ image['osystem'],
2378 image['architecture'],
2379 image['subarchitecture'],
2380 image['release'],
2381@@ -158,12 +160,13 @@
2382 image = make_boot_image_params()
2383 del image['subarchitecture']
2384 del image['label']
2385- _, subarchitecture, _, label, _ = summarise_boot_image_dict(image)
2386+ _, _, subarchitecture, _, label, _ = summarise_boot_image_dict(image)
2387 self.assertEqual(('generic', 'release'), (subarchitecture, label))
2388
2389 def test_summarise_boot_image_functions_are_compatible(self):
2390 image_dict = make_boot_image_params()
2391 image_obj = factory.make_boot_image(
2392+ osystem=image_dict['osystem'],
2393 architecture=image_dict['architecture'],
2394 subarchitecture=image_dict['subarchitecture'],
2395 release=image_dict['release'], label=image_dict['label'],
2396
2397=== modified file 'src/maasserver/tests/test_api_node.py'
2398--- src/maasserver/tests/test_api_node.py 2014-04-22 10:26:47 +0000
2399+++ src/maasserver/tests/test_api_node.py 2014-04-24 17:04:47 +0000
2400@@ -23,7 +23,6 @@
2401 import bson
2402 from django.core.urlresolvers import reverse
2403 from maasserver.enum import (
2404- DISTRO_SERIES,
2405 NODE_STATUS,
2406 NODE_STATUS_CHOICES_DICT,
2407 )
2408@@ -48,6 +47,7 @@
2409 NodeUserData,
2410 )
2411 from metadataserver.nodeinituser import get_node_init_user
2412+from provisioningserver.osystems.ubuntu import UbuntuOS
2413
2414
2415 class NodeAnonAPITest(MAASServerTestCase):
2416@@ -221,11 +221,60 @@
2417 self.assertEqual(
2418 node.system_id, json.loads(response.content)['system_id'])
2419
2420- def test_POST_start_sets_distro_series(self):
2421- node = factory.make_node(
2422- owner=self.logged_in_user, mac=True,
2423- power_type='ether_wake')
2424- distro_series = factory.getRandomEnum(DISTRO_SERIES)
2425+ def test_POST_start_sets_osystem(self):
2426+ node = factory.make_node(
2427+ owner=self.logged_in_user, mac=True,
2428+ power_type='ether_wake')
2429+ osystem = factory.getRandomOS()
2430+ response = self.client.post(
2431+ self.get_node_uri(node),
2432+ {'op': 'start', 'osystem': osystem.name})
2433+ self.assertEqual(
2434+ (httplib.OK, node.system_id),
2435+ (response.status_code, json.loads(response.content)['system_id']))
2436+ self.assertEqual(
2437+ osystem.name, reload_object(node).osystem)
2438+
2439+ def test_POST_start_accepts_os(self):
2440+ node = factory.make_node(
2441+ owner=self.logged_in_user, mac=True,
2442+ power_type='ether_wake')
2443+ osystem = factory.getRandomOS()
2444+ response = self.client.post(
2445+ self.get_node_uri(node),
2446+ {'op': 'start', 'os': osystem.name})
2447+ self.assertEqual(
2448+ (httplib.OK, node.system_id),
2449+ (response.status_code, json.loads(response.content)['system_id']))
2450+ self.assertEqual(
2451+ osystem.name, reload_object(node).osystem)
2452+
2453+ def test_POST_start_sets_osystem_and_distro_series(self):
2454+ node = factory.make_node(
2455+ owner=self.logged_in_user, mac=True,
2456+ power_type='ether_wake')
2457+ osystem = factory.getRandomOS()
2458+ distro_series = factory.getRandomRelease(osystem)
2459+ response = self.client.post(
2460+ self.get_node_uri(node), {
2461+ 'op': 'start',
2462+ 'osystem': osystem.name,
2463+ 'distro_series': distro_series
2464+ })
2465+ self.assertEqual(
2466+ (httplib.OK, node.system_id),
2467+ (response.status_code, json.loads(response.content)['system_id']))
2468+ self.assertEqual(
2469+ osystem.name, reload_object(node).osystem)
2470+ self.assertEqual(
2471+ distro_series, reload_object(node).distro_series)
2472+
2473+ def test_POST_start_sets_distro_series_defaults_ubuntu(self):
2474+ node = factory.make_node(
2475+ owner=self.logged_in_user, mac=True,
2476+ power_type='ether_wake')
2477+ osystem = UbuntuOS()
2478+ distro_series = factory.getRandomRelease(osystem)
2479 response = self.client.post(
2480 self.get_node_uri(node),
2481 {'op': 'start', 'distro_series': distro_series})
2482@@ -233,8 +282,27 @@
2483 (httplib.OK, node.system_id),
2484 (response.status_code, json.loads(response.content)['system_id']))
2485 self.assertEqual(
2486+ osystem.name, reload_object(node).osystem)
2487+ self.assertEqual(
2488 distro_series, reload_object(node).distro_series)
2489
2490+ def test_POST_start_validates_osystem(self):
2491+ node = factory.make_node(
2492+ owner=self.logged_in_user, mac=True,
2493+ power_type='ether_wake')
2494+ invalid_osystem = factory.getRandomString()
2495+ response = self.client.post(
2496+ self.get_node_uri(node),
2497+ {'op': 'start', 'osystem': invalid_osystem})
2498+ self.assertEqual(
2499+ (
2500+ httplib.BAD_REQUEST,
2501+ {'osystem': [
2502+ "Value u'%s' is not a valid choice." %
2503+ invalid_osystem]}
2504+ ),
2505+ (response.status_code, json.loads(response.content)))
2506+
2507 def test_POST_start_validates_distro_series(self):
2508 node = factory.make_node(
2509 owner=self.logged_in_user, mac=True,
2510@@ -300,18 +368,23 @@
2511 self.client.post(self.get_node_uri(node), {'op': 'release'})
2512 self.assertTrue(reload_object(node).netboot)
2513
2514- def test_POST_release_resets_distro_series(self):
2515+ def test_POST_release_resets_osystem_and_distro_series(self):
2516+ osystem = factory.getRandomOS()
2517+ release = factory.getRandomRelease(osystem)
2518 node = factory.make_node(
2519 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user,
2520- distro_series=factory.getRandomEnum(DISTRO_SERIES))
2521+ osystem=osystem.name, distro_series=release)
2522 self.client.post(self.get_node_uri(node), {'op': 'release'})
2523+ self.assertEqual('', reload_object(node).osystem)
2524 self.assertEqual('', reload_object(node).distro_series)
2525
2526 def test_POST_release_resets_agent_name(self):
2527 agent_name = factory.make_name('agent-name')
2528+ osystem = factory.getRandomOS()
2529+ release = factory.getRandomRelease(osystem)
2530 node = factory.make_node(
2531 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user,
2532- distro_series=factory.getRandomEnum(DISTRO_SERIES),
2533+ osystem=osystem.name, distro_series=release,
2534 agent_name=agent_name)
2535 self.client.post(self.get_node_uri(node), {'op': 'release'})
2536 self.assertEqual('', reload_object(node).agent_name)
2537
2538=== modified file 'src/maasserver/tests/test_api_pxeconfig.py'
2539--- src/maasserver/tests/test_api_pxeconfig.py 2014-03-27 04:15:45 +0000
2540+++ src/maasserver/tests/test_api_pxeconfig.py 2014-04-24 17:04:47 +0000
2541@@ -133,9 +133,11 @@
2542 self.assertEqual(value, response_dict['extra_opts'])
2543
2544 def test_pxeconfig_uses_present_boot_image(self):
2545+ osystem = Config.objects.get_config('commissioning_osystem')
2546 release = Config.objects.get_config('commissioning_distro_series')
2547 nodegroup = factory.make_node_group()
2548 factory.make_boot_image(
2549+ osystem=osystem,
2550 architecture="amd64", release=release, nodegroup=nodegroup,
2551 purpose="commissioning")
2552 params = self.get_default_params()
2553@@ -282,7 +284,8 @@
2554 def test_get_boot_purpose_unknown_node(self):
2555 # A node that's not yet known to MAAS is assumed to be enlisting,
2556 # which uses a "commissioning" image.
2557- self.assertEqual("commissioning", api.get_boot_purpose(None))
2558+ self.assertEqual("commissioning", api.get_boot_purpose(
2559+ None, None, None, None, None, None))
2560
2561 def test_get_boot_purpose_known_node(self):
2562 # The following table shows the expected boot "purpose" for each set
2563@@ -305,11 +308,17 @@
2564 node.use_fastpath_installer()
2565 for name, value in parameters.items():
2566 setattr(node, name, value)
2567- self.assertEqual(purpose, api.get_boot_purpose(node))
2568+ osystem = node.get_osystem()
2569+ series = node.get_distro_series()
2570+ arch, subarch = node.architecture.split('/')
2571+ self.assertEqual(
2572+ purpose,
2573+ api.get_boot_purpose(
2574+ node, osystem, arch, subarch, series, None))
2575
2576 def test_pxeconfig_uses_boot_purpose(self):
2577 fake_boot_purpose = factory.make_name("purpose")
2578- self.patch(api, "get_boot_purpose", lambda node: fake_boot_purpose)
2579+ self.patch(api, "get_boot_purpose").return_value = fake_boot_purpose
2580 response = self.client.get(reverse('pxeconfig'),
2581 self.get_default_params())
2582 self.assertEqual(
2583
2584=== modified file 'src/maasserver/tests/test_compose_preseed.py'
2585--- src/maasserver/tests/test_compose_preseed.py 2013-10-07 09:12:40 +0000
2586+++ src/maasserver/tests/test_compose_preseed.py 2014-04-24 17:04:47 +0000
2587@@ -17,8 +17,10 @@
2588 from maasserver.compose_preseed import compose_preseed
2589 from maasserver.enum import NODE_STATUS
2590 from maasserver.testing.factory import factory
2591+from maasserver.testing.osystems import make_usable_osystem
2592 from maasserver.testing.testcase import MAASServerTestCase
2593 from maasserver.utils import absolute_reverse
2594+from maastesting.matchers import MockCalledOnceWith
2595 from metadataserver.models import NodeKey
2596 from testtools.matchers import (
2597 KeysEqual,
2598@@ -110,3 +112,15 @@
2599 self.assertEqual(
2600 absolute_reverse('curtin-metadata'),
2601 preseed['datasource']['MAAS']['metadata_url'])
2602+
2603+ def test_compose_preseed_with_osystem_compose_preseed(self):
2604+ osystem = make_usable_osystem(self)
2605+ mock_compose = self.patch(osystem, 'compose_preseed')
2606+ node = factory.make_node(osystem=osystem.name, status=NODE_STATUS.READY)
2607+
2608+ token = NodeKey.objects.get_token_for_node(node)
2609+ url = absolute_reverse('metadata')
2610+ compose_preseed(node)
2611+ self.assertThat(
2612+ mock_compose,
2613+ MockCalledOnceWith(node, token, url))
2614
2615=== modified file 'src/maasserver/tests/test_forms.py'
2616--- src/maasserver/tests/test_forms.py 2014-04-22 09:08:02 +0000
2617+++ src/maasserver/tests/test_forms.py 2014-04-24 17:04:47 +0000
2618@@ -90,6 +90,11 @@
2619 make_usable_architecture,
2620 patch_usable_architectures,
2621 )
2622+from maasserver.testing.osystems import (
2623+ make_osystem_with_releases,
2624+ make_usable_osystem,
2625+ patch_usable_osystems,
2626+ )
2627 from maasserver.testing.factory import factory
2628 from maasserver.testing.testcase import MAASServerTestCase
2629 from maasserver.utils import map_enum
2630@@ -421,6 +426,7 @@
2631 [
2632 'hostname',
2633 'architecture',
2634+ 'osystem',
2635 'distro_series',
2636 'nodegroup',
2637 ], list(form.fields))
2638@@ -480,6 +486,64 @@
2639 [NO_ARCHITECTURES_AVAILABLE],
2640 form.errors['architecture'])
2641
2642+ def test_accepts_osystem(self):
2643+ osystem = make_usable_osystem(self)
2644+ form = NodeForm(data={
2645+ 'hostname': factory.make_name('host'),
2646+ 'architecture': make_usable_architecture(self),
2647+ 'osystem': osystem.name,
2648+ })
2649+ self.assertTrue(form.is_valid(), form._errors)
2650+
2651+ def test_rejects_invalid_osystem(self):
2652+ patch_usable_osystems(self)
2653+ form = NodeForm(data={
2654+ 'hostname': factory.make_name('host'),
2655+ 'architecture': make_usable_architecture(self),
2656+ 'osystem': factory.make_name('os'),
2657+ })
2658+ self.assertFalse(form.is_valid())
2659+ self.assertItemsEqual(['osystem'], form._errors.keys())
2660+
2661+ def test_starts_with_default_osystem(self):
2662+ osystems = [make_osystem_with_releases(self) for _ in range(5)]
2663+ patch_usable_osystems(self, osystems)
2664+ form = NodeForm()
2665+ self.assertEqual(
2666+ '',
2667+ form.fields['osystem'].initial)
2668+
2669+ def test_accepts_osystem_distro_series(self):
2670+ osystem = make_usable_osystem(self)
2671+ release = osystem.get_default_release()
2672+ form = NodeForm(data={
2673+ 'hostname': factory.make_name('host'),
2674+ 'architecture': make_usable_architecture(self),
2675+ 'osystem': osystem.name,
2676+ 'distro_series': '%s/%s' % (osystem.name, release),
2677+ })
2678+ self.assertTrue(form.is_valid(), form._errors)
2679+
2680+ def test_rejects_invalid_osystem_distro_series(self):
2681+ osystem = make_usable_osystem(self)
2682+ release = factory.make_name('release')
2683+ form = NodeForm(data={
2684+ 'hostname': factory.make_name('host'),
2685+ 'architecture': make_usable_architecture(self),
2686+ 'osystem': osystem.name,
2687+ 'distro_series': '%s/%s' % (osystem.name, release),
2688+ })
2689+ self.assertFalse(form.is_valid())
2690+ self.assertItemsEqual(['distro_series'], form._errors.keys())
2691+
2692+ def test_starts_with_default_distro_series(self):
2693+ osystems = [make_osystem_with_releases(self) for _ in range(5)]
2694+ patch_usable_osystems(self, osystems)
2695+ form = NodeForm()
2696+ self.assertEqual(
2697+ '',
2698+ form.fields['distro_series'].initial)
2699+
2700
2701 class TestAdminNodeForm(MAASServerTestCase):
2702
2703@@ -491,6 +555,7 @@
2704 [
2705 'hostname',
2706 'architecture',
2707+ 'osystem',
2708 'distro_series',
2709 'power_type',
2710 'power_parameters',
2711
2712=== modified file 'src/maasserver/tests/test_preseed.py'
2713--- src/maasserver/tests/test_preseed.py 2014-04-21 11:43:26 +0000
2714+++ src/maasserver/tests/test_preseed.py 2014-04-24 17:04:47 +0000
2715@@ -22,7 +22,6 @@
2716 from django.conf import settings
2717 from django.core.urlresolvers import reverse
2718 from maasserver.enum import (
2719- DISTRO_SERIES,
2720 NODE_STATUS,
2721 NODEGROUPINTERFACE_MANAGEMENT,
2722 PRESEED_TYPE,
2723@@ -590,6 +589,7 @@
2724 node = factory.make_node()
2725 arch, subarch = node.architecture.split('/')
2726 factory.make_boot_image(
2727+ osystem=node.get_osystem(),
2728 architecture=arch, subarchitecture=subarch,
2729 release=node.get_distro_series(), purpose='xinstall',
2730 nodegroup=node.nodegroup)
2731@@ -677,34 +677,37 @@
2732 self.assertIn('cloud-init', context['curtin_preseed'])
2733
2734 def test_get_curtin_installer_url_returns_url(self):
2735- # Exclude DISTRO_SERIES.default. It's a special value that defers
2736- # to a run-time setting which we don't provide in this test.
2737- series = factory.getRandomEnum(
2738- DISTRO_SERIES, but_not=DISTRO_SERIES.default)
2739+ osystem = factory.getRandomOS()
2740+ series = factory.getRandomRelease(osystem)
2741 architecture = make_usable_architecture(self)
2742 node = factory.make_node(
2743- architecture=architecture, distro_series=series)
2744+ osystem=osystem.name, architecture=architecture,
2745+ distro_series=series)
2746 arch, subarch = architecture.split('/')
2747 boot_image = factory.make_boot_image(
2748- architecture=arch, subarchitecture=subarch, release=series,
2749+ osystem=osystem.name, architecture=arch,
2750+ subarchitecture=subarch, release=series,
2751 purpose='xinstall', nodegroup=node.nodegroup)
2752
2753 installer_url = get_curtin_installer_url(node)
2754
2755 [interface] = node.nodegroup.get_managed_interfaces()
2756 self.assertEqual(
2757- 'http://%s/MAAS/static/images/%s/%s/%s/%s/root-tgz' % (
2758- interface.ip, arch, subarch, series, boot_image.label),
2759+ 'http://%s/MAAS/static/images/%s/%s/%s/%s/%s/root-tgz' % (
2760+ interface.ip, osystem.name, arch, subarch,
2761+ series, boot_image.label),
2762 installer_url)
2763
2764 def test_get_curtin_installer_url_fails_if_no_boot_image(self):
2765- series = factory.getRandomEnum(
2766- DISTRO_SERIES, but_not=DISTRO_SERIES.default)
2767+ osystem = factory.getRandomOS()
2768+ series = factory.getRandomRelease(osystem)
2769 architecture = make_usable_architecture(self)
2770 node = factory.make_node(
2771+ osystem=osystem.name,
2772 architecture=architecture, distro_series=series)
2773 # Generate a boot image with a different arch/subarch.
2774 factory.make_boot_image(
2775+ osystem=osystem.name,
2776 architecture=factory.make_name('arch'),
2777 subarchitecture=factory.make_name('subarch'), release=series,
2778 purpose='xinstall', nodegroup=node.nodegroup)
2779@@ -714,7 +717,8 @@
2780 arch, subarch = architecture.split('/')
2781 msg = (
2782 "No image could be found for the given selection: "
2783- "arch=%s, subarch=%s, series=%s, purpose=xinstall." % (
2784+ "os=%s, arch=%s, subarch=%s, series=%s, purpose=xinstall." % (
2785+ osystem.name,
2786 arch,
2787 subarch,
2788 node.get_distro_series(),
2789
2790=== modified file 'src/maasserver/views/clusters.py'
2791--- src/maasserver/views/clusters.py 2014-04-03 11:20:03 +0000
2792+++ src/maasserver/views/clusters.py 2014-04-24 17:04:47 +0000
2793@@ -51,6 +51,7 @@
2794 NodeGroupInterface,
2795 )
2796 from maasserver.views import PaginatedListView
2797+from provisioningserver.osystems import OperatingSystemRegistry
2798
2799
2800 class ClusterListView(PaginatedListView, FormMixin, ProcessFormView):
2801@@ -270,15 +271,23 @@
2802 nodegroup_uuid = self.kwargs.get('uuid', None)
2803 return get_object_or_404(NodeGroup, uuid=nodegroup_uuid)
2804
2805+ def get_osystem_title(self, osystem):
2806+ osystem_obj = OperatingSystemRegistry.get_item(osystem, default=None)
2807+ if osystem_obj is None:
2808+ return osystem
2809+ return osystem_obj.title
2810+
2811 def get_context_data(self, **kwargs):
2812 context = super(
2813 BootImagesListView, self).get_context_data(**kwargs)
2814 context['nodegroup'] = self.get_nodegroup()
2815+ for bootimage in context['bootimage_list']:
2816+ bootimage.osystem_title = self.get_osystem_title(bootimage.osystem)
2817 return context
2818
2819 def get_queryset(self):
2820 nodegroup = self.get_nodegroup()
2821 # A sorted bootimages list.
2822 return nodegroup.bootimage_set.all().order_by(
2823- '-release', 'architecture', 'subarchitecture', 'purpose',
2824- 'label')
2825+ 'osystem', '-release', 'architecture', 'subarchitecture',
2826+ 'purpose', 'label')
2827
2828=== modified file 'src/maasserver/views/settings.py'
2829--- src/maasserver/views/settings.py 2014-04-10 13:43:33 +0000
2830+++ src/maasserver/views/settings.py 2014-04-24 17:04:47 +0000
2831@@ -41,6 +41,7 @@
2832 from maasserver.exceptions import CannotDeleteUserException
2833 from maasserver.forms import (
2834 CommissioningForm,
2835+ DeployForm,
2836 EditUserForm,
2837 GlobalKernelOptsForm,
2838 MAASAndNetworkForm,
2839@@ -177,6 +178,13 @@
2840 if response is not None:
2841 return response
2842
2843+ # Process the Deploy form.
2844+ deploy_form, response = process_form(
2845+ request, DeployForm, reverse('settings'), 'deploy',
2846+ "Configuration updated.")
2847+ if response is not None:
2848+ return response
2849+
2850 # Process the Ubuntu form.
2851 ubuntu_form, response = process_form(
2852 request, UbuntuForm, reverse('settings'), 'ubuntu',
2853@@ -202,6 +210,7 @@
2854 'maas_and_network_form': maas_and_network_form,
2855 'third_party_drivers_form': third_party_drivers_form,
2856 'commissioning_form': commissioning_form,
2857+ 'deploy_form': deploy_form,
2858 'ubuntu_form': ubuntu_form,
2859 'kernelopts_form': kernelopts_form,
2860 },
2861
2862=== modified file 'src/maasserver/views/tests/test_boot_image_list.py'
2863--- src/maasserver/views/tests/test_boot_image_list.py 2014-04-02 13:53:19 +0000
2864+++ src/maasserver/views/tests/test_boot_image_list.py 2014-04-24 17:04:47 +0000
2865@@ -22,6 +22,7 @@
2866 from maasserver.testing.factory import factory
2867 from maasserver.testing.testcase import MAASServerTestCase
2868 from maasserver.views.clusters import BootImagesListView
2869+from provisioningserver.boot.tests.test_tftppath import make_osystem
2870 from testtools.matchers import ContainsAll
2871
2872
2873@@ -32,6 +33,7 @@
2874 nodegroup = factory.make_node_group()
2875 images = [
2876 factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]
2877+ [make_osystem(bi.osystem, ['install'], self) for bi in images]
2878 response = self.client.get(
2879 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))
2880 self.assertEqual(
2881@@ -54,8 +56,11 @@
2882 self.client_log_in(as_admin=True)
2883 nodegroup = factory.make_node_group()
2884 # Create 4 images.
2885- [
2886- factory.make_boot_image(nodegroup=nodegroup) for _ in range(4)]
2887+ boot_images = [
2888+ factory.make_boot_image(nodegroup=nodegroup)
2889+ for _ in range(4)
2890+ ]
2891+ [make_osystem(bi.osystem, ['install'], self) for bi in boot_images]
2892 response = self.client.get(
2893 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))
2894 self.assertEqual(httplib.OK, response.status_code)
2895@@ -66,7 +71,8 @@
2896
2897 def test_displays_warning_if_boot_image_list_is_empty(self):
2898 # Create boot images in another nodegroup.
2899- [factory.make_boot_image() for _ in range(3)]
2900+ boot_images = [factory.make_boot_image() for _ in range(3)]
2901+ [make_osystem(bi.osystem, ['install'], self) for bi in boot_images]
2902 self.client_log_in(as_admin=True)
2903 nodegroup = factory.make_node_group()
2904 response = self.client.get(
2905
2906=== modified file 'src/maasserver/views/tests/test_clusters.py'
2907--- src/maasserver/views/tests/test_clusters.py 2014-04-04 06:46:05 +0000
2908+++ src/maasserver/views/tests/test_clusters.py 2014-04-24 17:04:47 +0000
2909@@ -41,6 +41,7 @@
2910 ANY,
2911 call,
2912 )
2913+from provisioningserver.boot.tests.test_tftppath import make_osystem
2914 from testtools.matchers import (
2915 AllMatch,
2916 Contains,
2917@@ -309,7 +310,11 @@
2918 def test_contains_link_to_boot_image_list(self):
2919 self.client_log_in(as_admin=True)
2920 nodegroup = factory.make_node_group()
2921- [factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]
2922+ boot_images = [
2923+ factory.make_boot_image(nodegroup=nodegroup)
2924+ for _ in range(3)
2925+ ]
2926+ [make_osystem(bi.osystem, ['install'], self) for bi in boot_images]
2927 response = self.client.get(
2928 reverse('cluster-edit', args=[nodegroup.uuid]))
2929 self.assertEqual(
2930@@ -320,7 +325,8 @@
2931
2932 def test_displays_warning_if_boot_image_list_is_empty(self):
2933 # Create boot images in another nodegroup.
2934- [factory.make_boot_image() for _ in range(3)]
2935+ boot_images = [factory.make_boot_image() for _ in range(3)]
2936+ [make_osystem(bi.osystem, ['install'], self) for bi in boot_images]
2937 self.client_log_in(as_admin=True)
2938 nodegroup = factory.make_node_group()
2939 response = self.client.get(
2940
2941=== modified file 'src/maasserver/views/tests/test_settings.py'
2942--- src/maasserver/views/tests/test_settings.py 2014-04-10 13:43:33 +0000
2943+++ src/maasserver/views/tests/test_settings.py 2014-04-24 17:04:47 +0000
2944@@ -20,10 +20,7 @@
2945 from django.contrib.auth.models import User
2946 from django.core.urlresolvers import reverse
2947 from lxml.html import fromstring
2948-from maasserver.enum import (
2949- COMMISSIONING_DISTRO_SERIES_CHOICES,
2950- DISTRO_SERIES,
2951- )
2952+from maasserver.models.config import DEFAULT_OS
2953 from maasserver.models import (
2954 Config,
2955 UserProfile,
2956@@ -112,8 +109,7 @@
2957 def test_settings_commissioning_POST(self):
2958 self.client_log_in(as_admin=True)
2959 new_check_compatibility = factory.getRandomBoolean()
2960- new_commissioning_distro_series = factory.getRandomChoice(
2961- COMMISSIONING_DISTRO_SERIES_CHOICES)
2962+ new_commissioning = factory.getRandomCommissioningRelease(DEFAULT_OS)
2963 response = self.client.post(
2964 reverse('settings'),
2965 get_prefixed_form_data(
2966@@ -121,14 +117,14 @@
2967 data={
2968 'check_compatibility': new_check_compatibility,
2969 'commissioning_distro_series': (
2970- new_commissioning_distro_series),
2971+ new_commissioning),
2972 }))
2973
2974 self.assertEqual(httplib.FOUND, response.status_code)
2975 self.assertEqual(
2976 (
2977 new_check_compatibility,
2978- new_commissioning_distro_series,
2979+ new_commissioning,
2980 ),
2981 (
2982 Config.objects.get_config('check_compatibility'),
2983@@ -156,11 +152,38 @@
2984 Config.objects.get_config('enable_third_party_drivers'),
2985 ))
2986
2987+ def test_settings_deploy_POST(self):
2988+ self.client_log_in(as_admin=True)
2989+ new_osystem = factory.getRandomOS()
2990+ new_default_osystem = new_osystem.name
2991+ new_default_distro_series = factory.getRandomRelease(new_osystem)
2992+ response = self.client.post(
2993+ reverse('settings'),
2994+ get_prefixed_form_data(
2995+ prefix='deploy',
2996+ data={
2997+ 'default_osystem': new_default_osystem,
2998+ 'default_distro_series': '%s/%s' % (
2999+ new_default_osystem,
3000+ new_default_distro_series
3001+ ),
3002+ }))
3003+
3004+ self.assertEqual(httplib.FOUND, response.status_code, response.content)
3005+ self.assertEqual(
3006+ (
3007+ new_default_osystem,
3008+ new_default_distro_series,
3009+ ),
3010+ (
3011+ Config.objects.get_config('default_osystem'),
3012+ Config.objects.get_config('default_distro_series'),
3013+ ))
3014+
3015 def test_settings_ubuntu_POST(self):
3016 self.client_log_in(as_admin=True)
3017 new_main_archive = 'http://test.example.com/archive'
3018 new_ports_archive = 'http://test2.example.com/archive'
3019- new_default_distro_series = factory.getRandomEnum(DISTRO_SERIES)
3020 response = self.client.post(
3021 reverse('settings'),
3022 get_prefixed_form_data(
3023@@ -168,7 +191,6 @@
3024 data={
3025 'main_archive': new_main_archive,
3026 'ports_archive': new_ports_archive,
3027- 'default_distro_series': new_default_distro_series,
3028 }))
3029
3030 self.assertEqual(httplib.FOUND, response.status_code, response.content)
3031@@ -176,12 +198,10 @@
3032 (
3033 new_main_archive,
3034 new_ports_archive,
3035- new_default_distro_series,
3036 ),
3037 (
3038 Config.objects.get_config('main_archive'),
3039 Config.objects.get_config('ports_archive'),
3040- Config.objects.get_config('default_distro_series'),
3041 ))
3042
3043 def test_settings_kernelopts_POST(self):
3044
3045=== modified file 'src/metadataserver/tests/test_api.py'
3046--- src/metadataserver/tests/test_api.py 2014-03-24 13:02:28 +0000
3047+++ src/metadataserver/tests/test_api.py 2014-04-24 17:04:47 +0000
3048@@ -394,6 +394,7 @@
3049 node = factory.make_node()
3050 arch, subarch = node.architecture.split('/')
3051 factory.make_boot_image(
3052+ osystem=node.get_osystem(),
3053 architecture=arch, subarchitecture=subarch,
3054 release=node.get_distro_series(), purpose='xinstall',
3055 nodegroup=node.nodegroup)
3056
3057=== modified file 'src/provisioningserver/boot/__init__.py'
3058--- src/provisioningserver/boot/__init__.py 2014-03-28 16:46:55 +0000
3059+++ src/provisioningserver/boot/__init__.py 2014-04-24 17:04:47 +0000
3060@@ -168,7 +168,7 @@
3061 """
3062 def image_dir(params):
3063 return compose_image_path(
3064- params.arch, params.subarch,
3065+ params.osystem, params.arch, params.subarch,
3066 params.release, params.label)
3067
3068 def initrd_path(params):
3069
3070=== modified file 'src/provisioningserver/boot/tests/test_pxe.py'
3071--- src/provisioningserver/boot/tests/test_pxe.py 2014-03-28 04:31:32 +0000
3072+++ src/provisioningserver/boot/tests/test_pxe.py 2014-04-24 17:04:47 +0000
3073@@ -163,7 +163,7 @@
3074 self.assertThat(output, StartsWith("DEFAULT "))
3075 # The PXE parameters are all set according to the options.
3076 image_dir = compose_image_path(
3077- arch=params.arch, subarch=params.subarch,
3078+ osystem=params.osystem, arch=params.arch, subarch=params.subarch,
3079 release=params.release, label=params.label)
3080 self.assertThat(
3081 output, MatchesAll(
3082@@ -243,9 +243,11 @@
3083 method = PXEBootMethod()
3084 get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
3085 get_ephemeral_name.return_value = factory.make_name("ephemeral")
3086+ osystem = factory.make_name('osystem')
3087 options = {
3088 "kernel_params": make_kernel_parameters(
3089- testcase=self, subarch="generic", purpose=self.purpose),
3090+ testcase=self, osystem=osystem, subarch="generic",
3091+ purpose=self.purpose),
3092 }
3093 output = method.render_config(**options)
3094 config = parse_pxe_config(output)
3095@@ -268,7 +270,8 @@
3096 section = config[section_label]
3097 self.assertThat(
3098 section, ContainsAll(("KERNEL", "INITRD", "APPEND")))
3099- contains_arch_path = StartsWith("%s/" % section_label)
3100+ contains_arch_path = StartsWith(
3101+ "%s/%s/" % (osystem, section_label))
3102 self.assertThat(section["KERNEL"], contains_arch_path)
3103 self.assertThat(section["INITRD"], contains_arch_path)
3104 self.assertIn("APPEND", section)
3105
3106=== modified file 'src/provisioningserver/boot/tests/test_tftppath.py'
3107--- src/provisioningserver/boot/tests/test_tftppath.py 2014-04-03 09:26:31 +0000
3108+++ src/provisioningserver/boot/tests/test_tftppath.py 2014-04-24 17:04:47 +0000
3109@@ -30,6 +30,10 @@
3110 list_subdirs,
3111 locate_tftp_path,
3112 )
3113+from provisioningserver.osystems import (
3114+ OperatingSystem,
3115+ OperatingSystemRegistry,
3116+ )
3117 from provisioningserver.testing.boot_images import (
3118 make_boot_image_storage_params,
3119 )
3120@@ -51,6 +55,63 @@
3121 return image
3122
3123
3124+def make_osystem(osystem, purpose, testcase):
3125+ """Makes the operating system class and registers it."""
3126+ if osystem not in OperatingSystemRegistry:
3127+
3128+ class FakeOS(OperatingSystem):
3129+
3130+ name = osystem
3131+ title = osystem
3132+
3133+ def __init__(self, purpose, releases=None):
3134+ self.purpose = purpose
3135+ if releases is None:
3136+ self.fake_list = [
3137+ factory.getRandomString()
3138+ for _ in range(3)
3139+ ]
3140+ else:
3141+ self.fake_list = releases
3142+
3143+ def get_boot_image_purposes(self, *args):
3144+ return self.purpose
3145+
3146+ def get_supported_releases(self):
3147+ return self.fake_list
3148+
3149+ def get_default_release(self):
3150+ return self.fake_list[0]
3151+
3152+ def format_release_choices(self, releases):
3153+ return [
3154+ (release, release)
3155+ for release in releases
3156+ if release in self.fake_list
3157+ ]
3158+
3159+ fake = FakeOS(purpose)
3160+ OperatingSystemRegistry.register_item(fake.name, fake)
3161+
3162+ testcase.addCleanup(
3163+ OperatingSystemRegistry.unregister_item, osystem)
3164+
3165+ return fake
3166+
3167+ else:
3168+
3169+ obj = OperatingSystemRegistry[osystem]
3170+ old_func = obj.get_boot_image_purposes
3171+ testcase.patch(obj, 'get_boot_image_purposes').return_value = purpose
3172+
3173+ def reset_func(obj, old_func):
3174+ obj.get_boot_image_purposes = old_func
3175+
3176+ testcase.addCleanup(reset_func, obj, old_func)
3177+
3178+ return obj
3179+
3180+
3181 class TestTFTPPath(MAASTestCase):
3182
3183 def setUp(self):
3184@@ -62,6 +123,7 @@
3185 """Fake a boot image matching `image_params` under `tftproot`."""
3186 image_dir = locate_tftp_path(
3187 compose_image_path(
3188+ osystem=image_params['osystem'],
3189 arch=image_params['architecture'],
3190 subarch=image_params['subarchitecture'],
3191 release=image_params['release'],
3192@@ -72,21 +134,23 @@
3193 factory.make_file(image_dir, 'initrd.gz')
3194
3195 def test_compose_image_path_follows_storage_directory_layout(self):
3196+ osystem = factory.make_name('osystem')
3197 arch = factory.make_name('arch')
3198 subarch = factory.make_name('subarch')
3199 release = factory.make_name('release')
3200 label = factory.make_name('label')
3201 self.assertEqual(
3202- '%s/%s/%s/%s' % (arch, subarch, release, label),
3203- compose_image_path(arch, subarch, release, label))
3204+ '%s/%s/%s/%s/%s' % (osystem, arch, subarch, release, label),
3205+ compose_image_path(osystem, arch, subarch, release, label))
3206
3207 def test_compose_image_path_does_not_include_tftp_root(self):
3208+ osystem = factory.make_name('osystem')
3209 arch = factory.make_name('arch')
3210 subarch = factory.make_name('subarch')
3211 release = factory.make_name('release')
3212 label = factory.make_name('label')
3213 self.assertThat(
3214- compose_image_path(arch, subarch, release, label),
3215+ compose_image_path(osystem, arch, subarch, release, label),
3216 Not(StartsWith(self.tftproot)))
3217
3218 def test_locate_tftp_path_prefixes_tftp_root(self):
3219@@ -120,19 +184,22 @@
3220 params = make_boot_image_storage_params()
3221 self.make_image_dir(params, self.tftproot)
3222 purposes = ['install', 'commissioning', 'xinstall']
3223+ make_osystem(params['osystem'], purposes, self)
3224 self.assertItemsEqual(
3225 [make_image(params, purpose) for purpose in purposes],
3226 list_boot_images(self.tftproot))
3227
3228 def test_list_boot_images_enumerates_boot_images(self):
3229+ purposes = ['install', 'commissioning', 'xinstall']
3230 params = [make_boot_image_storage_params() for counter in range(3)]
3231 for param in params:
3232 self.make_image_dir(param, self.tftproot)
3233+ make_osystem(param['osystem'], purposes, self)
3234 self.assertItemsEqual(
3235 [
3236 make_image(param, purpose)
3237 for param in params
3238- for purpose in ['install', 'commissioning', 'xinstall']
3239+ for purpose in purposes
3240 ],
3241 list_boot_images(self.tftproot))
3242
3243
3244=== modified file 'src/provisioningserver/boot/tests/test_uefi.py'
3245--- src/provisioningserver/boot/tests/test_uefi.py 2014-03-28 19:03:46 +0000
3246+++ src/provisioningserver/boot/tests/test_uefi.py 2014-04-24 17:04:47 +0000
3247@@ -73,7 +73,7 @@
3248 self.assertThat(output, StartsWith("set default=\"0\""))
3249 # The UEFI parameters are all set according to the options.
3250 image_dir = compose_image_path(
3251- arch=params.arch, subarch=params.subarch,
3252+ osystem=params.osystem, arch=params.arch, subarch=params.subarch,
3253 release=params.release, label=params.label)
3254
3255 self.assertThat(
3256
3257=== modified file 'src/provisioningserver/boot/tftppath.py'
3258--- src/provisioningserver/boot/tftppath.py 2014-04-03 16:36:15 +0000
3259+++ src/provisioningserver/boot/tftppath.py 2014-04-24 17:04:47 +0000
3260@@ -25,16 +25,21 @@
3261 from logging import getLogger
3262 import os.path
3263
3264+from provisioningserver.osystems import (
3265+ OperatingSystemError,
3266+ OperatingSystemRegistry,
3267+ )
3268
3269 logger = getLogger(__name__)
3270
3271
3272-def compose_image_path(arch, subarch, release, label):
3273+def compose_image_path(osystem, arch, subarch, release, label):
3274 """Compose the TFTP path for a PXE kernel/initrd directory.
3275
3276 The path returned is relative to the TFTP root, as it would be
3277 identified by clients on the network.
3278
3279+ :param osystem: Operating system.
3280 :param arch: Main machine architecture.
3281 :param subarch: Sub-architecture, or "generic" if there is none.
3282 :param release: Operating system release, e.g. "precise".
3283@@ -43,7 +48,7 @@
3284 kernel and initrd) as exposed over TFTP.
3285 """
3286 # This is a TFTP path, not a local filesystem path, so hard-code the slash.
3287- return '/'.join([arch, subarch, release, label])
3288+ return '/'.join([osystem, arch, subarch, release, label])
3289
3290
3291 def locate_tftp_path(path, tftproot):
3292@@ -113,19 +118,20 @@
3293 def extract_image_params(path):
3294 """Represent a list of TFTP path elements as a list of boot-image dicts.
3295
3296- The path must consist of a full [architecture, subarchitecture, release]
3297- that identify a kind of boot that we may need an image for.
3298+ The path must consist of a full [osystem, architecture, subarchitecture,
3299+ release] that identify a kind of boot that we may need an image for.
3300 """
3301- arch, subarch, release, label = path
3302- # XXX: rvb 2014-03-24: The images import script currently imports all the
3303- # images for the configured selections (where a selection is an
3304- # arch/subarch/series/label combination). When the import script grows the
3305- # ability to import the images for a particular purpose, we need to change
3306- # this code to report what is actually present.
3307- purposes = ['commissioning', 'install', 'xinstall']
3308+ osystem, arch, subarch, release, label = path
3309+ osystem_obj = OperatingSystemRegistry.get_item(osystem, default=None)
3310+ if osystem_obj is None:
3311+ raise OperatingSystemError(
3312+ "Unsupported operating system: %s" % osystem)
3313+
3314+ purposes = osystem_obj.get_boot_image_purposes(
3315+ arch, subarch, release, label)
3316 return [
3317 dict(
3318- architecture=arch, subarchitecture=subarch,
3319+ osystem=osystem, architecture=arch, subarchitecture=subarch,
3320 release=release, label=label, purpose=purpose)
3321 for purpose in purposes
3322 ]
3323@@ -139,9 +145,9 @@
3324 `report_boot_images` API call.
3325 """
3326 # The sub-directories directly under tftproot, if they contain
3327- # images, represent architectures.
3328+ # images, represent operating systems.
3329 try:
3330- potential_archs = list_subdirs(tftproot)
3331+ potential_osystems = list_subdirs(tftproot)
3332 except OSError as exception:
3333 if exception.errno == errno.ENOENT:
3334 # Directory does not exist, so return empty list.
3335@@ -153,12 +159,12 @@
3336
3337 # Starting point for iteration: paths that contain only the
3338 # top-level subdirectory of tftproot, i.e. the architecture name.
3339- paths = [[subdir] for subdir in potential_archs]
3340+ paths = [[subdir] for subdir in potential_osystems]
3341
3342 # Extend paths deeper into the filesystem, through the levels that
3343- # represent sub-architecture, release, and label. Any directory
3344- # that doesn't extend this deep isn't a boot image.
3345- for level in ['subarch', 'release', 'label']:
3346+ # represent architecture, sub-architecture, release, and label.
3347+ # Any directory that doesn't extend this deep isn't a boot image.
3348+ for level in ['arch', 'subarch', 'release', 'label']:
3349 paths = drill_down(tftproot, paths)
3350
3351 # Each path we find this way should be a boot image.
3352
3353=== modified file 'src/provisioningserver/import_images/boot_resources.py'
3354--- src/provisioningserver/import_images/boot_resources.py 2014-04-11 03:00:13 +0000
3355+++ src/provisioningserver/import_images/boot_resources.py 2014-04-24 17:04:47 +0000
3356@@ -80,7 +80,7 @@
3357 return reverse
3358
3359
3360-def tgt_entry(arch, subarch, release, label, image):
3361+def tgt_entry(osystem, arch, subarch, release, label, image):
3362 """Generate tgt target used to commission arch/subarch with release
3363
3364 Tgt target used to commission arch/subarch machine with a specific Ubuntu
3365@@ -94,6 +94,7 @@
3366 use the same inode for different tgt targets (even read-only targets which
3367 looks like a bug to me) without this option enabled.
3368
3369+ :param osystem: Operating System name we generate tgt target for
3370 :param arch: Architecture name we generate tgt target for
3371 :param subarch: Subarchitecture name we generate tgt target for
3372 :param release: Ubuntu release we generate tgt target for
3373@@ -102,7 +103,13 @@
3374 :return Tgt entry which can be written to tgt-admin configuration file
3375 """
3376 prefix = 'iqn.2004-05.com.ubuntu:maas'
3377- target_name = 'ephemeral-%s-%s-%s-%s' % (arch, subarch, release, label)
3378+ target_name = 'ephemeral-%s-%s-%s-%s-%s' % (
3379+ osystem,
3380+ arch,
3381+ subarch,
3382+ release,
3383+ label
3384+ )
3385 entry = dedent("""\
3386 <target {prefix}:{target_name}>
3387 readonly 1
3388@@ -229,17 +236,20 @@
3389 # Use a set to make sure we don't register duplicate entries in tgt.
3390 entries = set()
3391 for item in list_boot_images(snapshot_path):
3392+ osystem = item['osystem']
3393 arch = item['architecture']
3394 subarch = item['subarchitecture']
3395 release = item['release']
3396 label = item['label']
3397- entries.add((arch, subarch, release, label))
3398+ entries.add((osystem, arch, subarch, release, label))
3399 tgt_entries = []
3400- for arch, subarch, release, label in sorted(entries):
3401+ for osystem, arch, subarch, release, label in sorted(entries):
3402 root_image = os.path.join(
3403- snapshot_path, arch, subarch, release, label, 'root-image')
3404+ snapshot_path, osystem, arch, subarch,
3405+ release, label, 'root-image')
3406 if os.path.isfile(root_image):
3407- entry = tgt_entry(arch, subarch, release, label, root_image)
3408+ entry = tgt_entry(
3409+ osystem, arch, subarch, release, label, root_image)
3410 tgt_entries.append(entry)
3411 text = ''.join(tgt_entries)
3412 return text.encode('utf-8')
3413@@ -319,7 +329,8 @@
3414 snapshot_path = compose_snapshot_path(storage)
3415 cache_path = os.path.join(storage, 'cache')
3416 targets_conf = os.path.join(snapshot_path, 'maas.tgt')
3417- writer = RepoWriter(snapshot_path, cache_path, reverse_boot)
3418+ ubuntu_path = os.path.join(snapshot_path, 'ubuntu')
3419+ writer = RepoWriter(ubuntu_path, cache_path, reverse_boot)
3420
3421 for source in sources:
3422 writer.write(source['path'], source['keyring'])
3423
3424=== modified file 'src/provisioningserver/import_images/tests/test_boot_resources.py'
3425--- src/provisioningserver/import_images/tests/test_boot_resources.py 2014-04-11 02:58:32 +0000
3426+++ src/provisioningserver/import_images/tests/test_boot_resources.py 2014-04-24 17:04:47 +0000
3427@@ -101,9 +101,10 @@
3428
3429 def test_generates_one_target(self):
3430 spec = make_image_spec()
3431+ osystem = factory.make_name('osystem')
3432 image = self.make_file()
3433 entry = boot_resources.tgt_entry(
3434- spec.arch, spec.subarch, spec.release, spec.label, image)
3435+ osystem, spec.arch, spec.subarch, spec.release, spec.label, image)
3436 # The entry looks a bit like XML, but isn't well-formed. So don't try
3437 # to parse it as such!
3438 self.assertIn('<target iqn.2004-05.com.ubuntu:maas:', entry)
3439@@ -113,8 +114,9 @@
3440 def test_produces_suitable_output_for_tgt_admin(self):
3441 spec = make_image_spec()
3442 image = self.make_file()
3443+ osystem = factory.make_name('osystem')
3444 entry = boot_resources.tgt_entry(
3445- spec.arch, spec.subarch, spec.release, spec.label, image)
3446+ osystem, spec.arch, spec.subarch, spec.release, spec.label, image)
3447 config = self.make_file(contents=entry)
3448 # Pretend to be root, but without requiring the actual privileges and
3449 # without prompting for a password. In that state, run tgt-admin.
3450@@ -326,7 +328,7 @@
3451 self.assertThat(os.path.join(current, 'maas.meta'), FileExists())
3452 self.assertThat(os.path.join(current, 'maas.tgt'), FileExists())
3453 self.assertThat(
3454- os.path.join(current, arch, subarch, release, label),
3455+ os.path.join(current, 'ubuntu', arch, subarch, release, label),
3456 DirExists())
3457
3458 # Verify the contents of the "meta" file.
3459
3460=== modified file 'src/provisioningserver/kernel_opts.py'
3461--- src/provisioningserver/kernel_opts.py 2014-03-20 10:44:56 +0000
3462+++ src/provisioningserver/kernel_opts.py 2014-04-24 17:04:47 +0000
3463@@ -30,9 +30,10 @@
3464
3465 KernelParametersBase = namedtuple(
3466 "KernelParametersBase", (
3467+ "osystem", # Operating system, e.g. "ubuntu"
3468 "arch", # Machine architecture, e.g. "i386"
3469 "subarch", # Machine subarchitecture, e.g. "generic"
3470- "release", # Ubuntu release, e.g. "precise"
3471+ "release", # OS release, e.g. "precise"
3472 "label", # Image label, e.g. "release"
3473 "purpose", # Boot purpose, e.g. "commissioning"
3474 "hostname", # Machine hostname, e.g. "coleman"
3475@@ -90,9 +91,15 @@
3476 ISCSI_TARGET_NAME_PREFIX = "iqn.2004-05.com.ubuntu:maas"
3477
3478
3479-def get_ephemeral_name(arch, subarch, release, label):
3480+def get_ephemeral_name(osystem, arch, subarch, release, label):
3481 """Return the name of the most recent ephemeral image."""
3482- return "ephemeral-%s-%s-%s-%s" % (arch, subarch, release, label)
3483+ return "ephemeral-%s-%s-%s-%s-%s" % (
3484+ osystem,
3485+ arch,
3486+ subarch,
3487+ release,
3488+ label
3489+ )
3490
3491
3492 def compose_hostname_opts(params):
3493@@ -119,7 +126,8 @@
3494 # These are kernel parameters read by the ephemeral environment.
3495 tname = prefix_target_name(
3496 get_ephemeral_name(
3497- params.arch, params.subarch, params.release, params.label))
3498+ params.osystem, params.arch, params.subarch,
3499+ params.release, params.label))
3500 kernel_params = [
3501 # Read by the open-iscsi initramfs code.
3502 "iscsi_target_name=%s" % tname,
3503
3504=== added directory 'src/provisioningserver/osystems'
3505=== added file 'src/provisioningserver/osystems/__init__.py'
3506--- src/provisioningserver/osystems/__init__.py 1970-01-01 00:00:00 +0000
3507+++ src/provisioningserver/osystems/__init__.py 2014-04-24 17:04:47 +0000
3508@@ -0,0 +1,103 @@
3509+# Copyright 2014 Canonical Ltd. This software is licensed under the
3510+# GNU Affero General Public License version 3 (see the file LICENSE).
3511+
3512+"""Provides the support for Operating Systems."""
3513+
3514+from __future__ import (
3515+ absolute_import,
3516+ print_function,
3517+ unicode_literals,
3518+ )
3519+
3520+str = None
3521+
3522+__metaclass__ = type
3523+__all__ = [
3524+ "OperatingSystem",
3525+ "OperatingSystemRegistry",
3526+ ]
3527+
3528+from abc import (
3529+ ABCMeta,
3530+ abstractmethod,
3531+ abstractproperty,
3532+ )
3533+
3534+from provisioningserver.utils.registry import Registry
3535+
3536+
3537+class BOOT_IMAGE_PURPOSE:
3538+ """The vocabulary of a `BootImage`'s purpose."""
3539+ #: Usable for commissioning
3540+ COMMISSIONING = 'commissioning'
3541+ #: Usable for install
3542+ INSTALL = 'install'
3543+ #: Usable for fast-path install
3544+ XINSTALL = 'xinstall'
3545+
3546+
3547+class OperatingSystemError(Exception):
3548+ """Exception raised for errors from a OperatingSystem."""
3549+
3550+
3551+class OperatingSystem:
3552+ """Skeleton for a operating system."""
3553+
3554+ __metaclass__ = ABCMeta
3555+
3556+ @abstractproperty
3557+ def name(self):
3558+ """Name of the operating system."""
3559+
3560+ @abstractproperty
3561+ def title(self):
3562+ """Title of the operating system."""
3563+
3564+ @abstractmethod
3565+ def get_supported_releases(self):
3566+ """Gets list of supported releases for Ubuntu.
3567+
3568+ :returns: list of supported releases
3569+ """
3570+
3571+ @abstractmethod
3572+ def get_default_release(self):
3573+ """Gets the default release to use when a release is not
3574+ explicit.
3575+
3576+ :returns: default release to use
3577+ """
3578+
3579+ @abstractmethod
3580+ def format_release_choices(self, releases):
3581+ """Formats the release choices that are presented to the user.
3582+
3583+ :param releases: list of installed boot image releases
3584+ :returns: Return Django "choices" list
3585+ """
3586+
3587+ @abstractmethod
3588+ def get_boot_image_purposes(self, arch, subarch, release, label):
3589+ """Returns the supported purposes of a boot image.
3590+
3591+ :param arch: Architecture of boot image.
3592+ :param subarch: Sub-architecture of boot image.
3593+ :param release: Release of boot image.
3594+ :param label: Label of boot image.
3595+ :returns: list of supported purposes
3596+ """
3597+
3598+
3599+class OperatingSystemRegistry(Registry):
3600+ """Registry for operating system classes."""
3601+
3602+
3603+# Import the supported operating systems after defining OperatingSystem.
3604+from provisioningserver.osystems.ubuntu import UbuntuOS
3605+
3606+
3607+builtin_ossytems = [
3608+ UbuntuOS(),
3609+]
3610+for osystem in builtin_ossytems:
3611+ OperatingSystemRegistry.register_item(osystem.name, osystem)
3612
3613=== added directory 'src/provisioningserver/osystems/tests'
3614=== added file 'src/provisioningserver/osystems/tests/__init__.py'
3615=== added file 'src/provisioningserver/osystems/tests/test_ubuntu.py'
3616--- src/provisioningserver/osystems/tests/test_ubuntu.py 1970-01-01 00:00:00 +0000
3617+++ src/provisioningserver/osystems/tests/test_ubuntu.py 2014-04-24 17:04:47 +0000
3618@@ -0,0 +1,31 @@
3619+# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
3620+# GNU Affero General Public License version 3 (see the file LICENSE).
3621+
3622+"""Tests for the UbuntuOS module."""
3623+
3624+from __future__ import (
3625+ absolute_import,
3626+ print_function,
3627+ unicode_literals,
3628+ )
3629+
3630+str = None
3631+
3632+__metaclass__ = type
3633+__all__ = []
3634+
3635+from maastesting.testcase import MAASTestCase
3636+from provisioningserver.osystems.ubuntu import (
3637+ DISTRO_SERIES_CHOICES,
3638+ UbuntuOS,
3639+ )
3640+
3641+
3642+class TestUbuntuOS(MAASTestCase):
3643+
3644+ def test_format_release_choices(self):
3645+ osystem = UbuntuOS()
3646+ releases = osystem.get_supported_releases()
3647+ formatted = osystem.format_release_choices(releases)
3648+ for name, title in formatted:
3649+ self.assertEqual(DISTRO_SERIES_CHOICES[name], title)
3650
3651=== added file 'src/provisioningserver/osystems/ubuntu.py'
3652--- src/provisioningserver/osystems/ubuntu.py 1970-01-01 00:00:00 +0000
3653+++ src/provisioningserver/osystems/ubuntu.py 2014-04-24 17:04:47 +0000
3654@@ -0,0 +1,89 @@
3655+# Copyright 2014 Canonical Ltd. This software is licensed under the
3656+# GNU Affero General Public License version 3 (see the file LICENSE).
3657+
3658+"""Ubuntu Operating System."""
3659+
3660+from __future__ import (
3661+ absolute_import,
3662+ print_function,
3663+ unicode_literals,
3664+ )
3665+
3666+str = None
3667+
3668+__metaclass__ = type
3669+__all__ = [
3670+ "UbuntuOS",
3671+ ]
3672+
3673+from provisioningserver.osystems import (
3674+ BOOT_IMAGE_PURPOSE,
3675+ OperatingSystem,
3676+ )
3677+
3678+
3679+DISTRO_SERIES_CHOICES = {
3680+ 'precise': 'Ubuntu 12.04 LTS "Precise Pangolin"',
3681+ 'quantal': 'Ubuntu 12.10 "Quantal Quetzal"',
3682+ 'raring': 'Ubuntu 13.04 "Raring Ringtail"',
3683+ 'saucy': 'Ubuntu 13.10 "Saucy Salamander"',
3684+ 'trusty': 'Ubuntu 14.04 LTS "Trusty Tahr"',
3685+}
3686+
3687+COMMISIONING_DISTRO_SERIES = [
3688+ 'trusty',
3689+]
3690+
3691+DISTRO_SERIES_DEFAULT = 'trusty'
3692+COMMISIONING_DISTRO_SERIES_DEFAULT = 'trusty'
3693+
3694+
3695+class UbuntuOS(OperatingSystem):
3696+ """Ubuntu operating system."""
3697+
3698+ name = "ubuntu"
3699+ title = "Ubuntu"
3700+
3701+ def get_boot_image_purposes(self, arch, subarch, release, label):
3702+ """Gets the purpose of each boot image."""
3703+ return [
3704+ BOOT_IMAGE_PURPOSE.COMMISSIONING,
3705+ BOOT_IMAGE_PURPOSE.INSTALL,
3706+ BOOT_IMAGE_PURPOSE.XINSTALL
3707+ ]
3708+
3709+ def get_supported_releases(self):
3710+ """Gets list of supported releases for Ubuntu."""
3711+ # To make this data better, could pull this information from
3712+ # simplestreams. So only need to update simplestreams for a
3713+ # new release.
3714+ return DISTRO_SERIES_CHOICES.keys()
3715+
3716+ def get_default_release(self):
3717+ """Gets the default release to use when a release is not
3718+ explicit."""
3719+ return DISTRO_SERIES_DEFAULT
3720+
3721+ def get_supported_commissioning_releases(self):
3722+ """Gets the supported commissioning releases for Ubuntu. This
3723+ only exists on Ubuntu, because that is the only operating
3724+ system that supports commissioning.
3725+ """
3726+ return COMMISIONING_DISTRO_SERIES
3727+
3728+ def get_default_commissioning_release(self):
3729+ """Gets the default commissioning release for Ubuntu. This only exists
3730+ on Ubuntu, because that is the only operating system that supports
3731+ commissioning.
3732+ """
3733+ return COMMISIONING_DISTRO_SERIES_DEFAULT
3734+
3735+ def format_release_choices(self, releases):
3736+ """Formats the release choices that are presented to the user."""
3737+ choices = []
3738+ releases = sorted(releases, reverse=True)
3739+ for release in releases:
3740+ title = DISTRO_SERIES_CHOICES.get(release)
3741+ if title is not None:
3742+ choices.append((release, title))
3743+ return choices
3744
3745=== modified file 'src/provisioningserver/rpc/cluster.py'
3746--- src/provisioningserver/rpc/cluster.py 2014-03-20 22:36:32 +0000
3747+++ src/provisioningserver/rpc/cluster.py 2014-04-24 17:04:47 +0000
3748@@ -33,7 +33,8 @@
3749 arguments = []
3750 response = [
3751 (b"images", amp.AmpList(
3752- [(b"architecture", amp.Unicode()),
3753+ [(b"osystem", amp.Unicode()),
3754+ (b"architecture", amp.Unicode()),
3755 (b"subarchitecture", amp.Unicode()),
3756 (b"release", amp.Unicode()),
3757 (b"label", amp.Unicode()),
3758
3759=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
3760--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-03-28 04:31:32 +0000
3761+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-04-24 17:04:47 +0000
3762@@ -34,6 +34,7 @@
3763 sentinel,
3764 )
3765 from provisioningserver.boot import tftppath
3766+from provisioningserver.boot.tests.test_tftppath import make_osystem
3767 from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS
3768 from provisioningserver.rpc import (
3769 cluster,
3770@@ -171,6 +172,7 @@
3771 # serialised correctly.
3772
3773 # Example boot image definitions.
3774+ osystems = "ubuntu", "centos"
3775 archs = "i386", "amd64"
3776 subarchs = "generic", "special"
3777 releases = "precise", "trusty"
3778@@ -179,22 +181,24 @@
3779
3780 # Create a TFTP file tree with a variety of subdirectories.
3781 tftpdir = self.make_dir()
3782- for options in product(archs, subarchs, releases, labels):
3783+ for options in product(osystems, archs, subarchs, releases, labels):
3784 os.makedirs(os.path.join(tftpdir, *options))
3785+ make_osystem(options[0], purposes, self)
3786
3787 # Ensure that list_boot_images() uses the above TFTP file tree.
3788 self.useFixture(set_tftp_root(tftpdir))
3789
3790 expected_images = [
3791 {
3792+ "osystem": osystem,
3793 "architecture": arch,
3794 "subarchitecture": subarch,
3795 "release": release,
3796 "label": label,
3797 "purpose": purpose,
3798 }
3799- for arch, subarch, release, label, purpose in product(
3800- archs, subarchs, releases, labels, purposes)
3801+ for osystem, arch, subarch, release, label, purpose in product(
3802+ osystems, archs, subarchs, releases, labels, purposes)
3803 ]
3804
3805 response = yield call_responder(Cluster(), cluster.ListBootImages, {})
3806
3807=== modified file 'src/provisioningserver/testing/boot_images.py'
3808--- src/provisioningserver/testing/boot_images.py 2014-03-21 03:21:57 +0000
3809+++ src/provisioningserver/testing/boot_images.py 2014-04-24 17:04:47 +0000
3810@@ -23,10 +23,11 @@
3811 """Create an arbitrary dict of boot-image parameters.
3812
3813 These are the parameters that together describe a kind of boot for
3814- which we may need a kernel and initrd: architecture,
3815+ which we may need a kernel and initrd: operating system, architecture,
3816 sub-architecture, Ubuntu release, boot purpose, and release label.
3817 """
3818 return dict(
3819+ osystem=factory.make_name('osystem'),
3820 architecture=factory.make_name('architecture'),
3821 subarchitecture=factory.make_name('subarchitecture'),
3822 release=factory.make_name('release'),
3823@@ -39,9 +40,11 @@
3824 """Create a dict of boot-image parameters as used to store the image.
3825
3826 These are the parameters that together describe a path to store a boot
3827- image: architecture, sub-architecture, Ubuntu release, and release label.
3828+ image: operating system, architecture, sub-architecture, Ubuntu release,
3829+ and release label.
3830 """
3831 return dict(
3832+ osystem=factory.make_name('osystem'),
3833 architecture=factory.make_name('architecture'),
3834 subarchitecture=factory.make_name('subarchitecture'),
3835 release=factory.make_name('release'),
3836
3837=== modified file 'src/provisioningserver/tests/test_kernel_opts.py'
3838--- src/provisioningserver/tests/test_kernel_opts.py 2014-03-28 16:46:55 +0000
3839+++ src/provisioningserver/tests/test_kernel_opts.py 2014-04-24 17:04:47 +0000
3840@@ -228,7 +228,8 @@
3841 # options for a "xinstall" node.
3842 params = self.make_kernel_parameters(purpose="xinstall")
3843 ephemeral_name = get_ephemeral_name(
3844- params.arch, params.subarch, params.release, params.label)
3845+ params.osystem, params.arch, params.subarch,
3846+ params.release, params.label)
3847 self.assertThat(
3848 compose_kernel_command_line(params),
3849 ContainsAll([
3850@@ -243,7 +244,8 @@
3851 # options for a "commissioning" node.
3852 params = self.make_kernel_parameters(purpose="commissioning")
3853 ephemeral_name = get_ephemeral_name(
3854- params.arch, params.subarch, params.release, params.label)
3855+ params.osystem, params.arch, params.subarch,
3856+ params.release, params.label)
3857 self.assertThat(
3858 compose_kernel_command_line(params),
3859 ContainsAll([