← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/maas/bug-974175 into lp:maas

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/bug-974175 into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #974175 in MAAS: "Set MAAS_PRESEED on commissioning node"
  https://bugs.launchpad.net/maas/+bug/974175

For more details, see:
https://code.launchpad.net/~jtv/maas/bug-974175/+merge/100998

Design as agreed with Daviey and Scott.  Implementation as pre-imped with Gavin.

We need a special, different preseed for commissioning nodes.  That was a bit of a downer, because it's currently the provisioning server's responsibility to compose a preseed variable and feed it to Cobbler in the right way.  And alas, the provisioning server has no inkling of whether a node is commissioning or not.  It works at a lower level.

This branch does not yet ensure that we actually use the new preseed for commissioning; that's work for a separate branch.  In this branch I merely rearrange how and where we produce it.

 → The maasserver now composes the preseed string directly, and passes it to pserv's add_node().
 → And so the metadata dict it used to pass is supplanted by a preseed_data string.
 → Preseeds are computed for a specific node.  It now produces the desired special format for Commissioning nodes.

Now maasserver knows how the preseed is put together, which is part of its contract with cloud-init; pserv knows how it fits in with ksmeta etc. and how to feed all that to Cobbler; and Cobbler knows how to feed it into the node.

Things get simpler, all in all, but having two formats is a bit warty and we still need to work out how to apply it.  It may happen as a matter of course because we call add_node() whenever we save a node in maasserver, or it may need special effort.  Test first.
-- 
https://code.launchpad.net/~jtv/maas/bug-974175/+merge/100998
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/bug-974175 into lp:maas.
=== modified file 'src/maasserver/provisioning.py'
--- src/maasserver/provisioning.py	2012-04-05 14:50:39 +0000
+++ src/maasserver/provisioning.py	2012-04-05 16:04:19 +0000
@@ -187,26 +187,65 @@
     return urljoin(maas_url, path)
 
 
-def compose_metadata(node):
-    """Put together metadata information for `node`.
-
-    :param node: The node to provide with metadata.
-    :type node: Node
-    :return: A dict containing metadata information that will be seeded to
-        the node, so that it can access the metadata service.
-    """
-    # Circular import.
-    from metadataserver.models import NodeKey
-    token = NodeKey.objects.get_token_for_node(node)
+def compose_cloud_init_preseed(token):
+    """Compose the preseed value for a node in any state but Commissioning."""
     credentials = urlencode({
         'oauth_consumer_key': token.consumer.key,
         'oauth_token_key': token.key,
         'oauth_token_secret': token.secret,
         })
-    return {
-        'maas-metadata-url': get_metadata_server_url(),
-        'maas-metadata-credentials': credentials,
-    }
+
+    # Preseed data to send to cloud-init.  We set this as MAAS_PRESEED in
+    # ks_meta, and it gets fed straight into debconf.
+    metadata_preseed_items = [
+        ('datasources', 'multiselect', 'MAAS'),
+        ('maas-metadata-url', 'string', get_metadata_server_url()),
+        ('maas-metadata-credentials', 'string', credentials),
+        ]
+
+    return '\n'.join(
+        "cloud-init   cloud-init/%s  %s %s" % (
+            item_name,
+            item_type,
+            item_value,
+            )
+        for item_name, item_type, item_value in metadata_preseed_items)
+
+
+def compose_commissioning_preseed(token):
+    """Compose the preseed value for a Commissioning node."""
+    template = dedent("""
+        #cloud-config
+        datasource:
+         MAAS:
+          metadata_url: %s
+          consumer_key: %s
+          token_key: %s
+          token_secret: %s
+        """.lstrip('\n'))
+    return template % (
+        get_metadata_server_url(),
+        token.consumer.key,
+        token.key,
+        token.secret,
+        )
+
+
+def compose_preseed(node):
+    """Put together preseed data for `node`.
+
+    :param node: The node to compose preseed data for.
+    :type node: Node
+    :return: Preseed data containing the information the node needs in order
+        to acces the metadata service: its URL and auth token.
+    """
+    # Circular import.
+    from metadataserver.models import NodeKey
+    token = NodeKey.objects.get_token_for_node(node)
+    if node.status == NODE_STATUS.COMMISSIONING:
+        return compose_commissioning_preseed(token)
+    else:
+        return compose_cloud_init_preseed(token)
 
 
 def name_arch_in_cobbler_style(architecture):
@@ -245,9 +284,10 @@
     papi = get_provisioning_api_proxy()
     profile = select_profile_for_node(instance)
     power_type = instance.get_effective_power_type()
-    metadata = compose_metadata(instance)
+    preseed_data = compose_preseed(instance)
     papi.add_node(
         instance.system_id, instance.hostname,
+<<<<<<< TREE
         profile, power_type, metadata)
     # When the node is allocated this must not modify the netboot_enabled
     # parameter. The node, once it has booted and installed itself, asks the
@@ -265,6 +305,9 @@
                 }
             }
         papi.modify_nodes(deltas)
+=======
+        profile, power_type, preseed_data)
+>>>>>>> MERGE-SOURCE
 
 
 def set_node_mac_addresses(node):

=== modified file 'src/maasserver/tests/test_provisioning.py'
--- src/maasserver/tests/test_provisioning.py	2012-04-05 14:40:40 +0000
+++ src/maasserver/tests/test_provisioning.py	2012-04-05 16:04:19 +0000
@@ -12,7 +12,6 @@
 __all__ = []
 
 from abc import ABCMeta
-from urlparse import parse_qs
 from xmlrpclib import Fault
 
 from django.conf import settings
@@ -27,7 +26,7 @@
     NODE_STATUS_CHOICES,
     )
 from maasserver.provisioning import (
-    compose_metadata,
+    compose_preseed,
     get_metadata_server_url,
     name_arch_in_cobbler_style,
     present_user_friendly_fault,
@@ -230,22 +229,31 @@
             % Config.objects.get_config('maas_url').rstrip('/'),
             get_metadata_server_url())
 
-    def test_compose_metadata_includes_metadata_url(self):
-        node = factory.make_node()
-        self.assertEqual(
-            get_metadata_server_url(),
-            compose_metadata(node)['maas-metadata-url'])
-
-    def test_compose_metadata_includes_node_oauth_token(self):
-        node = factory.make_node()
-        metadata = compose_metadata(node)
-        token = NodeKey.objects.get_token_for_node(node)
-        self.assertEqual({
-            'oauth_consumer_key': [token.consumer.key],
-            'oauth_token_key': [token.key],
-            'oauth_token_secret': [token.secret],
-            },
-            parse_qs(metadata['maas-metadata-credentials']))
+    def test_compose_preseed_includes_metadata_url(self):
+        node = factory.make_node(status=NODE_STATUS.READY)
+        self.assertIn(get_metadata_server_url(), compose_preseed(node))
+
+    def test_compose_preseed_for_commissioning_includes_metadata_url(self):
+        node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+        self.assertIn(
+            'metadata_url: %s\n' % get_metadata_server_url(),
+            compose_preseed(node))
+
+    def test_compose_preseed_includes_node_oauth_token(self):
+        node = factory.make_node(status=NODE_STATUS.READY)
+        preseed = compose_preseed(node)
+        token = NodeKey.objects.get_token_for_node(node)
+        self.assertIn('oauth_consumer_key=%s' % token.consumer.key, preseed)
+        self.assertIn('oauth_token_key=%s' % token.key, preseed)
+        self.assertIn('oauth_token_secret=%s' % token.secret, preseed)
+
+    def test_compose_preseed_for_commissioning_includes_auth_token(self):
+        node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+        preseed = compose_preseed(node)
+        token = NodeKey.objects.get_token_for_node(node)
+        self.assertIn('consumer_key: %s\n' % token.consumer.key, preseed)
+        self.assertIn('token_key: %s\n' % token.key, preseed)
+        self.assertIn('token_secret: %s\n' % token.secret, preseed)
 
     def test_papi_xmlrpc_faults_are_reported_helpfully(self):
 
@@ -255,7 +263,7 @@
         self.papi.patch('add_node', raise_fault)
 
         with ExpectedException(MAASAPIException, ".*provisioning server.*"):
-            self.papi.add_node('node', 'profile', 'power', {})
+            self.papi.add_node('node', 'profile', 'power', '')
 
     def test_provisioning_errors_are_reported_helpfully(self):
 
@@ -265,7 +273,7 @@
         self.papi.patch('add_node', raise_provisioning_error)
 
         with ExpectedException(MAASAPIException, ".*Cobbler.*"):
-            self.papi.add_node('node', 'profile', 'power', {})
+            self.papi.add_node('node', 'profile', 'power', '')
 
     def test_present_user_friendly_fault_describes_pserv_fault(self):
         self.assertIn(

=== modified file 'src/provisioningserver/api.py'
--- src/provisioningserver/api.py	2012-04-05 13:58:19 +0000
+++ src/provisioningserver/api.py	2012-04-05 16:04:19 +0000
@@ -162,18 +162,6 @@
             pass  # No change.
 
 
-# Preseed data to send to cloud-init.  We set this as MAAS_PRESEED in
-# ks_meta, and it gets fed straight into debconf.
-metadata_preseed_items = [
-    ('datasources', 'multiselect', 'MAAS'),
-    ('maas-metadata-url', 'string', '%(maas-metadata-url)s'),
-    ('maas-metadata-credentials', 'string', '%(maas-metadata-credentials)s'),
-    ]
-metadata_preseed = '\n'.join(
-    "cloud-init   cloud-init/%s  %s %s" % (item_name, item_type, item_value)
-    for item_name, item_type, item_value in metadata_preseed_items)
-
-
 class ProvisioningAPI:
 
     implements(IProvisioningAPI)
@@ -203,13 +191,13 @@
         returnValue(profile.name)
 
     @inlineCallbacks
-    def add_node(self, name, hostname, profile, power_type, metadata):
+    def add_node(self, name, hostname, profile, power_type, preseed_data):
         assert isinstance(name, basestring)
         assert isinstance(hostname, basestring)
         assert isinstance(profile, basestring)
         assert power_type in (POWER_TYPE.VIRSH, POWER_TYPE.WAKE_ON_LAN)
-        assert isinstance(metadata, dict)
-        preseed = b64encode(metadata_preseed % metadata)
+        assert isinstance(preseed_data, basestring)
+        preseed = b64encode(preseed_data)
         attributes = {
             "hostname": hostname,
             "profile": profile,

=== modified file 'src/provisioningserver/interfaces.py'
--- src/provisioningserver/interfaces.py	2012-03-29 10:18:58 +0000
+++ src/provisioningserver/interfaces.py	2012-04-05 16:04:19 +0000
@@ -50,19 +50,17 @@
         :return: The name of the new profile.
         """
 
-    def add_node(name, hostname, profile, power_type, metadata):
+    def add_node(name, hostname, profile, power_type, preseed_data):
         """Add a node with the given `name`.
 
         :param hostname: The fully-qualified hostname for the node.
         :param profile: Name of profile to associate the node with.
         :param power_type: A choice of power-control method, as in
             :class:`POWER_TYPE`.
-        :param metadata: Dict of ks_meta items to pre-seed into the node.
-            Should include maas-metadata-url (URL for the metadata service)
-            and maas-metadata-credentials (OAuth token for accessing the
-            metadata service).  The maas-metadata-credentials entry details
-            oauth_consumer_key, oauth_token_key, and oauth_token_secret
-            encoded as a URL query section suitable for urlparse.parse_qs.
+        :param preseed_data: Data to pre-seed into the node as MAAS_PRESEED.
+            Should include the URL for the metadata service, and credentials
+            for accessing it.  The credentials consist of oauth_consumer_key,
+                oauth_token_key, and oauth_token_secret.
         :return: The name of the new node.
         """
 

=== modified file 'src/provisioningserver/testing/factory.py'
--- src/provisioningserver/testing/factory.py	2012-04-04 13:40:05 +0000
+++ src/provisioningserver/testing/factory.py	2012-04-05 16:04:19 +0000
@@ -10,7 +10,6 @@
 
 __metaclass__ = type
 __all__ = [
-    'fake_node_metadata',
     'ProvisioningFakeFactory',
     ]
 
@@ -34,14 +33,6 @@
     return next(names)
 
 
-def fake_node_metadata():
-    """Produce fake metadata parameters for adding a node."""
-    return {
-        'maas-metadata-url': 'http://%s:5240/metadata/' % fake_name(),
-        'maas-metadata-credentials': 'fake/%s' % fake_name(),
-        }
-
-
 class ProvisioningFakeFactory:
     """Mixin for test cases: factory of fake provisioning objects.
 
@@ -110,14 +101,12 @@
 
     @inlineCallbacks
     def add_node(self, papi, name=None, hostname=None, profile_name=None,
-                 power_type=None, metadata=None):
+                 power_type=None, preseed_data=None):
         """Creates a new node object via `papi`.
 
         Arranges for it to be deleted during test clean-up. If `name` is not
         specified, `fake_name` will be called to obtain one. If `profile_name`
-        is not specified, one will be obtained by calling `add_profile`. If
-        `metadata` is not specified, it will be obtained by calling
-        `fake_node_metadata`.
+        is not specified, one will be obtained by calling `add_profile`.
         """
         if name is None:
             name = fake_name()
@@ -127,10 +116,10 @@
             profile_name = yield self.add_profile(papi)
         if power_type is None:
             power_type = POWER_TYPE.WAKE_ON_LAN
-        if metadata is None:
-            metadata = fake_node_metadata()
+        if preseed_data is None:
+            preseed_data = ""
         node_name = yield papi.add_node(
-            name, hostname, profile_name, power_type, metadata)
+            name, hostname, profile_name, power_type, preseed_data)
         self.addCleanup(
             self.clean_up_objects,
             papi.delete_nodes_by_name,

=== modified file 'src/provisioningserver/testing/fakeapi.py'
--- src/provisioningserver/testing/fakeapi.py	2012-04-05 14:05:52 +0000
+++ src/provisioningserver/testing/fakeapi.py	2012-04-05 16:04:19 +0000
@@ -88,11 +88,11 @@
         self.profiles[name]["distro"] = distro
         return name
 
-    def add_node(self, name, hostname, profile, power_type, metadata):
+    def add_node(self, name, hostname, profile, power_type, preseed_data):
         self.nodes[name]["hostname"] = hostname
         self.nodes[name]["profile"] = profile
         self.nodes[name]["mac_addresses"] = []
-        self.nodes[name]["metadata"] = metadata
+        self.nodes[name]["preseed_data"] = preseed_data
         self.nodes[name]["power_type"] = power_type
         return name
 

=== modified file 'src/provisioningserver/tests/test_api.py'
--- src/provisioningserver/tests/test_api.py	2012-04-05 13:58:19 +0000
+++ src/provisioningserver/tests/test_api.py	2012-04-05 16:04:19 +0000
@@ -34,10 +34,7 @@
 from provisioningserver.cobblerclient import CobblerSystem
 from provisioningserver.enum import POWER_TYPE
 from provisioningserver.interfaces import IProvisioningAPI
-from provisioningserver.testing.factory import (
-    fake_node_metadata,
-    ProvisioningFakeFactory,
-    )
+from provisioningserver.testing.factory import ProvisioningFakeFactory
 from provisioningserver.testing.fakeapi import FakeAsynchronousProvisioningAPI
 from provisioningserver.testing.fakecobbler import make_fake_cobbler_session
 from provisioningserver.testing.realcobbler import RealCobbler
@@ -626,15 +623,13 @@
         return ProvisioningAPI(make_fake_cobbler_session())
 
     @inlineCallbacks
-    def test_add_node_preseeds_metadata(self):
+    def test_add_node_provides_preseed(self):
         papi = self.get_provisioning_api()
-        metadata = fake_node_metadata()
-        node_name = yield self.add_node(papi, metadata=metadata)
+        preseed_data = factory.getRandomString()
+        node_name = yield self.add_node(papi, preseed_data=preseed_data)
         attrs = yield CobblerSystem(papi.session, node_name).get_values()
-        preseed = attrs['ks_meta']['MAAS_PRESEED']
-        preseed = b64decode(preseed)
-        self.assertIn(metadata['maas-metadata-url'], preseed)
-        self.assertIn(metadata['maas-metadata-credentials'], preseed)
+        self.assertEqual(
+            preseed_data, b64decode(attrs['ks_meta']['MAAS_PRESEED']))
 
 
 class TestProvisioningAPIWithRealCobbler(ProvisioningAPITests,