Merge lp:~jameinel/maas/land-kernel-opts-in-trunk into lp:~maas-committers/maas/trunk
- land-kernel-opts-in-trunk
- Merge into 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 |
Related bugs: |
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_
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.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/api.py' |
2 | --- src/maasserver/api.py 2012-11-08 09:12:44 +0000 |
3 | +++ src/maasserver/api.py 2012-11-08 10:09:36 +0000 |
4 | @@ -748,6 +748,13 @@ |
5 | The minimum data required is: |
6 | architecture=<arch string> (e.g "i386/generic") |
7 | mac_address=<value> |
8 | + |
9 | + :param architecture: A string containing the architecture type of |
10 | + the node. |
11 | + :param mac_address: The MAC address of the node. |
12 | + :param hostname: A hostname. If not given, one will be generated. |
13 | + :param powertype: A power management type, if applicable (e.g. |
14 | + "virsh", "ipmi"). |
15 | """ |
16 | node = create_node(request) |
17 | if request.user.is_superuser: |
18 | @@ -1527,6 +1534,7 @@ |
19 | 'name', |
20 | 'definition', |
21 | 'comment', |
22 | + 'kernel_opts', |
23 | ) |
24 | |
25 | def read(self, request, name): |
26 | @@ -1658,6 +1666,11 @@ |
27 | It is meant as a human readable description of the tag. |
28 | :param definition: An XPATH query that will be evaluated against the |
29 | hardware_details stored for all nodes (output of `lshw -xml`). |
30 | + :param kernel_opts: Can be None. If set, nodes associated with this tag |
31 | + will add this string to their kernel options when booting. The |
32 | + value overrides the global 'kernel_opts' setting. If more than one |
33 | + tag is associated with a node, the one with the lowest alphabetical |
34 | + name will be picked (eg 01-my-tag will be taken over 99-tag-name). |
35 | """ |
36 | if not request.user.is_superuser: |
37 | raise PermissionDenied() |
38 | @@ -1892,6 +1905,13 @@ |
39 | else: |
40 | series = node.get_distro_series() |
41 | |
42 | + if node is not None: |
43 | + # We don't care if the kernel opts is from the global setting or a tag, |
44 | + # just get the options |
45 | + _, extra_kernel_opts = node.get_effective_kernel_options() |
46 | + else: |
47 | + extra_kernel_opts = None |
48 | + |
49 | purpose = get_boot_purpose(node) |
50 | server_address = get_maas_facing_server_address() |
51 | cluster_address = get_mandatory_param(request.GET, "local") |
52 | @@ -1899,7 +1919,8 @@ |
53 | params = KernelParameters( |
54 | arch=arch, subarch=subarch, release=series, purpose=purpose, |
55 | hostname=hostname, domain=domain, preseed_url=preseed_url, |
56 | - log_host=server_address, fs_host=cluster_address) |
57 | + log_host=server_address, fs_host=cluster_address, |
58 | + extra_opts=extra_kernel_opts) |
59 | |
60 | return HttpResponse( |
61 | json.dumps(params._asdict()), |
62 | |
63 | === modified file 'src/maasserver/forms.py' |
64 | --- src/maasserver/forms.py 2012-11-08 08:33:59 +0000 |
65 | +++ src/maasserver/forms.py 2012-11-08 10:09:36 +0000 |
66 | @@ -864,6 +864,7 @@ |
67 | 'name', |
68 | 'comment', |
69 | 'definition', |
70 | + 'kernel_opts', |
71 | ) |
72 | |
73 | def clean_definition(self): |
74 | |
75 | === added file 'src/maasserver/migrations/0045_add_tag_kernel_opts.py' |
76 | --- src/maasserver/migrations/0045_add_tag_kernel_opts.py 1970-01-01 00:00:00 +0000 |
77 | +++ src/maasserver/migrations/0045_add_tag_kernel_opts.py 2012-11-08 10:09:36 +0000 |
78 | @@ -0,0 +1,203 @@ |
79 | +# -*- coding: utf-8 -*- |
80 | +import datetime |
81 | +from south.db import db |
82 | +from south.v2 import SchemaMigration |
83 | +from django.db import models |
84 | + |
85 | + |
86 | +class Migration(SchemaMigration): |
87 | + |
88 | + def forwards(self, orm): |
89 | + # Adding field 'Tag.kernel_opts' |
90 | + db.add_column(u'maasserver_tag', 'kernel_opts', |
91 | + self.gf('django.db.models.fields.TextField')(null=True, blank=True), |
92 | + keep_default=False) |
93 | + |
94 | + |
95 | + def backwards(self, orm): |
96 | + # Deleting field 'Tag.kernel_opts' |
97 | + db.delete_column(u'maasserver_tag', 'kernel_opts') |
98 | + |
99 | + |
100 | + models = { |
101 | + 'auth.group': { |
102 | + 'Meta': {'object_name': 'Group'}, |
103 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
104 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), |
105 | + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) |
106 | + }, |
107 | + 'auth.permission': { |
108 | + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, |
109 | + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
110 | + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), |
111 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
112 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) |
113 | + }, |
114 | + 'auth.user': { |
115 | + 'Meta': {'object_name': 'User'}, |
116 | + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
117 | + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}), |
118 | + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
119 | + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), |
120 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
121 | + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
122 | + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
123 | + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
124 | + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
125 | + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
126 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), |
127 | + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), |
128 | + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) |
129 | + }, |
130 | + 'contenttypes.contenttype': { |
131 | + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, |
132 | + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
133 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
134 | + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
135 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
136 | + }, |
137 | + u'maasserver.bootimage': { |
138 | + 'Meta': {'unique_together': "((u'nodegroup', u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'}, |
139 | + 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
140 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
141 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}), |
142 | + 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
143 | + 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
144 | + 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}) |
145 | + }, |
146 | + u'maasserver.componenterror': { |
147 | + 'Meta': {'object_name': 'ComponentError'}, |
148 | + 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}), |
149 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
150 | + 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), |
151 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
152 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
153 | + }, |
154 | + u'maasserver.config': { |
155 | + 'Meta': {'object_name': 'Config'}, |
156 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
157 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
158 | + 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'}) |
159 | + }, |
160 | + u'maasserver.dhcplease': { |
161 | + 'Meta': {'object_name': 'DHCPLease'}, |
162 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
163 | + 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}), |
164 | + 'mac': ('maasserver.fields.MACAddressField', [], {}), |
165 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}) |
166 | + }, |
167 | + u'maasserver.filestorage': { |
168 | + 'Meta': {'object_name': 'FileStorage'}, |
169 | + 'content': ('metadataserver.fields.BinaryField', [], {}), |
170 | + 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), |
171 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) |
172 | + }, |
173 | + u'maasserver.macaddress': { |
174 | + 'Meta': {'object_name': 'MACAddress'}, |
175 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
176 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
177 | + 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}), |
178 | + 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}), |
179 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
180 | + }, |
181 | + u'maasserver.node': { |
182 | + 'Meta': {'object_name': 'Node'}, |
183 | + 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
184 | + 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}), |
185 | + 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
186 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
187 | + 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}), |
188 | + 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
189 | + 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), |
190 | + 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}), |
191 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
192 | + 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
193 | + 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
194 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}), |
195 | + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), |
196 | + 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}), |
197 | + 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}), |
198 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}), |
199 | + 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-a776a79c-298b-11e2-90d1-080027748fea'", 'unique': 'True', 'max_length': '41'}), |
200 | + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}), |
201 | + 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}), |
202 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
203 | + }, |
204 | + u'maasserver.nodegroup': { |
205 | + 'Meta': {'object_name': 'NodeGroup'}, |
206 | + 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}), |
207 | + 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}), |
208 | + 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}), |
209 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
210 | + 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
211 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
212 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}), |
213 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
214 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
215 | + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}) |
216 | + }, |
217 | + u'maasserver.nodegroupinterface': { |
218 | + 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'}, |
219 | + 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
220 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
221 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
222 | + 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
223 | + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), |
224 | + 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
225 | + 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
226 | + 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
227 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}), |
228 | + 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
229 | + 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
230 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
231 | + }, |
232 | + u'maasserver.sshkey': { |
233 | + 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'}, |
234 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
235 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
236 | + 'key': ('django.db.models.fields.TextField', [], {}), |
237 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
238 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) |
239 | + }, |
240 | + u'maasserver.tag': { |
241 | + 'Meta': {'object_name': 'Tag'}, |
242 | + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
243 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
244 | + 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
245 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
246 | + 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
247 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), |
248 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
249 | + }, |
250 | + u'maasserver.userprofile': { |
251 | + 'Meta': {'object_name': 'UserProfile'}, |
252 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
253 | + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) |
254 | + }, |
255 | + 'piston.consumer': { |
256 | + 'Meta': {'object_name': 'Consumer'}, |
257 | + 'description': ('django.db.models.fields.TextField', [], {}), |
258 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
259 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}), |
260 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
261 | + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}), |
262 | + 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}), |
263 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"}) |
264 | + }, |
265 | + 'piston.token': { |
266 | + 'Meta': {'object_name': 'Token'}, |
267 | + 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
268 | + 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
269 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}), |
270 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
271 | + 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
272 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}), |
273 | + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}), |
274 | + 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1352369055L'}), |
275 | + 'token_type': ('django.db.models.fields.IntegerField', [], {}), |
276 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}), |
277 | + 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'}) |
278 | + } |
279 | + } |
280 | + |
281 | + complete_apps = ['maasserver'] |
282 | \ No newline at end of file |
283 | |
284 | === modified file 'src/maasserver/models/node.py' |
285 | --- src/maasserver/models/node.py 2012-11-05 08:23:36 +0000 |
286 | +++ src/maasserver/models/node.py 2012-11-08 10:09:36 +0000 |
287 | @@ -73,7 +73,10 @@ |
288 | get_db_state, |
289 | strip_domain, |
290 | ) |
291 | -from maasserver.utils.orm import get_first |
292 | +from maasserver.utils.orm import ( |
293 | + get_first, |
294 | + get_one, |
295 | + ) |
296 | from piston.models import Token |
297 | from provisioningserver.enum import ( |
298 | POWER_TYPE, |
299 | @@ -722,6 +725,26 @@ |
300 | else: |
301 | return None |
302 | |
303 | + def get_effective_kernel_options(self): |
304 | + """Determine any special kernel parameters for this node. |
305 | + |
306 | + :return: (tag, kernel_options) |
307 | + tag is a Tag object or None. If None, the kernel_options came from |
308 | + the global setting. |
309 | + kernel_options, a string indicating extra kernel_options that |
310 | + should be used when booting this node. May be None if no tags match |
311 | + and no global setting has been configured. |
312 | + """ |
313 | + # First, see if there are any tags associated with this node that has a |
314 | + # custom kernel parameter |
315 | + tags = self.tags.filter(kernel_opts__isnull=False) |
316 | + tags = tags.order_by('name')[:1] |
317 | + tag = get_one(tags) |
318 | + if tag is not None: |
319 | + return tag, tag.kernel_opts |
320 | + global_value = Config.objects.get_config('kernel_opts') |
321 | + return None, global_value |
322 | + |
323 | @property |
324 | def work_queue(self): |
325 | """The name of the queue for tasks specific to this node.""" |
326 | |
327 | === modified file 'src/maasserver/models/tag.py' |
328 | --- src/maasserver/models/tag.py 2012-10-24 16:07:00 +0000 |
329 | +++ src/maasserver/models/tag.py 2012-11-08 10:09:36 +0000 |
330 | @@ -89,6 +89,8 @@ |
331 | tag. |
332 | :ivar comment: A long-form description for humans about what this tag is |
333 | trying to accomplish. |
334 | + :ivar kernel_opts: Optional kernel command-line parameters string to be |
335 | + used in the PXE config for nodes with this tags. |
336 | :ivar objects: The :class:`TagManager`. |
337 | """ |
338 | |
339 | @@ -101,6 +103,7 @@ |
340 | validators=[RegexValidator(_tag_name_regex)]) |
341 | definition = TextField(blank=True) |
342 | comment = TextField(blank=True) |
343 | + kernel_opts = TextField(blank=True, null=True) |
344 | |
345 | objects = TagManager() |
346 | |
347 | |
348 | === modified file 'src/maasserver/testing/factory.py' |
349 | --- src/maasserver/testing/factory.py 2012-11-01 15:59:22 +0000 |
350 | +++ src/maasserver/testing/factory.py 2012-11-08 10:09:36 +0000 |
351 | @@ -251,14 +251,15 @@ |
352 | key.save() |
353 | return key |
354 | |
355 | - def make_tag(self, name=None, definition=None, comment='', created=None, |
356 | - updated=None): |
357 | + def make_tag(self, name=None, definition=None, comment='', |
358 | + kernel_opts=None, created=None, updated=None): |
359 | if name is None: |
360 | name = self.make_name('tag') |
361 | if definition is None: |
362 | # Is there a 'node' in this xml? |
363 | definition = '//node' |
364 | - tag = Tag(name=name, definition=definition, comment=comment) |
365 | + tag = Tag(name=name, definition=definition, comment=comment, |
366 | + kernel_opts=kernel_opts) |
367 | self._save_node_unchecked(tag) |
368 | # Update the 'updated'/'created' fields with a call to 'update' |
369 | # preventing a call to save() from overriding the values. |
370 | |
371 | === modified file 'src/maasserver/tests/test_api.py' |
372 | --- src/maasserver/tests/test_api.py 2012-11-08 09:12:44 +0000 |
373 | +++ src/maasserver/tests/test_api.py 2012-11-08 10:09:36 +0000 |
374 | @@ -3141,6 +3141,30 @@ |
375 | % (invalid,)) |
376 | self.assertFalse(Tag.objects.filter(name=invalid).exists()) |
377 | |
378 | + def test_POST_new_kernel_opts(self): |
379 | + self.become_admin() |
380 | + name = factory.getRandomString() |
381 | + definition = '//node' |
382 | + comment = factory.getRandomString() |
383 | + extra_kernel_opts = factory.getRandomString() |
384 | + response = self.client.post( |
385 | + self.get_uri('tags/'), |
386 | + { |
387 | + 'op': 'new', |
388 | + 'name': name, |
389 | + 'comment': comment, |
390 | + 'definition': definition, |
391 | + 'kernel_opts': extra_kernel_opts, |
392 | + }) |
393 | + self.assertEqual(httplib.OK, response.status_code) |
394 | + parsed_result = json.loads(response.content) |
395 | + self.assertEqual(name, parsed_result['name']) |
396 | + self.assertEqual(comment, parsed_result['comment']) |
397 | + self.assertEqual(definition, parsed_result['definition']) |
398 | + self.assertEqual(extra_kernel_opts, parsed_result['kernel_opts']) |
399 | + self.assertEqual( |
400 | + extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts) |
401 | + |
402 | def test_POST_new_populates_nodes(self): |
403 | self.become_admin() |
404 | node1 = factory.make_node() |
405 | @@ -3504,6 +3528,23 @@ |
406 | kernel_params = KernelParameters(**self.get_pxeconfig(params)) |
407 | self.assertEqual(params["local"], kernel_params.fs_host) |
408 | |
409 | + def test_pxeconfig_returns_extra_kernel_options(self): |
410 | + node = factory.make_node() |
411 | + extra_kernel_opts = factory.getRandomString() |
412 | + Config.objects.set_config('kernel_opts', extra_kernel_opts) |
413 | + mac = factory.make_mac_address(node=node) |
414 | + params = self.get_default_params() |
415 | + params['mac'] = mac.mac_address |
416 | + pxe_config = self.get_pxeconfig(params) |
417 | + self.assertEqual(extra_kernel_opts, pxe_config['extra_opts']) |
418 | + |
419 | + def test_pxeconfig_returns_None_for_extra_kernel_opts(self): |
420 | + mac = factory.make_mac_address() |
421 | + params = self.get_default_params() |
422 | + params['mac'] = mac.mac_address |
423 | + pxe_config = self.get_pxeconfig(params) |
424 | + self.assertEqual(None, pxe_config['extra_opts']) |
425 | + |
426 | |
427 | class TestNodeGroupsAPI(APIv10TestMixin, MultipleUsersScenarios, TestCase): |
428 | scenarios = [ |
429 | |
430 | === modified file 'src/maasserver/tests/test_node.py' |
431 | --- src/maasserver/tests/test_node.py 2012-11-05 08:23:36 +0000 |
432 | +++ src/maasserver/tests/test_node.py 2012-11-08 10:09:36 +0000 |
433 | @@ -317,6 +317,62 @@ |
434 | successful_types = [node_power_types[node] for node in started_nodes] |
435 | self.assertItemsEqual(configless_power_types, successful_types) |
436 | |
437 | + def test_get_effective_kernel_options_with_nothing_set(self): |
438 | + node = factory.make_node() |
439 | + self.assertEqual((None, None), node.get_effective_kernel_options()) |
440 | + |
441 | + def test_get_effective_kernel_options_sees_global_config(self): |
442 | + node = factory.make_node() |
443 | + kernel_opts = factory.getRandomString() |
444 | + Config.objects.set_config('kernel_opts', kernel_opts) |
445 | + self.assertEqual( |
446 | + (None, kernel_opts), node.get_effective_kernel_options()) |
447 | + |
448 | + def test_get_effective_kernel_options_not_confused_by_empty_tag(self): |
449 | + node = factory.make_node() |
450 | + tag = factory.make_tag() |
451 | + node.tags.add(tag) |
452 | + kernel_opts = factory.getRandomString() |
453 | + Config.objects.set_config('kernel_opts', kernel_opts) |
454 | + self.assertEqual( |
455 | + (None, kernel_opts), node.get_effective_kernel_options()) |
456 | + |
457 | + def test_get_effective_kernel_options_ignores_unassociated_tag_value(self): |
458 | + node = factory.make_node() |
459 | + factory.make_tag(kernel_opts=factory.getRandomString()) |
460 | + self.assertEqual((None, None), node.get_effective_kernel_options()) |
461 | + |
462 | + def test_get_effective_kernel_options_uses_tag_value(self): |
463 | + node = factory.make_node() |
464 | + tag = factory.make_tag(kernel_opts=factory.getRandomString()) |
465 | + node.tags.add(tag) |
466 | + self.assertEqual( |
467 | + (tag, tag.kernel_opts), node.get_effective_kernel_options()) |
468 | + |
469 | + def test_get_effective_kernel_options_tag_overrides_global(self): |
470 | + node = factory.make_node() |
471 | + global_opts = factory.getRandomString() |
472 | + Config.objects.set_config('kernel_opts', global_opts) |
473 | + tag = factory.make_tag(kernel_opts=factory.getRandomString()) |
474 | + node.tags.add(tag) |
475 | + self.assertEqual( |
476 | + (tag, tag.kernel_opts), node.get_effective_kernel_options()) |
477 | + |
478 | + def test_get_effective_kernel_options_uses_first_real_tag_value(self): |
479 | + node = factory.make_node() |
480 | + # Intentionally create them in reverse order, so the default 'db' order |
481 | + # doesn't work, and we have asserted that we sort them. |
482 | + tag3 = factory.make_tag(factory.make_name('tag-03-'), |
483 | + kernel_opts=factory.getRandomString()) |
484 | + tag2 = factory.make_tag(factory.make_name('tag-02-'), |
485 | + kernel_opts=factory.getRandomString()) |
486 | + tag1 = factory.make_tag(factory.make_name('tag-01-'), kernel_opts=None) |
487 | + self.assertTrue(tag1.name < tag2.name) |
488 | + self.assertTrue(tag2.name < tag3.name) |
489 | + node.tags.add(tag1, tag2, tag3) |
490 | + self.assertEqual( |
491 | + (tag2, tag2.kernel_opts), node.get_effective_kernel_options()) |
492 | + |
493 | def test_acquire(self): |
494 | node = factory.make_node(status=NODE_STATUS.READY) |
495 | user = factory.make_user() |
496 | |
497 | === modified file 'src/maasserver/tests/test_tag.py' |
498 | --- src/maasserver/tests/test_tag.py 2012-10-10 09:41:48 +0000 |
499 | +++ src/maasserver/tests/test_tag.py 2012-11-08 10:09:36 +0000 |
500 | @@ -29,6 +29,16 @@ |
501 | self.assertEqual('tag-name', tag.name) |
502 | self.assertEqual('//node[@id=display]', tag.definition) |
503 | self.assertEqual('', tag.comment) |
504 | + self.assertIs(None, tag.kernel_opts) |
505 | + self.assertIsNot(None, tag.updated) |
506 | + self.assertIsNot(None, tag.created) |
507 | + |
508 | + def test_factory_make_tag_with_hardware_details(self): |
509 | + tag = factory.make_tag('a-tag', 'true', kernel_opts="console=ttyS0") |
510 | + self.assertEqual('a-tag', tag.name) |
511 | + self.assertEqual('true', tag.definition) |
512 | + self.assertEqual('', tag.comment) |
513 | + self.assertEqual('console=ttyS0', tag.kernel_opts) |
514 | self.assertIsNot(None, tag.updated) |
515 | self.assertIsNot(None, tag.created) |
516 | |
517 | |
518 | === modified file 'src/provisioningserver/kernel_opts.py' |
519 | --- src/provisioningserver/kernel_opts.py 2012-10-09 15:43:33 +0000 |
520 | +++ src/provisioningserver/kernel_opts.py 2012-11-08 10:09:36 +0000 |
521 | @@ -37,6 +37,8 @@ |
522 | "preseed_url", # URL from which a preseed can be obtained. |
523 | "log_host", # Host/IP to which syslog can be streamed. |
524 | "fs_host", # Host/IP on which ephemeral filesystems are hosted. |
525 | + "extra_opts", # String of extra options to supply, will be appended |
526 | + # verbatim to the kernel command line |
527 | )) |
528 | |
529 | |
530 | @@ -176,4 +178,6 @@ |
531 | # as it would be nice to have. |
532 | options += compose_logging_opts(params.log_host) |
533 | options += compose_arch_opts(params) |
534 | + if params.extra_opts: |
535 | + options.append(params.extra_opts) |
536 | return ' '.join(options) |
537 | |
538 | === modified file 'src/provisioningserver/tests/test_kernel_opts.py' |
539 | --- src/provisioningserver/tests/test_kernel_opts.py 2012-10-09 15:39:54 +0000 |
540 | +++ src/provisioningserver/tests/test_kernel_opts.py 2012-11-08 10:09:36 +0000 |
541 | @@ -133,6 +133,19 @@ |
542 | "overlayroot=tmpfs", |
543 | "ip=::::%s:BOOTIF" % params.hostname])) |
544 | |
545 | + def test_commissioning_compose_kernel_command_line_inc_extra_opts(self): |
546 | + extra_opts = "special console=ABCD -- options to pass" |
547 | + params = make_kernel_parameters(extra_opts=extra_opts) |
548 | + cmdline = compose_kernel_command_line(params) |
549 | + # There should be a blank space before the options, but otherwise added |
550 | + # verbatim. |
551 | + self.assertThat(cmdline, Contains(' ' + extra_opts)) |
552 | + |
553 | + def test_commissioning_compose_kernel_handles_extra_opts_None(self): |
554 | + params = make_kernel_parameters(extra_opts=None) |
555 | + cmdline = compose_kernel_command_line(params) |
556 | + self.assertNotIn(cmdline, "None") |
557 | + |
558 | def test_compose_kernel_command_line_inc_common_opts(self): |
559 | # Test that some kernel arguments appear on both commissioning |
560 | # and install command lines. |
Looks good.