← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/maas/commissioning-files into lp:maas

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/commissioning-files into lp:maas with lp:~julian-edwards/maas/commission-result-model as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jtv/maas/commissioning-files/+merge/101497

Pre-imp with Julian (and basic-commissioning design document updated accordingly).

We are required to support storage of commissioning information in the release.  This branch puts several of the pieces together.  As a node goes through its commissioning steps, it calls the metadata server's signalling API.  Regardless of status, any such call may attach files.  They all go into Julian's brand-new NodeCommissionResult model.

The files are specified to be UTF-8 encoded text, up to 1MiB in length.
-- 
https://code.launchpad.net/~jtv/maas/commissioning-files/+merge/101497
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/commissioning-files into lp:maas.
=== modified file 'src/metadataserver/api.py'
--- src/metadataserver/api.py	2012-04-04 17:15:46 +0000
+++ src/metadataserver/api.py	2012-04-11 07:07:20 +0000
@@ -36,6 +36,7 @@
     SSHKey,
     )
 from metadataserver.models import (
+    NodeCommissionResult,
     NodeKey,
     NodeUserData,
     )
@@ -126,6 +127,12 @@
             shown_fields.remove('user-data')
         return make_list_response(sorted(shown_fields))
 
+    def _store_commissioning_results(self, node, request):
+        """Store commissioning result files for `node`."""
+        for name, uploaded_file in request.FILES.items():
+            contents = uploaded_file.read().decode('utf-8')
+            NodeCommissionResult.objects.store_data(node, name, contents)
+
     @api_exported('signal', 'POST')
     def signal(self, request, version=None):
         """Signal commissioning status.
@@ -161,6 +168,8 @@
             # Already registered.  Nothing to be done.
             return rc.ALL_OK
 
+        self._store_commissioning_results(node, request)
+
         target_status = self.signaling_statuses.get(status)
         if target_status in (None, node.status):
             # No status change.  Nothing to be done.

=== modified file 'src/metadataserver/migrations/0002_add_nodecommissionresult.py'
--- src/metadataserver/migrations/0002_add_nodecommissionresult.py	2012-04-11 07:07:20 +0000
+++ src/metadataserver/migrations/0002_add_nodecommissionresult.py	2012-04-11 07:07:20 +0000
@@ -4,9 +4,11 @@
 
 # encoding: utf-8
 import datetime
+
+from django.db import models
 from south.db import db
 from south.v2 import SchemaMigration
-from django.db import models
+
 
 class Migration(SchemaMigration):
 

=== modified file 'src/metadataserver/tests/test_api.py'
--- src/metadataserver/tests/test_api.py	2012-04-11 05:42:20 +0000
+++ src/metadataserver/tests/test_api.py	2012-04-11 07:07:20 +0000
@@ -34,6 +34,7 @@
     UnknownMetadataVersion,
     )
 from metadataserver.models import (
+    NodeCommissionResult,
     NodeKey,
     NodeUserData,
     )
@@ -142,6 +143,7 @@
         params.update(kwargs)
         for name, content in files.items():
             params[name] = BytesIO(content)
+            params[name].name = name
         return client.post(self.make_url('/%s/' % version), params)
 
     def test_no_anonymous_access(self):
@@ -371,3 +373,71 @@
         response = self.call_signal(client)
         self.assertEqual(httplib.OK, response.status_code)
         self.assertEqual('', reload_object(node).error)
+
+    def test_signalling_stores_files_for_any_status(self):
+        statuses = ['WORKING', 'OK', 'FAILED']
+        filename = factory.getRandomString()
+        nodes = {
+            status: factory.make_node(status=NODE_STATUS.COMMISSIONING)
+            for status in statuses}
+        for status, node in nodes.items():
+            client = self.make_node_client(node=node)
+            self.call_signal(
+                client, status=status,
+                files={filename: factory.getRandomString().encode('ascii')})
+        self.assertEqual(
+            {status: filename for status in statuses},
+            {
+                status: NodeCommissionResult.objects.get(node=node).name
+                for status, node in nodes.items()})
+
+    def test_signal_stores_file_contents(self):
+        node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+        client = self.make_node_client(node=node)
+        text = factory.getRandomString().encode('ascii')
+        response = self.call_signal(client, files={'file.txt': text})
+        self.assertEqual(httplib.OK, response.status_code)
+        self.assertEqual(
+            text, NodeCommissionResult.objects.get_data(node, 'file.txt'))
+
+    def test_signal_decodes_file_from_UTF8(self):
+        unicode_text = '<\u2621>'
+        node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+        client = self.make_node_client(node=node)
+        response = self.call_signal(
+            client, files={'file.txt': unicode_text.encode('utf-8')})
+        self.assertEqual(httplib.OK, response.status_code)
+        self.assertEqual(
+            unicode_text,
+            NodeCommissionResult.objects.get_data(node, 'file.txt'))
+
+    def test_signal_stores_multiple_files(self):
+        contents = {
+            factory.getRandomString(): factory.getRandomString().encode(
+                'ascii')
+            for counter in range(3)}
+        node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+        client = self.make_node_client(node=node)
+        response = self.call_signal(client, files=contents)
+        self.assertEqual(httplib.OK, response.status_code)
+        self.assertEqual(
+            contents,
+            {
+                result.name: result.data
+                for result in NodeCommissionResult.objects.filter(node=node)
+            })
+
+    def test_signal_stores_files_up_to_documented_size_limit(self):
+        # The documented size limit for commissioning result files:
+        # one megabyte.  What happens above this limit is none of
+        # anybody's business, but files up to this size should work.
+        size_limit = 2 ** 20
+        contents = factory.getRandomString(size_limit, spaces=True)
+        node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+        client = self.make_node_client(node=node)
+        response = self.call_signal(
+            client, files={'output.txt': contents.encode('utf-8')})
+        self.assertEqual(httplib.OK, response.status_code)
+        stored_data = NodeCommissionResult.objects.get_data(
+            node, 'output.txt')
+        self.assertEqual(size_limit, len(stored_data))

=== modified file 'src/metadataserver/tests/test_models.py'
--- src/metadataserver/tests/test_models.py	2012-04-11 07:07:20 +0000
+++ src/metadataserver/tests/test_models.py	2012-04-11 07:07:20 +0000
@@ -13,7 +13,6 @@
 
 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 (