← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~julian-edwards/maas/commission-result-model into lp:maas

 

Julian Edwards has proposed merging lp:~julian-edwards/maas/commission-result-model into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~julian-edwards/maas/commission-result-model/+merge/101487

This branch adds support for a new model class, NodeCommissionResult, and is the product of an extensive pre-imp with Jeroen.

The idea is that nodes that go through commissioning will have data about the commissioning that they wish to store. We assume that each piece of data will have a name, and is text-based.  The manager object for the new model allows the user to store this data, retrieve it by name and to clear out all existing rows of data (this is required when re-commissioning).


-- 
https://code.launchpad.net/~julian-edwards/maas/commission-result-model/+merge/101487
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~julian-edwards/maas/commission-result-model into lp:maas.
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py	2012-04-10 07:32:27 +0000
+++ src/maasserver/testing/factory.py	2012-04-11 05:25:21 +0000
@@ -29,6 +29,7 @@
 from maasserver.testing import get_data
 from maasserver.testing.enum import map_enum
 import maastesting.factory
+from metadataserver.models import NodeCommissionResult
 
 # We have a limited number of public keys:
 # src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub
@@ -75,6 +76,17 @@
         node.save(skip_check=True)
         return node
 
+    def make_node_commission_result(self, node=None, name=None, data=None):
+        if node is None:
+            node = self.make_node()
+        if name is None:
+            name = self.getRandomString(100)
+        if data is None:
+            data = b'this is not random'
+        ncr = NodeCommissionResult(node=node, name=name, data=data)
+        ncr.save()
+        return ncr
+
     def make_mac_address(self, address):
         """Create a MAC address."""
         node = Node()

=== added file 'src/metadataserver/migrations/0002_add_nodecommissionresult.py'
--- src/metadataserver/migrations/0002_add_nodecommissionresult.py	1970-01-01 00:00:00 +0000
+++ src/metadataserver/migrations/0002_add_nodecommissionresult.py	2012-04-11 05:25:21 +0000
@@ -0,0 +1,129 @@
+# flake8: noqa
+# SKIP this file when reformatting.
+# The rest of this file was generated by South.
+
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding model 'NodeCommissionResult'
+        db.create_table('metadataserver_nodecommissionresult', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('node', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['maasserver.Node'])),
+            ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=100)),
+            ('data', self.gf('django.db.models.fields.CharField')(max_length=1048576)),
+        ))
+        db.send_create_signal('metadataserver', ['NodeCommissionResult'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'NodeCommissionResult'
+        db.delete_table('metadataserver_nodecommissionresult')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'maasserver.node': {
+            'Meta': {'object_name': 'Node'},
+            'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386'", 'max_length': '10'}),
+            'created': ('django.db.models.fields.DateField', [], {}),
+            'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+            'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+            'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+            'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-bf0bc2e0-838a-11e1-94e5-002215205ce8'", 'unique': 'True', 'max_length': '41'}),
+            'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {})
+        },
+        'metadataserver.nodecommissionresult': {
+            'Meta': {'object_name': 'NodeCommissionResult'},
+            'data': ('django.db.models.fields.CharField', [], {'max_length': '1048576'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
+            'node': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.Node']"})
+        },
+        'metadataserver.nodekey': {
+            'Meta': {'object_name': 'NodeKey'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
+            'node': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.Node']", 'unique': 'True'}),
+            'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'})
+        },
+        'metadataserver.nodeuserdata': {
+            'Meta': {'object_name': 'NodeUserData'},
+            'data': ('metadataserver.fields.BinaryField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'node': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.Node']", 'unique': 'True'})
+        },
+        'piston.consumer': {
+            'Meta': {'object_name': 'Consumer'},
+            'description': ('django.db.models.fields.TextField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
+        },
+        'piston.token': {
+            'Meta': {'object_name': 'Token'},
+            'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+            'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1334116773L'}),
+            'token_type': ('django.db.models.fields.IntegerField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+        }
+    }
+
+    complete_apps = ['metadataserver']

=== modified file 'src/metadataserver/models.py'
--- src/metadataserver/models.py	2012-04-04 09:31:32 +0000
+++ src/metadataserver/models.py	2012-04-11 05:25:21 +0000
@@ -20,6 +20,7 @@
     Manager,
     Model,
     )
+from django.shortcuts import get_object_or_404
 from maasserver.models import (
     create_auth_token,
     Node,
@@ -177,3 +178,46 @@
 
     node = ForeignKey(Node, null=False, editable=False, unique=True)
     data = BinaryField(null=False)
+
+
+class NodeCommissionResultManager(Manager):
+    """Utility to manage a collection of :class:`NodeCommissionResult`s."""
+
+    def clear_results(self, node):
+        """Remove all existing results for a node."""
+        self.filter(node=node).delete()
+
+    def store_data(self, node, name, data):
+        """Store data about a node."""
+        existing, created = self.get_or_create(
+            node=node, name=name, defaults=dict(data=data))
+        if not created:
+            existing.data = data
+            existing.save()
+
+    def get_data(self, node, name):
+        """Get data about a node."""
+        ncr = get_object_or_404(NodeCommissionResult, node=node, name=name)
+        return ncr.data
+
+
+class NodeCommissionResult(Model):
+    """Storage for data returned from node commissioning.
+
+    Commissioning a node results in various bits of data that need to be
+    stored, such as lshw output.  This model allows storing of this data
+    as unicode text, with an arbitrary name, for later retrieval.
+
+    :ivar node: The context :class:`Node`.
+    :ivar name: A unique name to use for the data being stored.
+    :ivar data: The file's actual data, unicode only.
+    """
+
+    class Meta:
+        unique_together = ('node', 'name')
+
+    objects = NodeCommissionResultManager()
+
+    node = ForeignKey(Node, null=False, editable=False, unique=False)
+    name = CharField(max_length=100, unique=True, editable=False)
+    data = CharField(max_length=1024 * 1024, editable=True)

=== modified file 'src/metadataserver/tests/test_models.py'
--- src/metadataserver/tests/test_models.py	2012-04-04 06:11:50 +0000
+++ src/metadataserver/tests/test_models.py	2012-04-11 05:25:21 +0000
@@ -11,9 +11,13 @@
 __metaclass__ = type
 __all__ = []
 
+from django.db import IntegrityError
+from django.http import Http404
+
 from maasserver.testing.factory import factory
 from maastesting.testcase import TestCase
 from metadataserver.models import (
+    NodeCommissionResult,
     NodeKey,
     NodeUserData,
     )
@@ -131,3 +135,86 @@
         node = factory.make_node()
         NodeUserData.objects.set_user_data(node, b"This node has user data.")
         self.assertTrue(NodeUserData.objects.has_user_data(node))
+
+
+class TestNodeCommissionResult(TestCase):
+    """Test the NodeCommissionResult model."""
+
+    def test_can_store_data(self):
+        node = factory.make_node()
+        name = factory.getRandomString(100)
+        data = factory.getRandomString(1025)
+        factory.make_node_commission_result(node=node, name=name, data=data)
+
+        ncr = NodeCommissionResult.objects.get(name=name)
+        self.assertAttributes(ncr, dict(node=node, data=data))
+
+    def test_node_name_uniqueness(self):
+        # You cannot have two result rows with the same name for the
+        # same node.
+        node = factory.make_node()
+        factory.make_node_commission_result(node=node, name="foo")
+        self.assertRaises(
+            IntegrityError,
+            factory.make_node_commission_result, node=node, name="foo")
+
+
+class TestNodeCommissionResultManager(TestCase):
+    """Test the manager utility for NodeCommissionResult."""
+
+    def test_clear_results_removes_rows(self):
+        # clear_results should remove all a node's results.
+        node = factory.make_node()
+        factory.make_node_commission_result(node=node)
+        factory.make_node_commission_result(node=node)
+        factory.make_node_commission_result(node=node)
+
+        NodeCommissionResult.objects.clear_results(node)
+        self.assertFalse(
+            NodeCommissionResult.objects.filter(node=node).exists())
+
+    def test_clear_results_ignores_other_nodes(self):
+        # clear_results should only remove results for the supplied
+        # node.
+        node1 = factory.make_node()
+        factory.make_node_commission_result(node=node1)
+        node2 = factory.make_node()
+        factory.make_node_commission_result(node=node2)
+
+        NodeCommissionResult.objects.clear_results(node1)
+        self.assertTrue(
+            NodeCommissionResult.objects.filter(node=node2).exists())
+
+    def test_store_data(self):
+        node = factory.make_node()
+        name = factory.getRandomString(100)
+        data = factory.getRandomString(1024 * 1024)
+        NodeCommissionResult.objects.store_data(
+            node, name=name, data=data)
+
+        results = NodeCommissionResult.objects.filter(node=node)
+        [ncr] = results
+        self.assertAttributes(ncr, dict(name=name, data=data))
+
+    def test_store_data_updates_existing(self):
+        node = factory.make_node()
+        name = factory.getRandomString(100)
+        factory.make_node_commission_result(node=node, name=name)
+        data = factory.getRandomString(1024 * 1024)
+        NodeCommissionResult.objects.store_data(
+            node, name=name, data=data)
+
+        results = NodeCommissionResult.objects.filter(node=node)
+        [ncr] = results
+        self.assertAttributes(ncr, dict(name=name, data=data))
+
+    def test_get_data(self):
+        ncr = factory.make_node_commission_result()
+        result = NodeCommissionResult.objects.get_data(ncr.node, ncr.name)
+        self.assertEqual(ncr.data, result)
+
+    def test_get_data_404s_when_not_found(self):
+        ncr = factory.make_node_commission_result()
+        self.assertRaises(
+            Http404,
+            NodeCommissionResult.objects.get_data, ncr.node, "bad name")


Follow ups