Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/bug-1034523 into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1034523 in MAAS: "MAAS fails to upgrade"
https://bugs.launchpad.net/maas/+bug/1034523
For more details, see:
https://code.launchpad.net/~jtv/maas/bug-1034523/+merge/119091
This is a sad branch. A retreat after a long battle over a small piece of
ground. There just does not seem to be any way to get a NOT NULL constraint
onto Node.nodegroup. We can't reasonably create the master nodegroup from a
database migration, which we need in order to update pre-existing null fields,
so we have to do both after. And therefore we have to allow null values during
the schema-migration phase. Forever.
In this branch I remove the schema migration that added the NOT NULL
constraint, because it was breaking upgrades. This affects the subsequent
migration as well, because it also has to specify its notion of what the schema
looks like. The field is still non-blankable, which in theory should mean that
django will reject null values. It doesn't seem to do much in practice though.
Debugging showed that, because Node.nodegroup is non-editable, Django's field
validation will quietly skip it. I also tried custom model validation at the
model level (rather than validating at the field level), but that failed
because the form code needs to set a default nodegroup *after* saving the form
— which unfortunately entails model validation, and so breaks.
This also highlights an inconsistency in Django's design, at least in the
version we use. On the one hand, the developers have made it clear that a
model absolutely should go through validation during save(), and the only
reason why it doesn't do so by default is backwards compatibility. That's why
we have the CleanSave mix-in class, and as far as I can see, that mix-in is the
only reason why “Django will prevent any attempt to save an incomplete model,”
as the documentation misleadingly puts it.
On the other hand, Django lacks proper separations between storage, models, and
forms. The documentation I just quoted was talking about model forms, but
called them models. Model specification and validation are designed to support
validation of model forms, but not of models as such (or of forms as such, I
guess). The “editable” option on a model field only affects forms, but
validation seems to assume that a non-editable field never needs to be
validated again once an object has been created. Django's recommended practice
creates a paradox: setting a field to None will be disallowed, as you would
expect, if the field is non-blankable but editable; but is accepted if you try
to disallow both None values _and_ changing the field in the first place.
I tried writing tests for the blank=False validation, but couldn't get it to
raise an error even with Node(nodegroup=None).save(). We'll just have to leave
this column poorly protected against missing values. ☹
Jeroen
--
https://code.launchpad.net/~jtv/maas/bug-1034523/+merge/119091
Your team Launchpad code reviewers is requested to review the proposed merge of
lp:~jtv/maas/bug-1034523 into lp:maas.
=== modified file 'src/maasserver/migrations/0016_node_nodegroup_not_null.py'
--- src/maasserver/migrations/0016_node_nodegroup_not_null.py 2012-07-31 07:04:29 +0000
+++ src/maasserver/migrations/0016_node_nodegroup_not_null.py 2012-08-10 08:20:29 +0000
@@ -21,10 +21,10 @@
def forwards(self, orm):
- # Adding NOT NULL constraint on Node.nodegroup.
+ # Disallowing nulls in new Node.nodegroup values.
db.alter_column(
'maasserver_node', 'nodegroup_id',
- models.ForeignKey(NodeGroup, editable=False))
+ models.ForeignKey(NodeGroup, editable=False, null=True, blank=False))
def backwards(self, orm):
@@ -108,7 +108,7 @@
'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
=== modified file 'src/maasserver/migrations/0017_add_dhcp_key_to_nodegroup.py'
--- src/maasserver/migrations/0017_add_dhcp_key_to_nodegroup.py 2012-08-01 10:26:11 +0000
+++ src/maasserver/migrations/0017_add_dhcp_key_to_nodegroup.py 2012-08-10 08:20:29 +0000
@@ -91,7 +91,7 @@
'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2012-08-09 05:39:28 +0000
+++ src/maasserver/models/node.py 2012-08-10 08:20:29 +0000
@@ -383,7 +383,14 @@
netboot = BooleanField(default=True)
- nodegroup = ForeignKey('maasserver.NodeGroup', editable=False)
+ # This field can't be null, but we can't enforce that in the
+ # database schema: we make sure that no existing nodes have a null
+ # here, but a NOT NULL constraint would have to be applied at an
+ # earlier stage.
+ # To get around this, we allow nulls in the database ("null=True")
+ # but reject them in validation ("blank=False").
+ nodegroup = ForeignKey(
+ 'maasserver.NodeGroup', editable=False, null=True, blank=False)
objects = NodeManager()
_______________________________________________
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to : [email protected]
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help : https://help.launchpad.net/ListHelp