Merge lp:~jameinel/maas/land-kernel-opts-in-trunk into lp:maas/trunk
- land-kernel-opts-in-trunk
- Merge into trunk
Proposed by
John A Meinel
on 2012-11-08
| Status: | Merged |
|---|---|
| Approved by: | John A Meinel on 2012-11-08 |
| Approved revision: | 1338 |
| Merged at revision: | 1337 |
| Proposed branch: | lp:~jameinel/maas/land-kernel-opts-in-trunk |
| Merge into: | lp: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) | 2012-11-08 | Approve on 2012-11-08 | |
|
Review via email:
|
|||
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.