← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/preseed-customisation into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/preseed-customisation into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/preseed-customisation/+merge/110861

This branch adds the method 'load_preseed_template' to the preseed module.  This method loads a preseed template form a given node.

= Pre-imp =

This was discussed with Gavin (see http://bit.ly/KXW5ZU for details).  More than that, this branch was actually started by Gavin.  I needed this to continue the preseed work so I decided to finish it off since Gavin is away today and tomorrow.

= Notes =

- Add PRESEED_TEMPLATE_LOCATIONS which lists locations where the templates can be found.

- Change get_preseed_filenames to be able to specify if the default template ('generic') should be returned as a last resort or not: we want that template to be used only when looking for preseed templates, no when looking for parent templates (from which preseed templates inherits).
-- 
https://code.launchpad.net/~rvb/maas/preseed-customisation/+merge/110861
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/preseed-customisation into lp:maas.
=== modified file 'src/maas/development.py'
--- src/maas/development.py	2012-06-01 06:52:08 +0000
+++ src/maas/development.py	2012-06-18 16:59:26 +0000
@@ -83,5 +83,11 @@
 # on a production MAAS.
 ALLOW_ANONYMOUS_METADATA_ACCESS = True
 
+# Use in-branch preseed templates.
+PRESEED_TEMPLATE_LOCATIONS = (
+    abspath("etc/preseeds"),
+    abspath("contrib/preseeds"),
+    )
+
 # Allow the user to override settings in maas_local_settings.
 import_local_settings()

=== modified file 'src/maas/settings.py'
--- src/maas/settings.py	2012-06-13 11:02:58 +0000
+++ src/maas/settings.py	2012-06-18 16:59:26 +0000
@@ -293,5 +293,12 @@
 # for all nodes, will be exposed on your network.
 ALLOW_ANONYMOUS_METADATA_ACCESS = False
 
+# Earlier locations in the following list will shadow, or overlay, later
+# locations.
+PRESEED_TEMPLATE_LOCATIONS = (
+    "/etc/maas/preseeds",
+    "/usr/share/maas/preseeds",
+    )
+
 # Allow the user to override settings in maas_local_settings.
 import_local_settings()

=== modified file 'src/maasserver/preseed.py'
--- src/maasserver/preseed.py	2012-06-14 16:17:27 +0000
+++ src/maasserver/preseed.py	2012-06-18 16:59:26 +0000
@@ -1,7 +1,7 @@
 # Copyright 2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-"""Preseed module."""
+"""Preseed generation."""
 
 from __future__ import (
     absolute_import,
@@ -12,36 +12,47 @@
 __metaclass__ = type
 __all__ = []
 
+import base64
+from os.path import join
+
+from django.conf import settings
+import tempita
+
 
 GENERIC_FILENAME = 'generic'
 
 
 # XXX: rvb 2012-06-14 bug=1013146:  'precise' is hardcoded here.
-def get_preseed_filenames(node, type, release='precise'):
+def get_preseed_filenames(node, prefix, release='precise', default=False):
     """List possible preseed template filenames for the given node.
 
     :param node: The node to return template preseed filenames for.
     :type node: :class:`maasserver.models.Node`
-    :param type: The preseed type (will be used as a prefix in the template
-        filenames).  Usually one of {'', 'enlist', 'commissioning'}.
-    :type type: basestring
+    :param prefix: At the top level, this is the preseed type (will be used as
+        a prefix in the template filenames).  Usually one of {'', 'enlist',
+        'commissioning'}.
+    :type prefix: basestring
     :param release: The Ubuntu release to be used.
-    :type type: basestring
+    :type release: basestring
+    :param default: Should we return the default ('generic') template as a
+        last resort template?
+    :type default: boolean
 
     Returns a list of possible preseed template filenames using the following
     lookup order:
-    {type}_{node_architecture}_{node_subarchitecture}_{release}_{node_hostname}
-    {type}_{node_architecture}_{node_subarchitecture}_{release}
-    {type}_{node_architecture}
-    {type}
+    {prefix}_{node_architecture}_{node_subarchitecture}_{release}_{node_name}
+    {prefix}_{node_architecture}_{node_subarchitecture}_{release}
+    {prefix}_{node_architecture}
+    {prefix}
     'generic'
     """
     arch = split_subarch(node.architecture)
-    elements = [type] + arch + [release, node.hostname]
+    elements = [prefix] + arch + [release, node.hostname]
     while elements:
         yield compose_filename(elements)
         elements.pop()
-    yield GENERIC_FILENAME
+    if default:
+        yield GENERIC_FILENAME
 
 
 def split_subarch(architecture):
@@ -52,3 +63,59 @@
 def compose_filename(elements):
     """Create a preseed filename from a list of elements."""
     return '_'.join(elements)
+
+
+def get_preseed_template(filenames):
+    """Get the path and content for the first template found.
+
+    :param filenames: An iterable of relative filenames.
+    """
+    assert not isinstance(filenames, basestring)
+    for location in settings.PRESEED_TEMPLATE_LOCATIONS:
+        for filename in filenames:
+            filepath = join(location, filename)
+            try:
+                with open(filepath, "rb") as stream:
+                    content = stream.read()
+                    return filepath, content
+            except IOError:
+                pass  # Ignore.
+    else:
+        return None, None
+
+
+class PreseedTemplate(tempita.Template):
+    """A Tempita template specialised for preseed rendering."""
+
+    default_namespace = dict(
+        tempita.Template.default_namespace,
+        b64decode=base64.b64decode,
+        b64encode=base64.b64encode)
+
+
+class TemplateNotFoundError(Exception):
+    """The template has not been found."""
+
+    def __init__(self, name):
+        super(TemplateNotFoundError, self).__init__(name)
+        self.name = name
+
+
+# XXX: rvb 2012-06-18 bug=1013146:  'precise' is hardcoded here.
+def load_preseed_template(node, prefix, release="precise"):
+    """Find and load a `PreseedTemplate` for the given node.
+
+    :param node: See `get_preseed_filenames`.
+    :param prefix: See `get_preseed_filenames`.
+    :param release: See `get_preseed_filenames`.
+    """
+
+    def get_template(name, from_template, default=False):
+        filenames = get_preseed_filenames(node, name, release, default)
+        filepath, content = get_preseed_template(filenames)
+        if filepath is None:
+            raise TemplateNotFoundError(name)
+        return PreseedTemplate(
+            content, name=filepath, get_template=get_template)
+
+    return get_template(prefix, None, default=True)

=== modified file 'src/maasserver/tests/test_preseed.py'
--- src/maasserver/tests/test_preseed.py	2012-06-15 07:08:42 +0000
+++ src/maasserver/tests/test_preseed.py	2012-06-18 16:59:26 +0000
@@ -1,7 +1,7 @@
 # Copyright 2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-"""Test preseed module."""
+"""Test `maasserver.preseed` and related bits and bobs."""
 
 from __future__ import (
     absolute_import,
@@ -12,15 +12,29 @@
 __metaclass__ = type
 __all__ = []
 
+import base64
+import os
+
+from django.conf import settings
 from maasserver.preseed import (
+    GENERIC_FILENAME,
     get_preseed_filenames,
+    get_preseed_template,
+    load_preseed_template,
+    PreseedTemplate,
     split_subarch,
+    TemplateNotFoundError,
     )
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
-
-
-class TestPreseedUtilities(TestCase):
+from testtools.matchers import (
+    AllMatch,
+    IsInstance,
+    )
+
+
+class TestSplitSubArch(TestCase):
+    """Tests for `split_subarch`."""
 
     def test_split_subarch_returns_list(self):
         self.assertEqual(['amd64'], split_subarch('amd64'))
@@ -28,6 +42,10 @@
     def test_split_subarch_splits_sub_architecture(self):
         self.assertEqual(['amd64', 'test'], split_subarch('amd64/test'))
 
+
+class TestGetPreseedFilenames(TestCase):
+    """Tests for `get_preseed_filenames`."""
+
     def test_get_preseed_filenames_returns_filenames(self):
         hostname = factory.getRandomString()
         type = factory.getRandomString()
@@ -41,7 +59,7 @@
                 '%s' % type,
                 'generic',
             ],
-            list(get_preseed_filenames(node, type, release)))
+            list(get_preseed_filenames(node, type, release, True)))
 
     def test_get_preseed_filenames_returns_filenames_with_subarch(self):
         arch = factory.getRandomString()
@@ -63,4 +81,208 @@
                 '%s' % type,
                 'generic',
             ],
-            list(get_preseed_filenames(node, type, release)))
+            list(get_preseed_filenames(node, type, release, True)))
+
+    def test_get_preseed_filenames_returns_list_without_default(self):
+        # If default=False is passed to get_preseed_filenames, the
+        # returned list won't include the default template name as a
+        # last resort template.
+        hostname = factory.getRandomString()
+        prefix = factory.getRandomString()
+        release = factory.getRandomString()
+        node = factory.make_node(hostname=hostname)
+        self.assertSequenceEqual(
+            'generic',
+            list(get_preseed_filenames(node, prefix, release, True))[-1])
+
+    def test_get_preseed_filenames_returns_list_with_default(self):
+        # If default=True is passed to get_preseed_filenames, the
+        # returned list will include the default template name as a
+        # last resort template.
+        hostname = factory.getRandomString()
+        prefix = factory.getRandomString()
+        release = factory.getRandomString()
+        node = factory.make_node(hostname=hostname)
+        self.assertSequenceEqual(
+            prefix,
+            list(get_preseed_filenames(node, prefix, release, False))[-1])
+
+
+class TestConfiguration(TestCase):
+    """Test for correct configuration of the preseed component."""
+
+    def test_setting_defined(self):
+        self.assertThat(
+            settings.PRESEED_TEMPLATE_LOCATIONS,
+            AllMatch(IsInstance(basestring)))
+
+
+class TestPreseedTemplate(TestCase):
+    """Tests for :class:`PreseedTemplate`."""
+
+    def test_preseed_template_b64decode(self):
+        content = factory.getRandomString()
+        encoded_content = base64.b64encode(content)
+        template = PreseedTemplate("{{b64decode('%s')}}" % encoded_content)
+        self.assertEqual(content, template.substitute())
+
+    def test_preseed_template_b64encode(self):
+        content = factory.getRandomString()
+        template = PreseedTemplate("{{b64encode('%s')}}" % content)
+        self.assertEqual(base64.b64encode(content), template.substitute())
+
+
+class TestGetPreseedTemplate(TestCase):
+    """Tests for `get_preseed_template`."""
+
+    def test_get_preseed_template_returns_None_if_no_template_locations(self):
+        # get_preseed_template() returns None when no template locations are
+        # defined.
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [])
+        self.assertEqual(
+            (None, None),
+            get_preseed_template(
+                (factory.getRandomString(), factory.getRandomString())))
+
+    def test_get_preseed_template_returns_None_when_no_filenames(self):
+        # get_preseed_template() returns None when no filenames are passed in.
+        location = self.make_dir()
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [location])
+        self.assertEqual((None, None), get_preseed_template(()))
+
+    def test_get_preseed_template_find_template_in_first_location(self):
+        template_content = factory.getRandomString()
+        template_path = self.make_file(contents=template_content)
+        template_filename = os.path.basename(template_path)
+        locations = [
+            os.path.dirname(template_path),
+            self.make_dir(),
+            ]
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", locations)
+        self.assertEqual(
+            (template_path, template_content),
+            get_preseed_template([template_filename]))
+
+    def test_get_preseed_template_find_template_in_last_location(self):
+        template_content = factory.getRandomString()
+        template_path = self.make_file(contents=template_content)
+        template_filename = os.path.basename(template_path)
+        locations = [
+            self.make_dir(),
+            os.path.dirname(template_path),
+            ]
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", locations)
+        self.assertEqual(
+            (template_path, template_content),
+            get_preseed_template([template_filename]))
+
+
+class TestLoadPreseedTemplate(TestCase):
+    """Tests for `load_preseed_template`."""
+
+    def create_template(self, location, name, content=None):
+        # Create a tempita template in the given `location` with the
+        # given `name`.  If content is not provided, a random content
+        # will be put inside the template.
+        path = os.path.join(location, name)
+        rendered_content = None
+        if content is None:
+            rendered_content = factory.getRandomString()
+            content = b'{{def stuff}}%s{{enddef}}{{stuff}}' % rendered_content
+        with open(path, "wb") as outf:
+            outf.write(content)
+        return rendered_content
+
+    def test_load_preseed_template_returns_PreseedTemplate(self):
+        location = self.make_dir()
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [location])
+        name = factory.getRandomString()
+        self.create_template(location, name)
+        node = factory.make_node()
+        template = load_preseed_template(node, name)
+        self.assertIsInstance(template, PreseedTemplate)
+
+    def test_load_preseed_template_raises_if_no_template(self):
+        location = self.make_dir()
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [location])
+        node = factory.make_node()
+        unknown_template_name = factory.getRandomString()
+        self.assertRaises(
+            TemplateNotFoundError, load_preseed_template, node,
+            unknown_template_name)
+
+    def test_load_preseed_template_generic_lookup(self):
+        # The template lookup method ends up picking up a template named
+        # 'generic' if no more specific template exist.
+        location = self.make_dir()
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [location])
+        content = self.create_template(location, GENERIC_FILENAME)
+        node = factory.make_node(hostname=factory.getRandomString())
+        template = load_preseed_template(node, factory.getRandomString())
+        self.assertEqual(content, template.substitute())
+
+    def test_load_preseed_template_prefix_lookup(self):
+        # 2nd last in the hierarchy is a template named 'prefix'.
+        location = self.make_dir()
+        prefix = factory.getRandomString()
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [location])
+        # Create the generic template.  This one will be ignored due to the
+        # presence of a more specific template.
+        self.create_template(location, GENERIC_FILENAME)
+        # Create the 'prefix' template.  This is the one which will be
+        # picked up.
+        content = self.create_template(location, prefix)
+        node = factory.make_node(hostname=factory.getRandomString())
+        template = load_preseed_template(node, prefix)
+        self.assertEqual(content, template.substitute())
+
+    def test_load_preseed_template_node_specific_lookup(self):
+        # At the top of the lookup hierarchy is a template specific to this
+        # node.  It will be used first if it's present.
+        location = self.make_dir()
+        prefix = factory.getRandomString()
+        release = factory.getRandomString()
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [location])
+        # Create the generic and 'prefix' templates.  They will be ignored
+        # due to the presence of a more specific template.
+        self.create_template(location, GENERIC_FILENAME)
+        self.create_template(location, prefix)
+        node = factory.make_node(hostname=factory.getRandomString())
+        node_template_name = "%s_%s_%s_%s" % (
+            prefix, node.architecture, release, node.hostname)
+        # Create the node-specific template.
+        content = self.create_template(location, node_template_name)
+        template = load_preseed_template(node, prefix, release)
+        self.assertEqual(content, template.substitute())
+
+    def test_load_preseed_template_with_inherits(self):
+        # A preseed file can "inherit" from another file.
+        location = self.make_dir()
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [location])
+        prefix = factory.getRandomString()
+        # Create preseed template.
+        master_template_name = factory.getRandomString()
+        preseed_content = '{{inherit "%s"}}' % master_template_name
+        self.create_template(location, prefix, preseed_content)
+        master_content = self.create_template(location, master_template_name)
+        node = factory.make_node()
+        template = load_preseed_template(node, prefix)
+        self.assertEqual(master_content, template.substitute())
+
+    def test_load_preseed_template_parent_lookup_doesnt_include_default(self):
+        # The lookup for parent templates does not include the default
+        # 'generic' file.
+        location = self.make_dir()
+        self.patch(settings, "PRESEED_TEMPLATE_LOCATIONS", [location])
+        prefix = factory.getRandomString()
+        # Create 'generic' template.  It won't be used because the
+        # lookup for parent templates does not use the 'generic' template.
+        self.create_template(location, GENERIC_FILENAME)
+        unknown_master_template_name = factory.getRandomString()
+        # Create preseed template.
+        preseed_content = '{{inherit "%s"}}' % unknown_master_template_name
+        self.create_template(location, prefix, preseed_content)
+        node = factory.make_node()
+        template = load_preseed_template(node, prefix)
+        self.assertRaises(
+            TemplateNotFoundError, template.substitute)


Follow ups