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