Commit b4559024 authored by Ryan Harper's avatar Ryan Harper Committed by Scott Moser

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).
parent c52b8eb9
# vi: ts=4 expandtab
#
# Copyright (C) 2016 Canonical Ltd.
#
# Author: Ryan Harper <ryan.harper@canonical.com>
#
# 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/>.
from cloudinit import log as logging
from cloudinit.settings import PER_INSTANCE
from cloudinit import templater
from cloudinit import type_utils
from cloudinit import util
import os
LOG = logging.getLogger(__name__)
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.debug("Skipping module named %s,"
"not present or disabled by cfg", name)
return True
install_ntp(cloud.distro.install_packages, packages=['ntp'],
check_exe="ntpd")
rename_ntp_conf()
write_ntp_config_template(ntp_cfg, cloud)
def install_ntp(install_func, packages=None, check_exe="ntpd"):
if util.which(check_exe):
return
if packages is None:
packages = ['ntp']
install_func(packages)
def rename_ntp_conf(config=NTP_CONF):
if os.path.exists(config):
util.rename(config, config + ".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):
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:
raise RuntimeError(("No template found, "
"not rendering %s"), NTP_CONF)
templater.render_to_file(template_fn, NTP_CONF, params)
......@@ -45,6 +45,7 @@ cloud_config_modules:
- emit_upstart
- disk_setup
- mounts
- ntp
- ssh-import-id
- locale
- set-passwords
......
#cloud-config
# ntp: configure ntp services
# 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']
## 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
## 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
## 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
## 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@suse.de>
## Michael Skibbe, <mskibbe@suse.de>
##
################################################################################
##
## 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
## 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
from cloudinit.config import cc_ntp
from cloudinit.sources import DataSourceNone
from cloudinit import templater
from cloudinit import (distros, helpers, cloud, util)
from ..helpers import FilesystemMockingTestCase, mock
import logging
import os
import shutil
import tempfile
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(FilesystemMockingTestCase):
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
install_func = mock.MagicMock()
cc_ntp.install_ntp(install_func, packages=['ntpx'], check_exe='ntpdx')
self.assertTrue(install_func.called)
mock_util.which.assert_called_with('ntpdx')
install_pkg = install_func.call_args_list[0][0][0]
self.assertEqual(sorted(install_pkg), ['ntpx'])
@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)
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)
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)
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)
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)