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

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

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

Commit message

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

Description of the change

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

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

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

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2014-04-24 14:57:25 +0000
+++ src/maasserver/api.py 2014-04-24 17:04:47 +0000
@@ -246,6 +246,10 @@
246from piston.handler import typemapper246from piston.handler import typemapper
247from piston.utils import rc247from piston.utils import rc
248from provisioningserver.kernel_opts import KernelParameters248from provisioningserver.kernel_opts import KernelParameters
249from provisioningserver.osystems import (
250 BOOT_IMAGE_PURPOSE,
251 OperatingSystemRegistry,
252 )
249from provisioningserver.power_schema import UNKNOWN_POWER_TYPE253from provisioningserver.power_schema import UNKNOWN_POWER_TYPE
250import simplejson as json254import simplejson as json
251255
@@ -402,8 +406,11 @@
402 :param user_data: If present, this blob of user-data to be made406 :param user_data: If present, this blob of user-data to be made
403 available to the nodes through the metadata service.407 available to the nodes through the metadata service.
404 :type user_data: base64-encoded unicode408 :type user_data: base64-encoded unicode
409 :param osystem: If present, this parameter specifies the
410 operating system the node will use.
411 :type osystem: unicode
405 :param distro_series: If present, this parameter specifies the412 :param distro_series: If present, this parameter specifies the
406 Ubuntu Release the node will use.413 os relase the node will use.
407 :type distro_series: unicode414 :type distro_series: unicode
408415
409 Ideally we'd have MIME multipart and content-transfer-encoding etc.416 Ideally we'd have MIME multipart and content-transfer-encoding etc.
@@ -412,14 +419,25 @@
412 encoding instead.419 encoding instead.
413 """420 """
414 user_data = request.POST.get('user_data', None)421 user_data = request.POST.get('user_data', None)
422 osystem = request.POST.get('os', request.POST.get('osystem', None))
415 series = request.POST.get('distro_series', None)423 series = request.POST.get('distro_series', None)
416 if user_data is not None:424 if user_data is not None:
417 user_data = b64decode(user_data)425 user_data = b64decode(user_data)
418 if series is not None:426
427 if osystem is not None or series is not None:
428 # If series is set and not osystem this means that we
429 # should use the default operating system
430 if osystem is None and series is not None:
431 osystem = Config.objects.get_config('default_osystem')
432 # If osystem is set and not series, then we need to set
433 # the series to be default for the osystem.
434 elif osystem is not None and series is None:
435 series = ''
436
419 node = Node.objects.get_node_or_404(437 node = Node.objects.get_node_or_404(
420 system_id=system_id, user=request.user,438 system_id=system_id, user=request.user,
421 perm=NODE_PERMISSION.EDIT)439 perm=NODE_PERMISSION.EDIT)
422 node.set_distro_series(series=series)440 node.set_osystem_and_distro_series(osystem, series)
423 nodes = Node.objects.start_nodes(441 nodes = Node.objects.start_nodes(
424 [system_id], request.user, user_data=user_data)442 [system_id], request.user, user_data=user_data)
425 if len(nodes) == 0:443 if len(nodes) == 0:
@@ -432,7 +450,7 @@
432 """Release a node. Opposite of `NodesHandler.acquire`."""450 """Release a node. Opposite of `NodesHandler.acquire`."""
433 node = Node.objects.get_node_or_404(451 node = Node.objects.get_node_or_404(
434 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)452 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
435 node.set_distro_series(series='')453 node.set_osystem_and_distro_series(osystem='', series='')
436 if node.status == NODE_STATUS.READY:454 if node.status == NODE_STATUS.READY:
437 # Nothing to do. This may be a redundant retry, and the455 # Nothing to do. This may be a redundant retry, and the
438 # postcondition is achieved, so call this success.456 # postcondition is achieved, so call this success.
@@ -2321,7 +2339,7 @@
2321 context_instance=RequestContext(request))2339 context_instance=RequestContext(request))
23222340
23232341
2324def get_boot_purpose(node):2342def get_boot_purpose(node, osystem, arch, subarch, series, label):
2325 """Return a suitable "purpose" for this boot, e.g. "install"."""2343 """Return a suitable "purpose" for this boot, e.g. "install"."""
2326 # XXX: allenap bug=1031406 2012-07-31: The boot purpose is still in2344 # XXX: allenap bug=1031406 2012-07-31: The boot purpose is still in
2327 # flux. It may be that there will just be an "ephemeral" environment and2345 # flux. It may be that there will just be an "ephemeral" environment and
@@ -2341,7 +2359,15 @@
2341 if node.should_use_traditional_installer():2359 if node.should_use_traditional_installer():
2342 return "install"2360 return "install"
2343 else:2361 else:
2344 return "xinstall"2362 # Check that the booting operating system, actually supports
2363 # fast-path installation. If it does not then, we need to
2364 # return normal install.
2365 osystem_obj = OperatingSystemRegistry[osystem]
2366 purposes = osystem_obj.get_boot_image_purposes(
2367 arch, subarch, series, label)
2368 if BOOT_IMAGE_PURPOSE.XINSTALL in purposes:
2369 return "xinstall"
2370 return "install"
2345 else:2371 else:
2346 return "local" # TODO: Investigate.2372 return "local" # TODO: Investigate.
2347 else:2373 else:
@@ -2409,12 +2435,12 @@
2409 node = get_node_from_mac_string(request.GET.get('mac', None))2435 node = get_node_from_mac_string(request.GET.get('mac', None))
24102436
2411 if node is None or node.status == NODE_STATUS.COMMISSIONING:2437 if node is None or node.status == NODE_STATUS.COMMISSIONING:
2438 osystem = Config.objects.get_config('commissioning_osystem')
2412 series = Config.objects.get_config('commissioning_distro_series')2439 series = Config.objects.get_config('commissioning_distro_series')
2413 else:2440 else:
2441 osystem = node.get_osystem()
2414 series = node.get_distro_series()2442 series = node.get_distro_series()
24152443
2416 purpose = get_boot_purpose(node)
2417
2418 if node:2444 if node:
2419 arch, subarch = node.architecture.split('/')2445 arch, subarch = node.architecture.split('/')
2420 preseed_url = compose_preseed_url(node)2446 preseed_url = compose_preseed_url(node)
@@ -2440,7 +2466,7 @@
2440 # current series. If nothing is found, fall back to i386 like2466 # current series. If nothing is found, fall back to i386 like
2441 # we used to. LP #11813342467 # we used to. LP #1181334
2442 image = BootImage.objects.get_default_arch_image_in_nodegroup(2468 image = BootImage.objects.get_default_arch_image_in_nodegroup(
2443 nodegroup, series, purpose=purpose)2469 nodegroup, osystem, series, purpose='commissioning')
2444 if image is None:2470 if image is None:
2445 arch = 'i386'2471 arch = 'i386'
2446 else:2472 else:
@@ -2448,12 +2474,17 @@
24482474
2449 subarch = get_optional_param(request.GET, 'subarch', 'generic')2475 subarch = get_optional_param(request.GET, 'subarch', 'generic')
24502476
2477 # Get the purpose, checking that if node is using xinstall, the operating
2478 # system actaully supports that mode. We pass None for the label, here
2479 # because it is unknown which label will be used yet.
2480 purpose = get_boot_purpose(node, osystem, arch, subarch, series, None)
2481
2451 # We use as our default label the label of the most recent image for2482 # We use as our default label the label of the most recent image for
2452 # the criteria we've assembled above. If there is no latest image2483 # the criteria we've assembled above. If there is no latest image
2453 # (which should never happen in reality but may happen in tests), we2484 # (which should never happen in reality but may happen in tests), we
2454 # fall back to using 'no-such-image' as our default.2485 # fall back to using 'no-such-image' as our default.
2455 latest_image = BootImage.objects.get_latest_image(2486 latest_image = BootImage.objects.get_latest_image(
2456 nodegroup, arch, subarch, series, purpose)2487 nodegroup, osystem, arch, subarch, series, purpose)
2457 if latest_image is None:2488 if latest_image is None:
2458 # XXX 2014-03-18 gmb bug=1294131:2489 # XXX 2014-03-18 gmb bug=1294131:
2459 # We really ought to raise an exception here so that client2490 # We really ought to raise an exception here so that client
@@ -2465,6 +2496,10 @@
2465 latest_label = latest_image.label2496 latest_label = latest_image.label
2466 label = get_optional_param(request.GET, 'label', latest_label)2497 label = get_optional_param(request.GET, 'label', latest_label)
24672498
2499 # Now that we have the correct label, lets check the boot purpose again
2500 # to make sure that the boot image with that label, is the correct purpose.
2501 purpose = get_boot_purpose(node, osystem, arch, subarch, series, label)
2502
2468 if node is not None:2503 if node is not None:
2469 # We don't care if the kernel opts is from the global setting or a tag,2504 # We don't care if the kernel opts is from the global setting or a tag,
2470 # just get the options2505 # just get the options
@@ -2494,8 +2529,8 @@
2494 cluster_address = get_mandatory_param(request.GET, "local")2529 cluster_address = get_mandatory_param(request.GET, "local")
24952530
2496 params = KernelParameters(2531 params = KernelParameters(
2497 arch=arch, subarch=subarch, release=series, label=label,2532 osystem=osystem, arch=arch, subarch=subarch, release=series,
2498 purpose=purpose, hostname=hostname, domain=domain,2533 label=label, purpose=purpose, hostname=hostname, domain=domain,
2499 preseed_url=preseed_url, log_host=server_address,2534 preseed_url=preseed_url, log_host=server_address,
2500 fs_host=cluster_address, extra_opts=extra_kernel_opts)2535 fs_host=cluster_address, extra_opts=extra_kernel_opts)
25012536
@@ -2536,10 +2571,11 @@
2536 This function has a counterpart, `summarise_boot_image_dict`. The two2571 This function has a counterpart, `summarise_boot_image_dict`. The two
2537 return the same value for the same boot image.2572 return the same value for the same boot image.
25382573
2539 :return: A tuple of the image's architecture, subarchitecture, release,2574 :return: A tuple of the image's osystem, architecture, subarchitecture,
2540 label, and purpose.2575 release, label, and purpose.
2541 """2576 """
2542 return (2577 return (
2578 image_object.osystem,
2543 image_object.architecture,2579 image_object.architecture,
2544 image_object.subarchitecture,2580 image_object.subarchitecture,
2545 image_object.release,2581 image_object.release,
@@ -2554,10 +2590,11 @@
2554 This is the counterpart to `summarise_boot_image_object`. The two return2590 This is the counterpart to `summarise_boot_image_object`. The two return
2555 the same value for the same boot image.2591 the same value for the same boot image.
25562592
2557 :return: A tuple of the image's architecture, subarchitecture, release,2593 :return: A tuple of the image's osystem, architecture, subarchitecture,
2558 label, and purpose.2594 release, label, and purpose.
2559 """2595 """
2560 return (2596 return (
2597 image_dict['osystem'],
2561 image_dict['architecture'],2598 image_dict['architecture'],
2562 image_dict.get('subarchitecture', 'generic'),2599 image_dict.get('subarchitecture', 'generic'),
2563 image_dict['release'],2600 image_dict['release'],
@@ -2600,10 +2637,11 @@
2600 `summarise_stored_images`.2637 `summarise_stored_images`.
2601 """2638 """
2602 new_images = reported_images - stored_images2639 new_images = reported_images - stored_images
2603 for arch, subarch, release, label, purpose in new_images:2640 for osystem, arch, subarch, release, label, purpose in new_images:
2604 BootImage.objects.register_image(2641 BootImage.objects.register_image(
2605 nodegroup=nodegroup, architecture=arch, subarchitecture=subarch,2642 nodegroup=nodegroup, osystem=osystem, architecture=arch,
2606 release=release, purpose=purpose, label=label)2643 subarchitecture=subarch, release=release, purpose=purpose,
2644 label=label)
26072645
26082646
2609def prune_boot_images(nodegroup, reported_images, stored_images):2647def prune_boot_images(nodegroup, reported_images, stored_images):
@@ -2620,15 +2658,16 @@
2620 `summarise_stored_images`.2658 `summarise_stored_images`.
2621 """2659 """
2622 removed_images = stored_images - reported_images2660 removed_images = stored_images - reported_images
2623 for arch, subarch, release, label, purpose in removed_images:2661 for osystem, arch, subarch, release, label, purpose in removed_images:
2624 db_images = BootImage.objects.filter(2662 db_images = BootImage.objects.filter(
2625 architecture=arch, subarchitecture=subarch,2663 osystem=osystem, architecture=arch, subarchitecture=subarch,
2626 release=release, label=label, purpose=purpose)2664 release=release, label=label, purpose=purpose)
2627 db_images.delete()2665 db_images.delete()
26282666
26292667
2630DISPLAYED_BOOTIMAGE_FIELDS = (2668DISPLAYED_BOOTIMAGE_FIELDS = (
2631 'id',2669 'id',
2670 'osystem',
2632 'release',2671 'release',
2633 'architecture',2672 'architecture',
2634 'subarchitecture',2673 'subarchitecture',
@@ -2694,10 +2733,10 @@
2694 :param uuid: The UUID of the cluster for which the images are2733 :param uuid: The UUID of the cluster for which the images are
2695 being reported.2734 being reported.
2696 :param images: A list of dicts, each describing a boot image with2735 :param images: A list of dicts, each describing a boot image with
2697 these properties: `architecture`, `subarchitecture`, `release`,2736 these properties: `os`, `architecture`, `subarchitecture`,
2698 `purpose`, and optionally, `label` (which defaults to "release").2737 `release`, `purpose`, and optionally, `label` (which defaults
2699 These should match the code that determines TFTP paths for these2738 to "release"). These should match the code that determines TFTP
2700 images.2739 paths for these images.
2701 """2740 """
2702 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)2741 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
2703 check_nodegroup_access(request, nodegroup)2742 check_nodegroup_access(request, nodegroup)
27042743
=== modified file 'src/maasserver/compose_preseed.py'
--- src/maasserver/compose_preseed.py 2013-10-18 09:54:17 +0000
+++ src/maasserver/compose_preseed.py 2014-04-24 17:04:47 +0000
@@ -20,6 +20,7 @@
2020
21from maasserver.enum import NODE_STATUS21from maasserver.enum import NODE_STATUS
22from maasserver.utils import absolute_reverse22from maasserver.utils import absolute_reverse
23from provisioningserver.osystems import OperatingSystemRegistry
23import yaml24import yaml
2425
2526
@@ -104,6 +105,12 @@
104 if node.status == NODE_STATUS.COMMISSIONING:105 if node.status == NODE_STATUS.COMMISSIONING:
105 return compose_commissioning_preseed(token, base_url)106 return compose_commissioning_preseed(token, base_url)
106 else:107 else:
108 # Operating system might support a custom preseed generation
109 osystem = OperatingSystemRegistry.get_item(node.get_osystem())
110 if osystem is not None and hasattr(osystem, 'compose_preseed'):
111 metadata_url = absolute_reverse('metadata', base_url=base_url)
112 return osystem.compose_preseed(node, token, metadata_url)
113
107 if node.should_use_traditional_installer():114 if node.should_use_traditional_installer():
108 return compose_cloud_init_preseed(token, base_url)115 return compose_cloud_init_preseed(token, base_url)
109 else:116 else:
110117
=== modified file 'src/maasserver/context_processors.py'
--- src/maasserver/context_processors.py 2014-04-01 06:54:45 +0000
+++ src/maasserver/context_processors.py 2014-04-24 17:04:47 +0000
@@ -63,6 +63,7 @@
63 'js/node_views.js',63 'js/node_views.js',
64 'js/longpoll.js',64 'js/longpoll.js',
65 'js/enums.js',65 'js/enums.js',
66 'js/os_distro_select.js',
66 'js/power_parameters.js',67 'js/power_parameters.js',
67 'js/nodes_chart.js',68 'js/nodes_chart.js',
68 'js/reveal.js',69 'js/reveal.js',
6970
=== modified file 'src/maasserver/enum.py'
--- src/maasserver/enum.py 2014-03-28 16:36:32 +0000
+++ src/maasserver/enum.py 2014-04-24 17:04:47 +0000
@@ -86,36 +86,6 @@
86NODE_STATUS_CHOICES_DICT = OrderedDict(NODE_STATUS_CHOICES)86NODE_STATUS_CHOICES_DICT = OrderedDict(NODE_STATUS_CHOICES)
8787
8888
89class DISTRO_SERIES:
90 """List of supported ubuntu releases."""
91 #:
92 default = ''
93 #:
94 precise = 'precise'
95 #:
96 quantal = 'quantal'
97 #:
98 raring = 'raring'
99 #:
100 saucy = 'saucy'
101 #:
102 trusty = 'trusty'
103
104DISTRO_SERIES_CHOICES = (
105 (DISTRO_SERIES.default, 'Default Ubuntu Release'),
106 (DISTRO_SERIES.precise, 'Ubuntu 12.04 LTS "Precise Pangolin"'),
107 (DISTRO_SERIES.quantal, 'Ubuntu 12.10 "Quantal Quetzal"'),
108 (DISTRO_SERIES.raring, 'Ubuntu 13.04 "Raring Ringtail"'),
109 (DISTRO_SERIES.saucy, 'Ubuntu 13.10 "Saucy Salamander"'),
110 (DISTRO_SERIES.trusty, 'Ubuntu 14.04 LTS "Trusty Tahr"'),
111)
112
113
114COMMISSIONING_DISTRO_SERIES_CHOICES = (
115 (DISTRO_SERIES.trusty, dict(DISTRO_SERIES_CHOICES)[DISTRO_SERIES.trusty]),
116)
117
118
119class NODE_PERMISSION:89class NODE_PERMISSION:
120 """Permissions relating to nodes."""90 """Permissions relating to nodes."""
121 VIEW = 'view_node'91 VIEW = 'view_node'
12292
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2014-04-22 09:08:02 +0000
+++ src/maasserver/forms.py 2014-04-24 17:04:47 +0000
@@ -71,9 +71,6 @@
71 )71 )
72from maasserver.config_forms import SKIP_CHECK_NAME72from maasserver.config_forms import SKIP_CHECK_NAME
73from maasserver.enum import (73from maasserver.enum import (
74 COMMISSIONING_DISTRO_SERIES_CHOICES,
75 DISTRO_SERIES,
76 DISTRO_SERIES_CHOICES,
77 NODE_STATUS,74 NODE_STATUS,
78 NODEGROUPINTERFACE_MANAGEMENT,75 NODEGROUPINTERFACE_MANAGEMENT,
79 NODEGROUPINTERFACE_MANAGEMENT_CHOICES,76 NODEGROUPINTERFACE_MANAGEMENT_CHOICES,
@@ -86,7 +83,7 @@
86from maasserver.forms_settings import (83from maasserver.forms_settings import (
87 CONFIG_ITEMS_KEYS,84 CONFIG_ITEMS_KEYS,
88 get_config_field,85 get_config_field,
89 INVALID_DISTRO_SERIES_MESSAGE,86 list_commisioning_choices,
90 INVALID_SETTING_MSG_TEMPLATE,87 INVALID_SETTING_MSG_TEMPLATE,
91 )88 )
92from maasserver.models import (89from maasserver.models import (
@@ -113,6 +110,7 @@
113from maasserver.utils.network import make_network110from maasserver.utils.network import make_network
114from metadataserver.fields import Bin111from metadataserver.fields import Bin
115from metadataserver.models import CommissioningScript112from metadataserver.models import CommissioningScript
113from provisioningserver.osystems import OperatingSystemRegistry
116114
117# A reusable null-option for choice fields.115# A reusable null-option for choice fields.
118BLANK_CHOICE = ('', '-------')116BLANK_CHOICE = ('', '-------')
@@ -202,6 +200,81 @@
202 return all_architectures[0]200 return all_architectures[0]
203201
204202
203def list_all_usable_osystems():
204 """Return all operating systems that can be used for nodes.
205
206 These are the operating systems for which any nodegroup has the boot images
207 required to boot the node.
208 """
209 # The Node edit form offers all usable operating systems as options for the
210 # osystem field. Not all of these may be available in the node's
211 # nodegroup, but to represent that accurately in the UI would depend on
212 # the currently selected nodegroup. Narrowing the options down further
213 # would have to happen browser-side.
214 osystems = set()
215 for nodegroup in NodeGroup.objects.all():
216 osystems = osystems.union(
217 BootImage.objects.get_usable_osystems(nodegroup))
218 osystems = [OperatingSystemRegistry[osystem] for osystem in osystems]
219 return sorted(osystems, key=lambda osystem: osystem.title)
220
221
222def list_osystem_choices(osystems):
223 """Return Django "choices" list for `osystem`."""
224 choices = [('', 'Default OS')]
225 choices += [
226 (osystem.name, osystem.title)
227 for osystem in osystems
228 ]
229 return choices
230
231
232def list_all_usable_releases(osystems):
233 """Return dictionary of usable `releases` for each opertaing system."""
234 distro_series = {}
235 nodegroups = list(NodeGroup.objects.all())
236 for osystem in osystems:
237 releases = set()
238 for nodegroup in nodegroups:
239 releases = releases.union(
240 BootImage.objects.get_usable_releases(nodegroup, osystem.name))
241 distro_series[osystem.name] = sorted(releases)
242 return distro_series
243
244
245def list_release_choices(releases):
246 """Return Django "choices" list for `releases`."""
247 choices = [('', 'Default OS Release')]
248 for key, value in releases.items():
249 osystem = OperatingSystemRegistry[key]
250 options = osystem.format_release_choices(value)
251 choices += [(
252 '%s/' % osystem.name,
253 'Latest %s Release' % osystem.title
254 )]
255 choices += [
256 ('%s/%s' % (osystem.name, name), title)
257 for name, title in options
258 ]
259 choices += options
260 return choices
261
262
263def clean_distro_series_field(form, field, os_field):
264 # distro_series field can be supplied the value os/release, that is the
265 # way the web UI provides the value.
266 new_distro_series = form.cleaned_data.get(field)
267 if new_distro_series is not None and '/' in new_distro_series:
268 os, release = new_distro_series.split('/', 1)
269 if 'os' in form.cleaned_data:
270 new_os = form.cleaned_data[os_field]
271 if os != new_os:
272 raise ValidationError(
273 "%s option does not match osystem option." % field)
274 return release
275 return new_distro_series
276
277
205class NodeForm(ModelForm):278class NodeForm(ModelForm):
206279
207 def __init__(self, request=None, *args, **kwargs):280 def __init__(self, request=None, *args, **kwargs):
@@ -214,6 +287,7 @@
214 self.fields['nodegroup'] = NodeGroupFormField(287 self.fields['nodegroup'] = NodeGroupFormField(
215 required=False, empty_label="Default (master)")288 required=False, empty_label="Default (master)")
216 self.set_up_architecture_field()289 self.set_up_architecture_field()
290 self.set_up_osystem_and_distro_series_fields()
217291
218 def set_up_architecture_field(self):292 def set_up_architecture_field(self):
219 """Create the `architecture` field.293 """Create the `architecture` field.
@@ -233,6 +307,27 @@
233 choices=choices, required=True, initial=default_arch,307 choices=choices, required=True, initial=default_arch,
234 error_messages={'invalid_choice': invalid_arch_message})308 error_messages={'invalid_choice': invalid_arch_message})
235309
310 def set_up_osystem_and_distro_series_fields(self):
311 """Create the `osystem` and `distro_series` fields.
312
313 This needs to be done on the fly so that we can pass a dynamic list of
314 usable operating systems and distro_series.
315 """
316 osystems = list_all_usable_osystems()
317 releases = list_all_usable_releases(osystems)
318 choices = list_osystem_choices(osystems)
319 distro_choices = list_release_choices(releases)
320 invalid_osystem_message = compose_invalid_choice_text(
321 'osystem', choices)
322 invalid_distro_series_message = compose_invalid_choice_text(
323 'distro_series', distro_choices)
324 self.fields['osystem'] = forms.ChoiceField(
325 choices=choices, required=False, initial='',
326 error_messages={'invalid_choice': invalid_osystem_message})
327 self.fields['distro_series'] = forms.ChoiceField(
328 choices=distro_choices, required=False, initial='',
329 error_messages={'invalid_choice': invalid_distro_series_message})
330
236 def clean_hostname(self):331 def clean_hostname(self):
237 # Don't allow the hostname to be changed if the node is332 # Don't allow the hostname to be changed if the node is
238 # currently allocated. Juju knows the node by its old name, so333 # currently allocated. Juju knows the node by its old name, so
@@ -246,6 +341,9 @@
246341
247 return new_hostname342 return new_hostname
248343
344 def clean_distro_series(self):
345 return clean_distro_series_field(self, 'distro_series', 'osystem')
346
249 def is_valid(self):347 def is_valid(self):
250 is_valid = super(NodeForm, self).is_valid()348 is_valid = super(NodeForm, self).is_valid()
251 if len(list_all_usable_architectures()) == 0:349 if len(list_all_usable_architectures()) == 0:
@@ -254,12 +352,6 @@
254 is_valid = False352 is_valid = False
255 return is_valid353 return is_valid
256354
257 distro_series = forms.ChoiceField(
258 choices=DISTRO_SERIES_CHOICES, required=False,
259 initial=DISTRO_SERIES.default,
260 label="Release",
261 error_messages={'invalid_choice': INVALID_DISTRO_SERIES_MESSAGE})
262
263 hostname = forms.CharField(355 hostname = forms.CharField(
264 label="Host name", required=False, help_text=(356 label="Host name", required=False, help_text=(
265 "The FQDN (Fully Qualified Domain Name) is derived from the "357 "The FQDN (Fully Qualified Domain Name) is derived from the "
@@ -277,6 +369,7 @@
277 fields = (369 fields = (
278 'hostname',370 'hostname',
279 'architecture',371 'architecture',
372 'osystem',
280 'distro_series',373 'distro_series',
281 )374 )
282375
@@ -858,16 +951,34 @@
858 """Settings page, Commissioning section."""951 """Settings page, Commissioning section."""
859 check_compatibility = get_config_field('check_compatibility')952 check_compatibility = get_config_field('check_compatibility')
860 commissioning_distro_series = forms.ChoiceField(953 commissioning_distro_series = forms.ChoiceField(
861 choices=COMMISSIONING_DISTRO_SERIES_CHOICES, required=False,954 choices=list_commisioning_choices(), required=False,
862 label="Default distro series used for commissioning",955 label="Default Ubuntu release used for commissioning",
863 error_messages={'invalid_choice': compose_invalid_choice_text(956 error_messages={'invalid_choice': compose_invalid_choice_text(
864 'commissioning_distro_series',957 'commissioning_distro_series',
865 COMMISSIONING_DISTRO_SERIES_CHOICES)})958 list_commisioning_choices())})
959
960
961class DeployForm(ConfigForm):
962 """Settings page, Deploy section."""
963 default_osystem = get_config_field('default_osystem')
964 default_distro_series = get_config_field('default_distro_series')
965
966 def _load_initials(self):
967 super(DeployForm, self)._load_initials()
968 initial_os = self.initial['default_osystem']
969 initial_series = self.initial['default_distro_series']
970 self.initial['default_distro_series'] = '%s/%s' % (
971 initial_os,
972 initial_series
973 )
974
975 def clean_default_distro_series(self):
976 return clean_distro_series_field(
977 self, 'default_distro_series', 'default_osystem')
866978
867979
868class UbuntuForm(ConfigForm):980class UbuntuForm(ConfigForm):
869 """Settings page, Ubuntu section."""981 """Settings page, Ubuntu section."""
870 default_distro_series = get_config_field('default_distro_series')
871 main_archive = get_config_field('main_archive')982 main_archive = get_config_field('main_archive')
872 ports_archive = get_config_field('ports_archive')983 ports_archive = get_config_field('ports_archive')
873984
874985
=== modified file 'src/maasserver/forms_settings.py'
--- src/maasserver/forms_settings.py 2014-04-10 20:21:24 +0000
+++ src/maasserver/forms_settings.py 2014-04-24 17:04:47 +0000
@@ -23,19 +23,39 @@
23from socket import gethostname23from socket import gethostname
2424
25from django import forms25from django import forms
26from maasserver.enum import (26from maasserver.models.config import DEFAULT_OS
27 COMMISSIONING_DISTRO_SERIES_CHOICES,
28 DISTRO_SERIES,
29 DISTRO_SERIES_CHOICES,
30 )
31from maasserver.utils.forms import compose_invalid_choice_text27from maasserver.utils.forms import compose_invalid_choice_text
28from provisioningserver.osystems import OperatingSystemRegistry
3229
3330
34INVALID_URL_MESSAGE = "Enter a valid url (e.g. http://host.example.com)."31INVALID_URL_MESSAGE = "Enter a valid url (e.g. http://host.example.com)."
3532
3633
37INVALID_DISTRO_SERIES_MESSAGE = compose_invalid_choice_text(34def list_osystem_choices():
38 'distro_series', DISTRO_SERIES_CHOICES)35 return [
36 (osystem.name, osystem.title)
37 for _, osystem in OperatingSystemRegistry
38 ]
39
40
41def list_release_choices():
42 osystems = [osystem for _, osystem in OperatingSystemRegistry]
43 choices = []
44 for osystem in osystems:
45 supported = sorted(osystem.get_supported_releases())
46 options = osystem.format_release_choices(supported)
47 options = [
48 ('%s/%s' % (osystem.name, name), title)
49 for name, title in options
50 ]
51 choices += options
52 return choices
53
54
55def list_commisioning_choices():
56 releases = DEFAULT_OS.get_supported_commissioning_releases()
57 options = DEFAULT_OS.format_release_choices(releases)
58 return [(name, title) for name, title in options]
3959
4060
41CONFIG_ITEMS = {61CONFIG_ITEMS = {
@@ -139,28 +159,46 @@
139 "e.g. for ntp.ubuntu.com: '91.189.94.4'")159 "e.g. for ntp.ubuntu.com: '91.189.94.4'")
140 }160 }
141 },161 },
162 'default_osystem': {
163 'default': DEFAULT_OS.name,
164 'form': forms.ChoiceField,
165 'form_kwargs': {
166 'label': "Default operating system used for deployment",
167 'choices': list_osystem_choices(),
168 'required': False,
169 'error_messages': {
170 'invalid_choice': compose_invalid_choice_text(
171 'osystem',
172 list_osystem_choices())},
173 }
174 },
142 'default_distro_series': {175 'default_distro_series': {
143 'default': DISTRO_SERIES.trusty,176 'default': '%s/%s' % (
177 DEFAULT_OS.name,
178 DEFAULT_OS.get_default_release()
179 ),
144 'form': forms.ChoiceField,180 'form': forms.ChoiceField,
145 'form_kwargs': {181 'form_kwargs': {
146 'label': "Default distro series used for deployment",182 'label': "Default OS release used for deployment",
147 'choices': DISTRO_SERIES_CHOICES,183 'choices': list_release_choices(),
148 'required': False,184 'required': False,
149 'error_messages': {185 'error_messages': {
150 'invalid_choice': INVALID_DISTRO_SERIES_MESSAGE},186 'invalid_choice': compose_invalid_choice_text(
187 'distro_series',
188 list_release_choices())},
151 }189 }
152 },190 },
153 'commissioning_distro_series': {191 'commissioning_distro_series': {
154 'default': DISTRO_SERIES.trusty,192 'default': DEFAULT_OS.get_default_commissioning_release(),
155 'form': forms.ChoiceField,193 'form': forms.ChoiceField,
156 'form_kwargs': {194 'form_kwargs': {
157 'label': "Default distro series used for commissioning",195 'label': "Default Ubuntu release used for commissioning",
158 'choices': COMMISSIONING_DISTRO_SERIES_CHOICES,196 'choices': list_commisioning_choices(),
159 'required': False,197 'required': False,
160 'error_messages': {198 'error_messages': {
161 'invalid_choice': compose_invalid_choice_text(199 'invalid_choice': compose_invalid_choice_text(
162 'commissioning_distro_series',200 'commissioning_distro_series',
163 COMMISSIONING_DISTRO_SERIES_CHOICES)},201 list_commisioning_choices())},
164 }202 }
165 },203 },
166 'enable_third_party_drivers': {204 'enable_third_party_drivers': {
167205
=== added file 'src/maasserver/migrations/0075_add_osystem_to_bootimage.py'
--- src/maasserver/migrations/0075_add_osystem_to_bootimage.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0075_add_osystem_to_bootimage.py 2014-04-24 17:04:47 +0000
@@ -0,0 +1,254 @@
1# -*- coding: utf-8 -*-
2from south.utils import datetime_utils as datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Removing unique constraint on 'BootImage', fields ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
12 db.delete_unique(u'maasserver_bootimage', ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
13
14 # Adding field 'BootImage.osystem'
15 db.add_column(u'maasserver_bootimage', 'osystem',
16 self.gf('django.db.models.fields.CharField')(default='ubuntu', max_length=255),
17 keep_default=False)
18
19 # Adding unique constraint on 'BootImage', fields ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
20 db.create_unique(u'maasserver_bootimage', ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
21
22
23 def backwards(self, orm):
24 # Removing unique constraint on 'BootImage', fields ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
25 db.delete_unique(u'maasserver_bootimage', ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
26
27 # Deleting field 'BootImage.osystem'
28 db.delete_column(u'maasserver_bootimage', 'osystem')
29
30 # Adding unique constraint on 'BootImage', fields ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
31 db.create_unique(u'maasserver_bootimage', ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
32
33
34 models = {
35 u'auth.group': {
36 'Meta': {'object_name': 'Group'},
37 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
38 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
39 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
40 },
41 u'auth.permission': {
42 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
43 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
44 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
45 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
46 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
47 },
48 u'auth.user': {
49 'Meta': {'object_name': 'User'},
50 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
51 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
52 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
53 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
54 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
56 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
57 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
58 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
59 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
60 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
61 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
62 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
63 },
64 u'contenttypes.contenttype': {
65 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
66 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
67 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
68 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
69 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
70 },
71 u'maasserver.bootimage': {
72 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
73 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
74 'created': ('django.db.models.fields.DateTimeField', [], {}),
75 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
76 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
77 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
78 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
79 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
80 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
81 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
82 'updated': ('django.db.models.fields.DateTimeField', [], {})
83 },
84 u'maasserver.componenterror': {
85 'Meta': {'object_name': 'ComponentError'},
86 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
87 'created': ('django.db.models.fields.DateTimeField', [], {}),
88 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
89 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
90 'updated': ('django.db.models.fields.DateTimeField', [], {})
91 },
92 u'maasserver.config': {
93 'Meta': {'object_name': 'Config'},
94 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
95 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
96 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
97 },
98 u'maasserver.dhcplease': {
99 'Meta': {'object_name': 'DHCPLease'},
100 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
101 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
102 'mac': ('maasserver.fields.MACAddressField', [], {}),
103 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
104 },
105 u'maasserver.downloadprogress': {
106 'Meta': {'object_name': 'DownloadProgress'},
107 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
108 'created': ('django.db.models.fields.DateTimeField', [], {}),
109 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
110 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
111 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
112 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
113 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
114 'updated': ('django.db.models.fields.DateTimeField', [], {})
115 },
116 u'maasserver.filestorage': {
117 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
118 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
119 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
120 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
121 'key': ('django.db.models.fields.CharField', [], {'default': "u'26215e0a-cafa-11e3-8554-bcee7b78dc5b'", 'unique': 'True', 'max_length': '36'}),
122 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
123 },
124 u'maasserver.macaddress': {
125 'Meta': {'object_name': 'MACAddress'},
126 'created': ('django.db.models.fields.DateTimeField', [], {}),
127 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
128 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
129 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
130 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
131 'updated': ('django.db.models.fields.DateTimeField', [], {})
132 },
133 u'maasserver.network': {
134 'Meta': {'object_name': 'Network'},
135 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
136 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
137 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
138 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
139 'netmask': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
140 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
141 },
142 u'maasserver.node': {
143 'Meta': {'object_name': 'Node'},
144 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
145 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
146 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
147 'created': ('django.db.models.fields.DateTimeField', [], {}),
148 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'null': 'True', 'blank': 'True'}),
149 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
150 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
151 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
152 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
153 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
154 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
155 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
156 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
157 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
158 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
159 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
160 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
161 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-26226a84-cafa-11e3-8554-bcee7b78dc5b'", 'unique': 'True', 'max_length': '41'}),
162 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
163 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
164 'updated': ('django.db.models.fields.DateTimeField', [], {}),
165 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
166 },
167 u'maasserver.nodegroup': {
168 'Meta': {'object_name': 'NodeGroup'},
169 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
170 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
171 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
172 'created': ('django.db.models.fields.DateTimeField', [], {}),
173 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
174 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
175 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
176 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
177 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
178 'updated': ('django.db.models.fields.DateTimeField', [], {}),
179 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
180 },
181 u'maasserver.nodegroupinterface': {
182 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
183 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
184 'created': ('django.db.models.fields.DateTimeField', [], {}),
185 'foreign_dhcp_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
186 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
187 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
188 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
189 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
190 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
191 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
192 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
193 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
194 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
195 'updated': ('django.db.models.fields.DateTimeField', [], {})
196 },
197 u'maasserver.sshkey': {
198 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
199 'created': ('django.db.models.fields.DateTimeField', [], {}),
200 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
201 'key': ('django.db.models.fields.TextField', [], {}),
202 'updated': ('django.db.models.fields.DateTimeField', [], {}),
203 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
204 },
205 u'maasserver.tag': {
206 'Meta': {'object_name': 'Tag'},
207 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
208 'created': ('django.db.models.fields.DateTimeField', [], {}),
209 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
210 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
211 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
212 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
213 'updated': ('django.db.models.fields.DateTimeField', [], {})
214 },
215 u'maasserver.userprofile': {
216 'Meta': {'object_name': 'UserProfile'},
217 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
218 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
219 },
220 u'maasserver.zone': {
221 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
222 'created': ('django.db.models.fields.DateTimeField', [], {}),
223 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
224 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
225 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
226 'updated': ('django.db.models.fields.DateTimeField', [], {})
227 },
228 u'piston.consumer': {
229 'Meta': {'object_name': 'Consumer'},
230 'description': ('django.db.models.fields.TextField', [], {}),
231 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
232 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
233 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
234 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
235 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
236 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
237 },
238 u'piston.token': {
239 'Meta': {'object_name': 'Token'},
240 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
241 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
242 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
243 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
244 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
245 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
246 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
247 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1398266138L'}),
248 'token_type': ('django.db.models.fields.IntegerField', [], {}),
249 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
250 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
251 }
252 }
253
254 complete_apps = ['maasserver']
0\ No newline at end of file255\ No newline at end of file
1256
=== added file 'src/maasserver/migrations/0076_add_osystem_to_node.py'
--- src/maasserver/migrations/0076_add_osystem_to_node.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0076_add_osystem_to_node.py 2014-04-24 17:04:47 +0000
@@ -0,0 +1,243 @@
1# -*- coding: utf-8 -*-
2from south.utils import datetime_utils as datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding field 'Node.osystem'
12 db.add_column(u'maasserver_node', 'osystem',
13 self.gf('django.db.models.fields.CharField')(default=u'', max_length=20, null=True, blank=True),
14 keep_default=False)
15
16
17 def backwards(self, orm):
18 # Deleting field 'Node.osystem'
19 db.delete_column(u'maasserver_node', 'osystem')
20
21
22 models = {
23 u'auth.group': {
24 'Meta': {'object_name': 'Group'},
25 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
26 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
27 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
28 },
29 u'auth.permission': {
30 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
31 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
32 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
33 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
35 },
36 u'auth.user': {
37 'Meta': {'object_name': 'User'},
38 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
39 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
40 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
41 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
42 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
43 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
44 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
45 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
46 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
47 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
48 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
49 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
50 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
51 },
52 u'contenttypes.contenttype': {
53 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
54 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
55 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
56 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
57 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
58 },
59 u'maasserver.bootimage': {
60 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
61 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
62 'created': ('django.db.models.fields.DateTimeField', [], {}),
63 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
64 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
65 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
66 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
67 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
68 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
69 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
70 'updated': ('django.db.models.fields.DateTimeField', [], {})
71 },
72 u'maasserver.componenterror': {
73 'Meta': {'object_name': 'ComponentError'},
74 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
75 'created': ('django.db.models.fields.DateTimeField', [], {}),
76 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
77 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
78 'updated': ('django.db.models.fields.DateTimeField', [], {})
79 },
80 u'maasserver.config': {
81 'Meta': {'object_name': 'Config'},
82 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
83 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
84 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
85 },
86 u'maasserver.dhcplease': {
87 'Meta': {'object_name': 'DHCPLease'},
88 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
89 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
90 'mac': ('maasserver.fields.MACAddressField', [], {}),
91 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
92 },
93 u'maasserver.downloadprogress': {
94 'Meta': {'object_name': 'DownloadProgress'},
95 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
96 'created': ('django.db.models.fields.DateTimeField', [], {}),
97 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
98 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
99 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
100 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
101 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
102 'updated': ('django.db.models.fields.DateTimeField', [], {})
103 },
104 u'maasserver.filestorage': {
105 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
106 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
107 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
108 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
109 'key': ('django.db.models.fields.CharField', [], {'default': "u'9bbf01e4-cbbd-11e3-afb3-bcee7b78dc5b'", 'unique': 'True', 'max_length': '36'}),
110 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
111 },
112 u'maasserver.macaddress': {
113 'Meta': {'object_name': 'MACAddress'},
114 'created': ('django.db.models.fields.DateTimeField', [], {}),
115 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
116 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
117 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
118 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
119 'updated': ('django.db.models.fields.DateTimeField', [], {})
120 },
121 u'maasserver.network': {
122 'Meta': {'object_name': 'Network'},
123 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
124 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
125 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
126 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
127 'netmask': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
128 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
129 },
130 u'maasserver.node': {
131 'Meta': {'object_name': 'Node'},
132 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
133 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
134 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
135 'created': ('django.db.models.fields.DateTimeField', [], {}),
136 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'null': 'True', 'blank': 'True'}),
137 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
138 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
139 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
140 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
141 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
142 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
143 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'null': 'True', 'blank': 'True'}),
144 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
145 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
146 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
147 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
148 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
149 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
150 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-9bbde11a-cbbd-11e3-afb3-bcee7b78dc5b'", 'unique': 'True', 'max_length': '41'}),
151 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
152 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
153 'updated': ('django.db.models.fields.DateTimeField', [], {}),
154 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
155 },
156 u'maasserver.nodegroup': {
157 'Meta': {'object_name': 'NodeGroup'},
158 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
159 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
160 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
161 'created': ('django.db.models.fields.DateTimeField', [], {}),
162 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
163 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
164 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
165 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
166 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
167 'updated': ('django.db.models.fields.DateTimeField', [], {}),
168 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
169 },
170 u'maasserver.nodegroupinterface': {
171 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
172 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
173 'created': ('django.db.models.fields.DateTimeField', [], {}),
174 'foreign_dhcp_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
175 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
176 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
177 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
178 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
179 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
180 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
181 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
182 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
183 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
184 'updated': ('django.db.models.fields.DateTimeField', [], {})
185 },
186 u'maasserver.sshkey': {
187 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
188 'created': ('django.db.models.fields.DateTimeField', [], {}),
189 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
190 'key': ('django.db.models.fields.TextField', [], {}),
191 'updated': ('django.db.models.fields.DateTimeField', [], {}),
192 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
193 },
194 u'maasserver.tag': {
195 'Meta': {'object_name': 'Tag'},
196 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
197 'created': ('django.db.models.fields.DateTimeField', [], {}),
198 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
199 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
200 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
201 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
202 'updated': ('django.db.models.fields.DateTimeField', [], {})
203 },
204 u'maasserver.userprofile': {
205 'Meta': {'object_name': 'UserProfile'},
206 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
207 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
208 },
209 u'maasserver.zone': {
210 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
211 'created': ('django.db.models.fields.DateTimeField', [], {}),
212 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
213 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
214 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
215 'updated': ('django.db.models.fields.DateTimeField', [], {})
216 },
217 u'piston.consumer': {
218 'Meta': {'object_name': 'Consumer'},
219 'description': ('django.db.models.fields.TextField', [], {}),
220 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
221 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
222 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
223 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
224 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
225 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
226 },
227 u'piston.token': {
228 'Meta': {'object_name': 'Token'},
229 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
230 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
231 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
232 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
233 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
234 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
235 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
236 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1398350097L'}),
237 'token_type': ('django.db.models.fields.IntegerField', [], {}),
238 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
239 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
240 }
241 }
242
243 complete_apps = ['maasserver']
0\ No newline at end of file244\ No newline at end of file
1245
=== modified file 'src/maasserver/models/bootimage.py'
--- src/maasserver/models/bootimage.py 2014-03-26 16:01:34 +0000
+++ src/maasserver/models/bootimage.py 2014-04-24 17:04:47 +0000
@@ -34,44 +34,47 @@
34 Don't import or instantiate this directly; access as `BootImage.objects`.34 Don't import or instantiate this directly; access as `BootImage.objects`.
35 """35 """
3636
37 def get_by_natural_key(self, nodegroup, architecture, subarchitecture,37 def get_by_natural_key(self, nodegroup, osystem, architecture,
38 release, purpose, label):38 subarchitecture, release, purpose, label):
39 """Look up a specific image."""39 """Look up a specific image."""
40 return self.get(40 return self.get(
41 nodegroup=nodegroup, architecture=architecture,41 nodegroup=nodegroup, osystem=osystem, architecture=architecture,
42 subarchitecture=subarchitecture, release=release,42 subarchitecture=subarchitecture, release=release,
43 purpose=purpose, label=label)43 purpose=purpose, label=label)
4444
45 def register_image(self, nodegroup, architecture, subarchitecture,45 def register_image(self, nodegroup, osystem, architecture, subarchitecture,
46 release, purpose, label):46 release, purpose, label):
47 """Register an image if it wasn't already registered."""47 """Register an image if it wasn't already registered."""
48 self.get_or_create(48 self.get_or_create(
49 nodegroup=nodegroup, architecture=architecture,49 nodegroup=nodegroup, osystem=osystem, architecture=architecture,
50 subarchitecture=subarchitecture, release=release,50 subarchitecture=subarchitecture, release=release,
51 purpose=purpose, label=label)51 purpose=purpose, label=label)
5252
53 def have_image(self, nodegroup, architecture, subarchitecture, release,53 def have_image(self, nodegroup, osystem, architecture, subarchitecture,
54 purpose, label=None):54 release, purpose, label=None):
55 """Is an image for the given kind of boot available?"""55 """Is an image for the given kind of boot available?"""
56 if label is None:56 if label is None:
57 label = "release"57 label = "release"
58 try:58 try:
59 self.get_by_natural_key(59 self.get_by_natural_key(
60 nodegroup=nodegroup, architecture=architecture,60 nodegroup=nodegroup, osystem=osystem,
61 subarchitecture=subarchitecture, release=release,61 architecture=architecture, subarchitecture=subarchitecture,
62 purpose=purpose, label=label)62 release=release, purpose=purpose, label=label)
63 return True63 return True
64 except BootImage.DoesNotExist:64 except BootImage.DoesNotExist:
65 return False65 return False
6666
67 def get_default_arch_image_in_nodegroup(self, nodegroup, series, purpose):67 def get_default_arch_image_in_nodegroup(self, nodegroup, osystem, series,
68 """Return any image for the given nodegroup, series, and purpose.68 purpose):
69 """Return any image for the given nodegroup, osystem, series,
70 and purpose.
6971
70 Prefers `i386` images if available. Returns `None` if no images match72 Prefers `i386` images if available. Returns `None` if no images match
71 requirements.73 requirements.
72 """74 """
73 images = BootImage.objects.filter(75 images = BootImage.objects.filter(
74 release=series, nodegroup=nodegroup, purpose=purpose)76 osystem=osystem, release=series, nodegroup=nodegroup,
77 purpose=purpose)
75 for image in images:78 for image in images:
76 # Prefer i386, any available subarchitecture (usually just79 # Prefer i386, any available subarchitecture (usually just
77 # "generic"). It will work for most cases where we don't know80 # "generic"). It will work for most cases where we don't know
@@ -106,14 +109,28 @@
106 nodegroup, 'install')109 nodegroup, 'install')
107 return arches_commissioning & arches_install110 return arches_commissioning & arches_install
108111
109 def get_latest_image(self, nodegroup, architecture, subarchitecture,112 def get_latest_image(self, nodegroup, osystem, architecture,
110 release, purpose):113 subarchitecture, release, purpose):
111 """Return the latest image for a set of criteria."""114 """Return the latest image for a set of criteria."""
112 return BootImage.objects.filter(115 return BootImage.objects.filter(
113 nodegroup=nodegroup, architecture=architecture,116 nodegroup=nodegroup, osystem=osystem, architecture=architecture,
114 subarchitecture=subarchitecture, release=release,117 subarchitecture=subarchitecture, release=release,
115 purpose=purpose).order_by('id').last()118 purpose=purpose).order_by('id').last()
116119
120 def get_usable_osystems(self, nodegroup):
121 """Return the list of usable operating systems for a nodegroup.
122 """
123 query = BootImage.objects.filter(nodegroup=nodegroup)
124 return set(query.values_list('osystem', flat=True))
125
126 def get_usable_releases(self, nodegroup, osystem):
127 """Return the list of usable releases for a nodegroup and
128 operating system.
129 """
130 query = BootImage.objects.filter(nodegroup=nodegroup, osystem=osystem)
131 releases = query.values_list('release', flat=True)
132 return set(releases)
133
117134
118class BootImage(TimestampedModel):135class BootImage(TimestampedModel):
119 """Available boot image (i.e. kernel and initrd).136 """Available boot image (i.e. kernel and initrd).
@@ -131,8 +148,8 @@
131148
132 class Meta(DefaultMeta):149 class Meta(DefaultMeta):
133 unique_together = (150 unique_together = (
134 ('nodegroup', 'architecture', 'subarchitecture', 'release',151 ('nodegroup', 'osystem', 'architecture', 'subarchitecture',
135 'purpose', 'label'),152 'release', 'purpose', 'label'),
136 )153 )
137154
138 objects = BootImageManager()155 objects = BootImageManager()
@@ -140,6 +157,9 @@
140 # Nodegroup (cluster controller) that has the images.157 # Nodegroup (cluster controller) that has the images.
141 nodegroup = ForeignKey(NodeGroup, null=False, editable=False, unique=False)158 nodegroup = ForeignKey(NodeGroup, null=False, editable=False, unique=False)
142159
160 # Operating system (e.g. "ubuntu") that the image boots.
161 osystem = CharField(max_length=255, blank=False, editable=False)
162
143 # System architecture (e.g. "i386") that the image is for.163 # System architecture (e.g. "i386") that the image is for.
144 architecture = CharField(max_length=255, blank=False, editable=False)164 architecture = CharField(max_length=255, blank=False, editable=False)
145165
@@ -148,7 +168,7 @@
148 # such as i386 and amd64, we use "generic").168 # such as i386 and amd64, we use "generic").
149 subarchitecture = CharField(max_length=255, blank=False, editable=False)169 subarchitecture = CharField(max_length=255, blank=False, editable=False)
150170
151 # Ubuntu release (e.g. "precise") that the image boots.171 # OS release (e.g. "precise") that the image boots.
152 release = CharField(max_length=255, blank=False, editable=False)172 release = CharField(max_length=255, blank=False, editable=False)
153173
154 # Boot purpose (e.g. "commissioning" or "install") that the image is for.174 # Boot purpose (e.g. "commissioning" or "install") that the image is for.
@@ -159,7 +179,8 @@
159 max_length=255, blank=False, editable=False, default="release")179 max_length=255, blank=False, editable=False, default="release")
160180
161 def __repr__(self):181 def __repr__(self):
162 return "<BootImage %s/%s-%s-%s-%s>" % (182 return "<BootImage %s-%s/%s-%s-%s-%s>" % (
183 self.osystem,
163 self.architecture,184 self.architecture,
164 self.subarchitecture,185 self.subarchitecture,
165 self.release,186 self.release,
166187
=== modified file 'src/maasserver/models/config.py'
--- src/maasserver/models/config.py 2014-04-14 21:42:00 +0000
+++ src/maasserver/models/config.py 2014-04-24 17:04:47 +0000
@@ -28,8 +28,11 @@
28 )28 )
29from django.db.models.signals import post_save29from django.db.models.signals import post_save
30from maasserver import DefaultMeta30from maasserver import DefaultMeta
31from maasserver.enum import DISTRO_SERIES
32from maasserver.fields import JSONObjectField31from maasserver.fields import JSONObjectField
32from provisioningserver.osystems.ubuntu import UbuntuOS
33
34
35DEFAULT_OS = UbuntuOS()
3336
3437
35def get_default_config():38def get_default_config():
@@ -40,11 +43,14 @@
40 # Ubuntu section configuration.43 # Ubuntu section configuration.
41 'main_archive': 'http://archive.ubuntu.com/ubuntu',44 'main_archive': 'http://archive.ubuntu.com/ubuntu',
42 'ports_archive': 'http://ports.ubuntu.com/ubuntu-ports',45 'ports_archive': 'http://ports.ubuntu.com/ubuntu-ports',
43 'commissioning_distro_series': DISTRO_SERIES.trusty,46 'commissioning_osystem': DEFAULT_OS.name,
47 'commissioning_distro_series':
48 DEFAULT_OS.get_default_commissioning_release(),
44 # Network section configuration.49 # Network section configuration.
45 'maas_name': gethostname(),50 'maas_name': gethostname(),
46 'enlistment_domain': b'local',51 'enlistment_domain': b'local',
47 'default_distro_series': DISTRO_SERIES.trusty,52 'default_osystem': DEFAULT_OS.name,
53 'default_distro_series': DEFAULT_OS.get_default_release(),
48 'http_proxy': None,54 'http_proxy': None,
49 'upstream_dns': None,55 'upstream_dns': None,
50 'ntp_server': '91.189.94.4', # ntp.ubuntu.com56 'ntp_server': '91.189.94.4', # ntp.ubuntu.com
5157
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2014-04-21 11:09:09 +0000
+++ src/maasserver/models/node.py 2014-04-24 17:04:47 +0000
@@ -49,8 +49,6 @@
49 logger,49 logger,
50 )50 )
51from maasserver.enum import (51from maasserver.enum import (
52 DISTRO_SERIES,
53 DISTRO_SERIES_CHOICES,
54 NODE_PERMISSION,52 NODE_PERMISSION,
55 NODE_STATUS,53 NODE_STATUS,
56 NODE_STATUS_CHOICES,54 NODE_STATUS_CHOICES,
@@ -72,6 +70,7 @@
72 strip_domain,70 strip_domain,
73 )71 )
74from piston.models import Token72from piston.models import Token
73from provisioningserver.osystems import OperatingSystemRegistry
75from provisioningserver.tasks import (74from provisioningserver.tasks import (
76 power_off,75 power_off,
77 power_on,76 power_on,
@@ -190,6 +189,33 @@
190 "Name contains disallowed characters: %r." % label)189 "Name contains disallowed characters: %r." % label)
191190
192191
192def validate_osystem(name):
193 """Validator for operating system.
194
195 :raise ValidationError: If invalid operating system selected.
196 """
197 if name is not None and name != '':
198 osystem = OperatingSystemRegistry.get_item(name)
199 if osystem is None:
200 raise ValidationError(
201 "Value u'%s' is not a valid choice." % name)
202
203
204def validate_distro_series(name):
205 """Validator for distro_series.
206
207 :raise ValidationError: If invalid distro series selected.
208 """
209 if name is None or name == '':
210 return
211 releases = set()
212 for _, obj in OperatingSystemRegistry:
213 releases = releases.union(obj.get_supported_releases())
214 if name not in releases:
215 raise ValidationError(
216 "Value u'%s' is not a valid choice." % name)
217
218
193class NodeManager(Manager):219class NodeManager(Manager):
194 """A utility to manage the collection of Nodes."""220 """A utility to manage the collection of Nodes."""
195221
@@ -477,9 +503,13 @@
477 owner = ForeignKey(503 owner = ForeignKey(
478 User, default=None, blank=True, null=True, editable=False)504 User, default=None, blank=True, null=True, editable=False)
479505
506 osystem = CharField(
507 max_length=20, null=True, blank=True, default='',
508 validators=[validate_osystem])
509
480 distro_series = CharField(510 distro_series = CharField(
481 max_length=20, choices=DISTRO_SERIES_CHOICES, null=True,511 max_length=20, null=True, blank=True, default='',
482 blank=True, default='')512 validators=[validate_distro_series])
483513
484 architecture = CharField(max_length=31, blank=False)514 architecture = CharField(max_length=31, blank=False)
485515
@@ -774,21 +804,48 @@
774 """The name of the queue for tasks specific to this node."""804 """The name of the queue for tasks specific to this node."""
775 return self.nodegroup.work_queue805 return self.nodegroup.work_queue
776806
807 def get_osystem(self):
808 """Return the operating system to install that node."""
809 use_default_osystem = (
810 not self.osystem or
811 self.osystem == '')
812 if use_default_osystem:
813 return Config.objects.get_config('default_osystem')
814 else:
815 return self.osystem
816
777 def get_distro_series(self):817 def get_distro_series(self):
778 """Return the distro series to install that node."""818 """Return the distro series to install that node."""
819 use_default_osystem = (
820 not self.osystem or
821 self.osystem == '')
779 use_default_distro_series = (822 use_default_distro_series = (
780 not self.distro_series or823 not self.distro_series or
781 self.distro_series == DISTRO_SERIES.default)824 self.distro_series == '')
782 if use_default_distro_series:825 if use_default_osystem and use_default_distro_series:
783 return Config.objects.get_config('default_distro_series')826 return Config.objects.get_config('default_distro_series')
827 elif use_default_distro_series:
828 osystem = OperatingSystemRegistry[self.osystem]
829 return osystem.get_default_release()
784 else:830 else:
785 return self.distro_series831 return self.distro_series
786832
833 def set_osystem(self, osystem=''):
834 """Set the operating system to install that node."""
835 self.osystem = osystem
836 self.save()
837
787 def set_distro_series(self, series=''):838 def set_distro_series(self, series=''):
788 """Set the distro series to install that node."""839 """Set the distro series to install that node."""
789 self.distro_series = series840 self.distro_series = series
790 self.save()841 self.save()
791842
843 def set_osystem_and_distro_series(self, osystem='', series=''):
844 """Set the oeprating system to install that node."""
845 self.osystem = osystem
846 self.distro_series = series
847 self.save()
848
792 def get_effective_power_parameters(self):849 def get_effective_power_parameters(self):
793 """Return effective power parameters, including any defaults."""850 """Return effective power parameters, including any defaults."""
794 if self.power_parameters:851 if self.power_parameters:
795852
=== modified file 'src/maasserver/models/tests/test_bootimage.py'
--- src/maasserver/models/tests/test_bootimage.py 2014-03-18 14:39:11 +0000
+++ src/maasserver/models/tests/test_bootimage.py 2014-04-24 17:04:47 +0000
@@ -50,69 +50,79 @@
50 self.assertTrue(BootImage.objects.have_image(nodegroup, **params))50 self.assertTrue(BootImage.objects.have_image(nodegroup, **params))
5151
52 def test_default_arch_image_returns_None_if_no_images_match(self):52 def test_default_arch_image_returns_None_if_no_images_match(self):
53 osystem = Config.objects.get_config('commissioning_osystem')
53 series = Config.objects.get_config('commissioning_distro_series')54 series = Config.objects.get_config('commissioning_distro_series')
54 result = BootImage.objects.get_default_arch_image_in_nodegroup(55 result = BootImage.objects.get_default_arch_image_in_nodegroup(
55 factory.make_node_group(), series, factory.make_name('purpose'))56 factory.make_node_group(), osystem, series,
57 factory.make_name('purpose'))
56 self.assertIsNone(result)58 self.assertIsNone(result)
5759
58 def test_default_arch_image_returns_only_matching_image(self):60 def test_default_arch_image_returns_only_matching_image(self):
59 nodegroup = factory.make_node_group()61 nodegroup = factory.make_node_group()
62 osystem = factory.make_name('os')
60 series = factory.make_name('series')63 series = factory.make_name('series')
61 label = factory.make_name('label')64 label = factory.make_name('label')
62 arch = factory.make_name('arch')65 arch = factory.make_name('arch')
63 purpose = factory.make_name("purpose")66 purpose = factory.make_name("purpose")
64 factory.make_boot_image(67 factory.make_boot_image(
65 architecture=arch, release=series, label=label,68 osystem=osystem, architecture=arch,
69 release=series, label=label,
66 nodegroup=nodegroup, purpose=purpose)70 nodegroup=nodegroup, purpose=purpose)
67 result = BootImage.objects.get_default_arch_image_in_nodegroup(71 result = BootImage.objects.get_default_arch_image_in_nodegroup(
68 nodegroup, series, purpose=purpose)72 nodegroup, osystem, series, purpose=purpose)
69 self.assertEqual(result.architecture, arch)73 self.assertEqual(result.architecture, arch)
7074
71 def test_default_arch_image_prefers_i386(self):75 def test_default_arch_image_prefers_i386(self):
72 nodegroup = factory.make_node_group()76 nodegroup = factory.make_node_group()
77 osystem = factory.make_name('os')
73 series = factory.make_name('series')78 series = factory.make_name('series')
74 label = factory.make_name('label')79 label = factory.make_name('label')
75 purpose = factory.make_name("purpose")80 purpose = factory.make_name("purpose")
76 for arch in ['amd64', 'axp', 'i386', 'm88k']:81 for arch in ['amd64', 'axp', 'i386', 'm88k']:
77 factory.make_boot_image(82 factory.make_boot_image(
78 architecture=arch, release=series, nodegroup=nodegroup,83 osystem=osystem, architecture=arch,
84 release=series, nodegroup=nodegroup,
79 purpose=purpose, label=label)85 purpose=purpose, label=label)
80 result = BootImage.objects.get_default_arch_image_in_nodegroup(86 result = BootImage.objects.get_default_arch_image_in_nodegroup(
81 nodegroup, series, purpose=purpose)87 nodegroup, osystem, series, purpose=purpose)
82 self.assertEqual(result.architecture, "i386")88 self.assertEqual(result.architecture, "i386")
8389
84 def test_default_arch_image_returns_arbitrary_pick_if_all_else_fails(self):90 def test_default_arch_image_returns_arbitrary_pick_if_all_else_fails(self):
85 nodegroup = factory.make_node_group()91 nodegroup = factory.make_node_group()
92 osystem = factory.make_name('os')
86 series = factory.make_name('series')93 series = factory.make_name('series')
87 label = factory.make_name('label')94 label = factory.make_name('label')
88 purpose = factory.make_name("purpose")95 purpose = factory.make_name("purpose")
89 images = [96 images = [
90 factory.make_boot_image(97 factory.make_boot_image(
91 architecture=factory.make_name('arch'), release=series,98 osystem=osystem, architecture=factory.make_name('arch'),
92 label=label, nodegroup=nodegroup, purpose=purpose)99 release=series, label=label, nodegroup=nodegroup,
100 purpose=purpose)
93 for _ in range(3)101 for _ in range(3)
94 ]102 ]
95 self.assertIn(103 self.assertIn(
96 BootImage.objects.get_default_arch_image_in_nodegroup(104 BootImage.objects.get_default_arch_image_in_nodegroup(
97 nodegroup, series, purpose=purpose),105 nodegroup, osystem, series, purpose=purpose),
98 images)106 images)
99107
100 def test_default_arch_image_copes_with_subarches(self):108 def test_default_arch_image_copes_with_subarches(self):
101 nodegroup = factory.make_node_group()109 nodegroup = factory.make_node_group()
102 arch = 'i386'110 arch = 'i386'
111 osystem = factory.make_name('os')
103 series = factory.make_name('series')112 series = factory.make_name('series')
104 label = factory.make_name('label')113 label = factory.make_name('label')
105 purpose = factory.make_name("purpose")114 purpose = factory.make_name("purpose")
106 images = [115 images = [
107 factory.make_boot_image(116 factory.make_boot_image(
108 architecture=arch, subarchitecture=factory.make_name('sub'),117 osystem=osystem, architecture=arch,
118 subarchitecture=factory.make_name('sub'),
109 release=series, label=label, nodegroup=nodegroup,119 release=series, label=label, nodegroup=nodegroup,
110 purpose=purpose)120 purpose=purpose)
111 for _ in range(3)121 for _ in range(3)
112 ]122 ]
113 self.assertIn(123 self.assertIn(
114 BootImage.objects.get_default_arch_image_in_nodegroup(124 BootImage.objects.get_default_arch_image_in_nodegroup(
115 nodegroup, series, purpose=purpose),125 nodegroup, osystem, series, purpose=purpose),
116 images)126 images)
117127
118 def test_get_usable_architectures_returns_supported_arches(self):128 def test_get_usable_architectures_returns_supported_arches(self):
@@ -164,54 +174,62 @@
164 BootImage.objects.get_usable_architectures(nodegroup))174 BootImage.objects.get_usable_architectures(nodegroup))
165175
166 def test_get_latest_image_returns_latest_image_for_criteria(self):176 def test_get_latest_image_returns_latest_image_for_criteria(self):
177 osystem = factory.make_name('os')
167 arch = factory.make_name('arch')178 arch = factory.make_name('arch')
168 subarch = factory.make_name('sub')179 subarch = factory.make_name('sub')
169 release = factory.make_name('release')180 release = factory.make_name('release')
170 nodegroup = factory.make_node_group()181 nodegroup = factory.make_node_group()
171 purpose = factory.make_name("purpose")182 purpose = factory.make_name("purpose")
172 boot_image = factory.make_boot_image(183 boot_image = factory.make_boot_image(
173 nodegroup=nodegroup, architecture=arch,184 nodegroup=nodegroup, osystem=osystem, architecture=arch,
174 subarchitecture=subarch, release=release, purpose=purpose,185 subarchitecture=subarch, release=release, purpose=purpose,
175 label=factory.make_name('label'))186 label=factory.make_name('label'))
176 self.assertEqual(187 self.assertEqual(
177 boot_image,188 boot_image,
178 BootImage.objects.get_latest_image(189 BootImage.objects.get_latest_image(
179 nodegroup, arch, subarch, release, purpose))190 nodegroup, osystem, arch, subarch, release, purpose))
180191
181 def test_get_latest_image_doesnt_return_images_for_other_purposes(self):192 def test_get_latest_image_doesnt_return_images_for_other_purposes(self):
193 osystem = factory.make_name('os')
182 arch = factory.make_name('arch')194 arch = factory.make_name('arch')
183 subarch = factory.make_name('sub')195 subarch = factory.make_name('sub')
184 release = factory.make_name('release')196 release = factory.make_name('release')
185 nodegroup = factory.make_node_group()197 nodegroup = factory.make_node_group()
186 purpose = factory.make_name("purpose")198 purpose = factory.make_name("purpose")
187 relevant_image = factory.make_boot_image(199 relevant_image = factory.make_boot_image(
188 nodegroup=nodegroup, architecture=arch,200 nodegroup=nodegroup, osystem=osystem, architecture=arch,
189 subarchitecture=subarch, release=release, purpose=purpose,201 subarchitecture=subarch, release=release, purpose=purpose,
190 label=factory.make_name('label'))202 label=factory.make_name('label'))
191203
192 # Create a bunch of more recent but irrelevant BootImages..204 # Create a bunch of more recent but irrelevant BootImages..
193 factory.make_boot_image(205 factory.make_boot_image(
194 nodegroup=factory.make_node_group(), architecture=arch,206 nodegroup=factory.make_node_group(), osystem=osystem,
195 subarchitecture=subarch, release=release,207 architecture=arch, subarchitecture=subarch, release=release,
196 purpose=purpose, label=factory.make_name('label'))208 purpose=purpose, label=factory.make_name('label'))
197 factory.make_boot_image(209 factory.make_boot_image(
198 nodegroup=nodegroup,210 nodegroup=nodegroup, osystem=osystem,
199 architecture=factory.make_name('arch'),211 architecture=factory.make_name('arch'),
200 subarchitecture=subarch, release=release, purpose=purpose,212 subarchitecture=subarch, release=release, purpose=purpose,
201 label=factory.make_name('label'))213 label=factory.make_name('label'))
202 factory.make_boot_image(214 factory.make_boot_image(
203 nodegroup=nodegroup, architecture=arch,215 nodegroup=nodegroup, osystem=osystem, architecture=arch,
204 subarchitecture=factory.make_name('subarch'),216 subarchitecture=factory.make_name('subarch'),
205 release=release, purpose=purpose,217 release=release, purpose=purpose,
206 label=factory.make_name('label'))218 label=factory.make_name('label'))
207 factory.make_boot_image(219 factory.make_boot_image(
208 nodegroup=nodegroup,220 nodegroup=nodegroup, osystem=osystem,
209 architecture=factory.make_name('arch'),221 architecture=factory.make_name('arch'),
210 subarchitecture=subarch,222 subarchitecture=subarch,
211 release=factory.make_name('release'), purpose=purpose,223 release=factory.make_name('release'), purpose=purpose,
212 label=factory.make_name('label'))224 label=factory.make_name('label'))
213 factory.make_boot_image(225 factory.make_boot_image(
214 nodegroup=nodegroup,226 nodegroup=nodegroup, osystem=osystem,
227 architecture=factory.make_name('arch'),
228 subarchitecture=subarch, release=release,
229 purpose=factory.make_name('purpose'),
230 label=factory.make_name('label'))
231 factory.make_boot_image(
232 nodegroup=nodegroup, osystem=factory.make_name('os'),
215 architecture=factory.make_name('arch'),233 architecture=factory.make_name('arch'),
216 subarchitecture=subarch, release=release,234 subarchitecture=subarch, release=release,
217 purpose=factory.make_name('purpose'),235 purpose=factory.make_name('purpose'),
@@ -220,4 +238,66 @@
220 self.assertEqual(238 self.assertEqual(
221 relevant_image,239 relevant_image,
222 BootImage.objects.get_latest_image(240 BootImage.objects.get_latest_image(
223 nodegroup, arch, subarch, release, purpose))241 nodegroup, osystem, arch, subarch, release, purpose))
242
243 def test_get_usable_osystems_returns_supported_osystems(self):
244 nodegroup = factory.make_node_group()
245 osystems = [
246 factory.make_name('os'),
247 factory.make_name('os'),
248 ]
249 for osystem in osystems:
250 factory.make_boot_image(
251 osystem=osystem,
252 nodegroup=nodegroup)
253 self.assertItemsEqual(
254 osystems,
255 BootImage.objects.get_usable_osystems(nodegroup))
256
257 def test_get_usable_osystems_uses_given_nodegroup(self):
258 nodegroup = factory.make_node_group()
259 osystem = factory.make_name('os')
260 factory.make_boot_image(
261 osystem=osystem, nodegroup=nodegroup)
262 self.assertItemsEqual(
263 [],
264 BootImage.objects.get_usable_osystems(
265 factory.make_node_group()))
266
267 def test_get_usable_releases_returns_supported_releases(self):
268 nodegroup = factory.make_node_group()
269 osystem = factory.make_name('os')
270 releases = [
271 factory.make_name('release'),
272 factory.make_name('release'),
273 ]
274 for release in releases:
275 factory.make_boot_image(
276 osystem=osystem,
277 release=release,
278 nodegroup=nodegroup)
279 self.assertItemsEqual(
280 releases,
281 BootImage.objects.get_usable_releases(nodegroup, osystem))
282
283 def test_get_usable_releases_uses_given_nodegroup(self):
284 nodegroup = factory.make_node_group()
285 osystem = factory.make_name('os')
286 release = factory.make_name('release')
287 factory.make_boot_image(
288 osystem=osystem, release=release, nodegroup=nodegroup)
289 self.assertItemsEqual(
290 [],
291 BootImage.objects.get_usable_releases(
292 factory.make_node_group(), osystem))
293
294 def test_get_usable_releases_uses_given_osystem(self):
295 nodegroup = factory.make_node_group()
296 osystem = factory.make_name('os')
297 release = factory.make_name('release')
298 factory.make_boot_image(
299 osystem=osystem, release=release, nodegroup=nodegroup)
300 self.assertItemsEqual(
301 [],
302 BootImage.objects.get_usable_releases(
303 factory.make_node_group(), factory.make_name('os')))
224304
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2014-04-21 11:07:32 +0000
+++ src/maasserver/models/tests/test_node.py 2014-04-24 17:04:47 +0000
@@ -20,7 +20,6 @@
20from django.core.exceptions import ValidationError20from django.core.exceptions import ValidationError
21from maasserver.clusterrpc.power_parameters import get_power_types21from maasserver.clusterrpc.power_parameters import get_power_types
22from maasserver.enum import (22from maasserver.enum import (
23 DISTRO_SERIES,
24 NODE_PERMISSION,23 NODE_PERMISSION,
25 NODE_STATUS,24 NODE_STATUS,
26 NODE_STATUS_CHOICES,25 NODE_STATUS_CHOICES,
@@ -270,17 +269,41 @@
270 offset += timedelta(1)269 offset += timedelta(1)
271 self.assertEqual(macs[0], node.get_primary_mac().mac_address)270 self.assertEqual(macs[0], node.get_primary_mac().mac_address)
272271
273 def test_get_distro_series_returns_default_series(self):272 def test_get_osystem_returns_default_osystem_and_series(self):
274 node = factory.make_node()273 node = factory.make_node()
275 series = Config.objects.get_config('commissioning_distro_series')274 osystem = Config.objects.get_config('default_osystem')
276 self.assertEqual(series, node.get_distro_series())275 series = Config.objects.get_config('default_distro_series')
276 self.assertEqual(osystem, node.get_osystem())
277 self.assertEqual(series, node.get_distro_series())
278
279 def test_get_series_returns_default_for_osystem(self):
280 node = factory.make_node()
281 osystem = factory.getRandomOS()
282 series = osystem.get_default_release()
283 node.set_osystem(osystem.name)
284 self.assertEqual(series, node.get_distro_series())
285
286 def test_set_get_osystem_returns_osystem(self):
287 osystem = factory.getRandomOS()
288 node = factory.make_node()
289 node.set_osystem(osystem.name)
290 self.assertEqual(osystem.name, node.get_osystem())
277291
278 def test_set_get_distro_series_returns_series(self):292 def test_set_get_distro_series_returns_series(self):
293 osystem = factory.getRandomOS()
294 series = factory.getRandomRelease(osystem)
279 node = factory.make_node()295 node = factory.make_node()
280 series = DISTRO_SERIES.quantal
281 node.set_distro_series(series)296 node.set_distro_series(series)
282 self.assertEqual(series, node.get_distro_series())297 self.assertEqual(series, node.get_distro_series())
283298
299 def test_set_get_osystem_and_distro_series_returns_valid(self):
300 osystem = factory.getRandomOS()
301 series = factory.getRandomRelease(osystem)
302 node = factory.make_node()
303 node.set_osystem_and_distro_series(osystem.name, series)
304 self.assertEqual(osystem.name, node.get_osystem())
305 self.assertEqual(series, node.get_distro_series())
306
284 def test_delete_node_deletes_related_mac(self):307 def test_delete_node_deletes_related_mac(self):
285 node = factory.make_node()308 node = factory.make_node()
286 mac = node.add_mac_address('AA:BB:CC:DD:EE:FF')309 mac = node.add_mac_address('AA:BB:CC:DD:EE:FF')
287310
=== modified file 'src/maasserver/preseed.py'
--- src/maasserver/preseed.py 2014-04-10 13:43:33 +0000
+++ src/maasserver/preseed.py 2014-04-24 17:04:47 +0000
@@ -93,6 +93,7 @@
9393
94def get_curtin_installer_url(node):94def get_curtin_installer_url(node):
95 """Return the URL where curtin on the node can download its installer."""95 """Return the URL where curtin on the node can download its installer."""
96 osystem = node.get_osystem()
96 series = node.get_distro_series()97 series = node.get_distro_series()
97 cluster_host = pick_cluster_controller_address(node)98 cluster_host = pick_cluster_controller_address(node)
98 # XXX rvb(?): The path shouldn't be hardcoded like this, but rather synced99 # XXX rvb(?): The path shouldn't be hardcoded like this, but rather synced
@@ -100,18 +101,20 @@
100 arch, subarch = node.architecture.split('/')101 arch, subarch = node.architecture.split('/')
101 purpose = 'xinstall'102 purpose = 'xinstall'
102 image = BootImage.objects.get_latest_image(103 image = BootImage.objects.get_latest_image(
103 node.nodegroup, arch, subarch, series, purpose)104 node.nodegroup, osystem, arch, subarch, series, purpose)
104 if image is None:105 if image is None:
105 raise MAASAPIException(106 raise MAASAPIException(
106 "Error generating the URL of curtin's root-tgz file. "107 "Error generating the URL of curtin's root-tgz file. "
107 "No image could be found for the given selection: "108 "No image could be found for the given selection: "
108 "arch=%s, subarch=%s, series=%s, purpose=%s." % (109 "os=%s, arch=%s, subarch=%s, series=%s, purpose=%s." % (
110 osystem,
109 arch,111 arch,
110 subarch,112 subarch,
111 series,113 series,
112 purpose114 purpose
113 ))115 ))
114 dyn_uri = '/'.join([116 dyn_uri = '/'.join([
117 osystem,
115 arch,118 arch,
116 subarch,119 subarch,
117 series,120 series,
118121
=== modified file 'src/maasserver/static/js/node_add.js'
--- src/maasserver/static/js/node_add.js 2014-03-03 06:33:34 +0000
+++ src/maasserver/static/js/node_add.js 2014-04-24 17:04:47 +0000
@@ -228,11 +228,28 @@
228 var heading = Y.Node.create('<h2 />')228 var heading = Y.Node.create('<h2 />')
229 .set('text', "Add node");229 .set('text', "Add node");
230 this.get('srcNode').append(heading).append(this.createForm());230 this.get('srcNode').append(heading).append(this.createForm());
231 this.setUpDistroSeriesField();
231 this.setUpPowerParameterField();232 this.setUpPowerParameterField();
232 this.initializeNodes();233 this.initializeNodes();
233 },234 },
234235
235 /**236 /**
237 * If the 'distro_series' field is present, setup the linked
238 * 'distro_series' field.
239 *
240 * @method setUpDistroSeriesField
241 */
242 setUpDistroSeriesField: function() {
243 if (Y.Lang.isValue(Y.one('#id_distro_series'))) {
244 var widget = new Y.maas.os_distro_select.OSReleaseWidget({
245 srcNode: '#id_distro_series'
246 });
247 widget.bindTo(Y.one('#id_osystem'), 'change');
248 widget.render();
249 }
250 },
251
252 /**
236 * If the 'power_type' field is present, setup the linked253 * If the 'power_type' field is present, setup the linked
237 * 'power_parameter' field.254 * 'power_parameter' field.
238 *255 *
@@ -415,5 +432,6 @@
415};432};
416433
417}, '0.1', {'requires': ['io', 'node', 'widget', 'event', 'event-custom',434}, '0.1', {'requires': ['io', 'node', 'widget', 'event', 'event-custom',
418 'maas.morph', 'maas.enums', 'maas.power_parameters']}435 'maas.morph', 'maas.enums', 'maas.power_parameters',
436 'maas.os_distro_select']}
419);437);
420438
=== added file 'src/maasserver/static/js/os_distro_select.js'
--- src/maasserver/static/js/os_distro_select.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/os_distro_select.js 2014-04-24 17:04:47 +0000
@@ -0,0 +1,131 @@
1/* Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * OS/Release seletion utilities.
5 *
6 * @module Y.maas.power_parameter
7 */
8
9YUI.add('maas.os_distro_select', function(Y) {
10
11Y.log('loading maas.os_distro_select');
12var module = Y.namespace('maas.os_distro_select');
13
14// Only used to mockup io in tests.
15module._io = new Y.IO();
16
17var OSReleaseWidget;
18
19/**
20 * A widget class used to have the content of a node's release <select>
21 * modified based on the selected operating system.
22 *
23 */
24OSReleaseWidget = function() {
25 OSReleaseWidget.superclass.constructor.apply(this, arguments);
26};
27
28OSReleaseWidget.NAME = 'os-release-widget';
29
30Y.extend(OSReleaseWidget, Y.Widget, {
31
32 /**
33 * Initialize the widget.
34 * - cfg.srcNode is the node which will be updated when the selected
35 * value of the 'os node' will change.
36 * - cfg.osNode is the node containing a 'select' element. When
37 * the selected element will change, the srcNode HTML will be
38 * updated.
39 *
40 * @method initializer
41 */
42 initializer: function(cfg) {
43 this.initialSkip = true;
44 },
45
46 /**
47 * Bind the widget to events (to name 'event_name') generated by the given
48 * 'osNode'.
49 *
50 * @method bindTo
51 */
52 bindTo: function(osNode, event_name) {
53 var self = this;
54 Y.one(osNode).on(event_name, function(e) {
55 var osValue = e.currentTarget.get('value');
56 self.switchTo(osValue);
57 });
58 var osValue = Y.one(osNode).get('value');
59 self.switchTo(osValue);
60 },
61
62 /**
63 * React to a new value of the os node: update the HTML of
64 * 'srcNode'.
65 *
66 * @method switchTo
67 */
68 switchTo: function(newOSValue) {
69 var srcNode = this.get('srcNode');
70 var options = srcNode.all('option');
71 var selected = false;
72 options.each(function(option) {
73 var value = option.get('value');
74 var split_value = value.split("/");
75
76 // Only show the default option, if that
77 // is the selected os option as well.
78 if(newOSValue == '') {
79 if(value == '') {
80 option.removeClass('hidden');
81 option.set('selected', 'selected');
82 }
83 else {
84 option.addClass('hidden');
85 }
86 }
87 else {
88 if(split_value[0] == newOSValue) {
89 option.removeClass('hidden');
90 if(split_value[1] == '') {
91 selected = true;
92 option.set('selected', 'selected');
93 }
94 }
95 else {
96 option.addClass('hidden');
97 }
98 }
99 });
100
101 // See if this was the inital skip. As the following
102 // should only be done, after the first load, as the
103 // initial will already be selected correctly.
104 if(this.initialSkip == true) {
105 this.initialSkip = false;
106 return;
107 }
108
109 // See if a selection was made, if not then we need
110 // to select the first visible as a default is not
111 // present.
112 if(!selected) {
113 var first_option = null;
114 options.each(function(option) {
115 if(!option.hasClass('hidden')) {
116 if(first_option == null) {
117 first_option = option;
118 }
119 }
120 });
121 if(first_option != null) {
122 first_option.set('selected', 'selected');
123 }
124 }
125 }
126});
127
128module.OSReleaseWidget = OSReleaseWidget;
129
130}, '0.1', {'requires': ['widget', 'io']}
131);
0132
=== added file 'src/maasserver/static/js/tests/test_os_distro_select.html'
--- src/maasserver/static/js/tests/test_os_distro_select.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/tests/test_os_distro_select.html 2014-04-24 17:04:47 +0000
@@ -0,0 +1,38 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
3 <head>
4 <title>Test maas.os_distro_select</title>
5
6 <!-- YUI and test setup -->
7 <script type="text/javascript" src="../testing/yui_test_conf.js"></script>
8 <script type="text/javascript" src="/usr/share/javascript/yui3/yui/yui.js"></script>
9 <script type="text/javascript" src="../testing/testrunner.js"></script>
10 <script type="text/javascript" src="../testing/testing.js"></script>
11 <!-- The module under test -->
12 <script type="text/javascript" src="../os_distro_select.js"></script>
13 <!-- The test suite -->
14 <script type="text/javascript" src="test_os_distro_select.js"></script>
15 </head>
16 <body>
17 <span id="suite">maas.os_distro_select.tests</span>
18 <script type="text/x-template" id="select_node">
19 <select id="id_osystem">
20 <option value="" selected="selected">Default OS</option>
21 <option value="value1">Value1</option>
22 <option value="value2">Value2</option>
23 </select>
24 </script>
25 <script type="text/x-template" id="target_node">
26 <select id="id_distro_series">
27 <option value="" selected="selected">Default Release</option>
28 <option value="value1/series1">Value1Series1</option>
29 <option value="value1/series2">Value1Series2</option>
30 <option value="value1/series3">Value1Series3</option>
31 <option value="value2/series1">Value2Series1</option>
32 <option value="value2/series2">Value2Series2</option>
33 <option value="value2/series3">Value2Series3</option>
34 </select>
35 </script>
36 <div id="placeholder"></div>
37 </body>
38</html>
039
=== added file 'src/maasserver/static/js/tests/test_os_distro_select.js'
--- src/maasserver/static/js/tests/test_os_distro_select.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/tests/test_os_distro_select.js 2014-04-24 17:04:47 +0000
@@ -0,0 +1,106 @@
1/* Copyright 2012 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 */
4
5YUI({ useBrowserConsole: true }).add(
6 'maas.os_distro_select.tests', function(Y) {
7
8Y.log('loading maas.os_distro_select.tests');
9var namespace = Y.namespace('maas.os_distro_select.tests');
10
11var module = Y.maas.os_distro_select;
12var suite = new Y.Test.Suite("maas.os_distro_select Tests");
13
14var select_node_template = Y.one('#select_node').getContent();
15var target_node_template = Y.one('#target_node').getContent();
16
17suite.add(new Y.maas.testing.TestCase({
18 name: 'test-os_distro_select',
19
20 setUp: function () {
21 Y.one('#placeholder').empty().append(
22 Y.Node.create(select_node_template).append(
23 Y.Node.create(target_node_template)));
24 },
25
26 testBindToDefaultShowsDefaultRelease: function() {
27 var widget = new Y.maas.os_distro_select.OSReleaseWidget({
28 srcNode: '#id_distro_series',
29 });
30 widget.bindTo(Y.one('#id_osystem'), 'change');
31
32 var options = Y.one('#id_distro_series').all('option');
33 options.each(function(option) {
34 var value = option.get('value');
35 if(value == '') {
36 Y.Assert.isFalse(option.hasClass('hidden'));
37 }
38 else {
39 Y.Assert.isTrue(option.hasClass('hidden'));
40 }
41 });
42 },
43
44 testNonDefaultShowsRelatedReleases: function() {
45 var node = Y.one('#id_distro_series');
46 node.append('<option value="value1/">Value1Default</option>');
47
48 var widget = new Y.maas.os_distro_select.OSReleaseWidget({
49 srcNode: '#id_distro_series',
50 });
51 widget.bindTo(Y.one('#id_osystem'), 'change');
52
53 var newValue = 'value1';
54 var select = Y.one('#id_osystem');
55 select.set('value', newValue);
56 select.simulate('change');
57
58 var options = Y.one('#id_distro_series').all('option');
59 options.each(function(option) {
60 var value = option.get('value');
61 if(value == '') {
62 Y.Assert.isTrue(option.hasClass('hidden'));
63 }
64 else {
65 var split_value = value.split('/');
66 if(split_value[0] == newValue) {
67 Y.Assert.isFalse(option.hasClass('hidden'));
68 }
69 else {
70 Y.Assert.isTrue(option.hasClass('hidden'));
71 }
72 }
73 });
74
75 Y.Assert.areEqual(
76 newValue + '/', Y.one('#id_distro_series').get('value'))
77 },
78
79 testInitialSkipOnFirstChange: function() {
80 var newValue = 'value1/series1';
81 var select = Y.one('#id_osystem');
82 select.set('value', 'value1');
83 select.simulate('change');
84 select = Y.one('#id_distro_series');
85 select.set('value', newValue);
86 select.simulate('change');
87
88 var widget = new Y.maas.os_distro_select.OSReleaseWidget({
89 srcNode: '#id_distro_series',
90 });
91
92 Y.Assert.isTrue(widget.initialSkip);
93 widget.bindTo(Y.one('#id_osystem'), 'change');
94 Y.Assert.isFalse(widget.initialSkip);
95
96 Y.Assert.areEqual(
97 newValue, Y.one('#id_distro_series').get('value'))
98 }
99
100}));
101
102namespace.suite = suite;
103
104}, '0.1', {'requires': [
105 'node-event-simulate', 'test', 'maas.testing', 'maas.os_distro_select']}
106);
0107
=== modified file 'src/maasserver/templates/maasserver/bootimage-list.html'
--- src/maasserver/templates/maasserver/bootimage-list.html 2014-03-27 07:39:38 +0000
+++ src/maasserver/templates/maasserver/bootimage-list.html 2014-04-24 17:04:47 +0000
@@ -20,6 +20,7 @@
20 <thead>20 <thead>
21 <tr>21 <tr>
22 <th>ID</th>22 <th>ID</th>
23 <th>OS</th>
23 <th>Release</th>24 <th>Release</th>
24 <th>Architecture</th>25 <th>Architecture</th>
25 <th>Subarchitecture</th>26 <th>Subarchitecture</th>
@@ -32,6 +33,7 @@
32 {% for bootimage in bootimage_list %}33 {% for bootimage in bootimage_list %}
33 <tr class="bootimage {% cycle 'even' 'odd' %}">34 <tr class="bootimage {% cycle 'even' 'odd' %}">
34 <td>{{ bootimage.id }}</td>35 <td>{{ bootimage.id }}</td>
36 <td>{{ bootimage.osystem_title }}</td>
35 <td>{{ bootimage.release }}</td>37 <td>{{ bootimage.release }}</td>
36 <td>{{ bootimage.architecture }}</td>38 <td>{{ bootimage.architecture }}</td>
37 <td>{{ bootimage.subarchitecture }}</td>39 <td>{{ bootimage.subarchitecture }}</td>
3840
=== modified file 'src/maasserver/templates/maasserver/node_edit.html'
--- src/maasserver/templates/maasserver/node_edit.html 2014-03-03 09:44:25 +0000
+++ src/maasserver/templates/maasserver/node_edit.html 2014-04-24 17:04:47 +0000
@@ -11,9 +11,17 @@
11 <script type="text/javascript">11 <script type="text/javascript">
12 <!--12 <!--
13 YUI().use(13 YUI().use(
14 'maas.enums', 'maas.power_parameters',14 'maas.enums', 'maas.power_parameters', 'maas.os_distro_select',
15 function (Y) {15 function (Y) {
16 Y.on('load', function() {16 Y.on('load', function() {
17 // Create OSDistroWidget so that the release field will be
18 // updated each time the value of the os field changes.
19 var releaseWidget = new Y.maas.os_distro_select.OSReleaseWidget({
20 srcNode: '#id_distro_series'
21 });
22 releaseWidget.bindTo(Y.one('.osystem').one('select'), 'change');
23 releaseWidget.render();
24
17 // Create LinkedContentWidget widget so that the power_parameter field25 // Create LinkedContentWidget widget so that the power_parameter field
18 // will be updated each time the value of the power_type field changes.26 // will be updated each time the value of the power_type field changes.
19 var power_types = {{ power_types }};27 var power_types = {{ power_types }};
2028
=== modified file 'src/maasserver/templates/maasserver/settings.html'
--- src/maasserver/templates/maasserver/settings.html 2014-04-10 04:29:20 +0000
+++ src/maasserver/templates/maasserver/settings.html 2014-04-24 17:04:47 +0000
@@ -6,6 +6,23 @@
6{% block page-title %}Settings{% endblock %}6{% block page-title %}Settings{% endblock %}
77
8{% block head %}8{% block head %}
9 <script type="text/javascript">
10 <!--
11 YUI().use(
12 'maas.os_distro_select',
13 function (Y) {
14 Y.on('load', function() {
15 // Create OSDistroWidget so that the release field will be
16 // updated each time the value of the os field changes.
17 var releaseWidget = new Y.maas.os_distro_select.OSReleaseWidget({
18 srcNode: '#id_deploy-default_distro_series'
19 });
20 releaseWidget.bindTo(Y.one('#id_deploy-default_osystem'), 'change');
21 releaseWidget.render();
22 });
23 });
24 // -->
25 </script>
9{% endblock %}26{% endblock %}
1027
11{% block content %}28{% block content %}
@@ -90,6 +107,21 @@
90 <div class="clear"></div>107 <div class="clear"></div>
91 </div>108 </div>
92 <div class="divider"></div>109 <div class="divider"></div>
110 <div id="deploy" class="block size7 first">
111 <h2>Deploy</h2>
112 <form action="{% url "settings" %}" method="post">
113 {% csrf_token %}
114 <ul>
115 {% for field in deploy_form %}
116 {% include "maasserver/form_field.html" %}
117 {% endfor %}
118 </ul>
119 <input type="hidden" name="deploy_submit" value="1" />
120 <input type="submit" class="button right" value="Save" />
121 </form>
122 <div class="clear"></div>
123 </div>
124 <div class="divider"></div>
93 <div id="ubuntu" class="block size7 first">125 <div id="ubuntu" class="block size7 first">
94 <h2>Ubuntu</h2>126 <h2>Ubuntu</h2>
95 <form action="{% url "settings" %}" method="post">127 <form action="{% url "settings" %}" method="post">
96128
=== modified file 'src/maasserver/templates/maasserver/snippets.html'
--- src/maasserver/templates/maasserver/snippets.html 2014-03-25 13:45:52 +0000
+++ src/maasserver/templates/maasserver/snippets.html 2014-04-24 17:04:47 +0000
@@ -16,6 +16,10 @@
16 </div>16 </div>
17 </p>17 </p>
18 <p>18 <p>
19 <label for="id_osystem">OS</label>
20 {{ node_form.osystem }}
21 </p>
22 <p>
19 <label for="id_distro_series">Release</label>23 <label for="id_distro_series">Release</label>
20 {{ node_form.distro_series }}24 {{ node_form.distro_series }}
21 </p>25 </p>
2226
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2014-04-04 06:46:05 +0000
+++ src/maasserver/testing/factory.py 2014-04-24 17:04:47 +0000
@@ -56,6 +56,7 @@
56 NodeCommissionResult,56 NodeCommissionResult,
57 )57 )
58from netaddr import IPAddress58from netaddr import IPAddress
59from provisioningserver.osystems import OperatingSystemRegistry
5960
60# We have a limited number of public keys:61# We have a limited number of public keys:
61# src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub62# src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub
@@ -137,6 +138,21 @@
137 [choice for choice in list(get_power_types().keys())138 [choice for choice in list(get_power_types().keys())
138 if choice not in but_not])139 if choice not in but_not])
139140
141 def getRandomOS(self):
142 """Pick a random operating system from the registry."""
143 osystems = [obj for _, obj in OperatingSystemRegistry]
144 return random.choice(osystems)
145
146 def getRandomRelease(self, osystem):
147 """Pick a random release from operating system."""
148 releases = osystem.get_supported_releases()
149 return random.choice(releases)
150
151 def getRandomCommissioningRelease(self, osystem):
152 """Pick a random commissioning release from operating system."""
153 releases = osystem.get_supported_commissioning_releases()
154 return random.choice(releases)
155
140 def _save_node_unchecked(self, node):156 def _save_node_unchecked(self, node):
141 """Save a :class:`Node`, but circumvent status transition checks."""157 """Save a :class:`Node`, but circumvent status transition checks."""
142 valid_initial_states = NODE_TRANSITIONS[None]158 valid_initial_states = NODE_TRANSITIONS[None]
@@ -427,9 +443,11 @@
427 return "OAuth " + ", ".join([443 return "OAuth " + ", ".join([
428 '%s="%s"' % (key, value) for key, value in items.items()])444 '%s="%s"' % (key, value) for key, value in items.items()])
429445
430 def make_boot_image(self, architecture=None, subarchitecture=None,446 def make_boot_image(self, osystem=None, architecture=None,
431 release=None, purpose=None, nodegroup=None,447 subarchitecture=None, release=None, purpose=None,
432 label=None):448 nodegroup=None, label=None):
449 if osystem is None:
450 osystem = self.make_name('os')
433 if architecture is None:451 if architecture is None:
434 architecture = self.make_name('architecture')452 architecture = self.make_name('architecture')
435 if subarchitecture is None:453 if subarchitecture is None:
@@ -444,6 +462,7 @@
444 label = self.make_name('label')462 label = self.make_name('label')
445 return BootImage.objects.create(463 return BootImage.objects.create(
446 nodegroup=nodegroup,464 nodegroup=nodegroup,
465 osystem=osystem,
447 architecture=architecture,466 architecture=architecture,
448 subarchitecture=subarchitecture,467 subarchitecture=subarchitecture,
449 release=release,468 release=release,
450469
=== added file 'src/maasserver/testing/osystems.py'
--- src/maasserver/testing/osystems.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/testing/osystems.py 2014-04-24 17:04:47 +0000
@@ -0,0 +1,94 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Helpers for operating systems in testing."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 'make_usable_osystem',
17 'patch_usable_osystems',
18 ]
19
20from random import randint
21
22from maasserver import forms
23from maasserver.testing.factory import factory
24from provisioningserver.osystems import BOOT_IMAGE_PURPOSE
25from provisioningserver.boot.tests.test_tftppath import make_osystem
26
27
28def make_osystem_with_releases(testcase, with_releases=True, osystem_name=None,
29 releases=None):
30 """Generate an arbitrary operating system.
31
32 :param with_releases: Should the operating system include releases?
33 Defaults to `True`.
34 """
35 if osystem_name is None:
36 osystem_name = factory.make_name('os')
37 if with_releases:
38 if releases is None:
39 releases = [factory.make_name('release') for _ in range(3)]
40
41 osystem = make_osystem(
42 osystem_name,
43 [BOOT_IMAGE_PURPOSE.INSTALL, BOOT_IMAGE_PURPOSE.XINSTALL],
44 testcase)
45 osystem.fake_list = releases
46 return osystem
47
48
49def patch_usable_osystems(testcase, osystems=None, allow_empty=True):
50 """Set a fixed list of usable oeprating systems.
51
52 A usable operating system is one for which boot images are available.
53
54 :param testcase: A `TestCase` whose `patch` this function can use.
55 :param architectures: Optional list of oprating systems. If omitted,
56 defaults to a list (which may be empty) of random operating systems.
57 """
58 start = 0
59 if allow_empty is False:
60 start = 1
61 if osystems is None:
62 osystems = [
63 make_osystem_with_releases(testcase)
64 for _ in range(randint(start, 2))
65 ]
66 distro_series = {}
67 for osystem in osystems:
68 distro_series[osystem.name] = osystem.get_supported_releases()
69 patch = testcase.patch(forms, 'list_all_usable_osystems')
70 patch.return_value = osystems
71 patch = testcase.patch(forms, 'list_all_usable_releases')
72 patch.return_value = distro_series
73
74
75def make_usable_osystem(testcase, with_releases=True, osystem_name=None,
76 releases=None):
77 """Return arbitrary operating system, and make it "usable."
78
79 A usable operating system is one for which boot images are available.
80
81 :param testcase: A `TestCase` whose `patch` this function can pass to
82 `patch_usable_osystems`.
83 :param with_releases: Should the operating system include releases?
84 Defaults to `True`.
85 :param osystem_name: The operating system name. Useful in cases where
86 we need to test that not supplying an os works correctly.
87 :param releases: The list of releases name. Useful in cases where
88 we need to test that not supplying a release works correctly.
89 """
90 osystem = make_osystem_with_releases(
91 testcase, with_releases=with_releases, osystem_name=osystem_name,
92 releases=releases)
93 patch_usable_osystems(testcase, [osystem])
94 return osystem
095
=== modified file 'src/maasserver/tests/test_api_boot_images.py'
--- src/maasserver/tests/test_api_boot_images.py 2014-03-21 19:01:40 +0000
+++ src/maasserver/tests/test_api_boot_images.py 2014-04-24 17:04:47 +0000
@@ -134,6 +134,7 @@
134 image = factory.make_boot_image()134 image = factory.make_boot_image()
135 self.assertEqual(135 self.assertEqual(
136 (136 (
137 image.osystem,
137 image.architecture,138 image.architecture,
138 image.subarchitecture,139 image.subarchitecture,
139 image.release,140 image.release,
@@ -146,6 +147,7 @@
146 image = make_boot_image_params()147 image = make_boot_image_params()
147 self.assertEqual(148 self.assertEqual(
148 (149 (
150 image['osystem'],
149 image['architecture'],151 image['architecture'],
150 image['subarchitecture'],152 image['subarchitecture'],
151 image['release'],153 image['release'],
@@ -158,12 +160,13 @@
158 image = make_boot_image_params()160 image = make_boot_image_params()
159 del image['subarchitecture']161 del image['subarchitecture']
160 del image['label']162 del image['label']
161 _, subarchitecture, _, label, _ = summarise_boot_image_dict(image)163 _, _, subarchitecture, _, label, _ = summarise_boot_image_dict(image)
162 self.assertEqual(('generic', 'release'), (subarchitecture, label))164 self.assertEqual(('generic', 'release'), (subarchitecture, label))
163165
164 def test_summarise_boot_image_functions_are_compatible(self):166 def test_summarise_boot_image_functions_are_compatible(self):
165 image_dict = make_boot_image_params()167 image_dict = make_boot_image_params()
166 image_obj = factory.make_boot_image(168 image_obj = factory.make_boot_image(
169 osystem=image_dict['osystem'],
167 architecture=image_dict['architecture'],170 architecture=image_dict['architecture'],
168 subarchitecture=image_dict['subarchitecture'],171 subarchitecture=image_dict['subarchitecture'],
169 release=image_dict['release'], label=image_dict['label'],172 release=image_dict['release'], label=image_dict['label'],
170173
=== modified file 'src/maasserver/tests/test_api_node.py'
--- src/maasserver/tests/test_api_node.py 2014-04-22 10:26:47 +0000
+++ src/maasserver/tests/test_api_node.py 2014-04-24 17:04:47 +0000
@@ -23,7 +23,6 @@
23import bson23import bson
24from django.core.urlresolvers import reverse24from django.core.urlresolvers import reverse
25from maasserver.enum import (25from maasserver.enum import (
26 DISTRO_SERIES,
27 NODE_STATUS,26 NODE_STATUS,
28 NODE_STATUS_CHOICES_DICT,27 NODE_STATUS_CHOICES_DICT,
29 )28 )
@@ -48,6 +47,7 @@
48 NodeUserData,47 NodeUserData,
49 )48 )
50from metadataserver.nodeinituser import get_node_init_user49from metadataserver.nodeinituser import get_node_init_user
50from provisioningserver.osystems.ubuntu import UbuntuOS
5151
5252
53class NodeAnonAPITest(MAASServerTestCase):53class NodeAnonAPITest(MAASServerTestCase):
@@ -221,11 +221,60 @@
221 self.assertEqual(221 self.assertEqual(
222 node.system_id, json.loads(response.content)['system_id'])222 node.system_id, json.loads(response.content)['system_id'])
223223
224 def test_POST_start_sets_distro_series(self):224 def test_POST_start_sets_osystem(self):
225 node = factory.make_node(225 node = factory.make_node(
226 owner=self.logged_in_user, mac=True,226 owner=self.logged_in_user, mac=True,
227 power_type='ether_wake')227 power_type='ether_wake')
228 distro_series = factory.getRandomEnum(DISTRO_SERIES)228 osystem = factory.getRandomOS()
229 response = self.client.post(
230 self.get_node_uri(node),
231 {'op': 'start', 'osystem': osystem.name})
232 self.assertEqual(
233 (httplib.OK, node.system_id),
234 (response.status_code, json.loads(response.content)['system_id']))
235 self.assertEqual(
236 osystem.name, reload_object(node).osystem)
237
238 def test_POST_start_accepts_os(self):
239 node = factory.make_node(
240 owner=self.logged_in_user, mac=True,
241 power_type='ether_wake')
242 osystem = factory.getRandomOS()
243 response = self.client.post(
244 self.get_node_uri(node),
245 {'op': 'start', 'os': osystem.name})
246 self.assertEqual(
247 (httplib.OK, node.system_id),
248 (response.status_code, json.loads(response.content)['system_id']))
249 self.assertEqual(
250 osystem.name, reload_object(node).osystem)
251
252 def test_POST_start_sets_osystem_and_distro_series(self):
253 node = factory.make_node(
254 owner=self.logged_in_user, mac=True,
255 power_type='ether_wake')
256 osystem = factory.getRandomOS()
257 distro_series = factory.getRandomRelease(osystem)
258 response = self.client.post(
259 self.get_node_uri(node), {
260 'op': 'start',
261 'osystem': osystem.name,
262 'distro_series': distro_series
263 })
264 self.assertEqual(
265 (httplib.OK, node.system_id),
266 (response.status_code, json.loads(response.content)['system_id']))
267 self.assertEqual(
268 osystem.name, reload_object(node).osystem)
269 self.assertEqual(
270 distro_series, reload_object(node).distro_series)
271
272 def test_POST_start_sets_distro_series_defaults_ubuntu(self):
273 node = factory.make_node(
274 owner=self.logged_in_user, mac=True,
275 power_type='ether_wake')
276 osystem = UbuntuOS()
277 distro_series = factory.getRandomRelease(osystem)
229 response = self.client.post(278 response = self.client.post(
230 self.get_node_uri(node),279 self.get_node_uri(node),
231 {'op': 'start', 'distro_series': distro_series})280 {'op': 'start', 'distro_series': distro_series})
@@ -233,8 +282,27 @@
233 (httplib.OK, node.system_id),282 (httplib.OK, node.system_id),
234 (response.status_code, json.loads(response.content)['system_id']))283 (response.status_code, json.loads(response.content)['system_id']))
235 self.assertEqual(284 self.assertEqual(
285 osystem.name, reload_object(node).osystem)
286 self.assertEqual(
236 distro_series, reload_object(node).distro_series)287 distro_series, reload_object(node).distro_series)
237288
289 def test_POST_start_validates_osystem(self):
290 node = factory.make_node(
291 owner=self.logged_in_user, mac=True,
292 power_type='ether_wake')
293 invalid_osystem = factory.getRandomString()
294 response = self.client.post(
295 self.get_node_uri(node),
296 {'op': 'start', 'osystem': invalid_osystem})
297 self.assertEqual(
298 (
299 httplib.BAD_REQUEST,
300 {'osystem': [
301 "Value u'%s' is not a valid choice." %
302 invalid_osystem]}
303 ),
304 (response.status_code, json.loads(response.content)))
305
238 def test_POST_start_validates_distro_series(self):306 def test_POST_start_validates_distro_series(self):
239 node = factory.make_node(307 node = factory.make_node(
240 owner=self.logged_in_user, mac=True,308 owner=self.logged_in_user, mac=True,
@@ -300,18 +368,23 @@
300 self.client.post(self.get_node_uri(node), {'op': 'release'})368 self.client.post(self.get_node_uri(node), {'op': 'release'})
301 self.assertTrue(reload_object(node).netboot)369 self.assertTrue(reload_object(node).netboot)
302370
303 def test_POST_release_resets_distro_series(self):371 def test_POST_release_resets_osystem_and_distro_series(self):
372 osystem = factory.getRandomOS()
373 release = factory.getRandomRelease(osystem)
304 node = factory.make_node(374 node = factory.make_node(
305 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user,375 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user,
306 distro_series=factory.getRandomEnum(DISTRO_SERIES))376 osystem=osystem.name, distro_series=release)
307 self.client.post(self.get_node_uri(node), {'op': 'release'})377 self.client.post(self.get_node_uri(node), {'op': 'release'})
378 self.assertEqual('', reload_object(node).osystem)
308 self.assertEqual('', reload_object(node).distro_series)379 self.assertEqual('', reload_object(node).distro_series)
309380
310 def test_POST_release_resets_agent_name(self):381 def test_POST_release_resets_agent_name(self):
311 agent_name = factory.make_name('agent-name')382 agent_name = factory.make_name('agent-name')
383 osystem = factory.getRandomOS()
384 release = factory.getRandomRelease(osystem)
312 node = factory.make_node(385 node = factory.make_node(
313 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user,386 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user,
314 distro_series=factory.getRandomEnum(DISTRO_SERIES),387 osystem=osystem.name, distro_series=release,
315 agent_name=agent_name)388 agent_name=agent_name)
316 self.client.post(self.get_node_uri(node), {'op': 'release'})389 self.client.post(self.get_node_uri(node), {'op': 'release'})
317 self.assertEqual('', reload_object(node).agent_name)390 self.assertEqual('', reload_object(node).agent_name)
318391
=== modified file 'src/maasserver/tests/test_api_pxeconfig.py'
--- src/maasserver/tests/test_api_pxeconfig.py 2014-03-27 04:15:45 +0000
+++ src/maasserver/tests/test_api_pxeconfig.py 2014-04-24 17:04:47 +0000
@@ -133,9 +133,11 @@
133 self.assertEqual(value, response_dict['extra_opts'])133 self.assertEqual(value, response_dict['extra_opts'])
134134
135 def test_pxeconfig_uses_present_boot_image(self):135 def test_pxeconfig_uses_present_boot_image(self):
136 osystem = Config.objects.get_config('commissioning_osystem')
136 release = Config.objects.get_config('commissioning_distro_series')137 release = Config.objects.get_config('commissioning_distro_series')
137 nodegroup = factory.make_node_group()138 nodegroup = factory.make_node_group()
138 factory.make_boot_image(139 factory.make_boot_image(
140 osystem=osystem,
139 architecture="amd64", release=release, nodegroup=nodegroup,141 architecture="amd64", release=release, nodegroup=nodegroup,
140 purpose="commissioning")142 purpose="commissioning")
141 params = self.get_default_params()143 params = self.get_default_params()
@@ -282,7 +284,8 @@
282 def test_get_boot_purpose_unknown_node(self):284 def test_get_boot_purpose_unknown_node(self):
283 # A node that's not yet known to MAAS is assumed to be enlisting,285 # A node that's not yet known to MAAS is assumed to be enlisting,
284 # which uses a "commissioning" image.286 # which uses a "commissioning" image.
285 self.assertEqual("commissioning", api.get_boot_purpose(None))287 self.assertEqual("commissioning", api.get_boot_purpose(
288 None, None, None, None, None, None))
286289
287 def test_get_boot_purpose_known_node(self):290 def test_get_boot_purpose_known_node(self):
288 # The following table shows the expected boot "purpose" for each set291 # The following table shows the expected boot "purpose" for each set
@@ -305,11 +308,17 @@
305 node.use_fastpath_installer()308 node.use_fastpath_installer()
306 for name, value in parameters.items():309 for name, value in parameters.items():
307 setattr(node, name, value)310 setattr(node, name, value)
308 self.assertEqual(purpose, api.get_boot_purpose(node))311 osystem = node.get_osystem()
312 series = node.get_distro_series()
313 arch, subarch = node.architecture.split('/')
314 self.assertEqual(
315 purpose,
316 api.get_boot_purpose(
317 node, osystem, arch, subarch, series, None))
309318
310 def test_pxeconfig_uses_boot_purpose(self):319 def test_pxeconfig_uses_boot_purpose(self):
311 fake_boot_purpose = factory.make_name("purpose")320 fake_boot_purpose = factory.make_name("purpose")
312 self.patch(api, "get_boot_purpose", lambda node: fake_boot_purpose)321 self.patch(api, "get_boot_purpose").return_value = fake_boot_purpose
313 response = self.client.get(reverse('pxeconfig'),322 response = self.client.get(reverse('pxeconfig'),
314 self.get_default_params())323 self.get_default_params())
315 self.assertEqual(324 self.assertEqual(
316325
=== modified file 'src/maasserver/tests/test_compose_preseed.py'
--- src/maasserver/tests/test_compose_preseed.py 2013-10-07 09:12:40 +0000
+++ src/maasserver/tests/test_compose_preseed.py 2014-04-24 17:04:47 +0000
@@ -17,8 +17,10 @@
17from maasserver.compose_preseed import compose_preseed17from maasserver.compose_preseed import compose_preseed
18from maasserver.enum import NODE_STATUS18from maasserver.enum import NODE_STATUS
19from maasserver.testing.factory import factory19from maasserver.testing.factory import factory
20from maasserver.testing.osystems import make_usable_osystem
20from maasserver.testing.testcase import MAASServerTestCase21from maasserver.testing.testcase import MAASServerTestCase
21from maasserver.utils import absolute_reverse22from maasserver.utils import absolute_reverse
23from maastesting.matchers import MockCalledOnceWith
22from metadataserver.models import NodeKey24from metadataserver.models import NodeKey
23from testtools.matchers import (25from testtools.matchers import (
24 KeysEqual,26 KeysEqual,
@@ -110,3 +112,15 @@
110 self.assertEqual(112 self.assertEqual(
111 absolute_reverse('curtin-metadata'),113 absolute_reverse('curtin-metadata'),
112 preseed['datasource']['MAAS']['metadata_url'])114 preseed['datasource']['MAAS']['metadata_url'])
115
116 def test_compose_preseed_with_osystem_compose_preseed(self):
117 osystem = make_usable_osystem(self)
118 mock_compose = self.patch(osystem, 'compose_preseed')
119 node = factory.make_node(osystem=osystem.name, status=NODE_STATUS.READY)
120
121 token = NodeKey.objects.get_token_for_node(node)
122 url = absolute_reverse('metadata')
123 compose_preseed(node)
124 self.assertThat(
125 mock_compose,
126 MockCalledOnceWith(node, token, url))
113127
=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py 2014-04-22 09:08:02 +0000
+++ src/maasserver/tests/test_forms.py 2014-04-24 17:04:47 +0000
@@ -90,6 +90,11 @@
90 make_usable_architecture,90 make_usable_architecture,
91 patch_usable_architectures,91 patch_usable_architectures,
92 )92 )
93from maasserver.testing.osystems import (
94 make_osystem_with_releases,
95 make_usable_osystem,
96 patch_usable_osystems,
97 )
93from maasserver.testing.factory import factory98from maasserver.testing.factory import factory
94from maasserver.testing.testcase import MAASServerTestCase99from maasserver.testing.testcase import MAASServerTestCase
95from maasserver.utils import map_enum100from maasserver.utils import map_enum
@@ -421,6 +426,7 @@
421 [426 [
422 'hostname',427 'hostname',
423 'architecture',428 'architecture',
429 'osystem',
424 'distro_series',430 'distro_series',
425 'nodegroup',431 'nodegroup',
426 ], list(form.fields))432 ], list(form.fields))
@@ -480,6 +486,64 @@
480 [NO_ARCHITECTURES_AVAILABLE],486 [NO_ARCHITECTURES_AVAILABLE],
481 form.errors['architecture'])487 form.errors['architecture'])
482488
489 def test_accepts_osystem(self):
490 osystem = make_usable_osystem(self)
491 form = NodeForm(data={
492 'hostname': factory.make_name('host'),
493 'architecture': make_usable_architecture(self),
494 'osystem': osystem.name,
495 })
496 self.assertTrue(form.is_valid(), form._errors)
497
498 def test_rejects_invalid_osystem(self):
499 patch_usable_osystems(self)
500 form = NodeForm(data={
501 'hostname': factory.make_name('host'),
502 'architecture': make_usable_architecture(self),
503 'osystem': factory.make_name('os'),
504 })
505 self.assertFalse(form.is_valid())
506 self.assertItemsEqual(['osystem'], form._errors.keys())
507
508 def test_starts_with_default_osystem(self):
509 osystems = [make_osystem_with_releases(self) for _ in range(5)]
510 patch_usable_osystems(self, osystems)
511 form = NodeForm()
512 self.assertEqual(
513 '',
514 form.fields['osystem'].initial)
515
516 def test_accepts_osystem_distro_series(self):
517 osystem = make_usable_osystem(self)
518 release = osystem.get_default_release()
519 form = NodeForm(data={
520 'hostname': factory.make_name('host'),
521 'architecture': make_usable_architecture(self),
522 'osystem': osystem.name,
523 'distro_series': '%s/%s' % (osystem.name, release),
524 })
525 self.assertTrue(form.is_valid(), form._errors)
526
527 def test_rejects_invalid_osystem_distro_series(self):
528 osystem = make_usable_osystem(self)
529 release = factory.make_name('release')
530 form = NodeForm(data={
531 'hostname': factory.make_name('host'),
532 'architecture': make_usable_architecture(self),
533 'osystem': osystem.name,
534 'distro_series': '%s/%s' % (osystem.name, release),
535 })
536 self.assertFalse(form.is_valid())
537 self.assertItemsEqual(['distro_series'], form._errors.keys())
538
539 def test_starts_with_default_distro_series(self):
540 osystems = [make_osystem_with_releases(self) for _ in range(5)]
541 patch_usable_osystems(self, osystems)
542 form = NodeForm()
543 self.assertEqual(
544 '',
545 form.fields['distro_series'].initial)
546
483547
484class TestAdminNodeForm(MAASServerTestCase):548class TestAdminNodeForm(MAASServerTestCase):
485549
@@ -491,6 +555,7 @@
491 [555 [
492 'hostname',556 'hostname',
493 'architecture',557 'architecture',
558 'osystem',
494 'distro_series',559 'distro_series',
495 'power_type',560 'power_type',
496 'power_parameters',561 'power_parameters',
497562
=== modified file 'src/maasserver/tests/test_preseed.py'
--- src/maasserver/tests/test_preseed.py 2014-04-21 11:43:26 +0000
+++ src/maasserver/tests/test_preseed.py 2014-04-24 17:04:47 +0000
@@ -22,7 +22,6 @@
22from django.conf import settings22from django.conf import settings
23from django.core.urlresolvers import reverse23from django.core.urlresolvers import reverse
24from maasserver.enum import (24from maasserver.enum import (
25 DISTRO_SERIES,
26 NODE_STATUS,25 NODE_STATUS,
27 NODEGROUPINTERFACE_MANAGEMENT,26 NODEGROUPINTERFACE_MANAGEMENT,
28 PRESEED_TYPE,27 PRESEED_TYPE,
@@ -590,6 +589,7 @@
590 node = factory.make_node()589 node = factory.make_node()
591 arch, subarch = node.architecture.split('/')590 arch, subarch = node.architecture.split('/')
592 factory.make_boot_image(591 factory.make_boot_image(
592 osystem=node.get_osystem(),
593 architecture=arch, subarchitecture=subarch,593 architecture=arch, subarchitecture=subarch,
594 release=node.get_distro_series(), purpose='xinstall',594 release=node.get_distro_series(), purpose='xinstall',
595 nodegroup=node.nodegroup)595 nodegroup=node.nodegroup)
@@ -677,34 +677,37 @@
677 self.assertIn('cloud-init', context['curtin_preseed'])677 self.assertIn('cloud-init', context['curtin_preseed'])
678678
679 def test_get_curtin_installer_url_returns_url(self):679 def test_get_curtin_installer_url_returns_url(self):
680 # Exclude DISTRO_SERIES.default. It's a special value that defers680 osystem = factory.getRandomOS()
681 # to a run-time setting which we don't provide in this test.681 series = factory.getRandomRelease(osystem)
682 series = factory.getRandomEnum(
683 DISTRO_SERIES, but_not=DISTRO_SERIES.default)
684 architecture = make_usable_architecture(self)682 architecture = make_usable_architecture(self)
685 node = factory.make_node(683 node = factory.make_node(
686 architecture=architecture, distro_series=series)684 osystem=osystem.name, architecture=architecture,
685 distro_series=series)
687 arch, subarch = architecture.split('/')686 arch, subarch = architecture.split('/')
688 boot_image = factory.make_boot_image(687 boot_image = factory.make_boot_image(
689 architecture=arch, subarchitecture=subarch, release=series,688 osystem=osystem.name, architecture=arch,
689 subarchitecture=subarch, release=series,
690 purpose='xinstall', nodegroup=node.nodegroup)690 purpose='xinstall', nodegroup=node.nodegroup)
691691
692 installer_url = get_curtin_installer_url(node)692 installer_url = get_curtin_installer_url(node)
693693
694 [interface] = node.nodegroup.get_managed_interfaces()694 [interface] = node.nodegroup.get_managed_interfaces()
695 self.assertEqual(695 self.assertEqual(
696 'http://%s/MAAS/static/images/%s/%s/%s/%s/root-tgz' % (696 'http://%s/MAAS/static/images/%s/%s/%s/%s/%s/root-tgz' % (
697 interface.ip, arch, subarch, series, boot_image.label),697 interface.ip, osystem.name, arch, subarch,
698 series, boot_image.label),
698 installer_url)699 installer_url)
699700
700 def test_get_curtin_installer_url_fails_if_no_boot_image(self):701 def test_get_curtin_installer_url_fails_if_no_boot_image(self):
701 series = factory.getRandomEnum(702 osystem = factory.getRandomOS()
702 DISTRO_SERIES, but_not=DISTRO_SERIES.default)703 series = factory.getRandomRelease(osystem)
703 architecture = make_usable_architecture(self)704 architecture = make_usable_architecture(self)
704 node = factory.make_node(705 node = factory.make_node(
706 osystem=osystem.name,
705 architecture=architecture, distro_series=series)707 architecture=architecture, distro_series=series)
706 # Generate a boot image with a different arch/subarch.708 # Generate a boot image with a different arch/subarch.
707 factory.make_boot_image(709 factory.make_boot_image(
710 osystem=osystem.name,
708 architecture=factory.make_name('arch'),711 architecture=factory.make_name('arch'),
709 subarchitecture=factory.make_name('subarch'), release=series,712 subarchitecture=factory.make_name('subarch'), release=series,
710 purpose='xinstall', nodegroup=node.nodegroup)713 purpose='xinstall', nodegroup=node.nodegroup)
@@ -714,7 +717,8 @@
714 arch, subarch = architecture.split('/')717 arch, subarch = architecture.split('/')
715 msg = (718 msg = (
716 "No image could be found for the given selection: "719 "No image could be found for the given selection: "
717 "arch=%s, subarch=%s, series=%s, purpose=xinstall." % (720 "os=%s, arch=%s, subarch=%s, series=%s, purpose=xinstall." % (
721 osystem.name,
718 arch,722 arch,
719 subarch,723 subarch,
720 node.get_distro_series(),724 node.get_distro_series(),
721725
=== modified file 'src/maasserver/views/clusters.py'
--- src/maasserver/views/clusters.py 2014-04-03 11:20:03 +0000
+++ src/maasserver/views/clusters.py 2014-04-24 17:04:47 +0000
@@ -51,6 +51,7 @@
51 NodeGroupInterface,51 NodeGroupInterface,
52 )52 )
53from maasserver.views import PaginatedListView53from maasserver.views import PaginatedListView
54from provisioningserver.osystems import OperatingSystemRegistry
5455
5556
56class ClusterListView(PaginatedListView, FormMixin, ProcessFormView):57class ClusterListView(PaginatedListView, FormMixin, ProcessFormView):
@@ -270,15 +271,23 @@
270 nodegroup_uuid = self.kwargs.get('uuid', None)271 nodegroup_uuid = self.kwargs.get('uuid', None)
271 return get_object_or_404(NodeGroup, uuid=nodegroup_uuid)272 return get_object_or_404(NodeGroup, uuid=nodegroup_uuid)
272273
274 def get_osystem_title(self, osystem):
275 osystem_obj = OperatingSystemRegistry.get_item(osystem, default=None)
276 if osystem_obj is None:
277 return osystem
278 return osystem_obj.title
279
273 def get_context_data(self, **kwargs):280 def get_context_data(self, **kwargs):
274 context = super(281 context = super(
275 BootImagesListView, self).get_context_data(**kwargs)282 BootImagesListView, self).get_context_data(**kwargs)
276 context['nodegroup'] = self.get_nodegroup()283 context['nodegroup'] = self.get_nodegroup()
284 for bootimage in context['bootimage_list']:
285 bootimage.osystem_title = self.get_osystem_title(bootimage.osystem)
277 return context286 return context
278287
279 def get_queryset(self):288 def get_queryset(self):
280 nodegroup = self.get_nodegroup()289 nodegroup = self.get_nodegroup()
281 # A sorted bootimages list.290 # A sorted bootimages list.
282 return nodegroup.bootimage_set.all().order_by(291 return nodegroup.bootimage_set.all().order_by(
283 '-release', 'architecture', 'subarchitecture', 'purpose',292 'osystem', '-release', 'architecture', 'subarchitecture',
284 'label')293 'purpose', 'label')
285294
=== modified file 'src/maasserver/views/settings.py'
--- src/maasserver/views/settings.py 2014-04-10 13:43:33 +0000
+++ src/maasserver/views/settings.py 2014-04-24 17:04:47 +0000
@@ -41,6 +41,7 @@
41from maasserver.exceptions import CannotDeleteUserException41from maasserver.exceptions import CannotDeleteUserException
42from maasserver.forms import (42from maasserver.forms import (
43 CommissioningForm,43 CommissioningForm,
44 DeployForm,
44 EditUserForm,45 EditUserForm,
45 GlobalKernelOptsForm,46 GlobalKernelOptsForm,
46 MAASAndNetworkForm,47 MAASAndNetworkForm,
@@ -177,6 +178,13 @@
177 if response is not None:178 if response is not None:
178 return response179 return response
179180
181 # Process the Deploy form.
182 deploy_form, response = process_form(
183 request, DeployForm, reverse('settings'), 'deploy',
184 "Configuration updated.")
185 if response is not None:
186 return response
187
180 # Process the Ubuntu form.188 # Process the Ubuntu form.
181 ubuntu_form, response = process_form(189 ubuntu_form, response = process_form(
182 request, UbuntuForm, reverse('settings'), 'ubuntu',190 request, UbuntuForm, reverse('settings'), 'ubuntu',
@@ -202,6 +210,7 @@
202 'maas_and_network_form': maas_and_network_form,210 'maas_and_network_form': maas_and_network_form,
203 'third_party_drivers_form': third_party_drivers_form,211 'third_party_drivers_form': third_party_drivers_form,
204 'commissioning_form': commissioning_form,212 'commissioning_form': commissioning_form,
213 'deploy_form': deploy_form,
205 'ubuntu_form': ubuntu_form,214 'ubuntu_form': ubuntu_form,
206 'kernelopts_form': kernelopts_form,215 'kernelopts_form': kernelopts_form,
207 },216 },
208217
=== modified file 'src/maasserver/views/tests/test_boot_image_list.py'
--- src/maasserver/views/tests/test_boot_image_list.py 2014-04-02 13:53:19 +0000
+++ src/maasserver/views/tests/test_boot_image_list.py 2014-04-24 17:04:47 +0000
@@ -22,6 +22,7 @@
22from maasserver.testing.factory import factory22from maasserver.testing.factory import factory
23from maasserver.testing.testcase import MAASServerTestCase23from maasserver.testing.testcase import MAASServerTestCase
24from maasserver.views.clusters import BootImagesListView24from maasserver.views.clusters import BootImagesListView
25from provisioningserver.boot.tests.test_tftppath import make_osystem
25from testtools.matchers import ContainsAll26from testtools.matchers import ContainsAll
2627
2728
@@ -32,6 +33,7 @@
32 nodegroup = factory.make_node_group()33 nodegroup = factory.make_node_group()
33 images = [34 images = [
34 factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]35 factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]
36 [make_osystem(bi.osystem, ['install'], self) for bi in images]
35 response = self.client.get(37 response = self.client.get(
36 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))38 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))
37 self.assertEqual(39 self.assertEqual(
@@ -54,8 +56,11 @@
54 self.client_log_in(as_admin=True)56 self.client_log_in(as_admin=True)
55 nodegroup = factory.make_node_group()57 nodegroup = factory.make_node_group()
56 # Create 4 images.58 # Create 4 images.
57 [59 boot_images = [
58 factory.make_boot_image(nodegroup=nodegroup) for _ in range(4)]60 factory.make_boot_image(nodegroup=nodegroup)
61 for _ in range(4)
62 ]
63 [make_osystem(bi.osystem, ['install'], self) for bi in boot_images]
59 response = self.client.get(64 response = self.client.get(
60 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))65 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))
61 self.assertEqual(httplib.OK, response.status_code)66 self.assertEqual(httplib.OK, response.status_code)
@@ -66,7 +71,8 @@
6671
67 def test_displays_warning_if_boot_image_list_is_empty(self):72 def test_displays_warning_if_boot_image_list_is_empty(self):
68 # Create boot images in another nodegroup.73 # Create boot images in another nodegroup.
69 [factory.make_boot_image() for _ in range(3)]74 boot_images = [factory.make_boot_image() for _ in range(3)]
75 [make_osystem(bi.osystem, ['install'], self) for bi in boot_images]
70 self.client_log_in(as_admin=True)76 self.client_log_in(as_admin=True)
71 nodegroup = factory.make_node_group()77 nodegroup = factory.make_node_group()
72 response = self.client.get(78 response = self.client.get(
7379
=== modified file 'src/maasserver/views/tests/test_clusters.py'
--- src/maasserver/views/tests/test_clusters.py 2014-04-04 06:46:05 +0000
+++ src/maasserver/views/tests/test_clusters.py 2014-04-24 17:04:47 +0000
@@ -41,6 +41,7 @@
41 ANY,41 ANY,
42 call,42 call,
43 )43 )
44from provisioningserver.boot.tests.test_tftppath import make_osystem
44from testtools.matchers import (45from testtools.matchers import (
45 AllMatch,46 AllMatch,
46 Contains,47 Contains,
@@ -309,7 +310,11 @@
309 def test_contains_link_to_boot_image_list(self):310 def test_contains_link_to_boot_image_list(self):
310 self.client_log_in(as_admin=True)311 self.client_log_in(as_admin=True)
311 nodegroup = factory.make_node_group()312 nodegroup = factory.make_node_group()
312 [factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]313 boot_images = [
314 factory.make_boot_image(nodegroup=nodegroup)
315 for _ in range(3)
316 ]
317 [make_osystem(bi.osystem, ['install'], self) for bi in boot_images]
313 response = self.client.get(318 response = self.client.get(
314 reverse('cluster-edit', args=[nodegroup.uuid]))319 reverse('cluster-edit', args=[nodegroup.uuid]))
315 self.assertEqual(320 self.assertEqual(
@@ -320,7 +325,8 @@
320325
321 def test_displays_warning_if_boot_image_list_is_empty(self):326 def test_displays_warning_if_boot_image_list_is_empty(self):
322 # Create boot images in another nodegroup.327 # Create boot images in another nodegroup.
323 [factory.make_boot_image() for _ in range(3)]328 boot_images = [factory.make_boot_image() for _ in range(3)]
329 [make_osystem(bi.osystem, ['install'], self) for bi in boot_images]
324 self.client_log_in(as_admin=True)330 self.client_log_in(as_admin=True)
325 nodegroup = factory.make_node_group()331 nodegroup = factory.make_node_group()
326 response = self.client.get(332 response = self.client.get(
327333
=== modified file 'src/maasserver/views/tests/test_settings.py'
--- src/maasserver/views/tests/test_settings.py 2014-04-10 13:43:33 +0000
+++ src/maasserver/views/tests/test_settings.py 2014-04-24 17:04:47 +0000
@@ -20,10 +20,7 @@
20from django.contrib.auth.models import User20from django.contrib.auth.models import User
21from django.core.urlresolvers import reverse21from django.core.urlresolvers import reverse
22from lxml.html import fromstring22from lxml.html import fromstring
23from maasserver.enum import (23from maasserver.models.config import DEFAULT_OS
24 COMMISSIONING_DISTRO_SERIES_CHOICES,
25 DISTRO_SERIES,
26 )
27from maasserver.models import (24from maasserver.models import (
28 Config,25 Config,
29 UserProfile,26 UserProfile,
@@ -112,8 +109,7 @@
112 def test_settings_commissioning_POST(self):109 def test_settings_commissioning_POST(self):
113 self.client_log_in(as_admin=True)110 self.client_log_in(as_admin=True)
114 new_check_compatibility = factory.getRandomBoolean()111 new_check_compatibility = factory.getRandomBoolean()
115 new_commissioning_distro_series = factory.getRandomChoice(112 new_commissioning = factory.getRandomCommissioningRelease(DEFAULT_OS)
116 COMMISSIONING_DISTRO_SERIES_CHOICES)
117 response = self.client.post(113 response = self.client.post(
118 reverse('settings'),114 reverse('settings'),
119 get_prefixed_form_data(115 get_prefixed_form_data(
@@ -121,14 +117,14 @@
121 data={117 data={
122 'check_compatibility': new_check_compatibility,118 'check_compatibility': new_check_compatibility,
123 'commissioning_distro_series': (119 'commissioning_distro_series': (
124 new_commissioning_distro_series),120 new_commissioning),
125 }))121 }))
126122
127 self.assertEqual(httplib.FOUND, response.status_code)123 self.assertEqual(httplib.FOUND, response.status_code)
128 self.assertEqual(124 self.assertEqual(
129 (125 (
130 new_check_compatibility,126 new_check_compatibility,
131 new_commissioning_distro_series,127 new_commissioning,
132 ),128 ),
133 (129 (
134 Config.objects.get_config('check_compatibility'),130 Config.objects.get_config('check_compatibility'),
@@ -156,11 +152,38 @@
156 Config.objects.get_config('enable_third_party_drivers'),152 Config.objects.get_config('enable_third_party_drivers'),
157 ))153 ))
158154
155 def test_settings_deploy_POST(self):
156 self.client_log_in(as_admin=True)
157 new_osystem = factory.getRandomOS()
158 new_default_osystem = new_osystem.name
159 new_default_distro_series = factory.getRandomRelease(new_osystem)
160 response = self.client.post(
161 reverse('settings'),
162 get_prefixed_form_data(
163 prefix='deploy',
164 data={
165 'default_osystem': new_default_osystem,
166 'default_distro_series': '%s/%s' % (
167 new_default_osystem,
168 new_default_distro_series
169 ),
170 }))
171
172 self.assertEqual(httplib.FOUND, response.status_code, response.content)
173 self.assertEqual(
174 (
175 new_default_osystem,
176 new_default_distro_series,
177 ),
178 (
179 Config.objects.get_config('default_osystem'),
180 Config.objects.get_config('default_distro_series'),
181 ))
182
159 def test_settings_ubuntu_POST(self):183 def test_settings_ubuntu_POST(self):
160 self.client_log_in(as_admin=True)184 self.client_log_in(as_admin=True)
161 new_main_archive = 'http://test.example.com/archive'185 new_main_archive = 'http://test.example.com/archive'
162 new_ports_archive = 'http://test2.example.com/archive'186 new_ports_archive = 'http://test2.example.com/archive'
163 new_default_distro_series = factory.getRandomEnum(DISTRO_SERIES)
164 response = self.client.post(187 response = self.client.post(
165 reverse('settings'),188 reverse('settings'),
166 get_prefixed_form_data(189 get_prefixed_form_data(
@@ -168,7 +191,6 @@
168 data={191 data={
169 'main_archive': new_main_archive,192 'main_archive': new_main_archive,
170 'ports_archive': new_ports_archive,193 'ports_archive': new_ports_archive,
171 'default_distro_series': new_default_distro_series,
172 }))194 }))
173195
174 self.assertEqual(httplib.FOUND, response.status_code, response.content)196 self.assertEqual(httplib.FOUND, response.status_code, response.content)
@@ -176,12 +198,10 @@
176 (198 (
177 new_main_archive,199 new_main_archive,
178 new_ports_archive,200 new_ports_archive,
179 new_default_distro_series,
180 ),201 ),
181 (202 (
182 Config.objects.get_config('main_archive'),203 Config.objects.get_config('main_archive'),
183 Config.objects.get_config('ports_archive'),204 Config.objects.get_config('ports_archive'),
184 Config.objects.get_config('default_distro_series'),
185 ))205 ))
186206
187 def test_settings_kernelopts_POST(self):207 def test_settings_kernelopts_POST(self):
188208
=== modified file 'src/metadataserver/tests/test_api.py'
--- src/metadataserver/tests/test_api.py 2014-03-24 13:02:28 +0000
+++ src/metadataserver/tests/test_api.py 2014-04-24 17:04:47 +0000
@@ -394,6 +394,7 @@
394 node = factory.make_node()394 node = factory.make_node()
395 arch, subarch = node.architecture.split('/')395 arch, subarch = node.architecture.split('/')
396 factory.make_boot_image(396 factory.make_boot_image(
397 osystem=node.get_osystem(),
397 architecture=arch, subarchitecture=subarch,398 architecture=arch, subarchitecture=subarch,
398 release=node.get_distro_series(), purpose='xinstall',399 release=node.get_distro_series(), purpose='xinstall',
399 nodegroup=node.nodegroup)400 nodegroup=node.nodegroup)
400401
=== modified file 'src/provisioningserver/boot/__init__.py'
--- src/provisioningserver/boot/__init__.py 2014-03-28 16:46:55 +0000
+++ src/provisioningserver/boot/__init__.py 2014-04-24 17:04:47 +0000
@@ -168,7 +168,7 @@
168 """168 """
169 def image_dir(params):169 def image_dir(params):
170 return compose_image_path(170 return compose_image_path(
171 params.arch, params.subarch,171 params.osystem, params.arch, params.subarch,
172 params.release, params.label)172 params.release, params.label)
173173
174 def initrd_path(params):174 def initrd_path(params):
175175
=== modified file 'src/provisioningserver/boot/tests/test_pxe.py'
--- src/provisioningserver/boot/tests/test_pxe.py 2014-03-28 04:31:32 +0000
+++ src/provisioningserver/boot/tests/test_pxe.py 2014-04-24 17:04:47 +0000
@@ -163,7 +163,7 @@
163 self.assertThat(output, StartsWith("DEFAULT "))163 self.assertThat(output, StartsWith("DEFAULT "))
164 # The PXE parameters are all set according to the options.164 # The PXE parameters are all set according to the options.
165 image_dir = compose_image_path(165 image_dir = compose_image_path(
166 arch=params.arch, subarch=params.subarch,166 osystem=params.osystem, arch=params.arch, subarch=params.subarch,
167 release=params.release, label=params.label)167 release=params.release, label=params.label)
168 self.assertThat(168 self.assertThat(
169 output, MatchesAll(169 output, MatchesAll(
@@ -243,9 +243,11 @@
243 method = PXEBootMethod()243 method = PXEBootMethod()
244 get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")244 get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
245 get_ephemeral_name.return_value = factory.make_name("ephemeral")245 get_ephemeral_name.return_value = factory.make_name("ephemeral")
246 osystem = factory.make_name('osystem')
246 options = {247 options = {
247 "kernel_params": make_kernel_parameters(248 "kernel_params": make_kernel_parameters(
248 testcase=self, subarch="generic", purpose=self.purpose),249 testcase=self, osystem=osystem, subarch="generic",
250 purpose=self.purpose),
249 }251 }
250 output = method.render_config(**options)252 output = method.render_config(**options)
251 config = parse_pxe_config(output)253 config = parse_pxe_config(output)
@@ -268,7 +270,8 @@
268 section = config[section_label]270 section = config[section_label]
269 self.assertThat(271 self.assertThat(
270 section, ContainsAll(("KERNEL", "INITRD", "APPEND")))272 section, ContainsAll(("KERNEL", "INITRD", "APPEND")))
271 contains_arch_path = StartsWith("%s/" % section_label)273 contains_arch_path = StartsWith(
274 "%s/%s/" % (osystem, section_label))
272 self.assertThat(section["KERNEL"], contains_arch_path)275 self.assertThat(section["KERNEL"], contains_arch_path)
273 self.assertThat(section["INITRD"], contains_arch_path)276 self.assertThat(section["INITRD"], contains_arch_path)
274 self.assertIn("APPEND", section)277 self.assertIn("APPEND", section)
275278
=== modified file 'src/provisioningserver/boot/tests/test_tftppath.py'
--- src/provisioningserver/boot/tests/test_tftppath.py 2014-04-03 09:26:31 +0000
+++ src/provisioningserver/boot/tests/test_tftppath.py 2014-04-24 17:04:47 +0000
@@ -30,6 +30,10 @@
30 list_subdirs,30 list_subdirs,
31 locate_tftp_path,31 locate_tftp_path,
32 )32 )
33from provisioningserver.osystems import (
34 OperatingSystem,
35 OperatingSystemRegistry,
36 )
33from provisioningserver.testing.boot_images import (37from provisioningserver.testing.boot_images import (
34 make_boot_image_storage_params,38 make_boot_image_storage_params,
35 )39 )
@@ -51,6 +55,63 @@
51 return image55 return image
5256
5357
58def make_osystem(osystem, purpose, testcase):
59 """Makes the operating system class and registers it."""
60 if osystem not in OperatingSystemRegistry:
61
62 class FakeOS(OperatingSystem):
63
64 name = osystem
65 title = osystem
66
67 def __init__(self, purpose, releases=None):
68 self.purpose = purpose
69 if releases is None:
70 self.fake_list = [
71 factory.getRandomString()
72 for _ in range(3)
73 ]
74 else:
75 self.fake_list = releases
76
77 def get_boot_image_purposes(self, *args):
78 return self.purpose
79
80 def get_supported_releases(self):
81 return self.fake_list
82
83 def get_default_release(self):
84 return self.fake_list[0]
85
86 def format_release_choices(self, releases):
87 return [
88 (release, release)
89 for release in releases
90 if release in self.fake_list
91 ]
92
93 fake = FakeOS(purpose)
94 OperatingSystemRegistry.register_item(fake.name, fake)
95
96 testcase.addCleanup(
97 OperatingSystemRegistry.unregister_item, osystem)
98
99 return fake
100
101 else:
102
103 obj = OperatingSystemRegistry[osystem]
104 old_func = obj.get_boot_image_purposes
105 testcase.patch(obj, 'get_boot_image_purposes').return_value = purpose
106
107 def reset_func(obj, old_func):
108 obj.get_boot_image_purposes = old_func
109
110 testcase.addCleanup(reset_func, obj, old_func)
111
112 return obj
113
114
54class TestTFTPPath(MAASTestCase):115class TestTFTPPath(MAASTestCase):
55116
56 def setUp(self):117 def setUp(self):
@@ -62,6 +123,7 @@
62 """Fake a boot image matching `image_params` under `tftproot`."""123 """Fake a boot image matching `image_params` under `tftproot`."""
63 image_dir = locate_tftp_path(124 image_dir = locate_tftp_path(
64 compose_image_path(125 compose_image_path(
126 osystem=image_params['osystem'],
65 arch=image_params['architecture'],127 arch=image_params['architecture'],
66 subarch=image_params['subarchitecture'],128 subarch=image_params['subarchitecture'],
67 release=image_params['release'],129 release=image_params['release'],
@@ -72,21 +134,23 @@
72 factory.make_file(image_dir, 'initrd.gz')134 factory.make_file(image_dir, 'initrd.gz')
73135
74 def test_compose_image_path_follows_storage_directory_layout(self):136 def test_compose_image_path_follows_storage_directory_layout(self):
137 osystem = factory.make_name('osystem')
75 arch = factory.make_name('arch')138 arch = factory.make_name('arch')
76 subarch = factory.make_name('subarch')139 subarch = factory.make_name('subarch')
77 release = factory.make_name('release')140 release = factory.make_name('release')
78 label = factory.make_name('label')141 label = factory.make_name('label')
79 self.assertEqual(142 self.assertEqual(
80 '%s/%s/%s/%s' % (arch, subarch, release, label),143 '%s/%s/%s/%s/%s' % (osystem, arch, subarch, release, label),
81 compose_image_path(arch, subarch, release, label))144 compose_image_path(osystem, arch, subarch, release, label))
82145
83 def test_compose_image_path_does_not_include_tftp_root(self):146 def test_compose_image_path_does_not_include_tftp_root(self):
147 osystem = factory.make_name('osystem')
84 arch = factory.make_name('arch')148 arch = factory.make_name('arch')
85 subarch = factory.make_name('subarch')149 subarch = factory.make_name('subarch')
86 release = factory.make_name('release')150 release = factory.make_name('release')
87 label = factory.make_name('label')151 label = factory.make_name('label')
88 self.assertThat(152 self.assertThat(
89 compose_image_path(arch, subarch, release, label),153 compose_image_path(osystem, arch, subarch, release, label),
90 Not(StartsWith(self.tftproot)))154 Not(StartsWith(self.tftproot)))
91155
92 def test_locate_tftp_path_prefixes_tftp_root(self):156 def test_locate_tftp_path_prefixes_tftp_root(self):
@@ -120,19 +184,22 @@
120 params = make_boot_image_storage_params()184 params = make_boot_image_storage_params()
121 self.make_image_dir(params, self.tftproot)185 self.make_image_dir(params, self.tftproot)
122 purposes = ['install', 'commissioning', 'xinstall']186 purposes = ['install', 'commissioning', 'xinstall']
187 make_osystem(params['osystem'], purposes, self)
123 self.assertItemsEqual(188 self.assertItemsEqual(
124 [make_image(params, purpose) for purpose in purposes],189 [make_image(params, purpose) for purpose in purposes],
125 list_boot_images(self.tftproot))190 list_boot_images(self.tftproot))
126191
127 def test_list_boot_images_enumerates_boot_images(self):192 def test_list_boot_images_enumerates_boot_images(self):
193 purposes = ['install', 'commissioning', 'xinstall']
128 params = [make_boot_image_storage_params() for counter in range(3)]194 params = [make_boot_image_storage_params() for counter in range(3)]
129 for param in params:195 for param in params:
130 self.make_image_dir(param, self.tftproot)196 self.make_image_dir(param, self.tftproot)
197 make_osystem(param['osystem'], purposes, self)
131 self.assertItemsEqual(198 self.assertItemsEqual(
132 [199 [
133 make_image(param, purpose)200 make_image(param, purpose)
134 for param in params201 for param in params
135 for purpose in ['install', 'commissioning', 'xinstall']202 for purpose in purposes
136 ],203 ],
137 list_boot_images(self.tftproot))204 list_boot_images(self.tftproot))
138205
139206
=== modified file 'src/provisioningserver/boot/tests/test_uefi.py'
--- src/provisioningserver/boot/tests/test_uefi.py 2014-03-28 19:03:46 +0000
+++ src/provisioningserver/boot/tests/test_uefi.py 2014-04-24 17:04:47 +0000
@@ -73,7 +73,7 @@
73 self.assertThat(output, StartsWith("set default=\"0\""))73 self.assertThat(output, StartsWith("set default=\"0\""))
74 # The UEFI parameters are all set according to the options.74 # The UEFI parameters are all set according to the options.
75 image_dir = compose_image_path(75 image_dir = compose_image_path(
76 arch=params.arch, subarch=params.subarch,76 osystem=params.osystem, arch=params.arch, subarch=params.subarch,
77 release=params.release, label=params.label)77 release=params.release, label=params.label)
7878
79 self.assertThat(79 self.assertThat(
8080
=== modified file 'src/provisioningserver/boot/tftppath.py'
--- src/provisioningserver/boot/tftppath.py 2014-04-03 16:36:15 +0000
+++ src/provisioningserver/boot/tftppath.py 2014-04-24 17:04:47 +0000
@@ -25,16 +25,21 @@
25from logging import getLogger25from logging import getLogger
26import os.path26import os.path
2727
28from provisioningserver.osystems import (
29 OperatingSystemError,
30 OperatingSystemRegistry,
31 )
2832
29logger = getLogger(__name__)33logger = getLogger(__name__)
3034
3135
32def compose_image_path(arch, subarch, release, label):36def compose_image_path(osystem, arch, subarch, release, label):
33 """Compose the TFTP path for a PXE kernel/initrd directory.37 """Compose the TFTP path for a PXE kernel/initrd directory.
3438
35 The path returned is relative to the TFTP root, as it would be39 The path returned is relative to the TFTP root, as it would be
36 identified by clients on the network.40 identified by clients on the network.
3741
42 :param osystem: Operating system.
38 :param arch: Main machine architecture.43 :param arch: Main machine architecture.
39 :param subarch: Sub-architecture, or "generic" if there is none.44 :param subarch: Sub-architecture, or "generic" if there is none.
40 :param release: Operating system release, e.g. "precise".45 :param release: Operating system release, e.g. "precise".
@@ -43,7 +48,7 @@
43 kernel and initrd) as exposed over TFTP.48 kernel and initrd) as exposed over TFTP.
44 """49 """
45 # This is a TFTP path, not a local filesystem path, so hard-code the slash.50 # This is a TFTP path, not a local filesystem path, so hard-code the slash.
46 return '/'.join([arch, subarch, release, label])51 return '/'.join([osystem, arch, subarch, release, label])
4752
4853
49def locate_tftp_path(path, tftproot):54def locate_tftp_path(path, tftproot):
@@ -113,19 +118,20 @@
113def extract_image_params(path):118def extract_image_params(path):
114 """Represent a list of TFTP path elements as a list of boot-image dicts.119 """Represent a list of TFTP path elements as a list of boot-image dicts.
115120
116 The path must consist of a full [architecture, subarchitecture, release]121 The path must consist of a full [osystem, architecture, subarchitecture,
117 that identify a kind of boot that we may need an image for.122 release] that identify a kind of boot that we may need an image for.
118 """123 """
119 arch, subarch, release, label = path124 osystem, arch, subarch, release, label = path
120 # XXX: rvb 2014-03-24: The images import script currently imports all the125 osystem_obj = OperatingSystemRegistry.get_item(osystem, default=None)
121 # images for the configured selections (where a selection is an126 if osystem_obj is None:
122 # arch/subarch/series/label combination). When the import script grows the127 raise OperatingSystemError(
123 # ability to import the images for a particular purpose, we need to change128 "Unsupported operating system: %s" % osystem)
124 # this code to report what is actually present.129
125 purposes = ['commissioning', 'install', 'xinstall']130 purposes = osystem_obj.get_boot_image_purposes(
131 arch, subarch, release, label)
126 return [132 return [
127 dict(133 dict(
128 architecture=arch, subarchitecture=subarch,134 osystem=osystem, architecture=arch, subarchitecture=subarch,
129 release=release, label=label, purpose=purpose)135 release=release, label=label, purpose=purpose)
130 for purpose in purposes136 for purpose in purposes
131 ]137 ]
@@ -139,9 +145,9 @@
139 `report_boot_images` API call.145 `report_boot_images` API call.
140 """146 """
141 # The sub-directories directly under tftproot, if they contain147 # The sub-directories directly under tftproot, if they contain
142 # images, represent architectures.148 # images, represent operating systems.
143 try:149 try:
144 potential_archs = list_subdirs(tftproot)150 potential_osystems = list_subdirs(tftproot)
145 except OSError as exception:151 except OSError as exception:
146 if exception.errno == errno.ENOENT:152 if exception.errno == errno.ENOENT:
147 # Directory does not exist, so return empty list.153 # Directory does not exist, so return empty list.
@@ -153,12 +159,12 @@
153159
154 # Starting point for iteration: paths that contain only the160 # Starting point for iteration: paths that contain only the
155 # top-level subdirectory of tftproot, i.e. the architecture name.161 # top-level subdirectory of tftproot, i.e. the architecture name.
156 paths = [[subdir] for subdir in potential_archs]162 paths = [[subdir] for subdir in potential_osystems]
157163
158 # Extend paths deeper into the filesystem, through the levels that164 # Extend paths deeper into the filesystem, through the levels that
159 # represent sub-architecture, release, and label. Any directory165 # represent architecture, sub-architecture, release, and label.
160 # that doesn't extend this deep isn't a boot image.166 # Any directory that doesn't extend this deep isn't a boot image.
161 for level in ['subarch', 'release', 'label']:167 for level in ['arch', 'subarch', 'release', 'label']:
162 paths = drill_down(tftproot, paths)168 paths = drill_down(tftproot, paths)
163169
164 # Each path we find this way should be a boot image.170 # Each path we find this way should be a boot image.
165171
=== modified file 'src/provisioningserver/import_images/boot_resources.py'
--- src/provisioningserver/import_images/boot_resources.py 2014-04-11 03:00:13 +0000
+++ src/provisioningserver/import_images/boot_resources.py 2014-04-24 17:04:47 +0000
@@ -80,7 +80,7 @@
80 return reverse80 return reverse
8181
8282
83def tgt_entry(arch, subarch, release, label, image):83def tgt_entry(osystem, arch, subarch, release, label, image):
84 """Generate tgt target used to commission arch/subarch with release84 """Generate tgt target used to commission arch/subarch with release
8585
86 Tgt target used to commission arch/subarch machine with a specific Ubuntu86 Tgt target used to commission arch/subarch machine with a specific Ubuntu
@@ -94,6 +94,7 @@
94 use the same inode for different tgt targets (even read-only targets which94 use the same inode for different tgt targets (even read-only targets which
95 looks like a bug to me) without this option enabled.95 looks like a bug to me) without this option enabled.
9696
97 :param osystem: Operating System name we generate tgt target for
97 :param arch: Architecture name we generate tgt target for98 :param arch: Architecture name we generate tgt target for
98 :param subarch: Subarchitecture name we generate tgt target for99 :param subarch: Subarchitecture name we generate tgt target for
99 :param release: Ubuntu release we generate tgt target for100 :param release: Ubuntu release we generate tgt target for
@@ -102,7 +103,13 @@
102 :return Tgt entry which can be written to tgt-admin configuration file103 :return Tgt entry which can be written to tgt-admin configuration file
103 """104 """
104 prefix = 'iqn.2004-05.com.ubuntu:maas'105 prefix = 'iqn.2004-05.com.ubuntu:maas'
105 target_name = 'ephemeral-%s-%s-%s-%s' % (arch, subarch, release, label)106 target_name = 'ephemeral-%s-%s-%s-%s-%s' % (
107 osystem,
108 arch,
109 subarch,
110 release,
111 label
112 )
106 entry = dedent("""\113 entry = dedent("""\
107 <target {prefix}:{target_name}>114 <target {prefix}:{target_name}>
108 readonly 1115 readonly 1
@@ -229,17 +236,20 @@
229 # Use a set to make sure we don't register duplicate entries in tgt.236 # Use a set to make sure we don't register duplicate entries in tgt.
230 entries = set()237 entries = set()
231 for item in list_boot_images(snapshot_path):238 for item in list_boot_images(snapshot_path):
239 osystem = item['osystem']
232 arch = item['architecture']240 arch = item['architecture']
233 subarch = item['subarchitecture']241 subarch = item['subarchitecture']
234 release = item['release']242 release = item['release']
235 label = item['label']243 label = item['label']
236 entries.add((arch, subarch, release, label))244 entries.add((osystem, arch, subarch, release, label))
237 tgt_entries = []245 tgt_entries = []
238 for arch, subarch, release, label in sorted(entries):246 for osystem, arch, subarch, release, label in sorted(entries):
239 root_image = os.path.join(247 root_image = os.path.join(
240 snapshot_path, arch, subarch, release, label, 'root-image')248 snapshot_path, osystem, arch, subarch,
249 release, label, 'root-image')
241 if os.path.isfile(root_image):250 if os.path.isfile(root_image):
242 entry = tgt_entry(arch, subarch, release, label, root_image)251 entry = tgt_entry(
252 osystem, arch, subarch, release, label, root_image)
243 tgt_entries.append(entry)253 tgt_entries.append(entry)
244 text = ''.join(tgt_entries)254 text = ''.join(tgt_entries)
245 return text.encode('utf-8')255 return text.encode('utf-8')
@@ -319,7 +329,8 @@
319 snapshot_path = compose_snapshot_path(storage)329 snapshot_path = compose_snapshot_path(storage)
320 cache_path = os.path.join(storage, 'cache')330 cache_path = os.path.join(storage, 'cache')
321 targets_conf = os.path.join(snapshot_path, 'maas.tgt')331 targets_conf = os.path.join(snapshot_path, 'maas.tgt')
322 writer = RepoWriter(snapshot_path, cache_path, reverse_boot)332 ubuntu_path = os.path.join(snapshot_path, 'ubuntu')
333 writer = RepoWriter(ubuntu_path, cache_path, reverse_boot)
323334
324 for source in sources:335 for source in sources:
325 writer.write(source['path'], source['keyring'])336 writer.write(source['path'], source['keyring'])
326337
=== modified file 'src/provisioningserver/import_images/tests/test_boot_resources.py'
--- src/provisioningserver/import_images/tests/test_boot_resources.py 2014-04-11 02:58:32 +0000
+++ src/provisioningserver/import_images/tests/test_boot_resources.py 2014-04-24 17:04:47 +0000
@@ -101,9 +101,10 @@
101101
102 def test_generates_one_target(self):102 def test_generates_one_target(self):
103 spec = make_image_spec()103 spec = make_image_spec()
104 osystem = factory.make_name('osystem')
104 image = self.make_file()105 image = self.make_file()
105 entry = boot_resources.tgt_entry(106 entry = boot_resources.tgt_entry(
106 spec.arch, spec.subarch, spec.release, spec.label, image)107 osystem, spec.arch, spec.subarch, spec.release, spec.label, image)
107 # The entry looks a bit like XML, but isn't well-formed. So don't try108 # The entry looks a bit like XML, but isn't well-formed. So don't try
108 # to parse it as such!109 # to parse it as such!
109 self.assertIn('<target iqn.2004-05.com.ubuntu:maas:', entry)110 self.assertIn('<target iqn.2004-05.com.ubuntu:maas:', entry)
@@ -113,8 +114,9 @@
113 def test_produces_suitable_output_for_tgt_admin(self):114 def test_produces_suitable_output_for_tgt_admin(self):
114 spec = make_image_spec()115 spec = make_image_spec()
115 image = self.make_file()116 image = self.make_file()
117 osystem = factory.make_name('osystem')
116 entry = boot_resources.tgt_entry(118 entry = boot_resources.tgt_entry(
117 spec.arch, spec.subarch, spec.release, spec.label, image)119 osystem, spec.arch, spec.subarch, spec.release, spec.label, image)
118 config = self.make_file(contents=entry)120 config = self.make_file(contents=entry)
119 # Pretend to be root, but without requiring the actual privileges and121 # Pretend to be root, but without requiring the actual privileges and
120 # without prompting for a password. In that state, run tgt-admin.122 # without prompting for a password. In that state, run tgt-admin.
@@ -326,7 +328,7 @@
326 self.assertThat(os.path.join(current, 'maas.meta'), FileExists())328 self.assertThat(os.path.join(current, 'maas.meta'), FileExists())
327 self.assertThat(os.path.join(current, 'maas.tgt'), FileExists())329 self.assertThat(os.path.join(current, 'maas.tgt'), FileExists())
328 self.assertThat(330 self.assertThat(
329 os.path.join(current, arch, subarch, release, label),331 os.path.join(current, 'ubuntu', arch, subarch, release, label),
330 DirExists())332 DirExists())
331333
332 # Verify the contents of the "meta" file.334 # Verify the contents of the "meta" file.
333335
=== modified file 'src/provisioningserver/kernel_opts.py'
--- src/provisioningserver/kernel_opts.py 2014-03-20 10:44:56 +0000
+++ src/provisioningserver/kernel_opts.py 2014-04-24 17:04:47 +0000
@@ -30,9 +30,10 @@
3030
31KernelParametersBase = namedtuple(31KernelParametersBase = namedtuple(
32 "KernelParametersBase", (32 "KernelParametersBase", (
33 "osystem", # Operating system, e.g. "ubuntu"
33 "arch", # Machine architecture, e.g. "i386"34 "arch", # Machine architecture, e.g. "i386"
34 "subarch", # Machine subarchitecture, e.g. "generic"35 "subarch", # Machine subarchitecture, e.g. "generic"
35 "release", # Ubuntu release, e.g. "precise"36 "release", # OS release, e.g. "precise"
36 "label", # Image label, e.g. "release"37 "label", # Image label, e.g. "release"
37 "purpose", # Boot purpose, e.g. "commissioning"38 "purpose", # Boot purpose, e.g. "commissioning"
38 "hostname", # Machine hostname, e.g. "coleman"39 "hostname", # Machine hostname, e.g. "coleman"
@@ -90,9 +91,15 @@
90ISCSI_TARGET_NAME_PREFIX = "iqn.2004-05.com.ubuntu:maas"91ISCSI_TARGET_NAME_PREFIX = "iqn.2004-05.com.ubuntu:maas"
9192
9293
93def get_ephemeral_name(arch, subarch, release, label):94def get_ephemeral_name(osystem, arch, subarch, release, label):
94 """Return the name of the most recent ephemeral image."""95 """Return the name of the most recent ephemeral image."""
95 return "ephemeral-%s-%s-%s-%s" % (arch, subarch, release, label)96 return "ephemeral-%s-%s-%s-%s-%s" % (
97 osystem,
98 arch,
99 subarch,
100 release,
101 label
102 )
96103
97104
98def compose_hostname_opts(params):105def compose_hostname_opts(params):
@@ -119,7 +126,8 @@
119 # These are kernel parameters read by the ephemeral environment.126 # These are kernel parameters read by the ephemeral environment.
120 tname = prefix_target_name(127 tname = prefix_target_name(
121 get_ephemeral_name(128 get_ephemeral_name(
122 params.arch, params.subarch, params.release, params.label))129 params.osystem, params.arch, params.subarch,
130 params.release, params.label))
123 kernel_params = [131 kernel_params = [
124 # Read by the open-iscsi initramfs code.132 # Read by the open-iscsi initramfs code.
125 "iscsi_target_name=%s" % tname,133 "iscsi_target_name=%s" % tname,
126134
=== added directory 'src/provisioningserver/osystems'
=== added file 'src/provisioningserver/osystems/__init__.py'
--- src/provisioningserver/osystems/__init__.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/osystems/__init__.py 2014-04-24 17:04:47 +0000
@@ -0,0 +1,103 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Provides the support for Operating Systems."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 "OperatingSystem",
17 "OperatingSystemRegistry",
18 ]
19
20from abc import (
21 ABCMeta,
22 abstractmethod,
23 abstractproperty,
24 )
25
26from provisioningserver.utils.registry import Registry
27
28
29class BOOT_IMAGE_PURPOSE:
30 """The vocabulary of a `BootImage`'s purpose."""
31 #: Usable for commissioning
32 COMMISSIONING = 'commissioning'
33 #: Usable for install
34 INSTALL = 'install'
35 #: Usable for fast-path install
36 XINSTALL = 'xinstall'
37
38
39class OperatingSystemError(Exception):
40 """Exception raised for errors from a OperatingSystem."""
41
42
43class OperatingSystem:
44 """Skeleton for a operating system."""
45
46 __metaclass__ = ABCMeta
47
48 @abstractproperty
49 def name(self):
50 """Name of the operating system."""
51
52 @abstractproperty
53 def title(self):
54 """Title of the operating system."""
55
56 @abstractmethod
57 def get_supported_releases(self):
58 """Gets list of supported releases for Ubuntu.
59
60 :returns: list of supported releases
61 """
62
63 @abstractmethod
64 def get_default_release(self):
65 """Gets the default release to use when a release is not
66 explicit.
67
68 :returns: default release to use
69 """
70
71 @abstractmethod
72 def format_release_choices(self, releases):
73 """Formats the release choices that are presented to the user.
74
75 :param releases: list of installed boot image releases
76 :returns: Return Django "choices" list
77 """
78
79 @abstractmethod
80 def get_boot_image_purposes(self, arch, subarch, release, label):
81 """Returns the supported purposes of a boot image.
82
83 :param arch: Architecture of boot image.
84 :param subarch: Sub-architecture of boot image.
85 :param release: Release of boot image.
86 :param label: Label of boot image.
87 :returns: list of supported purposes
88 """
89
90
91class OperatingSystemRegistry(Registry):
92 """Registry for operating system classes."""
93
94
95# Import the supported operating systems after defining OperatingSystem.
96from provisioningserver.osystems.ubuntu import UbuntuOS
97
98
99builtin_ossytems = [
100 UbuntuOS(),
101]
102for osystem in builtin_ossytems:
103 OperatingSystemRegistry.register_item(osystem.name, osystem)
0104
=== added directory 'src/provisioningserver/osystems/tests'
=== added file 'src/provisioningserver/osystems/tests/__init__.py'
=== added file 'src/provisioningserver/osystems/tests/test_ubuntu.py'
--- src/provisioningserver/osystems/tests/test_ubuntu.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/osystems/tests/test_ubuntu.py 2014-04-24 17:04:47 +0000
@@ -0,0 +1,31 @@
1# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the UbuntuOS module."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17from maastesting.testcase import MAASTestCase
18from provisioningserver.osystems.ubuntu import (
19 DISTRO_SERIES_CHOICES,
20 UbuntuOS,
21 )
22
23
24class TestUbuntuOS(MAASTestCase):
25
26 def test_format_release_choices(self):
27 osystem = UbuntuOS()
28 releases = osystem.get_supported_releases()
29 formatted = osystem.format_release_choices(releases)
30 for name, title in formatted:
31 self.assertEqual(DISTRO_SERIES_CHOICES[name], title)
032
=== added file 'src/provisioningserver/osystems/ubuntu.py'
--- src/provisioningserver/osystems/ubuntu.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/osystems/ubuntu.py 2014-04-24 17:04:47 +0000
@@ -0,0 +1,89 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Ubuntu Operating System."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 "UbuntuOS",
17 ]
18
19from provisioningserver.osystems import (
20 BOOT_IMAGE_PURPOSE,
21 OperatingSystem,
22 )
23
24
25DISTRO_SERIES_CHOICES = {
26 'precise': 'Ubuntu 12.04 LTS "Precise Pangolin"',
27 'quantal': 'Ubuntu 12.10 "Quantal Quetzal"',
28 'raring': 'Ubuntu 13.04 "Raring Ringtail"',
29 'saucy': 'Ubuntu 13.10 "Saucy Salamander"',
30 'trusty': 'Ubuntu 14.04 LTS "Trusty Tahr"',
31}
32
33COMMISIONING_DISTRO_SERIES = [
34 'trusty',
35]
36
37DISTRO_SERIES_DEFAULT = 'trusty'
38COMMISIONING_DISTRO_SERIES_DEFAULT = 'trusty'
39
40
41class UbuntuOS(OperatingSystem):
42 """Ubuntu operating system."""
43
44 name = "ubuntu"
45 title = "Ubuntu"
46
47 def get_boot_image_purposes(self, arch, subarch, release, label):
48 """Gets the purpose of each boot image."""
49 return [
50 BOOT_IMAGE_PURPOSE.COMMISSIONING,
51 BOOT_IMAGE_PURPOSE.INSTALL,
52 BOOT_IMAGE_PURPOSE.XINSTALL
53 ]
54
55 def get_supported_releases(self):
56 """Gets list of supported releases for Ubuntu."""
57 # To make this data better, could pull this information from
58 # simplestreams. So only need to update simplestreams for a
59 # new release.
60 return DISTRO_SERIES_CHOICES.keys()
61
62 def get_default_release(self):
63 """Gets the default release to use when a release is not
64 explicit."""
65 return DISTRO_SERIES_DEFAULT
66
67 def get_supported_commissioning_releases(self):
68 """Gets the supported commissioning releases for Ubuntu. This
69 only exists on Ubuntu, because that is the only operating
70 system that supports commissioning.
71 """
72 return COMMISIONING_DISTRO_SERIES
73
74 def get_default_commissioning_release(self):
75 """Gets the default commissioning release for Ubuntu. This only exists
76 on Ubuntu, because that is the only operating system that supports
77 commissioning.
78 """
79 return COMMISIONING_DISTRO_SERIES_DEFAULT
80
81 def format_release_choices(self, releases):
82 """Formats the release choices that are presented to the user."""
83 choices = []
84 releases = sorted(releases, reverse=True)
85 for release in releases:
86 title = DISTRO_SERIES_CHOICES.get(release)
87 if title is not None:
88 choices.append((release, title))
89 return choices
090
=== modified file 'src/provisioningserver/rpc/cluster.py'
--- src/provisioningserver/rpc/cluster.py 2014-03-20 22:36:32 +0000
+++ src/provisioningserver/rpc/cluster.py 2014-04-24 17:04:47 +0000
@@ -33,7 +33,8 @@
33 arguments = []33 arguments = []
34 response = [34 response = [
35 (b"images", amp.AmpList(35 (b"images", amp.AmpList(
36 [(b"architecture", amp.Unicode()),36 [(b"osystem", amp.Unicode()),
37 (b"architecture", amp.Unicode()),
37 (b"subarchitecture", amp.Unicode()),38 (b"subarchitecture", amp.Unicode()),
38 (b"release", amp.Unicode()),39 (b"release", amp.Unicode()),
39 (b"label", amp.Unicode()),40 (b"label", amp.Unicode()),
4041
=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-03-28 04:31:32 +0000
+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-04-24 17:04:47 +0000
@@ -34,6 +34,7 @@
34 sentinel,34 sentinel,
35 )35 )
36from provisioningserver.boot import tftppath36from provisioningserver.boot import tftppath
37from provisioningserver.boot.tests.test_tftppath import make_osystem
37from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS38from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS
38from provisioningserver.rpc import (39from provisioningserver.rpc import (
39 cluster,40 cluster,
@@ -171,6 +172,7 @@
171 # serialised correctly.172 # serialised correctly.
172173
173 # Example boot image definitions.174 # Example boot image definitions.
175 osystems = "ubuntu", "centos"
174 archs = "i386", "amd64"176 archs = "i386", "amd64"
175 subarchs = "generic", "special"177 subarchs = "generic", "special"
176 releases = "precise", "trusty"178 releases = "precise", "trusty"
@@ -179,22 +181,24 @@
179181
180 # Create a TFTP file tree with a variety of subdirectories.182 # Create a TFTP file tree with a variety of subdirectories.
181 tftpdir = self.make_dir()183 tftpdir = self.make_dir()
182 for options in product(archs, subarchs, releases, labels):184 for options in product(osystems, archs, subarchs, releases, labels):
183 os.makedirs(os.path.join(tftpdir, *options))185 os.makedirs(os.path.join(tftpdir, *options))
186 make_osystem(options[0], purposes, self)
184187
185 # Ensure that list_boot_images() uses the above TFTP file tree.188 # Ensure that list_boot_images() uses the above TFTP file tree.
186 self.useFixture(set_tftp_root(tftpdir))189 self.useFixture(set_tftp_root(tftpdir))
187190
188 expected_images = [191 expected_images = [
189 {192 {
193 "osystem": osystem,
190 "architecture": arch,194 "architecture": arch,
191 "subarchitecture": subarch,195 "subarchitecture": subarch,
192 "release": release,196 "release": release,
193 "label": label,197 "label": label,
194 "purpose": purpose,198 "purpose": purpose,
195 }199 }
196 for arch, subarch, release, label, purpose in product(200 for osystem, arch, subarch, release, label, purpose in product(
197 archs, subarchs, releases, labels, purposes)201 osystems, archs, subarchs, releases, labels, purposes)
198 ]202 ]
199203
200 response = yield call_responder(Cluster(), cluster.ListBootImages, {})204 response = yield call_responder(Cluster(), cluster.ListBootImages, {})
201205
=== modified file 'src/provisioningserver/testing/boot_images.py'
--- src/provisioningserver/testing/boot_images.py 2014-03-21 03:21:57 +0000
+++ src/provisioningserver/testing/boot_images.py 2014-04-24 17:04:47 +0000
@@ -23,10 +23,11 @@
23 """Create an arbitrary dict of boot-image parameters.23 """Create an arbitrary dict of boot-image parameters.
2424
25 These are the parameters that together describe a kind of boot for25 These are the parameters that together describe a kind of boot for
26 which we may need a kernel and initrd: architecture,26 which we may need a kernel and initrd: operating system, architecture,
27 sub-architecture, Ubuntu release, boot purpose, and release label.27 sub-architecture, Ubuntu release, boot purpose, and release label.
28 """28 """
29 return dict(29 return dict(
30 osystem=factory.make_name('osystem'),
30 architecture=factory.make_name('architecture'),31 architecture=factory.make_name('architecture'),
31 subarchitecture=factory.make_name('subarchitecture'),32 subarchitecture=factory.make_name('subarchitecture'),
32 release=factory.make_name('release'),33 release=factory.make_name('release'),
@@ -39,9 +40,11 @@
39 """Create a dict of boot-image parameters as used to store the image.40 """Create a dict of boot-image parameters as used to store the image.
4041
41 These are the parameters that together describe a path to store a boot42 These are the parameters that together describe a path to store a boot
42 image: architecture, sub-architecture, Ubuntu release, and release label.43 image: operating system, architecture, sub-architecture, Ubuntu release,
44 and release label.
43 """45 """
44 return dict(46 return dict(
47 osystem=factory.make_name('osystem'),
45 architecture=factory.make_name('architecture'),48 architecture=factory.make_name('architecture'),
46 subarchitecture=factory.make_name('subarchitecture'),49 subarchitecture=factory.make_name('subarchitecture'),
47 release=factory.make_name('release'),50 release=factory.make_name('release'),
4851
=== modified file 'src/provisioningserver/tests/test_kernel_opts.py'
--- src/provisioningserver/tests/test_kernel_opts.py 2014-03-28 16:46:55 +0000
+++ src/provisioningserver/tests/test_kernel_opts.py 2014-04-24 17:04:47 +0000
@@ -228,7 +228,8 @@
228 # options for a "xinstall" node.228 # options for a "xinstall" node.
229 params = self.make_kernel_parameters(purpose="xinstall")229 params = self.make_kernel_parameters(purpose="xinstall")
230 ephemeral_name = get_ephemeral_name(230 ephemeral_name = get_ephemeral_name(
231 params.arch, params.subarch, params.release, params.label)231 params.osystem, params.arch, params.subarch,
232 params.release, params.label)
232 self.assertThat(233 self.assertThat(
233 compose_kernel_command_line(params),234 compose_kernel_command_line(params),
234 ContainsAll([235 ContainsAll([
@@ -243,7 +244,8 @@
243 # options for a "commissioning" node.244 # options for a "commissioning" node.
244 params = self.make_kernel_parameters(purpose="commissioning")245 params = self.make_kernel_parameters(purpose="commissioning")
245 ephemeral_name = get_ephemeral_name(246 ephemeral_name = get_ephemeral_name(
246 params.arch, params.subarch, params.release, params.label)247 params.osystem, params.arch, params.subarch,
248 params.release, params.label)
247 self.assertThat(249 self.assertThat(
248 compose_kernel_command_line(params),250 compose_kernel_command_line(params),
249 ContainsAll([251 ContainsAll([