← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~raharper/cloud-init:add-ntp into cloud-init:master

 

Ryan Harper has proposed merging ~raharper/cloud-init:add-ntp into cloud-init:master.

Requested reviews:
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/301498

add ntp config module

Add support for installing and configuring ntp service, exposing the
minimum config of servers or pools to be added.  If none are defined
then fallback on generating a list of pools by distro hosted at
pool.ntp.org (which matches what's found in the default ntp.conf
shipped in the respective distro).

-- 
Your team cloud init development team is requested to review the proposed merge of ~raharper/cloud-init:add-ntp into cloud-init:master.
diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
new file mode 100644
index 0000000..db5cbbe
--- /dev/null
+++ b/cloudinit/config/cc_ntp.py
@@ -0,0 +1,103 @@
+# vi: ts=4 expandtab
+#
+#    Copyright (C) 2016 Canonical Ltd.
+#
+#    Author: Ryan Harper <ryan.harper@xxxxxxxxxxxxx>
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import templater
+from cloudinit import type_utils
+from cloudinit import util
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+NTP_CONF = '/etc/ntp.conf'
+NR_POOL_SERVERS = 4
+distros = ['centos', 'debian', 'fedora', 'opensuse', 'ubuntu']
+
+
+def handle(name, cfg, cloud, log, _args):
+    """
+    Enable and configure ntp
+
+    ntp:
+       pools: ['0.{{distro}}.pool.ntp.org', '1.{{distro}}.pool.ntp.org']
+       servers: ['192.168.2.1']
+
+    """
+
+    ntp_cfg = cfg.get('ntp', {})
+
+    if not isinstance(ntp_cfg, (dict)):
+        raise RuntimeError(("'ntp' key existed in config,"
+                            " but not a dictionary type,"
+                            " is a %s %instead"), type_utils.obj_name(ntp_cfg))
+
+    if 'ntp' not in cfg:
+        log.warn('Skipping module named %s, not present or disabled by cfg',
+                 name)
+        return
+
+    try:
+        install_ntp(cloud)
+    except util.ProcessExecutionError as exc:
+        log.warn("failed to install ntp package: %s", exc)
+        return
+    rename_ntp_conf()
+    write_ntp_config_template(ntp_cfg, cloud, log)
+
+
+def install_ntp(cloud):
+    if not util.which("ntpd"):
+        cloud.distro.install_packages(['ntp'])
+
+
+def rename_ntp_conf():
+    if os.path.exists(NTP_CONF):
+        util.rename(NTP_CONF, NTP_CONF + ".dist")
+
+
+def generate_server_names(distro):
+    names = []
+    for x in range(0, NR_POOL_SERVERS):
+        name = "%d.%s.pool.ntp.org" % (x, distro)
+        names.append(name)
+    return names
+
+
+def write_ntp_config_template(cfg, cloud, log):
+    servers = cfg.get('servers', [])
+    pools = cfg.get('pools', [])
+
+    if len(servers) == 0 and len(pools) == 0:
+        log.debug('Adding distro default ntp pool servers')
+        pools = generate_server_names(cloud.distro.name)
+
+    params = {
+        'servers': servers,
+        'pools': pools,
+    }
+
+    template_fn = cloud.get_template_filename('ntp.conf.%s' %
+                                              (cloud.distro.name))
+    if not template_fn:
+        template_fn = cloud.get_template_filename('ntp.conf')
+        if not template_fn:
+            log.warn("No template found, not rendering %", NTP_CONF)
+            return
+
+    templater.render_to_file(template_fn, NTP_CONF, params)
diff --git a/config/cloud.cfg b/config/cloud.cfg
index a6afcc8..c97162d 100644
--- a/config/cloud.cfg
+++ b/config/cloud.cfg
@@ -42,6 +42,7 @@ cloud_init_modules:
 cloud_config_modules:
 # Emit the cloud config ready event
 # this can be used by upstart jobs for 'start on cloud-config'.
+ - ntp
  - emit_upstart
  - disk_setup
  - mounts
diff --git a/doc/examples/cloud-config-ntp.txt b/doc/examples/cloud-config-ntp.txt
new file mode 100644
index 0000000..2efcac4
--- /dev/null
+++ b/doc/examples/cloud-config-ntp.txt
@@ -0,0 +1,28 @@
+#cloud-config
+
+# ntp: configure ntp services
+#   bootsync: During boot, sync with NTP server and force clock jump.
+#   servers: List of NTP servers with which to sync
+#   pools: List of NTP pool servers with which to sync (pools are typically
+#          DNS hostnames which resolve to different specific servers to load
+#          balance a set of services)
+#
+# Each server in the list will be added in list-order in the following format:
+#
+# [pool|server] <server entry> iburst
+#
+#
+# If no servers or pools are defined but ntp is enabled, then cloud-init will
+# render the distro default list of pools
+#
+# pools = [
+#    '0.{distro}.pool.ntp.org',
+#    '1.{distro}.pool.ntp.org',
+#    '2.{distro}.pool.ntp.org',
+#    '3.{distro}.pool.ntp.org',
+# ]
+#
+
+ntp:
+  pools: ['0.company.pool.ntp.org', '1.company.pool.ntp.org', 'ntp.myorg.org']
+  servers: ['my.ntp.server.local', 'ntp.ubuntu.com', '192.168.23.2']
diff --git a/templates/ntp.conf.debian.tmpl b/templates/ntp.conf.debian.tmpl
new file mode 100644
index 0000000..3f07eea
--- /dev/null
+++ b/templates/ntp.conf.debian.tmpl
@@ -0,0 +1,63 @@
+## template:jinja
+
+# /etc/ntp.conf, configuration for ntpd; see ntp.conf(5) for help
+
+driftfile /var/lib/ntp/ntp.drift
+
+# Enable this if you want statistics to be logged.
+#statsdir /var/log/ntpstats/
+
+statistics loopstats peerstats clockstats
+filegen loopstats file loopstats type day enable
+filegen peerstats file peerstats type day enable
+filegen clockstats file clockstats type day enable
+
+
+# You do need to talk to an NTP server or two (or three).
+#server ntp.your-provider.example
+
+# pool.ntp.org maps to about 1000 low-stratum NTP servers.  Your server will
+# pick a different set every time it starts up.  Please consider joining the
+# pool: <http://www.pool.ntp.org/join.html>
+{% if pools -%}# pools{% endif %}
+{% for pool in pools -%}
+pool {{pool}} iburst
+{% endfor %}
+{%- if servers %}# servers
+{% endif %}
+{% for server in servers -%}
+server {{server}} iburst
+{% endfor %}
+
+# Access control configuration; see /usr/share/doc/ntp-doc/html/accopt.html for
+# details.  The web page <http://support.ntp.org/bin/view/Support/AccessRestrictions>
+# might also be helpful.
+#
+# Note that "restrict" applies to both servers and clients, so a configuration
+# that might be intended to block requests from certain clients could also end
+# up blocking replies from your own upstream servers.
+
+# By default, exchange time with everybody, but don't allow configuration.
+restrict -4 default kod notrap nomodify nopeer noquery limited
+restrict -6 default kod notrap nomodify nopeer noquery limited
+
+# Local users may interrogate the ntp server more closely.
+restrict 127.0.0.1
+restrict ::1
+
+# Needed for adding pool entries
+restrict source notrap nomodify noquery
+
+# Clients from this (example!) subnet have unlimited access, but only if
+# cryptographically authenticated.
+#restrict 192.168.123.0 mask 255.255.255.0 notrust
+
+
+# If you want to provide time to your local subnet, change the next line.
+# (Again, the address is an example only.)
+#broadcast 192.168.123.255
+
+# If you want to listen to time broadcasts on your local subnet, de-comment the
+# next lines.  Please do this only if you trust everybody on the network!
+#disable auth
+#broadcastclient
diff --git a/templates/ntp.conf.fedora.tmpl b/templates/ntp.conf.fedora.tmpl
new file mode 100644
index 0000000..af7b1b0
--- /dev/null
+++ b/templates/ntp.conf.fedora.tmpl
@@ -0,0 +1,66 @@
+## template:jinja
+
+# For more information about this file, see the man pages
+# ntp.conf(5), ntp_acc(5), ntp_auth(5), ntp_clock(5), ntp_misc(5), ntp_mon(5).
+
+driftfile /var/lib/ntp/drift
+
+# Permit time synchronization with our time source, but do not
+# permit the source to query or modify the service on this system.
+restrict default nomodify notrap nopeer noquery
+
+# Permit all access over the loopback interface.  This could
+# be tightened as well, but to do so would effect some of
+# the administrative functions.
+restrict 127.0.0.1 
+restrict ::1
+
+# Hosts on local network are less restricted.
+#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap
+
+# Use public servers from the pool.ntp.org project.
+# Please consider joining the pool (http://www.pool.ntp.org/join.html).
+{% if pools %}# pools
+{% endif %}
+{% for pool in pools -%}
+pool {{pool}} iburst
+{% endfor %}
+{%- if servers %}# servers
+{% endif %}
+{% for server in servers -%}
+server {{server}} iburst
+{% endfor %}
+
+#broadcast 192.168.1.255 autokey	# broadcast server
+#broadcastclient			# broadcast client
+#broadcast 224.0.1.1 autokey		# multicast server
+#multicastclient 224.0.1.1		# multicast client
+#manycastserver 239.255.254.254		# manycast server
+#manycastclient 239.255.254.254 autokey # manycast client
+
+# Enable public key cryptography.
+#crypto
+
+includefile /etc/ntp/crypto/pw
+
+# Key file containing the keys and key identifiers used when operating
+# with symmetric key cryptography. 
+keys /etc/ntp/keys
+
+# Specify the key identifiers which are trusted.
+#trustedkey 4 8 42
+
+# Specify the key identifier to use with the ntpdc utility.
+#requestkey 8
+
+# Specify the key identifier to use with the ntpq utility.
+#controlkey 8
+
+# Enable writing of statistics records.
+#statistics clockstats cryptostats loopstats peerstats
+
+# Disable the monitoring facility to prevent amplification attacks using ntpdc
+# monlist command when default restrict does not include the noquery flag. See
+# CVE-2013-5211 for more details.
+# Note: Monitoring will not be disabled with the limited restriction flag.
+disable monitor
diff --git a/templates/ntp.conf.rhel.tmpl b/templates/ntp.conf.rhel.tmpl
new file mode 100644
index 0000000..62b4776
--- /dev/null
+++ b/templates/ntp.conf.rhel.tmpl
@@ -0,0 +1,61 @@
+## template:jinja
+
+# For more information about this file, see the man pages
+# ntp.conf(5), ntp_acc(5), ntp_auth(5), ntp_clock(5), ntp_misc(5), ntp_mon(5).
+
+driftfile /var/lib/ntp/drift
+
+# Permit time synchronization with our time source, but do not
+# permit the source to query or modify the service on this system.
+restrict default kod nomodify notrap nopeer noquery
+restrict -6 default kod nomodify notrap nopeer noquery
+
+# Permit all access over the loopback interface.  This could
+# be tightened as well, but to do so would effect some of
+# the administrative functions.
+restrict 127.0.0.1 
+restrict -6 ::1
+
+# Hosts on local network are less restricted.
+#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap
+
+# Use public servers from the pool.ntp.org project.
+# Please consider joining the pool (http://www.pool.ntp.org/join.html).
+{% if pools %}# pools
+{% endif %}
+{% for pool in pools -%}
+pool {{pool}} iburst
+{% endfor %}
+{%- if servers %}# servers
+{% endif %}
+{% for server in servers -%}
+server {{server}} iburst
+{% endfor %}
+
+#broadcast 192.168.1.255 autokey	# broadcast server
+#broadcastclient			# broadcast client
+#broadcast 224.0.1.1 autokey		# multicast server
+#multicastclient 224.0.1.1		# multicast client
+#manycastserver 239.255.254.254		# manycast server
+#manycastclient 239.255.254.254 autokey # manycast client
+
+# Enable public key cryptography.
+#crypto
+
+includefile /etc/ntp/crypto/pw
+
+# Key file containing the keys and key identifiers used when operating
+# with symmetric key cryptography. 
+keys /etc/ntp/keys
+
+# Specify the key identifiers which are trusted.
+#trustedkey 4 8 42
+
+# Specify the key identifier to use with the ntpdc utility.
+#requestkey 8
+
+# Specify the key identifier to use with the ntpq utility.
+#controlkey 8
+
+# Enable writing of statistics records.
+#statistics clockstats cryptostats loopstats peerstats
diff --git a/templates/ntp.conf.sles.tmpl b/templates/ntp.conf.sles.tmpl
new file mode 100644
index 0000000..5c5fc4d
--- /dev/null
+++ b/templates/ntp.conf.sles.tmpl
@@ -0,0 +1,100 @@
+## template:jinja
+
+################################################################################
+## /etc/ntp.conf
+##
+## Sample NTP configuration file.
+## See package 'ntp-doc' for documentation, Mini-HOWTO and FAQ.
+## Copyright (c) 1998 S.u.S.E. GmbH Fuerth, Germany.
+##
+## Author: Michael Andres,  <ma@xxxxxxx>
+##         Michael Skibbe,  <mskibbe@xxxxxxx>
+##
+################################################################################
+
+##
+## Radio and modem clocks by convention have addresses in the 
+## form 127.127.t.u, where t is the clock type and u is a unit 
+## number in the range 0-3. 
+##
+## Most of these clocks require support in the form of a 
+## serial port or special bus peripheral. The particular  
+## device is normally specified by adding a soft link 
+## /dev/device-u to the particular hardware device involved, 
+## where u correspond to the unit number above. 
+## 
+## Generic DCF77 clock on serial port (Conrad DCF77)
+## Address:     127.127.8.u
+## Serial Port: /dev/refclock-u
+##  
+## (create soft link /dev/refclock-0 to the particular ttyS?)
+##
+# server 127.127.8.0 mode 5 prefer
+
+##
+## Undisciplined Local Clock. This is a fake driver intended for backup
+## and when no outside source of synchronized time is available.
+##
+# server 127.127.1.0		# local clock (LCL)
+# fudge  127.127.1.0 stratum 10	# LCL is unsynchronized
+
+##
+## Add external Servers using
+## # rcntpd addserver <yourserver>
+## The servers will only be added to the currently running instance, not
+## to /etc/ntp.conf.
+##
+{% if pools %}# pools
+{% endif %}
+{% for pool in pools -%}
+pool {{pool}} iburst
+{% endfor %}
+{%- if servers %}# servers
+{% endif %}
+{% for server in servers -%}
+server {{server}} iburst
+{% endfor %}
+
+# Access control configuration; see /usr/share/doc/packages/ntp/html/accopt.html for
+# details.  The web page <http://support.ntp.org/bin/view/Support/AccessRestrictions>
+# might also be helpful.
+#
+# Note that "restrict" applies to both servers and clients, so a configuration
+# that might be intended to block requests from certain clients could also end
+# up blocking replies from your own upstream servers.
+
+# By default, exchange time with everybody, but don't allow configuration.
+restrict -4 default notrap nomodify nopeer noquery
+restrict -6 default notrap nomodify nopeer noquery
+
+# Local users may interrogate the ntp server more closely.
+restrict 127.0.0.1
+restrict ::1
+
+# Clients from this (example!) subnet have unlimited access, but only if
+# cryptographically authenticated.
+#restrict 192.168.123.0 mask 255.255.255.0 notrust
+
+##
+## Miscellaneous stuff
+##
+
+driftfile /var/lib/ntp/drift/ntp.drift # path for drift file
+
+logfile   /var/log/ntp		# alternate log file
+# logconfig =syncstatus + sysevents
+# logconfig =all
+
+# statsdir /tmp/		# directory for statistics files
+# filegen peerstats  file peerstats  type day enable
+# filegen loopstats  file loopstats  type day enable
+# filegen clockstats file clockstats type day enable
+
+#
+# Authentication stuff
+#
+keys /etc/ntp.keys		# path for keys file
+trustedkey 1			# define trusted keys
+requestkey 1			# key (7) for accessing server variables
+controlkey 1			# key (6) for accessing server variables
+
diff --git a/templates/ntp.conf.ubuntu.tmpl b/templates/ntp.conf.ubuntu.tmpl
new file mode 100644
index 0000000..862a4fb
--- /dev/null
+++ b/templates/ntp.conf.ubuntu.tmpl
@@ -0,0 +1,75 @@
+## template:jinja
+
+# /etc/ntp.conf, configuration for ntpd; see ntp.conf(5) for help
+
+driftfile /var/lib/ntp/ntp.drift
+
+# Enable this if you want statistics to be logged.
+#statsdir /var/log/ntpstats/
+
+statistics loopstats peerstats clockstats
+filegen loopstats file loopstats type day enable
+filegen peerstats file peerstats type day enable
+filegen clockstats file clockstats type day enable
+
+# Specify one or more NTP servers.
+
+# Use servers from the NTP Pool Project. Approved by Ubuntu Technical Board
+# on 2011-02-08 (LP: #104525). See http://www.pool.ntp.org/join.html for
+# more information.
+{% if pools %}# pools
+{% endif %}
+{% for pool in pools -%}
+pool {{pool}} iburst
+{% endfor %}
+{%- if servers %}# servers
+{% endif %}
+{% for server in servers -%}
+server {{server}} iburst
+{% endfor %}
+
+# Use Ubuntu's ntp server as a fallback.
+# pool ntp.ubuntu.com
+
+# Access control configuration; see /usr/share/doc/ntp-doc/html/accopt.html for
+# details.  The web page <http://support.ntp.org/bin/view/Support/AccessRestrictions>
+# might also be helpful.
+#
+# Note that "restrict" applies to both servers and clients, so a configuration
+# that might be intended to block requests from certain clients could also end
+# up blocking replies from your own upstream servers.
+
+# By default, exchange time with everybody, but don't allow configuration.
+restrict -4 default kod notrap nomodify nopeer noquery limited
+restrict -6 default kod notrap nomodify nopeer noquery limited
+
+# Local users may interrogate the ntp server more closely.
+restrict 127.0.0.1
+restrict ::1
+
+# Needed for adding pool entries
+restrict source notrap nomodify noquery
+
+# Clients from this (example!) subnet have unlimited access, but only if
+# cryptographically authenticated.
+#restrict 192.168.123.0 mask 255.255.255.0 notrust
+
+
+# If you want to provide time to your local subnet, change the next line.
+# (Again, the address is an example only.)
+#broadcast 192.168.123.255
+
+# If you want to listen to time broadcasts on your local subnet, de-comment the
+# next lines.  Please do this only if you trust everybody on the network!
+#disable auth
+#broadcastclient
+
+#Changes recquired to use pps synchonisation as explained in documentation:
+#http://www.ntp.org/ntpfaq/NTP-s-config-adv.htm#AEN3918
+
+#server 127.127.8.1 mode 135 prefer    # Meinberg GPS167 with PPS
+#fudge 127.127.8.1 time1 0.0042        # relative to PPS for my hardware
+
+#server 127.127.22.1                   # ATOM(PPS)
+#fudge 127.127.22.1 flag3 1            # enable PPS API
+
diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
new file mode 100644
index 0000000..46eb30b
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_ntp.py
@@ -0,0 +1,284 @@
+from cloudinit.config import cc_ntp
+from cloudinit.sources import DataSourceNone
+from cloudinit import templater
+from cloudinit import (distros, helpers, cloud, util)
+from .. import helpers as t_help
+
+import logging
+import os
+import shutil
+import tempfile
+
+try:
+    from unittest import mock
+except ImportError:
+    import mock
+
+LOG = logging.getLogger(__name__)
+
+NTP_TEMPLATE = """
+## template: jinja
+
+{% if pools %}# pools
+{% endif %}
+{% for pool in pools -%}
+pool {{pool}} iburst
+{% endfor %}
+{%- if servers %}# servers
+{% endif %}
+{% for server in servers -%}
+server {{server}} iburst
+{% endfor %}
+
+"""
+
+
+NTP_EXPECTED_UBUNTU = """
+# pools
+pool 0.mycompany.pool.ntp.org iburst
+# servers
+server 192.168.23.3 iburst
+
+"""
+
+
+class TestNtp(t_help.FilesystemMockingTestCase):
+    ntp_cfg = {
+        'ntp': {
+            'servers': [],
+            'pools': []
+         }
+    }
+
+    def setUp(self):
+        super(TestNtp, self).setUp()
+        self.subp = util.subp
+        self.new_root = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.new_root)
+
+    def _get_cloud(self, distro, metadata=None):
+        self.patchUtils(self.new_root)
+        paths = helpers.Paths({})
+        cls = distros.fetch(distro)
+        mydist = cls(distro, {}, paths)
+        myds = DataSourceNone.DataSourceNone({}, mydist, paths)
+        if metadata:
+            myds.metadata.update(metadata)
+        return cloud.Cloud(myds, paths, {}, mydist, None)
+
+    @mock.patch("cloudinit.config.cc_ntp.util")
+    def test_ntp_install(self, mock_util):
+        cc = self._get_cloud('ubuntu')
+        cc.distro = mock.MagicMock()
+        cc.distro.name = 'ubuntu'
+        mock_util.which.return_value = None
+        cc_ntp.install_ntp(cc)
+        self.assertTrue(cc.distro.install_packages.called)
+        install_pkg = cc.distro.install_packages.call_args_list[0][0][0]
+        self.assertEqual(sorted(install_pkg), ['ntp'])
+
+    @mock.patch("cloudinit.config.cc_ntp.util")
+    def test_ntp_install_not_needed(self, mock_util):
+        cc = self._get_cloud('ubuntu')
+        cc.distro = mock.MagicMock()
+        cc.distro.name = 'ubuntu'
+        mock_util.which.return_value = ["/usr/sbin/ntpd"]
+        cc_ntp.install_ntp(cc)
+        self.assertFalse(cc.distro.install_packages.called)
+
+    def test_ntp_rename_ntp_conf(self):
+        with mock.patch.object(os.path, 'exists',
+                               return_value=True) as mockpath:
+            with mock.patch.object(util, 'rename') as mockrename:
+                cc_ntp.rename_ntp_conf()
+
+        mockpath.assert_called_with('/etc/ntp.conf')
+        mockrename.assert_called_with('/etc/ntp.conf', '/etc/ntp.conf.dist')
+
+    def test_ntp_rename_ntp_conf_skip_missing(self):
+        with mock.patch.object(os.path, 'exists',
+                               return_value=False) as mockpath:
+            with mock.patch.object(util, 'rename') as mockrename:
+                cc_ntp.rename_ntp_conf()
+
+        mockpath.assert_called_with('/etc/ntp.conf')
+        mockrename.assert_not_called()
+
+    def ntp_conf_render(self, distro):
+        """ntp_conf_render
+        Test rendering of a ntp.conf from template for a given distro
+        """
+
+        cfg = {'ntp': {}}
+        mycloud = self._get_cloud(distro)
+        distro_names = cc_ntp.generate_server_names(distro)
+
+        with mock.patch.object(templater, 'render_to_file') as mocktmpl:
+            with mock.patch.object(os.path, 'isfile', return_value=True):
+                with mock.patch.object(util, 'rename'):
+                    cc_ntp.write_ntp_config_template(cfg, mycloud, LOG)
+
+        mocktmpl.assert_called_once_with(
+            ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
+            '/etc/ntp.conf',
+            {'servers': [], 'pools': distro_names})
+
+    def test_ntp_conf_render_rhel(self):
+        """Test templater.render_to_file() for rhel"""
+        self.ntp_conf_render('rhel')
+
+    def test_ntp_conf_render_debian(self):
+        """Test templater.render_to_file() for debian"""
+        self.ntp_conf_render('debian')
+
+    def test_ntp_conf_render_fedora(self):
+        """Test templater.render_to_file() for fedora"""
+        self.ntp_conf_render('fedora')
+
+    def test_ntp_conf_render_sles(self):
+        """Test templater.render_to_file() for sles"""
+        self.ntp_conf_render('sles')
+
+    def test_ntp_conf_render_ubuntu(self):
+        """Test templater.render_to_file() for ubuntu"""
+        self.ntp_conf_render('ubuntu')
+
+    def test_ntp_conf_servers_no_pools(self):
+        distro = 'ubuntu'
+        pools = []
+        servers = ['192.168.2.1']
+        cfg = {
+            'ntp': {
+                'pools': pools,
+                'servers': servers,
+            }
+        }
+        mycloud = self._get_cloud(distro)
+
+        with mock.patch.object(templater, 'render_to_file') as mocktmpl:
+            with mock.patch.object(os.path, 'isfile', return_value=True):
+                with mock.patch.object(util, 'rename'):
+                    cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud,
+                                                     LOG)
+
+        mocktmpl.assert_called_once_with(
+            ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
+            '/etc/ntp.conf',
+            {'servers': servers, 'pools': pools})
+
+    def test_ntp_conf_custom_pools_no_server(self):
+        distro = 'ubuntu'
+        pools = ['0.mycompany.pool.ntp.org']
+        servers = []
+        cfg = {
+            'ntp': {
+                'pools': pools,
+                'servers': servers,
+            }
+        }
+        mycloud = self._get_cloud(distro)
+
+        with mock.patch.object(templater, 'render_to_file') as mocktmpl:
+            with mock.patch.object(os.path, 'isfile', return_value=True):
+                with mock.patch.object(util, 'rename'):
+                    cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud,
+                                                     LOG)
+
+        mocktmpl.assert_called_once_with(
+            ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
+            '/etc/ntp.conf',
+            {'servers': servers, 'pools': pools})
+
+    def test_ntp_conf_custom_pools_and_server(self):
+        distro = 'ubuntu'
+        pools = ['0.mycompany.pool.ntp.org']
+        servers = ['192.168.23.3']
+        cfg = {
+            'ntp': {
+                'pools': pools,
+                'servers': servers,
+            }
+        }
+        mycloud = self._get_cloud(distro)
+
+        with mock.patch.object(templater, 'render_to_file') as mocktmpl:
+            with mock.patch.object(os.path, 'isfile', return_value=True):
+                with mock.patch.object(util, 'rename'):
+                    cc_ntp.write_ntp_config_template(cfg.get('ntp'),
+                                                     mycloud, LOG)
+
+        mocktmpl.assert_called_once_with(
+            ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
+            '/etc/ntp.conf',
+            {'servers': servers, 'pools': pools})
+
+    def test_ntp_conf_contents_match(self):
+        """Test rendered contents of /etc/ntp.conf for ubuntu"""
+        pools = ['0.mycompany.pool.ntp.org']
+        servers = ['192.168.23.3']
+        cfg = {
+            'ntp': {
+                'pools': pools,
+                'servers': servers,
+            }
+        }
+        mycloud = self._get_cloud('ubuntu')
+        side_effect = [NTP_TEMPLATE.lstrip()]
+
+        # work backwards from util.write_file and mock out call path
+        # write_ntp_config_template()
+        #   cloud.get_template_filename()
+        #     os.path.isfile()
+        #   templater.render_to_file()
+        #     templater.render_from_file()
+        #         util.load_file()
+        #     util.write_file()
+        #
+        with mock.patch.object(util, 'write_file') as mockwrite:
+            with mock.patch.object(util, 'load_file', side_effect=side_effect):
+                with mock.patch.object(os.path, 'isfile', return_value=True):
+                    with mock.patch.object(util, 'rename'):
+                        cc_ntp.write_ntp_config_template(cfg.get('ntp'),
+                                                         mycloud, LOG)
+
+        mockwrite.assert_called_once_with(
+            '/etc/ntp.conf',
+            NTP_EXPECTED_UBUNTU,
+            mode=420)
+
+    def test_ntp_handler(self):
+        """Test ntp handler renders ubuntu ntp.conf template"""
+        pools = ['0.mycompany.pool.ntp.org']
+        servers = ['192.168.23.3']
+        cfg = {
+            'ntp': {
+                'pools': pools,
+                'servers': servers,
+            }
+        }
+        mycloud = self._get_cloud('ubuntu')
+        side_effect = [NTP_TEMPLATE.lstrip()]
+
+        with mock.patch.object(util, 'which', return_value=None):
+            with mock.patch.object(os.path, 'exists'):
+                with mock.patch.object(util, 'write_file') as mockwrite:
+                    with mock.patch.object(util, 'load_file',
+                                           side_effect=side_effect):
+                        with mock.patch.object(os.path, 'isfile',
+                                               return_value=True):
+                            with mock.patch.object(util, 'rename'):
+                                cc_ntp.handle("notimportant", cfg,
+                                              mycloud, LOG, None)
+
+        mockwrite.assert_called_once_with(
+            '/etc/ntp.conf',
+            NTP_EXPECTED_UBUNTU,
+            mode=420)
+
+    @mock.patch("cloudinit.config.cc_ntp.util")
+    def test_no_ntpcfg_does_nothing(self, mock_util):
+        cc = self._get_cloud('ubuntu')
+        cc.distro = mock.MagicMock()
+        cc_ntp.handle('cc_ntp', {}, cc, LOG, [])
+        self.assertFalse(cc.distro.install_packages.called)
+        self.assertFalse(mock_util.subp.called)