Merge lp:~gz/maas/populate_tags into lp:maas/trunk
- populate_tags
- Merge into trunk
Proposed by
Martin Packman
on 2012-09-21
| Status: | Superseded |
|---|---|
| Proposed branch: | lp:~gz/maas/populate_tags |
| Merge into: | lp:maas/trunk |
| Diff against target: |
409 lines (+326/-3) 5 files modified
src/maasserver/migrations/0028_add_node_hardware_details.py (+203/-0) src/maasserver/models/node.py (+54/-1) src/maasserver/tests/test_node.py (+6/-0) src/metadataserver/api.py (+5/-2) src/metadataserver/tests/test_api.py (+58/-0) |
| To merge this branch: | bzr merge lp:~gz/maas/populate_tags |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| John A Meinel | 2012-09-21 | Approve on 2012-09-22 | |
|
Review via email:
|
|||
Commit Message
Description of the Change
When hardware_details is updated, fill in any matching tags and remove any that no longer match. Again, this means bypassing django, but all the messiness is contained in one function at least. There a few tidy ups to do still, but this should get us rolling with the basics needed from constraints.
To post a comment you must log in.
lp:~gz/maas/populate_tags
updated
on 2012-09-25
- 1042. By Martin Packman on 2012-09-25
-
Merge changes suggested in review of prerequisite branch
- 1043. By Martin Packman on 2012-09-25
-
Add note to update_
hardware_ details docstring about intended usage as suggested by allenap in review
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
| 1 | === added file 'src/maasserver/migrations/0028_add_node_hardware_details.py' |
| 2 | --- src/maasserver/migrations/0028_add_node_hardware_details.py 1970-01-01 00:00:00 +0000 |
| 3 | +++ src/maasserver/migrations/0028_add_node_hardware_details.py 2012-09-21 14:14:18 +0000 |
| 4 | @@ -0,0 +1,203 @@ |
| 5 | +# encoding: utf-8 |
| 6 | +import datetime |
| 7 | +from south.db import db |
| 8 | +from south.v2 import SchemaMigration |
| 9 | +from django.db import models |
| 10 | + |
| 11 | +class Migration(SchemaMigration): |
| 12 | + |
| 13 | + def forwards(self, orm): |
| 14 | + |
| 15 | + # Adding field 'Node.cpu_count' |
| 16 | + db.add_column(u'maasserver_node', 'cpu_count', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False) |
| 17 | + |
| 18 | + # Adding field 'Node.memory' |
| 19 | + db.add_column(u'maasserver_node', 'memory', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False) |
| 20 | + |
| 21 | + # Adding field 'Node.hardware_details' |
| 22 | + db.add_column(u'maasserver_node', 'hardware_details', self.gf('maasserver.fields.XMLField')(default=None, null=True, blank=True), keep_default=False) |
| 23 | + |
| 24 | + |
| 25 | + def backwards(self, orm): |
| 26 | + |
| 27 | + # Deleting field 'Node.cpu_count' |
| 28 | + db.delete_column(u'maasserver_node', 'cpu_count') |
| 29 | + |
| 30 | + # Deleting field 'Node.memory' |
| 31 | + db.delete_column(u'maasserver_node', 'memory') |
| 32 | + |
| 33 | + # Deleting field 'Node.hardware_details' |
| 34 | + db.delete_column(u'maasserver_node', 'hardware_details') |
| 35 | + |
| 36 | + |
| 37 | + models = { |
| 38 | + 'auth.group': { |
| 39 | + 'Meta': {'object_name': 'Group'}, |
| 40 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 41 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), |
| 42 | + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) |
| 43 | + }, |
| 44 | + 'auth.permission': { |
| 45 | + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, |
| 46 | + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
| 47 | + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), |
| 48 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 49 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) |
| 50 | + }, |
| 51 | + 'auth.user': { |
| 52 | + 'Meta': {'object_name': 'User'}, |
| 53 | + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
| 54 | + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}), |
| 55 | + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
| 56 | + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), |
| 57 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 58 | + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
| 59 | + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
| 60 | + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
| 61 | + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
| 62 | + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
| 63 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), |
| 64 | + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), |
| 65 | + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) |
| 66 | + }, |
| 67 | + 'contenttypes.contenttype': { |
| 68 | + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, |
| 69 | + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
| 70 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 71 | + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
| 72 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
| 73 | + }, |
| 74 | + u'maasserver.bootimage': { |
| 75 | + 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'}, |
| 76 | + 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
| 77 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 78 | + 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
| 79 | + 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
| 80 | + 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}) |
| 81 | + }, |
| 82 | + u'maasserver.config': { |
| 83 | + 'Meta': {'object_name': 'Config'}, |
| 84 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 85 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
| 86 | + 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'}) |
| 87 | + }, |
| 88 | + u'maasserver.dhcplease': { |
| 89 | + 'Meta': {'object_name': 'DHCPLease'}, |
| 90 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 91 | + 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}), |
| 92 | + 'mac': ('maasserver.fields.MACAddressField', [], {}), |
| 93 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}) |
| 94 | + }, |
| 95 | + u'maasserver.filestorage': { |
| 96 | + 'Meta': {'object_name': 'FileStorage'}, |
| 97 | + 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), |
| 98 | + 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}), |
| 99 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) |
| 100 | + }, |
| 101 | + u'maasserver.macaddress': { |
| 102 | + 'Meta': {'object_name': 'MACAddress'}, |
| 103 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
| 104 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 105 | + 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}), |
| 106 | + 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}), |
| 107 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
| 108 | + }, |
| 109 | + u'maasserver.node': { |
| 110 | + 'Meta': {'object_name': 'Node'}, |
| 111 | + 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
| 112 | + 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386'", 'max_length': '10'}), |
| 113 | + 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
| 114 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
| 115 | + 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}), |
| 116 | + 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
| 117 | + 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), |
| 118 | + 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
| 119 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 120 | + 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
| 121 | + 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
| 122 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}), |
| 123 | + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), |
| 124 | + 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}), |
| 125 | + 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}), |
| 126 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}), |
| 127 | + 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-4f4bbb90-032d-11e2-8bc6-fa163e17f81b'", 'unique': 'True', 'max_length': '41'}), |
| 128 | + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}), |
| 129 | + 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}), |
| 130 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
| 131 | + }, |
| 132 | + u'maasserver.nodegroup': { |
| 133 | + 'Meta': {'object_name': 'NodeGroup'}, |
| 134 | + 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}), |
| 135 | + 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}), |
| 136 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
| 137 | + 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
| 138 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 139 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), |
| 140 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
| 141 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
| 142 | + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}) |
| 143 | + }, |
| 144 | + u'maasserver.nodegroupinterface': { |
| 145 | + 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'}, |
| 146 | + 'broadcast_ip': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', 'null': 'True', 'blank': 'True'}), |
| 147 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
| 148 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 149 | + 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
| 150 | + 'ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), |
| 151 | + 'ip_range_high': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', 'unique': 'True', 'null': 'True', 'blank': 'True'}), |
| 152 | + 'ip_range_low': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', 'unique': 'True', 'null': 'True', 'blank': 'True'}), |
| 153 | + 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
| 154 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}), |
| 155 | + 'router_ip': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', 'null': 'True', 'blank': 'True'}), |
| 156 | + 'subnet_mask': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', 'null': 'True', 'blank': 'True'}), |
| 157 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
| 158 | + }, |
| 159 | + u'maasserver.sshkey': { |
| 160 | + 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'}, |
| 161 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
| 162 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 163 | + 'key': ('django.db.models.fields.TextField', [], {}), |
| 164 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
| 165 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) |
| 166 | + }, |
| 167 | + u'maasserver.tag': { |
| 168 | + 'Meta': {'object_name': 'Tag'}, |
| 169 | + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
| 170 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
| 171 | + 'definition': ('django.db.models.fields.TextField', [], {}), |
| 172 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 173 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), |
| 174 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
| 175 | + }, |
| 176 | + u'maasserver.userprofile': { |
| 177 | + 'Meta': {'object_name': 'UserProfile'}, |
| 178 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 179 | + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) |
| 180 | + }, |
| 181 | + 'piston.consumer': { |
| 182 | + 'Meta': {'object_name': 'Consumer'}, |
| 183 | + 'description': ('django.db.models.fields.TextField', [], {}), |
| 184 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 185 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}), |
| 186 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
| 187 | + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}), |
| 188 | + 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}), |
| 189 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"}) |
| 190 | + }, |
| 191 | + 'piston.token': { |
| 192 | + 'Meta': {'object_name': 'Token'}, |
| 193 | + 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
| 194 | + 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
| 195 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}), |
| 196 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
| 197 | + 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
| 198 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}), |
| 199 | + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}), |
| 200 | + 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1348150391L'}), |
| 201 | + 'token_type': ('django.db.models.fields.IntegerField', [], {}), |
| 202 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}), |
| 203 | + 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'}) |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + complete_apps = ['maasserver'] |
| 208 | |
| 209 | === modified file 'src/maasserver/models/node.py' |
| 210 | --- src/maasserver/models/node.py 2012-09-21 07:37:56 +0000 |
| 211 | +++ src/maasserver/models/node.py 2012-09-21 14:14:18 +0000 |
| 212 | @@ -13,6 +13,7 @@ |
| 213 | __all__ = [ |
| 214 | "NODE_TRANSITIONS", |
| 215 | "Node", |
| 216 | + "update_hardware_details", |
| 217 | ] |
| 218 | |
| 219 | import os |
| 220 | @@ -25,6 +26,9 @@ |
| 221 | PermissionDenied, |
| 222 | ValidationError, |
| 223 | ) |
| 224 | +from django.db import ( |
| 225 | + connection, |
| 226 | + ) |
| 227 | from django.db.models import ( |
| 228 | BooleanField, |
| 229 | CharField, |
| 230 | @@ -49,7 +53,7 @@ |
| 231 | NODE_STATUS_CHOICES_DICT, |
| 232 | ) |
| 233 | from maasserver.exceptions import NodeStateViolation |
| 234 | -from maasserver.fields import JSONObjectField |
| 235 | +from maasserver.fields import JSONObjectField, XMLField |
| 236 | from maasserver.models.cleansave import CleanSave |
| 237 | from maasserver.models.config import Config |
| 238 | from maasserver.models.tag import Tag |
| 239 | @@ -318,6 +322,44 @@ |
| 240 | return processed_nodes |
| 241 | |
| 242 | |
| 243 | + |
| 244 | +def update_hardware_details(node, xmlbytes): |
| 245 | + """Set node hardware_details from lshw output and update related fields |
| 246 | + |
| 247 | + There are a bunch of suboptimal things here: |
| 248 | + * Is a function rather than method in hope south migration can reuse. |
| 249 | + * Doing UPDATE then transaction.commit_unless_managed doesn't work? |
| 250 | + * Scalar returns from xpath() work in postgres 9.2 or later only. |
| 251 | + """ |
| 252 | + node.hardware_details = xmlbytes |
| 253 | + node.save() |
| 254 | + cursor = connection.cursor() |
| 255 | + cursor.execute("SELECT" |
| 256 | + " array_length(xpath(%s, hardware_details), 1) AS count" |
| 257 | + ", (xpath(%s, hardware_details))[1]::text::bigint / 1073741824 AS mem" |
| 258 | + " FROM maasserver_node" |
| 259 | + " WHERE id = %s", |
| 260 | + [ |
| 261 | + "//node[@id='core']/node[@class='processor']", |
| 262 | + "//node[@id='memory']/size[@units='bytes']/text()", |
| 263 | + node.id, |
| 264 | + ]) |
| 265 | + cpu_count, memory = cursor.fetchone() |
| 266 | + node.cpu_count = cpu_count or 0 |
| 267 | + node.memory = memory or 0 |
| 268 | + for tag in Tag.objects.all(): |
| 269 | + cursor.execute( |
| 270 | + "SELECT xpath_exists(%s, hardware_details)" |
| 271 | + " FROM maasserver_node WHERE id = %s", |
| 272 | + [tag.definition, node.id]) |
| 273 | + has_tag, = cursor.fetchone() |
| 274 | + if has_tag: |
| 275 | + node.tags.add(tag) |
| 276 | + else: |
| 277 | + node.tags.remove(tag) |
| 278 | + node.save() |
| 279 | + |
| 280 | + |
| 281 | class Node(CleanSave, TimestampedModel): |
| 282 | """A `Node` represents a physical machine used by the MAAS Server. |
| 283 | |
| 284 | @@ -370,6 +412,13 @@ |
| 285 | max_length=10, choices=ARCHITECTURE_CHOICES, blank=False, |
| 286 | default=ARCHITECTURE.i386) |
| 287 | |
| 288 | + # Juju expects the following standard constraints, which are stored here |
| 289 | + # as a basic optimisation over querying the hardware_details field. |
| 290 | + cpu_count = IntegerField(default=0) |
| 291 | + memory = IntegerField(default=0) |
| 292 | + |
| 293 | + hardware_details = XMLField(default=None, blank=True, null=True) |
| 294 | + |
| 295 | # For strings, Django insists on abusing the empty string ("blank") |
| 296 | # to mean "none." |
| 297 | power_type = CharField( |
| 298 | @@ -625,3 +674,7 @@ |
| 299 | """Set netboot on or off.""" |
| 300 | self.netboot = on |
| 301 | self.save() |
| 302 | + |
| 303 | + def set_hardware_details(self, xmlbytes): |
| 304 | + """Set the `lshw -xml` output""" |
| 305 | + update_hardware_details(self, xmlbytes) |
| 306 | |
| 307 | === modified file 'src/maasserver/tests/test_node.py' |
| 308 | --- src/maasserver/tests/test_node.py 2012-09-21 06:37:02 +0000 |
| 309 | +++ src/maasserver/tests/test_node.py 2012-09-21 14:14:18 +0000 |
| 310 | @@ -447,6 +447,12 @@ |
| 311 | node.nodegroup = None |
| 312 | self.assertRaises(ValidationError, node.save) |
| 313 | |
| 314 | + def test_set_hardware_details(self): |
| 315 | + xmlbytes = "<test/>" |
| 316 | + node = factory.make_node(owner=factory.make_user()) |
| 317 | + node.set_hardware_details(xmlbytes) |
| 318 | + self.assertEqual(xmlbytes, node.hardware_details) |
| 319 | + |
| 320 | |
| 321 | class NodeTransitionsTests(TestCase): |
| 322 | """Test the structure of NODE_TRANSITIONS.""" |
| 323 | |
| 324 | === modified file 'src/metadataserver/api.py' |
| 325 | --- src/metadataserver/api.py 2012-08-24 10:28:29 +0000 |
| 326 | +++ src/metadataserver/api.py 2012-09-21 14:14:18 +0000 |
| 327 | @@ -180,8 +180,11 @@ |
| 328 | def _store_commissioning_results(self, node, request): |
| 329 | """Store commissioning result files for `node`.""" |
| 330 | for name, uploaded_file in request.FILES.items(): |
| 331 | - contents = uploaded_file.read().decode('utf-8') |
| 332 | - NodeCommissionResult.objects.store_data(node, name, contents) |
| 333 | + if name == "01-lshw.out": |
| 334 | + node.set_hardware_details(uploaded_file.read()) |
| 335 | + else: |
| 336 | + contents = uploaded_file.read().decode('utf-8') |
| 337 | + NodeCommissionResult.objects.store_data(node, name, contents) |
| 338 | |
| 339 | @api_exported('POST') |
| 340 | def signal(self, request, version=None, mac=None): |
| 341 | |
| 342 | === modified file 'src/metadataserver/tests/test_api.py' |
| 343 | --- src/metadataserver/tests/test_api.py 2012-08-16 10:34:56 +0000 |
| 344 | +++ src/metadataserver/tests/test_api.py 2012-09-21 14:14:18 +0000 |
| 345 | @@ -494,6 +494,64 @@ |
| 346 | node, 'output.txt') |
| 347 | self.assertEqual(size_limit, len(stored_data)) |
| 348 | |
| 349 | + def test_signal_stores_lshw_file_on_node(self): |
| 350 | + node = factory.make_node(status=NODE_STATUS.COMMISSIONING) |
| 351 | + client = self.make_node_client(node=node) |
| 352 | + xmlbytes = "<t\xe9st/>".encode("utf-8") |
| 353 | + response = self.call_signal(client, files={'01-lshw.out': xmlbytes}) |
| 354 | + self.assertEqual(httplib.OK, response.status_code) |
| 355 | + node = reload_object(node) |
| 356 | + self.assertEqual(xmlbytes, node.hardware_details) |
| 357 | + |
| 358 | + def test_signal_stores_lshw_with_cpu_count(self): |
| 359 | + node = factory.make_node(status=NODE_STATUS.COMMISSIONING) |
| 360 | + client = self.make_node_client(node=node) |
| 361 | + xmlbytes = ( |
| 362 | + '<node id="core">' |
| 363 | + '<node id="cpu:0" class="processor"/>' |
| 364 | + '<node id="cpu:1" class="processor"/>' |
| 365 | + '</node>').encode("utf-8") |
| 366 | + response = self.call_signal(client, files={'01-lshw.out': xmlbytes}) |
| 367 | + self.assertEqual(httplib.OK, response.status_code) |
| 368 | + node = reload_object(node) |
| 369 | + self.assertEqual(2, node.cpu_count) |
| 370 | + |
| 371 | + def test_signal_stores_lshw_with_memory(self): |
| 372 | + node = factory.make_node(status=NODE_STATUS.COMMISSIONING) |
| 373 | + client = self.make_node_client(node=node) |
| 374 | + xmlbytes = ( |
| 375 | + '<node id="memory">' |
| 376 | + '<size units="bytes">4294967296</size>' |
| 377 | + '</node>').encode("utf-8") |
| 378 | + response = self.call_signal(client, files={'01-lshw.out': xmlbytes}) |
| 379 | + self.assertEqual(httplib.OK, response.status_code) |
| 380 | + node = reload_object(node) |
| 381 | + self.assertEqual(4, node.memory) |
| 382 | + |
| 383 | + def test_signal_lshw_tags_match(self): |
| 384 | + tag1 = factory.make_tag(factory.getRandomString(10), "/node") |
| 385 | + tag2 = factory.make_tag(factory.getRandomString(10), "//node") |
| 386 | + node = factory.make_node(status=NODE_STATUS.COMMISSIONING) |
| 387 | + client = self.make_node_client(node=node) |
| 388 | + xmlbytes = '<node/>'.encode("utf-8") |
| 389 | + response = self.call_signal(client, files={'01-lshw.out': xmlbytes}) |
| 390 | + self.assertEqual(httplib.OK, response.status_code) |
| 391 | + node = reload_object(node) |
| 392 | + self.assertEqual([tag1, tag2], list(node.tags.all())) |
| 393 | + |
| 394 | + def test_signal_lshw_tags_no_match(self): |
| 395 | + tag1 = factory.make_tag(factory.getRandomString(10), "/missing") |
| 396 | + tag2 = factory.make_tag(factory.getRandomString(10), "/nothing") |
| 397 | + node = factory.make_node(status=NODE_STATUS.COMMISSIONING) |
| 398 | + node.tags = [tag2] |
| 399 | + node.save() |
| 400 | + client = self.make_node_client(node=node) |
| 401 | + xmlbytes = '<node/>'.encode("utf-8") |
| 402 | + response = self.call_signal(client, files={'01-lshw.out': xmlbytes}) |
| 403 | + self.assertEqual(httplib.OK, response.status_code) |
| 404 | + node = reload_object(node) |
| 405 | + self.assertEqual([], list(node.tags.all())) |
| 406 | + |
| 407 | def test_api_retrieves_node_metadata_by_mac(self): |
| 408 | mac = factory.make_mac_address() |
| 409 | url = reverse( |


330 for name, uploaded_file in request. FILES.items( ): file.read( ).decode( 'utf-8' ) esult.objects. store_data( node, name, contents) hardware_ details( uploaded_ file.read( )) file.read( ).decode( 'utf-8' ) esult.objects. store_data( node, name, contents)
331 - contents = uploaded_
332 - NodeCommissionR
333 + if name == "01-lshw.out":
334 + node.set_
335 + else:
336 + contents = uploaded_
337 + NodeCommissionR
I wonder if we should still read the file and store it. I don't feel there is a strong case for breaking existing habits.
It really feels like we should be using lxml rather than putting the data in psql just to pull it back out again. The one thing I do like from using psql is the 'new tag update 1M nodes' case. But for the 'new node update 20 tags' case, it feels better in python. I also thought we wanted to try to store something like cpu:size since that will give us a way to approximate ECU, etc.
Also for memory, you seem to be storying GB as an integer. However, that means you can't tell the difference between a machine with 1 GB and 1.5GB. I would probably recommend storing at least MB. Alternatively, we could use a 64-bit value, but I do think it is a bit silly to store down to the exact byte.
I think there are too many spaces here: hardware_ details( node, xmlbytes):
240 return processed_nodes
241
242
243 +
244 +def update_
Make sure to run 'make lint' so the landing machine doesn't reject it.
The rest looks worthy to land (IMO) and we can do further cleanups from there.