← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/dns-templates-render into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/dns-templates-render into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/dns-templates-render/+merge/113218

This branch implements a first step towards DNS support in MAAS.  It adds utilities to generate the rndc files (using utilities from the package bind9utils) and write the DNS configuration files.

In case you wonder, BlankDNSConfig will be used when DNS support will be disabled.  This way, no matter how we hook up MAAS' configuration inside the existing DNS instance configuration (manual edition of named.conf.local, automatic edition of named.conf.local or population of a future "named.d" directory), disabling DNS support in MAAS will be simple.
-- 
https://code.launchpad.net/~rvb/maas/dns-templates-render/+merge/113218
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/dns-templates-render into lp:maas.
=== modified file 'etc/celeryconfig.py'
--- etc/celeryconfig.py	2012-06-28 09:30:44 +0000
+++ etc/celeryconfig.py	2012-07-03 14:36:20 +0000
@@ -15,6 +15,8 @@
 
 __metaclass__ = type
 
+import os
+
 from maas import import_settings
 
 # Location of power action templates.  Use an absolute path, or leave as
@@ -28,6 +30,8 @@
 # TFTP server's root directory.
 TFTPROOT = "/var/lib/tftpboot"
 
+# Location of MAAS' bind configuration files.
+DNS_CONFIG_DIR = os.path.join(os.sep, 'var', 'cache', 'maas', 'bind')
 
 try:
     import user_maasceleryconfig

=== added file 'src/provisioningserver/dns/__init__.py'
=== added file 'src/provisioningserver/dns/config.py'
--- src/provisioningserver/dns/config.py	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/dns/config.py	2012-07-03 14:36:20 +0000
@@ -0,0 +1,135 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""DNS configuration."""
+
+from __future__ import (
+    absolute_import,
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = [
+    'BlankDNSConfig',
+    'DNSConfig',
+    'DNSZoneConfig',
+    'setup_rndc',
+    ]
+
+
+import os.path
+from subprocess import check_output
+
+from celery.conf import conf
+import tempita
+
+
+class DNSConfigFail(Exception):
+    """Raised if there's a problem with a DNS config."""
+
+
+def generate_rndc():
+    """Use `rndc-confgen` (from bind9utils) to generate a rndc+named
+    configuration.
+
+    `rndc-confgen` generates the rndc configuration which also contains (that
+    part is commented out) the named configuration.
+    """
+    # Generate the configuration:
+    # - 256 bits is the recommanded size for the key nowadays;
+    # - Use the unlocked random source to make the executing
+    # non-blocking.
+    rndc_content = check_output(
+        ['rndc-confgen', '-b', '256', '-r', '/dev/urandom',
+         '-k', 'rndc-maas-key'])
+    # Extract from the result the part which corresponds to the named
+    # configuration.
+    start_marker = (
+        "# Use with the following in named.conf, adjusting the "
+        "allow list as needed:")
+    end_marker = '# End of named.conf'
+    named_start = rndc_content.index(start_marker) + len(start_marker)
+    named_end = rndc_content.index(end_marker)
+    named_conf = rndc_content[named_start:named_end].replace('\n# ', '\n')
+    # Return a tuple of the two configurations.
+    return rndc_content, named_conf
+
+
+def setup_rndc():
+    """Writes out the two files needed to enable MAAS to use rndc commands:
+    rndc.conf and named.conf.rndc, both stored in conf.DNS_CONFIG_DIR.
+    """
+    rndc_content, named_content = generate_rndc()
+
+    target_file = os.path.join(conf.DNS_CONFIG_DIR, 'rndc.conf')
+    with open(target_file, "w") as f:
+        f.write(rndc_content)
+
+    target_file = os.path.join(conf.DNS_CONFIG_DIR, 'named.conf.rndc')
+    with open(target_file, "w") as f:
+        f.write(named_content)
+
+
+# Directory where the DNS configuration template files can be found.
+TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), 'templates')
+
+
+class DNSConfig:
+    """A DNS configuration file.
+
+    Encapsulation of DNS config templates and parameter substitution.
+
+    :param path: The directory where the template can be found.
+    :type path: string
+    :param target_path: The directory where the configuration will be written.
+    :type target_path: string
+    :param filename: The name of the template file.
+    :type filename: string
+    :param target_filename: The name of the configuration file to be written.
+    :type target_filename: string
+    :raises DNSConfigFail: if there's a problem with template parameters.
+    """
+
+    def __init__(self, path=TEMPLATES_PATH, target_path=conf.DNS_CONFIG_DIR,
+                 filename='named.conf.template',
+                 target_filename='named.conf'):
+        self.template_name = os.path.join(path, filename)
+        self.target_file = os.path.join(target_path, target_filename)
+
+    def get_template(self):
+        with open(self.template_name, "r") as f:
+            return tempita.Template(f.read(), name=self.template_name)
+
+    def render_template(self, template, **kwargs):
+        try:
+            return template.substitute(kwargs)
+        except NameError as error:
+            raise DNSConfigFail(*error.args)
+
+    def write_config(self, **kwargs):
+        """Write out this DNS config file."""
+        template = self.get_template()
+        rendered = self.render_template(template, **kwargs)
+        with open(self.target_file, "w") as f:
+            f.write(rendered)
+
+
+class BlankDNSConfig(DNSConfig):
+    """A specialized version of DNSConfig that simply writes a blank/empty
+    configuration file.
+    """
+
+    def write_config(self, **kwargs):
+        """Write out an empty DNS config file."""
+        with open(self.target_file, "w") as f:
+            f.write('')
+
+
+class DNSZoneConfig(DNSConfig):
+    """A specialized version of DNSConfig that writes zone files."""
+
+    def __init__(self, zone_id):
+        self.template_name = os.path.join(TEMPLATES_PATH, 'zone.template')
+        self.target_file = os.path.join(
+            conf.DNS_CONFIG_DIR, 'zone.%d' % zone_id)

=== added directory 'src/provisioningserver/dns/tests'
=== added file 'src/provisioningserver/dns/tests/test_config.py'
--- src/provisioningserver/dns/tests/test_config.py	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/dns/tests/test_config.py	2012-07-03 14:36:20 +0000
@@ -0,0 +1,129 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test cases for dns.config"""
+
+from __future__ import (
+    absolute_import,
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+import os.path
+import random
+
+from celery.conf import conf
+from maastesting.factory import factory
+from maastesting.testcase import TestCase
+from provisioningserver.dns.config import (
+    BlankDNSConfig,
+    DNSConfig,
+    DNSConfigFail,
+    DNSZoneConfig,
+    generate_rndc,
+    setup_rndc,
+    TEMPLATES_PATH,
+    )
+import tempita
+from testtools.matchers import FileContains
+
+
+class TestRNDCGeneration(TestCase):
+
+    def test_generate_rndc_returns_configurations(self):
+        rndc_content, named_content = generate_rndc()
+        # rndc_content and named_content look right.
+        self.assertIn('# Start of rndc.conf', rndc_content)
+        self.assertIn('controls {', named_content)
+        # named_content does not include any comment.
+        self.assertNotIn('\n#', named_content)
+
+    def test_setup_rndc_writes_configurations(self):
+        dns_conf_dir = self.make_dir()
+        self.patch(conf, 'DNS_CONFIG_DIR', dns_conf_dir)
+        setup_rndc()
+        expected = (
+            ('rndc.conf', '# Start of rndc.conf'),
+            ('named.conf.rndc', 'controls {'))
+        for filename, content in expected:
+            with open(os.path.join(dns_conf_dir, filename), "rb") as stream:
+                conf_content = stream.read()
+                self.assertIn(content, conf_content)
+
+
+class TestDNSConfig(TestCase):
+    """Tests for DNSConfig."""
+
+    def test_DNSConfig_defaults(self):
+        dnsconfig = DNSConfig()
+        self.assertEqual(
+            (
+                os.path.join(TEMPLATES_PATH, 'named.conf.template'),
+                os.path.join(conf.DNS_CONFIG_DIR, 'named.conf')
+            ),
+            (dnsconfig.template_name, dnsconfig.target_file))
+
+    def test_get_template_retrieves_template(self):
+        dnsconfig = DNSConfig()
+        template = dnsconfig.get_template()
+        self.assertIsInstance(template, tempita.Template)
+        self.assertThat(
+            dnsconfig.template_name, FileContains(template.content))
+
+    def test_render_template(self):
+        dnsconfig = DNSConfig()
+        random_content = factory.getRandomString()
+        template = tempita.Template("{{test}}")
+        rendered = dnsconfig.render_template(template, test=random_content)
+        self.assertEqual(random_content, rendered)
+
+    def test_render_template_raises_PXEConfigFail(self):
+        dnsconfig = DNSConfig()
+        template = tempita.Template("template: {{test}}")
+        exception = self.assertRaises(
+            DNSConfigFail, dnsconfig.render_template, template)
+        self.assertIn("'test' is not defined", exception.message)
+
+    def test_write_config_writes_config(self):
+        target_file = self.make_file()
+        target_file_name = os.path.basename(target_file)
+        target_dir = os.path.dirname(target_file)
+        template_file = self.make_file(contents="{{test}}")
+        template_file_name = os.path.basename(template_file)
+        template_dir = os.path.dirname(template_file)
+        dnsconfig = DNSConfig(
+            path=template_dir, target_path=target_dir,
+            filename=template_file_name, target_filename=target_file_name)
+        random_content = factory.getRandomString()
+        dnsconfig.write_config(test=random_content)
+        self.assertThat(target_file, FileContains(random_content))
+
+
+class TestBlankDNSConfig(TestCase):
+    """Tests for BlankDNSConfig."""
+
+    def test_write_config_writes_empty_config(self):
+        target_file = self.make_file()
+        target_file_name = os.path.basename(target_file)
+        target_dir = os.path.dirname(target_file)
+        dnsconfig = BlankDNSConfig(
+            target_path=target_dir, target_filename=target_file_name)
+        dnsconfig.write_config()
+        self.assertThat(target_file, FileContains(''))
+
+
+class TestDNSZoneConfig(TestCase):
+    """Tests for DNSZoneConfig."""
+
+    def test_DNSZoneConfig_fields(self):
+        zone_id = random.randint(0, 100)
+        dnszoneconfig = DNSZoneConfig(zone_id)
+        self.assertEqual(
+            (
+                os.path.join(TEMPLATES_PATH, 'zone.template'),
+                os.path.join(conf.DNS_CONFIG_DIR, 'zone.%d' % zone_id)
+            ),
+            (dnszoneconfig.template_name, dnszoneconfig.target_file))