← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/write-dhcp-server-config into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/write-dhcp-server-config into lp:maas with lp:~rvb/maas/nodegroup-dhcpd-key2 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/write-dhcp-server-config/+merge/117660

This branch:

- removes the generate-dhcp-config command
- adds a task to write the DHCP config file
- adds a task to restart the DHCP server
- adds a method on nodegroup to write the DHCP config file and restart the DHCP server
- changes the names of the variables in the template to make them similar to the fields names on the nodegroup object.

= Pre-imp =

Discussed with Julian about this.

= Notes =

The packaging will need to be updated:
- no more running the (removed) generate-dhcp-config command.
- the worker should be able to write the DHCP config file
- the worker should be able to issue the restart command

I'm not entirely sure that "next_server" (in the template) should be "nodegroup.worker_ip".  Seems logical because the TFTP server sits next to the worker but also, I see that the master nodegroup is created with a worker_ip = 127.0.0.1 so I think we've got a problem here.
-- 
https://code.launchpad.net/~rvb/maas/write-dhcp-server-config/+merge/117660
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/write-dhcp-server-config into lp:maas.
=== modified file 'etc/celeryconfig.py'
--- etc/celeryconfig.py	2012-07-30 13:10:39 +0000
+++ etc/celeryconfig.py	2012-08-01 14:05:22 +0000
@@ -36,6 +36,9 @@
 # DHCP leases file, as maintained by ISC dhcpd.
 DHCP_LEASES_FILE = '/var/lib/dhcp/dhcpd.leases'
 
+# ISC dhcpd configuration file.
+DHCP_CONFIG_FILE = '/etc/dhcp/dhcpd.conf'
+
 
 try:
     import user_maasceleryconfig

=== modified file 'etc/democeleryconfig.py'
--- etc/democeleryconfig.py	2012-07-17 09:51:38 +0000
+++ etc/democeleryconfig.py	2012-08-01 14:05:22 +0000
@@ -30,3 +30,7 @@
 
 DNS_RNDC_PORT = 9154
 
+
+DHCP_CONFIG_FILE = os.path.join(
+    DEV_ROOT_DIRECTORY, 'run/dhcpd.conf')
+

=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py	2012-08-01 14:05:22 +0000
+++ src/maasserver/models/nodegroup.py	2012-08-01 14:05:22 +0000
@@ -28,6 +28,10 @@
     Token,
     )
 from provisioningserver.omshell import generate_omapi_key
+from provisioningserver.tasks import (
+    restart_dhcp_server,
+    write_dhcp_config,
+    )
 
 
 worker_user_name = 'maas-nodegroup-worker'
@@ -132,6 +136,19 @@
     ip_range_high = IPAddressField(
         editable=True, unique=True, blank=True, null=True, default='')
 
+    def set_up_dhcp(self):
+        """Write the DHCP configuration file and restart the DHCP server."""
+        # Circular imports.
+        from maasserver.dns import get_dns_server_address
+        write_dhcp_config.delay(
+            subnet=self.ip_range_low,
+            next_server=self.worker_ip,
+            dhcp_key=self.dhcp_key, subnet_mask=self.subnet_mask,
+            broadcast_ip=self.broadcast_ip, router_ip=self.router_ip,
+            dns_servers=get_dns_server_address(),
+            ip_range_low=self.ip_range_low, ip_range_high=self.ip_range_high,
+            callback=restart_dhcp_server.subtask())
+
     def is_dhcp_enabled(self):
         """Is the DHCP for this nodegroup enabled?"""
         return all([

=== modified file 'src/maasserver/tests/test_nodegroup.py'
--- src/maasserver/tests/test_nodegroup.py	2012-08-01 14:05:22 +0000
+++ src/maasserver/tests/test_nodegroup.py	2012-08-01 14:05:22 +0000
@@ -17,8 +17,12 @@
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
 from maasserver.worker_user import get_worker_user
+from maastesting.fakemethod import FakeMethod
+from maastesting.matchers import ContainsAll
+from provisioningserver import tasks
 from provisioningserver.omshell import generate_omapi_key
 from testtools.matchers import (
+    FileContains,
     GreaterThan,
     MatchesStructure,
     )
@@ -172,3 +176,28 @@
             ip_range_high=factory.getRandomIPAddress(),
             )
         self.assertTrue(nodegroup.is_dhcp_enabled())
+
+    def test_set_up_dhcp_writes_dhcp_config(self):
+        conf_file = self.make_file(contents=factory.getRandomString())
+        self.patch(tasks, 'DHCP_CONFIG_FILE', conf_file)
+        # Silence dhcpd restart.
+        self.patch(tasks, 'check_call', FakeMethod())
+        nodegroup = factory.make_node_group(
+            dhcp_key=factory.getRandomString())
+        nodegroup.set_up_dhcp()
+        dhcp_params = [
+            'dhcp_key', 'subnet_mask', 'broadcast_ip', 'router_ip',
+            'ip_range_low', 'ip_range_high']
+        expected = [getattr(nodegroup, param) for param in dhcp_params]
+        self.assertThat(
+            conf_file,
+            FileContains(
+                matcher=ContainsAll(expected)))
+
+    def test_set_up_dhcp_reloads_dhcp_server(self):
+        self.patch(tasks, 'DHCP_CONFIG_FILE', self.make_file())
+        recorder = FakeMethod()
+        self.patch(tasks, 'check_call', recorder)
+        nodegroup = factory.make_node_group()
+        nodegroup.set_up_dhcp()
+        self.assertEqual(1, recorder.call_count)

=== modified file 'src/provisioningserver/__main__.py'
--- src/provisioningserver/__main__.py	2012-07-23 13:24:17 +0000
+++ src/provisioningserver/__main__.py	2012-08-01 14:05:22 +0000
@@ -12,7 +12,6 @@
 
 __metaclass__ = type
 
-import provisioningserver.dhcp.writer
 import provisioningserver.pxe.install_bootloader
 import provisioningserver.pxe.install_image
 from provisioningserver.utils import MainScript
@@ -20,9 +19,6 @@
 
 main = MainScript(__doc__)
 main.register(
-    "generate-dhcp-config",
-    provisioningserver.dhcp.writer)
-main.register(
     "install-pxe-bootloader",
     provisioningserver.pxe.install_bootloader)
 main.register(

=== modified file 'src/provisioningserver/dhcp/config.py'
--- src/provisioningserver/dhcp/config.py	2012-07-03 05:26:34 +0000
+++ src/provisioningserver/dhcp/config.py	2012-08-01 14:05:22 +0000
@@ -47,10 +47,10 @@
     subnet {{subnet}} netmask {{subnet_mask}} {
            next-server {{next_server}};
            option subnet-mask {{subnet_mask}};
-           option broadcast-address {{broadcast_address}};
+           option broadcast-address {{broadcast_ip}};
            option domain-name-servers {{dns_servers}};
-           option routers {{gateway}};
-           range dynamic-bootp {{low_range}} {{high_range}};
+           option routers {{router_ip}};
+           range dynamic-bootp {{ip_range_low}} {{ip_range_high}};
 
            pool {
                    allow members of "uboot-highbank";
@@ -61,6 +61,10 @@
                    filename "{{bootloaders['i386', 'generic']}}";
            }
     }
+    key maasupdate {
+        algorithm hmac-md5;
+        secret "{{dhcp_key}}";
+    };
 """)
 
 template = tempita.Template(

=== modified file 'src/provisioningserver/dhcp/tests/test_config.py'
--- src/provisioningserver/dhcp/tests/test_config.py	2012-07-03 08:29:51 +0000
+++ src/provisioningserver/dhcp/tests/test_config.py	2012-08-01 14:05:22 +0000
@@ -24,14 +24,15 @@
 # Simple test version of the DHCP template.  Contains parameter
 # substitutions, but none that aren't also in the real template.
 sample_template = dedent("""\
+    {{dhcp_key}}
     {{subnet}}
     {{subnet_mask}}
     {{next_server}}
-    {{broadcast_address}}
+    {{broadcast_ip}}
     {{dns_servers}}
-    {{gateway}}
-    {{low_range}}
-    {{high_range}}
+    {{router_ip}}
+    {{ip_range_low}}
+    {{ip_range_high}}
 """)
 
 
@@ -41,14 +42,15 @@
     The sample provides all parameters used by the DHCP template.
     """
     return dict(
+        dhcp_key="random",
         subnet="10.0.0.0",
         subnet_mask="255.0.0.0",
         next_server="10.0.0.1",
-        broadcast_address="10.255.255.255",
+        broadcast_ip="10.255.255.255",
         dns_servers="10.1.0.1 10.1.0.2",
-        gateway="10.0.0.2",
-        low_range="10.0.0.3",
-        high_range="10.0.0.254",
+        router_ip="10.0.0.2",
+        ip_range_low="10.0.0.3",
+        ip_range_high="10.0.0.254",
         )
 
 

=== removed file 'src/provisioningserver/dhcp/tests/test_writer.py'
--- src/provisioningserver/dhcp/tests/test_writer.py	2012-06-14 15:04:12 +0000
+++ src/provisioningserver/dhcp/tests/test_writer.py	1970-01-01 00:00:00 +0000
@@ -1,79 +0,0 @@
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for `provisioningserver.dhcp.writer`."""
-
-from __future__ import (
-    absolute_import,
-    print_function,
-    unicode_literals,
-    )
-
-__metaclass__ = type
-__all__ = []
-
-from argparse import ArgumentParser
-from io import BytesIO
-from os import path
-import sys
-
-from maastesting.matchers import ContainsAll
-from maastesting.testcase import TestCase
-from provisioningserver.dhcp import writer
-from testtools.matchers import MatchesStructure
-
-
-class TestScript(TestCase):
-    """Test the DHCP configuration writer."""
-
-    test_args = (
-        '--subnet', 'subnet',
-        '--subnet-mask', 'subnet-mask',
-        '--next-server', 'next-server',
-        '--broadcast-address', 'broadcast-address',
-        '--dns-servers', 'dns-servers',
-        '--gateway', 'gateway',
-        '--low-range', 'low-range',
-        '--high-range', 'high-range',
-        )
-
-    def test_arg_setup(self):
-        parser = ArgumentParser()
-        writer.add_arguments(parser)
-        args = parser.parse_args(self.test_args)
-        self.assertThat(
-            args, MatchesStructure.byEquality(
-                subnet='subnet',
-                subnet_mask='subnet-mask',
-                next_server='next-server',
-                broadcast_address='broadcast-address',
-                dns_servers='dns-servers',
-                gateway='gateway',
-                low_range='low-range',
-                high_range='high-range'))
-
-    def test_run(self):
-        self.patch(sys, "stdout", BytesIO())
-        parser = ArgumentParser()
-        writer.add_arguments(parser)
-        args = parser.parse_args(self.test_args)
-        writer.run(args)
-        output = sys.stdout.getvalue()
-        contains_all_params = ContainsAll(
-            ['subnet', 'subnet-mask', 'next-server', 'broadcast-address',
-             'dns-servers', 'gateway', 'low-range', 'high-range'])
-        self.assertThat(output, contains_all_params)
-
-    def test_run_save_to_file(self):
-        parser = ArgumentParser()
-        writer.add_arguments(parser)
-        outfile = path.join(self.make_dir(), "outfile.txt")
-        args = parser.parse_args(
-            self.test_args + ("--outfile", outfile))
-        writer.run(args)
-        with open(outfile, "rb") as stream:
-            output = stream.read()
-        contains_all_params = ContainsAll(
-            ['subnet', 'subnet-mask', 'next-server', 'broadcast-address',
-             'dns-servers', 'gateway', 'low-range', 'high-range'])
-        self.assertThat(output, contains_all_params)

=== removed file 'src/provisioningserver/dhcp/writer.py'
--- src/provisioningserver/dhcp/writer.py	2012-07-24 04:53:21 +0000
+++ src/provisioningserver/dhcp/writer.py	1970-01-01 00:00:00 +0000
@@ -1,70 +0,0 @@
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Generate a DHCP server configuration."""
-
-from __future__ import (
-    absolute_import,
-    print_function,
-    unicode_literals,
-    )
-
-__metaclass__ = type
-__all__ = [
-    "add_arguments",
-    "run",
-    ]
-
-import sys
-
-from provisioningserver.dhcp import config
-
-
-def add_arguments(parser):
-    """Initialise options for generating DHCP configuration.
-
-    :param parser: An instance of :class:`ArgumentParser`.
-    """
-    parser.add_argument(
-        "--subnet", action="store", required=True, help=(
-            "Base subnet declaration, e.g. 192.168.1.0"))
-    parser.add_argument(
-        "--subnet-mask", action="store", required=True, help=(
-            "The mask for the subnet, e.g. 255.255.255.0"))
-    parser.add_argument(
-        "--next-server", action="store", required=True, help=(
-            "The address of the TFTP server"))
-    parser.add_argument(
-        "--broadcast-address", action="store", required=True, help=(
-            "The broadcast IP address for the subnet, e.g. 192.168.1.255"))
-    parser.add_argument(
-        "--dns-servers", action="store", required=True, help=(
-            "One or more IP addresses of the DNS server for the subnet "
-            "separated by spaces."))
-    parser.add_argument(
-        "--gateway", action="store", required=True, help=(
-            "The router/gateway IP address for the subnet"))
-    parser.add_argument(
-        "--low-range", action="store", required=True, help=(
-            "The first IP address in the range of IP addresses to "
-            "allocate"))
-    parser.add_argument(
-        "--high-range", action="store", required=True, help=(
-            "The last IP address in the range of IP addresses to "
-            "allocate"))
-    parser.add_argument(
-        "-o", "--outfile", action="store", required=False, help=(
-            "A file to save to. When left unspecified the configuration "
-            "will be written to stdout. This option is useful when "
-            "running outside of a shell."))
-
-
-def run(args):
-    """Generate a DHCP server configuration, and write it to stdout."""
-    params = vars(args)
-    output = config.get_config(**params).encode("ascii")
-    if args.outfile is None:
-        sys.stdout.write(output)
-    else:
-        with open(args.outfile, "wb") as out:
-            out.write(output)

=== modified file 'src/provisioningserver/tasks.py'
--- src/provisioningserver/tasks.py	2012-07-26 00:52:00 +0000
+++ src/provisioningserver/tasks.py	2012-08-01 14:05:22 +0000
@@ -20,9 +20,14 @@
     'write_full_dns_config',
     ]
 
-from subprocess import CalledProcessError
+from subprocess import (
+    CalledProcessError,
+    check_call,
+    )
 
 from celery.task import task
+from celeryconfig import DHCP_CONFIG_FILE
+from provisioningserver.dhcp import config
 from provisioningserver.dns.config import (
     DNSConfig,
     execute_rndc_command,
@@ -33,7 +38,7 @@
     PowerAction,
     PowerActionFail,
     )
-
+from provisioningserver.utils import atomic_write
 
 # =====================================================================
 # Power-related tasks
@@ -107,8 +112,8 @@
             zone.write_config()
             zone.write_reverse_config()
     # Write main config file.
-    config = DNSConfig(zones=zones)
-    config.write_config(**kwargs)
+    dns_config = DNSConfig(zones=zones)
+    dns_config.write_config(**kwargs)
     if callback is not None:
         callback.delay()
 
@@ -124,8 +129,8 @@
     :type callback: callable
     :param **kwargs: Keyword args passed to DNSConfig.write_config()
     """
-    config = DNSConfig(zones=zones)
-    config.write_config(**kwargs)
+    dns_config = DNSConfig(zones=zones)
+    dns_config.write_config(**kwargs)
     if callback is not None:
         callback.delay()
 
@@ -203,3 +208,21 @@
         # Re-raise, so the job is marked as failed.  Only currently
         # useful for tests.
         raise
+
+
+@task
+def write_dhcp_config(callback=None, **kwargs):
+    """Write out the DHCP configuration file and restart the DHCP server.
+
+    :param **kwargs: Keyword args passed to dhcp.config.get_config()
+    """
+    output = config.get_config(**kwargs).encode("ascii")
+    atomic_write(output, DHCP_CONFIG_FILE)
+    if callback is not None:
+        callback.delay()
+
+
+@task
+def restart_dhcp_server():
+    """Restart the DHCP server."""
+    check_call(['sudo', 'service', 'isc-dhcp-server', 'restart'])

=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py	2012-07-27 03:41:47 +0000
+++ src/provisioningserver/tests/test_tasks.py	2012-08-01 14:05:22 +0000
@@ -34,12 +34,14 @@
 from provisioningserver.power.poweraction import PowerActionFail
 from provisioningserver.tasks import (
     add_new_dhcp_host_map,
+    Omshell,
     power_off,
     power_on,
     remove_dhcp_host_map,
+    restart_dhcp_server,
     rndc_command,
-    Omshell,
     setup_rndc_configuration,
+    write_dhcp_config,
     write_dns_config,
     write_dns_zone_config,
     write_full_dns_config,
@@ -48,6 +50,7 @@
 from testresources import FixtureResource
 from testtools.matchers import (
     Equals,
+    FileContains,
     FileExists,
     MatchesListwise,
     )
@@ -145,6 +148,32 @@
             CalledProcessError, remove_dhcp_host_map.delay,
             ip, server_address, key)
 
+    def test_write_dhcp_config_writes_config(self):
+        conf_file = self.make_file(contents=factory.getRandomString())
+        self.patch(tasks, 'DHCP_CONFIG_FILE', conf_file)
+        param_names = [
+             'dhcp_key', 'subnet', 'subnet_mask', 'next_server',
+             'broadcast_ip', 'dns_servers', 'router_ip', 'ip_range_low',
+             'ip_range_high']
+        params = {param: factory.getRandomString() for param in param_names}
+        write_dhcp_config(**params)
+        self.assertThat(
+            conf_file,
+            FileContains(
+                matcher=ContainsAll(
+                    [
+                        'class "pxe"',
+                        "option subnet-mask",
+                    ])))
+
+    def test_restart_dhcp_server_sends_command(self):
+        recorder = FakeMethod()
+        self.patch(tasks, 'check_call', recorder)
+        restart_dhcp_server()
+        self.assertEqual(
+            (1, (['sudo', 'service', 'isc-dhcp-server', 'restart'],)),
+            (recorder.call_count, recorder.extract_args()[0]))
+
 
 class TestDNSTasks(TestCase):
 


Follow ups