Merge lp:~blake-rouse/maas/osystem-preseed-cleanup into lp:~maas-committers/maas/trunk
- osystem-preseed-cleanup
- Merge into 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 | ||||
Related bugs: |
|
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 OperatingSystem
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([ |