launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #09703
[Merge] lp:~rvb/maas/dns-fixture into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/dns-fixture into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~rvb/maas/dns-fixture/+merge/113924
Add bindfixture.
--
https://code.launchpad.net/~rvb/maas/dns-fixture/+merge/113924
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/dns-fixture into lp:maas.
=== modified file 'HACKING.txt'
--- HACKING.txt 2012-07-06 09:06:12 +0000
+++ HACKING.txt 2012-07-09 09:38:28 +0000
@@ -45,7 +45,7 @@
python-twisted python-oops-wsgi python-oops-twisted \
python-psycopg2 python-yaml python-convoy python-django-south \
python-avahi python-dbus python-celery python-tempita distro-info \
- bind9utils libpq-dev
+ bind9utils libpq-dev dnsutils bind9
Additionally, you need to install the following python libraries
for development convenience::
@@ -127,6 +127,18 @@
tests will modify your Cobbler database.
+DNS tests
+^^^^^^^^^
+
+MAAS needs to have access to a bind executable to setup a test DNS
+service. You can either install the ``bind9`` package and MAAS will
+find the executable (in /usr/sbin/named) or define an
+environment variable named ``MAAS_NAMED_PATH`` with the path where
+the executable can be found::
+
+ $ MAAS_NAMED_PATH=/where/to/find/named
+
+
Running JavaScript tests
^^^^^^^^^^^^^^^^^^^^^^^^
=== added file 'src/maastesting/bindfixture.py'
--- src/maastesting/bindfixture.py 1970-01-01 00:00:00 +0000
+++ src/maastesting/bindfixture.py 2012-07-09 09:38:28 +0000
@@ -0,0 +1,295 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Server fixture for Bind."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ 'BindServer',
+ 'set_up_named',
+ ]
+
+import os
+from shutil import copy
+import subprocess
+import time
+
+import fixtures
+from provisioningserver.utils import atomic_write
+from provisioningserver.dns.config import generate_rndc
+from rabbitfixture.server import (
+ allocate_ports,
+ preexec_fn,
+ )
+import tempita
+from testtools.content import Content
+from testtools.content_type import UTF8_TEXT
+
+
+def get_named_path():
+ """Return the full path where the 'named' executable can be
+ found.
+
+ Note that it will be copied over to a temporary
+ location in order to by-pass the limitations imposed by
+ apparmor if the executable is in its default location
+ (/usr/sbin/named).
+ """
+ return os.environ.get(
+ 'MAAS_NAMED_PATH', '/usr/sbin/named')
+
+
+# Where the executable 'rndc' can be found (belongs to the package
+# 'bind9utils').
+RNDCBIN = "/usr/sbin/rndc"
+
+# The configuration template for the Bind server. The goal here
+# is to override the defaults (default configuration files location,
+# default port) to avoid clashing with the system's bind (if
+# running).
+NAMED_CONF_TEMPLATE = tempita.Template("""
+options {
+ directory "{{homedir}}";
+ listen-on port {{port}} {127.0.0.1;};
+};
+
+logging{
+ channel simple_log {
+ file "{{log_file}}";
+ severity info;
+ print-severity yes;
+ };
+ category default{
+ simple_log;
+ };
+};
+""")
+
+
+def set_up_named(homedir, port, rndc_port, log_file, named_file,
+ conf_file, rndcconf_file):
+ """Setup an environment to run 'named'.
+
+ - Create the default configuration for 'named' and setup rndc.
+ - Copies the 'named' executable inside homedir (to by-pass the
+ restrictions that apparmor imposes
+
+ :param homedir: Home directory where the executable should be
+ copied.
+ :param port: Port that will be used by 'named'.
+ :param rndc_port: rndc port that will be used by 'named'.
+ :param log_file: Full path of the main logging file.
+ :param named_file: Full path of where 'named' should be copied.
+ :param conf_file: Full path of the main configuration file.
+ :param rndcconf_file: Full path of the rndc configuration file.
+ """
+ # Generate rndc configuration (rndc config and named snippet).
+ rndcconf, namedrndcconf = generate_rndc(
+ rndc_port, 'dnsfixture-rndc-key')
+ # Write main bind config file.
+ named_conf = (
+ NAMED_CONF_TEMPLATE.substitute(
+ homedir=homedir, port=port, log_file=log_file)
+ + namedrndcconf)
+ atomic_write(named_conf, conf_file)
+ # Write rndc config file.
+ atomic_write(rndcconf, rndcconf_file)
+
+ # Copy named executable to home dir. This is done to avoid
+ # the limitations imposed by apparmor if the executable
+ # is in /usr/sbin/named.
+ named_path = get_named_path()
+ assert os.path.exists(named_path), (
+ "'%s' executable not found. Install the package "
+ "'bind9' or define an environment variable named "
+ "MAAS_NAMED_PATH with the path where the 'named' "
+ "executable can be found." % named_path)
+ copy(named_path, named_file)
+
+
+class BindServerResources(fixtures.Fixture):
+ """Allocate the resources a Bind server needs.
+
+ :ivar port: A port that was free at the time setUp() was
+ called.
+ :ivar rndc_port: A port that was free at the time setUp() was
+ called (used for rndc communication).
+ :ivar homedir: A directory where to put all the files the
+ Bind server needs (configuration files and executable).
+ :ivar log_file: The log_file allocated for the server.
+ """
+
+ def __init__(self, port=None, rndc_port=None, homedir=None,
+ log_file=None):
+ super(BindServerResources, self).__init__()
+ self._defaults = dict(
+ port=port,
+ rndc_port=rndc_port,
+ homedir=homedir,
+ log_file=log_file,
+ )
+
+ def setUp(self):
+ super(BindServerResources, self).setUp()
+ self.__dict__.update(self._defaults)
+ self.set_up_config()
+ set_up_named(
+ self.homedir, self.port, self.rndc_port, self.log_file,
+ self.named_file, self.conf_file, self.rndcconf_file)
+
+ def set_up_config(self):
+ if self.port is None:
+ [self.port] = allocate_ports(1)
+ if self.rndc_port is None:
+ [self.rndc_port] = allocate_ports(1)
+ if self.homedir is None:
+ self.homedir = self.useFixture(fixtures.TempDir()).path
+ if self.log_file is None:
+ self.log_file = os.path.join(self.homedir, 'named.log')
+ self.named_file = os.path.join(
+ self.homedir, os.path.basename(get_named_path()))
+ self.conf_file = os.path.join(self.homedir, 'named.conf')
+ self.rndcconf_file = os.path.join(self.homedir, 'rndc.conf')
+
+ def tearDown(self):
+ super(BindServerResources, self).tearDown()
+ # Restore defaults, setting dynamic values back to None for
+ # reallocation in setUp.
+ self.__dict__.update(self._defaults)
+
+
+class BindServerRunner(fixtures.Fixture):
+ """Run a Bind server."""
+
+ def __init__(self, config):
+ """Create a `BindServerRunner` instance.
+
+ :param config: An object exporting the variables
+ `BindServerResources` exports.
+ """
+ super(BindServerRunner, self).__init__()
+ self.config = config
+ self.process = None
+
+ def setUp(self):
+ super(BindServerRunner, self).setUp()
+ self._start()
+
+ def is_running(self):
+ """Is the Bind server process still running?"""
+ if self.process is None:
+ return False
+ else:
+ return self.process.poll() is None
+
+ def _spawn(self):
+ """Spawn the Bind server process."""
+ env = dict(os.environ, HOME=self.config.homedir)
+ with open(self.config.log_file, "wb") as log_file:
+ with open(os.devnull, "rb") as devnull:
+ self.process = subprocess.Popen(
+ [self.config.named_file, "-f", "-c",
+ self.config.conf_file],
+ stdin=devnull,
+ stdout=log_file, stderr=log_file,
+ close_fds=True, cwd=self.config.homedir,
+ env=env, preexec_fn=preexec_fn)
+ # Keep the log_file open for reading so that we can still get the log
+ # even if the log is deleted.
+ open_log_file = open(self.config.log_file, "rb")
+ self.addDetail(
+ os.path.basename(self.config.log_file),
+ Content(UTF8_TEXT, lambda: open_log_file))
+
+ def rndc(self, command):
+ """Executes a ``rndc`` command and returns status."""
+ if isinstance(command, basestring):
+ command = (command,)
+ ctl = subprocess.Popen(
+ (RNDCBIN, "-c", self.config.rndcconf_file) + command,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ preexec_fn=preexec_fn)
+ outstr, errstr = ctl.communicate()
+ return outstr, errstr
+
+ def is_server_running(self):
+ """Checks that the Bind server is up and running."""
+ outdata, errdata = self.rndc("status")
+ return "server is up and running" in outdata
+
+ def _start(self):
+ """Start the Bind server."""
+ self._spawn()
+ # Wait for the server to come up: stop when the process is dead, or
+ # the timeout expires, or the server responds.
+ timeout = time.time() + 15
+ while time.time() < timeout and self.is_running():
+ if self.is_server_running():
+ break
+ time.sleep(0.3)
+ else:
+ raise Exception(
+ "Timeout waiting for Bind server to start: log in %r." %
+ (self.config.log_file,))
+ self.addCleanup(self._stop)
+
+ def _request_stop(self):
+ outstr, errstr = self.rndc("stop")
+ if outstr:
+ self.addDetail('stop-out', Content(UTF8_TEXT, lambda: [outstr]))
+ if errstr:
+ self.addDetail('stop-err', Content(UTF8_TEXT, lambda: [errstr]))
+
+ def _stop(self):
+ """Stop the running server. Normally called by cleanups."""
+ self._request_stop()
+ # Wait for the server to go down...
+ timeout = time.time() + 15
+ while time.time() < timeout:
+ if not self.is_server_running():
+ break
+ time.sleep(0.3)
+ else:
+ raise Exception(
+ "Timeout waiting for Bind server to go down.")
+ # Wait at least 5 more seconds for the process to end...
+ timeout = max(timeout, time.time() + 5)
+ while time.time() < timeout:
+ if not self.is_running():
+ break
+ self.process.terminate()
+ time.sleep(0.1)
+ else:
+ # Die!!!
+ if self.is_running():
+ self.process.kill()
+ time.sleep(0.5)
+ if self.is_running():
+ raise Exception("Bind server just won't die.")
+
+
+class BindServer(fixtures.Fixture):
+ """A Bind server fixture.
+
+ When setup a Bind instance will be running.
+
+ :ivar config: The `BindServerResources` used to start the server.
+ """
+
+ def __init__(self, config=None):
+ super(BindServer, self).__init__()
+ self.config = config
+
+ def setUp(self):
+ super(BindServer, self).setUp()
+ if self.config is None:
+ self.config = BindServerResources()
+ self.useFixture(self.config)
+ self.runner = BindServerRunner(self.config)
+ self.useFixture(self.runner)
=== added file 'src/maastesting/tests/test_bindfixture.py'
--- src/maastesting/tests/test_bindfixture.py 1970-01-01 00:00:00 +0000
+++ src/maastesting/tests/test_bindfixture.py 2012-07-09 09:38:28 +0000
@@ -0,0 +1,126 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the Bind fixture."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = []
+
+
+from subprocess import (
+ CalledProcessError,
+ check_output,
+ )
+
+from maastesting.bindfixture import (
+ BindServer,
+ BindServerResources,
+ )
+from maastesting.testcase import TestCase
+import os
+from testtools.matchers import (
+ Contains,
+ Equals,
+ FileContains,
+ FileExists,
+ MatchesListwise,
+ )
+from testtools.testcase import gather_details
+
+
+def dig_call(port=53, server='127.0.0.1', command=''):
+ """Call `dig` with the given command.
+
+ Note that calling dig without a command will perform an NS
+ query for "." (the root) which is useful to check if there
+ is a running server.
+
+ :param port: Port of the queried DNS server (defaults to 53).
+ :param server: IP address of the queried DNS server (defaults
+ to '127.0.0.1').
+ :param command: Dig command to run (defaults to '').
+ :return: A tuple containing 2 elements: the returned string
+ output and the exit code.
+ :rtype: tuple
+ """
+ try:
+ cmd = [
+ 'dig', '+time=1', '+tries=1', '@%s' % server, '-p',
+ '%d' % port]
+ if command != '':
+ cmd.append(command)
+ return check_output(cmd), 0
+ except CalledProcessError, e:
+ return '', e.returncode
+
+
+class TestBindFixture(TestCase):
+
+ def test_start_check_shutdown(self):
+ # The fixture correctly starts and stops Bind.
+ with BindServer() as fixture:
+ try:
+ result, retcode = dig_call(fixture.config.port)
+ self.assertThat(
+ (result, retcode),
+ MatchesListwise(
+ [Contains("Got answer"), Equals(0)]))
+ except Exception:
+ # self.useFixture() is not being used because we want to
+ # handle the fixture's lifecycle, so we must also be
+ # responsible for propagating fixture details.
+ gather_details(fixture.getDetails(), self.getDetails())
+ raise
+ result, retcode = dig_call(fixture.config.port)
+ self.assertEqual(9, retcode) # return code 9 means timeout.
+
+ def test_config(self):
+ # The configuration can be passed in.
+ config = BindServerResources()
+ fixture = self.useFixture(BindServer(config))
+ self.assertIs(config, fixture.config)
+
+
+class TestBindServerResources(TestCase):
+
+ def test_defaults(self):
+ with BindServerResources() as resources:
+ self.assertIsInstance(resources.port, int)
+ self.assertIsInstance(resources.rndc_port, int)
+ self.assertIsInstance(resources.homedir, basestring)
+ self.assertIsInstance(resources.log_file, basestring)
+ self.assertIsInstance(resources.named_file, basestring)
+ self.assertIsInstance(resources.conf_file, basestring)
+ self.assertIsInstance(
+ resources.rndcconf_file, basestring)
+
+ def test_setUp_copies_executable(self):
+ with BindServerResources() as resources:
+ self.assertThat(resources.named_file, FileExists())
+
+ def test_setUp_creates_config_files(self):
+ with BindServerResources() as resources:
+ self.assertThat(
+ resources.conf_file,
+ FileContains(matcher=Contains(
+ b'listen-on port %s' % resources.port)))
+ self.assertThat(
+ resources.rndcconf_file,
+ FileContains(matcher=Contains(
+ b'default-port %s' % (
+ resources.rndc_port))))
+
+ def test_defaults_reallocated_after_teardown(self):
+ seen_homedirs = set()
+ resources = BindServerResources()
+ for i in range(2):
+ with resources:
+ self.assertTrue(os.path.exists(resources.homedir))
+ self.assertNotIn(resources.homedir, seen_homedirs)
+ seen_homedirs.add(resources.homedir)
=== modified file 'src/provisioningserver/dns/config.py'
--- src/provisioningserver/dns/config.py 2012-07-07 05:51:05 +0000
+++ src/provisioningserver/dns/config.py 2012-07-09 09:38:28 +0000
@@ -37,7 +37,7 @@
"""Raised if there's a problem with a DNS config."""
-def generate_rndc():
+def generate_rndc(port=953, key_name='rndc-maas-key'):
"""Use `rndc-confgen` (from bind9utils) to generate a rndc+named
configuration.
@@ -50,7 +50,7 @@
# non-blocking.
rndc_content = check_output(
['rndc-confgen', '-b', '256', '-r', '/dev/urandom',
- '-k', 'rndc-maas-key'])
+ '-k', key_name, '-p', str(port)])
# Extract from the result the part which corresponds to the named
# configuration.
start_marker = (