Merge lp:~jameinel/maas/land-kernel-opts-in-trunk into lp:~maas-committers/maas/trunk

Proposed by John A Meinel
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: 1337
Proposed branch: lp:~jameinel/maas/land-kernel-opts-in-trunk
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 560 lines (+381/-5)
11 files modified
src/maasserver/api.py (+22/-1)
src/maasserver/forms.py (+1/-0)
src/maasserver/migrations/0045_add_tag_kernel_opts.py (+203/-0)
src/maasserver/models/node.py (+24/-1)
src/maasserver/models/tag.py (+3/-0)
src/maasserver/testing/factory.py (+4/-3)
src/maasserver/tests/test_api.py (+41/-0)
src/maasserver/tests/test_node.py (+56/-0)
src/maasserver/tests/test_tag.py (+10/-0)
src/provisioningserver/kernel_opts.py (+4/-0)
src/provisioningserver/tests/test_kernel_opts.py (+13/-0)
To merge this branch: bzr merge lp:~jameinel/maas/land-kernel-opts-in-trunk
Reviewer Review Type Date Requested Status
Martin Packman (community) Approve
Review via email: mp+133434@code.launchpad.net

Commit message

Land the changes for kernel_opts into trunk.

This brings in the changes for the Tag table, node.get_effective_kernel_options, Tag API to allow setting and getting the new options, and pxeconfig to handle the extra_opts field.

Description of the change

This restores all of the kernel opts goodness into the trunk branch, rather than being in the 1.2 branch.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Looks good.

review: Approve

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 2012-11-08 09:12:44 +0000
+++ src/maasserver/api.py 2012-11-08 10:09:36 +0000
@@ -748,6 +748,13 @@
748 The minimum data required is:748 The minimum data required is:
749 architecture=<arch string> (e.g "i386/generic")749 architecture=<arch string> (e.g "i386/generic")
750 mac_address=<value>750 mac_address=<value>
751
752 :param architecture: A string containing the architecture type of
753 the node.
754 :param mac_address: The MAC address of the node.
755 :param hostname: A hostname. If not given, one will be generated.
756 :param powertype: A power management type, if applicable (e.g.
757 "virsh", "ipmi").
751 """758 """
752 node = create_node(request)759 node = create_node(request)
753 if request.user.is_superuser:760 if request.user.is_superuser:
@@ -1527,6 +1534,7 @@
1527 'name',1534 'name',
1528 'definition',1535 'definition',
1529 'comment',1536 'comment',
1537 'kernel_opts',
1530 )1538 )
15311539
1532 def read(self, request, name):1540 def read(self, request, name):
@@ -1658,6 +1666,11 @@
1658 It is meant as a human readable description of the tag.1666 It is meant as a human readable description of the tag.
1659 :param definition: An XPATH query that will be evaluated against the1667 :param definition: An XPATH query that will be evaluated against the
1660 hardware_details stored for all nodes (output of `lshw -xml`).1668 hardware_details stored for all nodes (output of `lshw -xml`).
1669 :param kernel_opts: Can be None. If set, nodes associated with this tag
1670 will add this string to their kernel options when booting. The
1671 value overrides the global 'kernel_opts' setting. If more than one
1672 tag is associated with a node, the one with the lowest alphabetical
1673 name will be picked (eg 01-my-tag will be taken over 99-tag-name).
1661 """1674 """
1662 if not request.user.is_superuser:1675 if not request.user.is_superuser:
1663 raise PermissionDenied()1676 raise PermissionDenied()
@@ -1892,6 +1905,13 @@
1892 else:1905 else:
1893 series = node.get_distro_series()1906 series = node.get_distro_series()
18941907
1908 if node is not None:
1909 # We don't care if the kernel opts is from the global setting or a tag,
1910 # just get the options
1911 _, extra_kernel_opts = node.get_effective_kernel_options()
1912 else:
1913 extra_kernel_opts = None
1914
1895 purpose = get_boot_purpose(node)1915 purpose = get_boot_purpose(node)
1896 server_address = get_maas_facing_server_address()1916 server_address = get_maas_facing_server_address()
1897 cluster_address = get_mandatory_param(request.GET, "local")1917 cluster_address = get_mandatory_param(request.GET, "local")
@@ -1899,7 +1919,8 @@
1899 params = KernelParameters(1919 params = KernelParameters(
1900 arch=arch, subarch=subarch, release=series, purpose=purpose,1920 arch=arch, subarch=subarch, release=series, purpose=purpose,
1901 hostname=hostname, domain=domain, preseed_url=preseed_url,1921 hostname=hostname, domain=domain, preseed_url=preseed_url,
1902 log_host=server_address, fs_host=cluster_address)1922 log_host=server_address, fs_host=cluster_address,
1923 extra_opts=extra_kernel_opts)
19031924
1904 return HttpResponse(1925 return HttpResponse(
1905 json.dumps(params._asdict()),1926 json.dumps(params._asdict()),
19061927
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2012-11-08 08:33:59 +0000
+++ src/maasserver/forms.py 2012-11-08 10:09:36 +0000
@@ -864,6 +864,7 @@
864 'name',864 'name',
865 'comment',865 'comment',
866 'definition',866 'definition',
867 'kernel_opts',
867 )868 )
868869
869 def clean_definition(self):870 def clean_definition(self):
870871
=== added file 'src/maasserver/migrations/0045_add_tag_kernel_opts.py'
--- src/maasserver/migrations/0045_add_tag_kernel_opts.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0045_add_tag_kernel_opts.py 2012-11-08 10:09:36 +0000
@@ -0,0 +1,203 @@
1# -*- coding: utf-8 -*-
2import 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 'Tag.kernel_opts'
12 db.add_column(u'maasserver_tag', 'kernel_opts',
13 self.gf('django.db.models.fields.TextField')(null=True, blank=True),
14 keep_default=False)
15
16
17 def backwards(self, orm):
18 # Deleting field 'Tag.kernel_opts'
19 db.delete_column(u'maasserver_tag', 'kernel_opts')
20
21
22 models = {
23 'auth.group': {
24 'Meta': {'object_name': 'Group'},
25 '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
28 },
29 'auth.permission': {
30 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
31 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
32 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
33 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
35 },
36 '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
42 '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
50 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
51 },
52 '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 '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'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
61 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
62 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
63 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
64 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
65 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
66 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
67 },
68 u'maasserver.componenterror': {
69 'Meta': {'object_name': 'ComponentError'},
70 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
71 'created': ('django.db.models.fields.DateTimeField', [], {}),
72 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
73 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74 'updated': ('django.db.models.fields.DateTimeField', [], {})
75 },
76 u'maasserver.config': {
77 'Meta': {'object_name': 'Config'},
78 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
80 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
81 },
82 u'maasserver.dhcplease': {
83 'Meta': {'object_name': 'DHCPLease'},
84 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
85 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
86 'mac': ('maasserver.fields.MACAddressField', [], {}),
87 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
88 },
89 u'maasserver.filestorage': {
90 'Meta': {'object_name': 'FileStorage'},
91 'content': ('metadataserver.fields.BinaryField', [], {}),
92 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
93 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
94 },
95 u'maasserver.macaddress': {
96 'Meta': {'object_name': 'MACAddress'},
97 'created': ('django.db.models.fields.DateTimeField', [], {}),
98 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
100 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
101 'updated': ('django.db.models.fields.DateTimeField', [], {})
102 },
103 u'maasserver.node': {
104 'Meta': {'object_name': 'Node'},
105 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
106 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
107 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
108 'created': ('django.db.models.fields.DateTimeField', [], {}),
109 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
110 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
111 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
112 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
113 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
115 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
116 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
117 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
118 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
119 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
120 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
121 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-a776a79c-298b-11e2-90d1-080027748fea'", 'unique': 'True', 'max_length': '41'}),
122 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
123 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
124 'updated': ('django.db.models.fields.DateTimeField', [], {})
125 },
126 u'maasserver.nodegroup': {
127 'Meta': {'object_name': 'NodeGroup'},
128 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
129 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
130 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
131 'created': ('django.db.models.fields.DateTimeField', [], {}),
132 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
133 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
134 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
135 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
136 'updated': ('django.db.models.fields.DateTimeField', [], {}),
137 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
138 },
139 u'maasserver.nodegroupinterface': {
140 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
141 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
142 'created': ('django.db.models.fields.DateTimeField', [], {}),
143 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
144 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
145 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
146 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
147 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
148 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
149 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
150 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
151 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
152 'updated': ('django.db.models.fields.DateTimeField', [], {})
153 },
154 u'maasserver.sshkey': {
155 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
156 'created': ('django.db.models.fields.DateTimeField', [], {}),
157 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
158 'key': ('django.db.models.fields.TextField', [], {}),
159 'updated': ('django.db.models.fields.DateTimeField', [], {}),
160 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
161 },
162 u'maasserver.tag': {
163 'Meta': {'object_name': 'Tag'},
164 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
165 'created': ('django.db.models.fields.DateTimeField', [], {}),
166 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
167 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
168 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
169 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
170 'updated': ('django.db.models.fields.DateTimeField', [], {})
171 },
172 u'maasserver.userprofile': {
173 'Meta': {'object_name': 'UserProfile'},
174 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
175 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
176 },
177 'piston.consumer': {
178 'Meta': {'object_name': 'Consumer'},
179 'description': ('django.db.models.fields.TextField', [], {}),
180 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
181 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
182 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
183 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
184 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
185 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
186 },
187 'piston.token': {
188 'Meta': {'object_name': 'Token'},
189 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
190 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
191 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
192 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
193 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
194 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
195 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
196 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1352369055L'}),
197 'token_type': ('django.db.models.fields.IntegerField', [], {}),
198 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
199 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
200 }
201 }
202
203 complete_apps = ['maasserver']
0\ No newline at end of file204\ No newline at end of file
1205
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2012-11-05 08:23:36 +0000
+++ src/maasserver/models/node.py 2012-11-08 10:09:36 +0000
@@ -73,7 +73,10 @@
73 get_db_state,73 get_db_state,
74 strip_domain,74 strip_domain,
75 )75 )
76from maasserver.utils.orm import get_first76from maasserver.utils.orm import (
77 get_first,
78 get_one,
79 )
77from piston.models import Token80from piston.models import Token
78from provisioningserver.enum import (81from provisioningserver.enum import (
79 POWER_TYPE,82 POWER_TYPE,
@@ -722,6 +725,26 @@
722 else:725 else:
723 return None726 return None
724727
728 def get_effective_kernel_options(self):
729 """Determine any special kernel parameters for this node.
730
731 :return: (tag, kernel_options)
732 tag is a Tag object or None. If None, the kernel_options came from
733 the global setting.
734 kernel_options, a string indicating extra kernel_options that
735 should be used when booting this node. May be None if no tags match
736 and no global setting has been configured.
737 """
738 # First, see if there are any tags associated with this node that has a
739 # custom kernel parameter
740 tags = self.tags.filter(kernel_opts__isnull=False)
741 tags = tags.order_by('name')[:1]
742 tag = get_one(tags)
743 if tag is not None:
744 return tag, tag.kernel_opts
745 global_value = Config.objects.get_config('kernel_opts')
746 return None, global_value
747
725 @property748 @property
726 def work_queue(self):749 def work_queue(self):
727 """The name of the queue for tasks specific to this node."""750 """The name of the queue for tasks specific to this node."""
728751
=== modified file 'src/maasserver/models/tag.py'
--- src/maasserver/models/tag.py 2012-10-24 16:07:00 +0000
+++ src/maasserver/models/tag.py 2012-11-08 10:09:36 +0000
@@ -89,6 +89,8 @@
89 tag.89 tag.
90 :ivar comment: A long-form description for humans about what this tag is90 :ivar comment: A long-form description for humans about what this tag is
91 trying to accomplish.91 trying to accomplish.
92 :ivar kernel_opts: Optional kernel command-line parameters string to be
93 used in the PXE config for nodes with this tags.
92 :ivar objects: The :class:`TagManager`.94 :ivar objects: The :class:`TagManager`.
93 """95 """
9496
@@ -101,6 +103,7 @@
101 validators=[RegexValidator(_tag_name_regex)])103 validators=[RegexValidator(_tag_name_regex)])
102 definition = TextField(blank=True)104 definition = TextField(blank=True)
103 comment = TextField(blank=True)105 comment = TextField(blank=True)
106 kernel_opts = TextField(blank=True, null=True)
104107
105 objects = TagManager()108 objects = TagManager()
106109
107110
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2012-11-01 15:59:22 +0000
+++ src/maasserver/testing/factory.py 2012-11-08 10:09:36 +0000
@@ -251,14 +251,15 @@
251 key.save()251 key.save()
252 return key252 return key
253253
254 def make_tag(self, name=None, definition=None, comment='', created=None,254 def make_tag(self, name=None, definition=None, comment='',
255 updated=None):255 kernel_opts=None, created=None, updated=None):
256 if name is None:256 if name is None:
257 name = self.make_name('tag')257 name = self.make_name('tag')
258 if definition is None:258 if definition is None:
259 # Is there a 'node' in this xml?259 # Is there a 'node' in this xml?
260 definition = '//node'260 definition = '//node'
261 tag = Tag(name=name, definition=definition, comment=comment)261 tag = Tag(name=name, definition=definition, comment=comment,
262 kernel_opts=kernel_opts)
262 self._save_node_unchecked(tag)263 self._save_node_unchecked(tag)
263 # Update the 'updated'/'created' fields with a call to 'update'264 # Update the 'updated'/'created' fields with a call to 'update'
264 # preventing a call to save() from overriding the values.265 # preventing a call to save() from overriding the values.
265266
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-11-08 09:12:44 +0000
+++ src/maasserver/tests/test_api.py 2012-11-08 10:09:36 +0000
@@ -3141,6 +3141,30 @@
3141 % (invalid,))3141 % (invalid,))
3142 self.assertFalse(Tag.objects.filter(name=invalid).exists())3142 self.assertFalse(Tag.objects.filter(name=invalid).exists())
31433143
3144 def test_POST_new_kernel_opts(self):
3145 self.become_admin()
3146 name = factory.getRandomString()
3147 definition = '//node'
3148 comment = factory.getRandomString()
3149 extra_kernel_opts = factory.getRandomString()
3150 response = self.client.post(
3151 self.get_uri('tags/'),
3152 {
3153 'op': 'new',
3154 'name': name,
3155 'comment': comment,
3156 'definition': definition,
3157 'kernel_opts': extra_kernel_opts,
3158 })
3159 self.assertEqual(httplib.OK, response.status_code)
3160 parsed_result = json.loads(response.content)
3161 self.assertEqual(name, parsed_result['name'])
3162 self.assertEqual(comment, parsed_result['comment'])
3163 self.assertEqual(definition, parsed_result['definition'])
3164 self.assertEqual(extra_kernel_opts, parsed_result['kernel_opts'])
3165 self.assertEqual(
3166 extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts)
3167
3144 def test_POST_new_populates_nodes(self):3168 def test_POST_new_populates_nodes(self):
3145 self.become_admin()3169 self.become_admin()
3146 node1 = factory.make_node()3170 node1 = factory.make_node()
@@ -3504,6 +3528,23 @@
3504 kernel_params = KernelParameters(**self.get_pxeconfig(params))3528 kernel_params = KernelParameters(**self.get_pxeconfig(params))
3505 self.assertEqual(params["local"], kernel_params.fs_host)3529 self.assertEqual(params["local"], kernel_params.fs_host)
35063530
3531 def test_pxeconfig_returns_extra_kernel_options(self):
3532 node = factory.make_node()
3533 extra_kernel_opts = factory.getRandomString()
3534 Config.objects.set_config('kernel_opts', extra_kernel_opts)
3535 mac = factory.make_mac_address(node=node)
3536 params = self.get_default_params()
3537 params['mac'] = mac.mac_address
3538 pxe_config = self.get_pxeconfig(params)
3539 self.assertEqual(extra_kernel_opts, pxe_config['extra_opts'])
3540
3541 def test_pxeconfig_returns_None_for_extra_kernel_opts(self):
3542 mac = factory.make_mac_address()
3543 params = self.get_default_params()
3544 params['mac'] = mac.mac_address
3545 pxe_config = self.get_pxeconfig(params)
3546 self.assertEqual(None, pxe_config['extra_opts'])
3547
35073548
3508class TestNodeGroupsAPI(APIv10TestMixin, MultipleUsersScenarios, TestCase):3549class TestNodeGroupsAPI(APIv10TestMixin, MultipleUsersScenarios, TestCase):
3509 scenarios = [3550 scenarios = [
35103551
=== modified file 'src/maasserver/tests/test_node.py'
--- src/maasserver/tests/test_node.py 2012-11-05 08:23:36 +0000
+++ src/maasserver/tests/test_node.py 2012-11-08 10:09:36 +0000
@@ -317,6 +317,62 @@
317 successful_types = [node_power_types[node] for node in started_nodes]317 successful_types = [node_power_types[node] for node in started_nodes]
318 self.assertItemsEqual(configless_power_types, successful_types)318 self.assertItemsEqual(configless_power_types, successful_types)
319319
320 def test_get_effective_kernel_options_with_nothing_set(self):
321 node = factory.make_node()
322 self.assertEqual((None, None), node.get_effective_kernel_options())
323
324 def test_get_effective_kernel_options_sees_global_config(self):
325 node = factory.make_node()
326 kernel_opts = factory.getRandomString()
327 Config.objects.set_config('kernel_opts', kernel_opts)
328 self.assertEqual(
329 (None, kernel_opts), node.get_effective_kernel_options())
330
331 def test_get_effective_kernel_options_not_confused_by_empty_tag(self):
332 node = factory.make_node()
333 tag = factory.make_tag()
334 node.tags.add(tag)
335 kernel_opts = factory.getRandomString()
336 Config.objects.set_config('kernel_opts', kernel_opts)
337 self.assertEqual(
338 (None, kernel_opts), node.get_effective_kernel_options())
339
340 def test_get_effective_kernel_options_ignores_unassociated_tag_value(self):
341 node = factory.make_node()
342 factory.make_tag(kernel_opts=factory.getRandomString())
343 self.assertEqual((None, None), node.get_effective_kernel_options())
344
345 def test_get_effective_kernel_options_uses_tag_value(self):
346 node = factory.make_node()
347 tag = factory.make_tag(kernel_opts=factory.getRandomString())
348 node.tags.add(tag)
349 self.assertEqual(
350 (tag, tag.kernel_opts), node.get_effective_kernel_options())
351
352 def test_get_effective_kernel_options_tag_overrides_global(self):
353 node = factory.make_node()
354 global_opts = factory.getRandomString()
355 Config.objects.set_config('kernel_opts', global_opts)
356 tag = factory.make_tag(kernel_opts=factory.getRandomString())
357 node.tags.add(tag)
358 self.assertEqual(
359 (tag, tag.kernel_opts), node.get_effective_kernel_options())
360
361 def test_get_effective_kernel_options_uses_first_real_tag_value(self):
362 node = factory.make_node()
363 # Intentionally create them in reverse order, so the default 'db' order
364 # doesn't work, and we have asserted that we sort them.
365 tag3 = factory.make_tag(factory.make_name('tag-03-'),
366 kernel_opts=factory.getRandomString())
367 tag2 = factory.make_tag(factory.make_name('tag-02-'),
368 kernel_opts=factory.getRandomString())
369 tag1 = factory.make_tag(factory.make_name('tag-01-'), kernel_opts=None)
370 self.assertTrue(tag1.name < tag2.name)
371 self.assertTrue(tag2.name < tag3.name)
372 node.tags.add(tag1, tag2, tag3)
373 self.assertEqual(
374 (tag2, tag2.kernel_opts), node.get_effective_kernel_options())
375
320 def test_acquire(self):376 def test_acquire(self):
321 node = factory.make_node(status=NODE_STATUS.READY)377 node = factory.make_node(status=NODE_STATUS.READY)
322 user = factory.make_user()378 user = factory.make_user()
323379
=== modified file 'src/maasserver/tests/test_tag.py'
--- src/maasserver/tests/test_tag.py 2012-10-10 09:41:48 +0000
+++ src/maasserver/tests/test_tag.py 2012-11-08 10:09:36 +0000
@@ -29,6 +29,16 @@
29 self.assertEqual('tag-name', tag.name)29 self.assertEqual('tag-name', tag.name)
30 self.assertEqual('//node[@id=display]', tag.definition)30 self.assertEqual('//node[@id=display]', tag.definition)
31 self.assertEqual('', tag.comment)31 self.assertEqual('', tag.comment)
32 self.assertIs(None, tag.kernel_opts)
33 self.assertIsNot(None, tag.updated)
34 self.assertIsNot(None, tag.created)
35
36 def test_factory_make_tag_with_hardware_details(self):
37 tag = factory.make_tag('a-tag', 'true', kernel_opts="console=ttyS0")
38 self.assertEqual('a-tag', tag.name)
39 self.assertEqual('true', tag.definition)
40 self.assertEqual('', tag.comment)
41 self.assertEqual('console=ttyS0', tag.kernel_opts)
32 self.assertIsNot(None, tag.updated)42 self.assertIsNot(None, tag.updated)
33 self.assertIsNot(None, tag.created)43 self.assertIsNot(None, tag.created)
3444
3545
=== modified file 'src/provisioningserver/kernel_opts.py'
--- src/provisioningserver/kernel_opts.py 2012-10-09 15:43:33 +0000
+++ src/provisioningserver/kernel_opts.py 2012-11-08 10:09:36 +0000
@@ -37,6 +37,8 @@
37 "preseed_url", # URL from which a preseed can be obtained.37 "preseed_url", # URL from which a preseed can be obtained.
38 "log_host", # Host/IP to which syslog can be streamed.38 "log_host", # Host/IP to which syslog can be streamed.
39 "fs_host", # Host/IP on which ephemeral filesystems are hosted.39 "fs_host", # Host/IP on which ephemeral filesystems are hosted.
40 "extra_opts", # String of extra options to supply, will be appended
41 # verbatim to the kernel command line
40 ))42 ))
4143
4244
@@ -176,4 +178,6 @@
176 # as it would be nice to have.178 # as it would be nice to have.
177 options += compose_logging_opts(params.log_host)179 options += compose_logging_opts(params.log_host)
178 options += compose_arch_opts(params)180 options += compose_arch_opts(params)
181 if params.extra_opts:
182 options.append(params.extra_opts)
179 return ' '.join(options)183 return ' '.join(options)
180184
=== modified file 'src/provisioningserver/tests/test_kernel_opts.py'
--- src/provisioningserver/tests/test_kernel_opts.py 2012-10-09 15:39:54 +0000
+++ src/provisioningserver/tests/test_kernel_opts.py 2012-11-08 10:09:36 +0000
@@ -133,6 +133,19 @@
133 "overlayroot=tmpfs",133 "overlayroot=tmpfs",
134 "ip=::::%s:BOOTIF" % params.hostname]))134 "ip=::::%s:BOOTIF" % params.hostname]))
135135
136 def test_commissioning_compose_kernel_command_line_inc_extra_opts(self):
137 extra_opts = "special console=ABCD -- options to pass"
138 params = make_kernel_parameters(extra_opts=extra_opts)
139 cmdline = compose_kernel_command_line(params)
140 # There should be a blank space before the options, but otherwise added
141 # verbatim.
142 self.assertThat(cmdline, Contains(' ' + extra_opts))
143
144 def test_commissioning_compose_kernel_handles_extra_opts_None(self):
145 params = make_kernel_parameters(extra_opts=None)
146 cmdline = compose_kernel_command_line(params)
147 self.assertNotIn(cmdline, "None")
148
136 def test_compose_kernel_command_line_inc_common_opts(self):149 def test_compose_kernel_command_line_inc_common_opts(self):
137 # Test that some kernel arguments appear on both commissioning150 # Test that some kernel arguments appear on both commissioning
138 # and install command lines.151 # and install command lines.