From 23f976be51ba9ad6e1e173f23c7220144beb942a Mon Sep 17 00:00:00 2001 From: Robert Schweikert Date: Tue, 14 Nov 2017 18:24:17 -0500 Subject: [PATCH 1/3] - Support chrony configuration (lp#1731619) + Add a template for chrony configuration + Add new set_timesync_client to distros base class - Set the timesync client provided in the config by the user with system_info: ntp_client - If no user config set the timesync client to one of the supported clients if the executable is installed - Fall back to the distribution default + Handle the new settings in cc_ntp while retaining current behavior as the fallback until all distro implementations have switched to the new implementation + Use new way of ntp client configuration for openSUSE and SLES + Unit tests --- cloudinit/config/cc_ntp.py | 59 +++++++++---- cloudinit/distros/__init__.py | 40 +++++++++ cloudinit/distros/arch.py | 4 + cloudinit/distros/debian.py | 4 + cloudinit/distros/freebsd.py | 4 + cloudinit/distros/gentoo.py | 4 + cloudinit/distros/opensuse.py | 41 +++++++++ cloudinit/distros/rhel.py | 4 + templates/chrony.conf.tmpl | 25 ++++++ tests/unittests/test_distros/test_generic.py | 101 +++++++++++++++++++++-- tests/unittests/test_distros/test_opensuse.py | 44 +++++++++- tests/unittests/test_distros/test_sles.py | 30 ++++++- tests/unittests/test_handler/test_handler_ntp.py | 80 ++++++++++++++---- 13 files changed, 400 insertions(+), 40 deletions(-) create mode 100644 templates/chrony.conf.tmpl diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index f50bcb35..2f662a9e 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -20,8 +20,9 @@ from textwrap import dedent LOG = logging.getLogger(__name__) frequency = PER_INSTANCE -NTP_CONF = '/etc/ntp.conf' -TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf' +CHRONY_CONF_FILE = '/etc/chrony.conf' +NTP_CONF_FILE = '/etc/ntp.conf' +TIMESYNCD_CONF_FILE = '/etc/systemd/timesyncd.conf.d/cloud-init.conf' NR_POOL_SERVERS = 4 distros = ['centos', 'debian', 'fedora', 'opensuse', 'sles', 'ubuntu'] @@ -110,26 +111,48 @@ def handle(name, cfg, cloud, log, _args): " but not a dictionary type," " is a %s %instead"), type_utils.obj_name(ntp_cfg)) - validate_cloudconfig_schema(cfg, schema) - if ntp_installable(): - service_name = 'ntp' - confpath = NTP_CONF - template_name = None - packages = ['ntp'] - check_exe = 'ntpd' + if ntp_cfg.get('enabled') and ntp_cfg.get('enabled') == 'true': + cloud.distro.set_timesync_client() else: - service_name = 'systemd-timesyncd' - confpath = TIMESYNCD_CONF - template_name = 'timesyncd.conf' - packages = [] - check_exe = '/lib/systemd/systemd-timesyncd' + # When all distro implementations are switched return here + pass - rename_ntp_conf() + validate_cloudconfig_schema(cfg, schema) + if hasattr(cloud.distro, 'timesync_client'): + client_name = cloud.distro.timesync_client + service_name = cloud.distro.timesync_service_name + if client_name == 'ntp': + confpath = NTP_CONF_FILE + template_name = 'ntp.conf.%s' % cloud.distro.name + elif client_name == 'systemd-timesyncd': + confpath = TIMESYNCD_CONF_FILE + template_name = 'timesyncd.conf' + elif client_name == 'chrony': + confpath = CHRONY_CONF_FILE + template_name = 'chrony.conf' + else: + if ntp_installable(): + service_name = 'ntp' + confpath = NTP_CONF_FILE + template_name = None + packages = ['ntp'] + check_exe = 'ntpd' + else: + service_name = 'systemd-timesyncd' + confpath = TIMESYNCD_CONF_FILE + template_name = 'timesyncd.conf' + packages = [] + check_exe = '/lib/systemd/systemd-timesyncd' + + rename_ntp_conf(confpath) # ensure when ntp is installed it has a configuration file # to use instead of starting up with packaged defaults write_ntp_config_template(ntp_cfg, cloud, confpath, template=template_name) - install_ntp(cloud.distro.install_packages, packages=packages, - check_exe=check_exe) + if not hasattr(cloud.distro, 'timesync_client'): + # Updated implementation installs a package is missing in + # distro._set_default_timesync_client + install_ntp(cloud.distro.install_packages, packages=packages, + check_exe=check_exe) try: reload_ntp(service_name, systemd=cloud.distro.uses_systemd()) @@ -167,7 +190,7 @@ def install_ntp(install_func, packages=None, check_exe="ntpd"): def rename_ntp_conf(config=None): """Rename any existing ntp.conf file""" if config is None: # For testing - config = NTP_CONF + config = NTP_CONF_FILE if os.path.exists(config): util.rename(config, config + ".dist") diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 99e60e7a..41ae097d 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -57,6 +57,9 @@ class Distro(object): init_cmd = ['service'] # systemctl, service etc renderer_configs = {} + __timesync_client_map = {} + __ntp_client_execs = [] + def __init__(self, name, cfg, paths): self._paths = paths self._cfg = cfg @@ -86,6 +89,43 @@ class Distro(object): renderer.render_network_config(network_config=network_config) return [] + def set_timesync_client(self): + system_info = self._cfg.get('system_info') + if system_info and isinstance(system_info, (dict)): + ntp_client = system_info.get('ntp_client') + if ntp_client and ntp_client in self.__timesync_client_map: + self.timesync_client, self.timesync_service_name = \ + self.__timesync_client_map.get(ntp_client) + LOG.debug('Using "%s" for timesync client per configuration', + ntp_client) + return + + found = False + for ntp_client in self.__ntp_client_execs: + ntp_exec = util.which(ntp_client) + if ntp_exec and not found: + found = ntp_client + # systemd-timesyncd is part of systemd and thus is probably + # always installed, do not consider it as a conflict + elif ntp_exec and found and 'systemd-timesyncd' not in ntp_exec: + msg = 'Found multiple timesync clients installed. Resolve ' + msg += 'ambigutity by falling back to distro default' + LOG.debug(msg) + found = False + break + + if found and found in self.__timesync_client_map: + self.timesync_client, self.timesync_service_name = \ + self.__timesync_client_map.get(found) + LOG.debug('Using "%s" for timesync based on installed exec', + ntp_client) + return + + self._set_default_timesync_client() + + def _set_default_timesync_client(self): + raise NotImplementedError() + def _find_tz_file(self, tz): tz_file = os.path.join(self.tz_zone_dir, str(tz)) if not os.path.isfile(tz_file): diff --git a/cloudinit/distros/arch.py b/cloudinit/distros/arch.py index f87a3432..fffc1c9c 100644 --- a/cloudinit/distros/arch.py +++ b/cloudinit/distros/arch.py @@ -153,6 +153,10 @@ class Distro(distros.Distro): self._runner.run("update-sources", self.package_command, ["-y"], freq=PER_INSTANCE) + def _set_default_timesync_client(self): + # Fall back to previous implementation + return + def _render_network(entries, target="/", conf_dir="etc/netctl", resolv_conf="etc/resolv.conf", enable_func=None): diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 33cc0bf1..46dd4173 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -212,6 +212,10 @@ class Distro(distros.Distro): (arch, _err) = util.subp(['dpkg', '--print-architecture']) return str(arch).strip() + def _set_default_timesync_client(self): + # Fall back to previous implementation + return + def _get_wrapper_prefix(cmd, mode): if isinstance(cmd, str): diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py index bad112fe..00b38917 100644 --- a/cloudinit/distros/freebsd.py +++ b/cloudinit/distros/freebsd.py @@ -649,4 +649,8 @@ class Distro(distros.Distro): self._runner.run("update-sources", self.package_command, ["update"], freq=PER_INSTANCE) + def _set_default_timesync_client(self): + # Fall back to previous implementation + return + # vi: ts=4 expandtab diff --git a/cloudinit/distros/gentoo.py b/cloudinit/distros/gentoo.py index dc57717d..5685b058 100644 --- a/cloudinit/distros/gentoo.py +++ b/cloudinit/distros/gentoo.py @@ -214,6 +214,10 @@ class Distro(distros.Distro): self._runner.run("update-sources", self.package_command, ["-u", "world"], freq=PER_INSTANCE) + def _set_default_timesync_client(self): + # Fall back to previous implementation + return + def convert_resolv_conf(settings): """Returns a settings string formatted for resolv.conf.""" diff --git a/cloudinit/distros/opensuse.py b/cloudinit/distros/opensuse.py index a219e9fb..092d6a11 100644 --- a/cloudinit/distros/opensuse.py +++ b/cloudinit/distros/opensuse.py @@ -8,6 +8,8 @@ # # This file is part of cloud-init. See LICENSE file for license information. +import platform + from cloudinit import distros from cloudinit.distros.parsers.hostname import HostnameConf @@ -36,6 +38,23 @@ class Distro(distros.Distro): systemd_locale_conf_fn = '/etc/locale.conf' tz_local_fn = '/etc/localtime' + __timesync_client_map = { + # Map the system_info supported values + 'chrony': ('chrony', 'chronyd'), + 'isc-ntp': ('ntp', 'ntpd'), + 'systemd-timesyncd': ('systemd-timesyncd', 'systemd-timesyncd'), + # Map the common names if different from system_info + 'chronyd': ('chrony', 'chronyd'), + 'ntpd': ('ntp', 'ntpd'), + '/usr/lib/systemd/systemd-timesyncd': + ('systemd-timesyncd', 'systemd-timesyncd') + } + __ntp_client_execs = [ + 'chronyd', + 'ntpd', + '/usr/lib/systemd/systemd-timesyncd' + ] + def __init__(self, name, cfg, paths): distros.Distro.__init__(self, name, cfg, paths) self._runner = helpers.Runners(paths) @@ -145,6 +164,28 @@ class Distro(distros.Distro): host_fn = self.hostname_conf_fn return (host_fn, self._read_hostname(host_fn)) + def _set_default_timesync_client(self): + """The default timesync client is dependent on the distribution.""" + # When we get here the user has configured ntp to be enabled but + # no client is installed + distro_info = platform.linux_distribution() + name = distro_info[0] + major_ver = int(distro_info[1].split('.')[0]) + + # This is horribly complicated because of a case of + # "we do not care if versions should be increasing syndrome" + if ( + (major_ver >= 15 and 'openSUSE' not in name) or + (major_ver >= 15 and 'openSUSE' in name and major_ver != 42) + ): + self.timesync_client = 'chrony' + self.timesync_service_name = 'chronyd' + self.install_packages(['chrony']) + else: + self.timesync_client = 'ntp' + self.timesync_service_name = 'ntpd' + self.install_packages(['ntp']) + def _write_hostname(self, hostname, out_fn): if self.uses_systemd() and out_fn.endswith('/previous-hostname'): util.write_file(out_fn, hostname) diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index 1fecb619..6d9c9f67 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -218,4 +218,8 @@ class Distro(distros.Distro): self._runner.run("update-sources", self.package_command, ["makecache"], freq=PER_INSTANCE) + def _set_default_timesync_client(self): + # Fall back to previous implementation + return + # vi: ts=4 expandtab diff --git a/templates/chrony.conf.tmpl b/templates/chrony.conf.tmpl new file mode 100644 index 00000000..38e84d85 --- /dev/null +++ b/templates/chrony.conf.tmpl @@ -0,0 +1,25 @@ +## template:jinja +# cloud-init generated file +# See chrony.conf(5) + +{% if pools %}# pools +{% endif %} +{% for pool in pools -%} +pool {{pool}} iburst +{% endfor %} +{%- if servers %}# servers +{% endif %} +{% for server in servers -%} +server {{server}} iburst +{% endfor %} + +# Record the rate at which the the system clock gains/losses time +driftfile /var/lib/chrony/drift + +# Allow the system clock to be stepped in the first three updates +# if its offset is larger than 1 second. +makestep 1.0 3 + +# Enable kernel synchronization of the real-time clock (RTC). +rtcsync + diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py index 791fe612..cdee4b1b 100644 --- a/tests/unittests/test_distros/test_generic.py +++ b/tests/unittests/test_distros/test_generic.py @@ -4,16 +4,12 @@ from cloudinit import distros from cloudinit import util from cloudinit.tests import helpers +from cloudinit.tests.helpers import mock import os import shutil import tempfile -try: - from unittest import mock -except ImportError: - import mock - unknown_arch_info = { 'arches': ['default'], 'failsafe': {'primary': 'http://fs-primary-default', @@ -35,6 +31,24 @@ package_mirrors = [ unknown_arch_info ] +timesync_user_cfg_chrony = { + 'system_info': { + 'ntp_client': 'chrony' + } +} + +timesync_user_cfg_ntp = { + 'system_info': { + 'ntp_client': 'isc-ntp' + } +} + +timesync_user_cfg_systemd = { + 'system_info': { + 'ntp_client': 'systemd-timesyncd' + } +} + gpmi = distros._get_package_mirror_info gapmi = distros._get_arch_package_mirror_info @@ -244,5 +258,82 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase): with self.assertRaises(NotImplementedError): d.get_locale() + def test_set_timesync_client_user_config_chrony_sles(self): + """Test sles distro sets proper values for chrony""" + cls = distros.fetch("sles") + d = cls("sles", timesync_user_cfg_chrony, None) + d.set_timesync_client() + self.assertEqual(d.timesync_client, 'chrony') + self.assertEqual(d.timesync_service_name, 'chronyd') + + def test_set_timesync_client_user_config_ntp_sles(self): + """Test sles distro sets proper values for ntp""" + cls = distros.fetch("sles") + d = cls("sles", timesync_user_cfg_ntp, None) + d.set_timesync_client() + self.assertEqual(d.timesync_client, 'ntp') + self.assertEqual(d.timesync_service_name, 'ntpd') + + def test_set_timesync_client_user_config_timesyncd_sles(self): + """Test sles distro sets proper values for timesyncd""" + cls = distros.fetch("sles") + d = cls("sles", timesync_user_cfg_systemd, None) + d.set_timesync_client() + self.assertEqual(d.timesync_client, 'systemd-timesyncd') + self.assertEqual(d.timesync_service_name, 'systemd-timesyncd') + + @mock.patch("cloudinit.distros.util") + def test_set_timesync_client_chrony_installed_sles(self, mock_util): + """Test sles distro sets proper values for chrony if chrony is + installed""" + mock_util.which.side_effect = side_effect_client_is_chrony + cls = distros.fetch("sles") + d = cls("sles", {}, None) + d.set_timesync_client() + self.assertEqual(d.timesync_client, 'chrony') + self.assertEqual(d.timesync_service_name, 'chronyd') + + @mock.patch("cloudinit.distros.util") + def test_set_timesync_client_ntp_installed_sles(self, mock_util): + """Test sles distro sets proper values for ntp if ntpd is + installed""" + mock_util.which.side_effect = side_effect_client_is_ntp + cls = distros.fetch("sles") + d = cls("sles", {}, None) + d.set_timesync_client() + self.assertEqual(d.timesync_client, 'ntp') + self.assertEqual(d.timesync_service_name, 'ntpd') + + @mock.patch("cloudinit.distros.util") + def test_set_timesync_client_timesycd_installed_sles(self, mock_util): + """Test sles distro sets proper values for timesycd if timesyncd is + installed""" + mock_util.which.side_effect = side_effect_client_is_timesyncd + cls = distros.fetch("sles") + d = cls("sles", {}, None) + d.set_timesync_client() + self.assertEqual(d.timesync_client, 'systemd-timesyncd') + self.assertEqual(d.timesync_service_name, 'systemd-timesyncd') + + +def side_effect_client_is_chrony(ntp_client): + if 'chrony' in ntp_client: + return '/usr/sbin/chronyd' + else: + return False + + +def side_effect_client_is_ntp(ntp_client): + if 'ntp' in ntp_client: + return '/usr/sbin/ntpd' + else: + return False + + +def side_effect_client_is_timesyncd(ntp_client): + if 'timesyncd' in ntp_client: + return ntp_client + else: + return False # vi: ts=4 expandtab diff --git a/tests/unittests/test_distros/test_opensuse.py b/tests/unittests/test_distros/test_opensuse.py index b9bb9b3e..9ed10af8 100644 --- a/tests/unittests/test_distros/test_opensuse.py +++ b/tests/unittests/test_distros/test_opensuse.py @@ -1,6 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests.helpers import CiTestCase +from cloudinit.tests.helpers import CiTestCase, mock from . import _get_distro @@ -10,3 +10,45 @@ class TestopenSUSE(CiTestCase): def test_get_distro(self): distro = _get_distro("opensuse") self.assertEqual(distro.osfamily, 'suse') + + @mock.patch("cloudinit.distros.opensuse.Distro.install_packages") + @mock.patch("platform.linux_distribution") + def test_set_default_timesync_client_osl42( + self, + mock_distro, + mock_install + ): + mock_distro.return_value = ('openSUSE ', '42.3', 'x86_64') + mock_install.return_value = True + distro = _get_distro("opensuse") + distro._set_default_timesync_client() + self.assertEqual(distro.timesync_client, 'ntp') + self.assertEqual(distro.timesync_service_name, 'ntpd') + + @mock.patch("cloudinit.distros.opensuse.Distro.install_packages") + @mock.patch("platform.linux_distribution") + def test_set_default_timesync_client_os13( + self, + mock_distro, + mock_install + ): + mock_distro.return_value = ('openSUSE ', '13.1', 'x86_64') + mock_install.return_value = True + distro = _get_distro("opensuse") + distro._set_default_timesync_client() + self.assertEqual(distro.timesync_client, 'ntp') + self.assertEqual(distro.timesync_service_name, 'ntpd') + + @mock.patch("cloudinit.distros.opensuse.Distro.install_packages") + @mock.patch("platform.linux_distribution") + def test_set_default_timesync_client_osl15( + self, + mock_distro, + mock_install + ): + mock_distro.return_value = ('openSUSE ', '15.1', 'x86_64') + mock_install.return_value = True + distro = _get_distro("opensuse") + distro._set_default_timesync_client() + self.assertEqual(distro.timesync_client, 'chrony') + self.assertEqual(distro.timesync_service_name, 'chronyd') diff --git a/tests/unittests/test_distros/test_sles.py b/tests/unittests/test_distros/test_sles.py index 33e3c457..13237a27 100644 --- a/tests/unittests/test_distros/test_sles.py +++ b/tests/unittests/test_distros/test_sles.py @@ -1,6 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests.helpers import CiTestCase +from cloudinit.tests.helpers import CiTestCase, mock from . import _get_distro @@ -10,3 +10,31 @@ class TestSLES(CiTestCase): def test_get_distro(self): distro = _get_distro("sles") self.assertEqual(distro.osfamily, 'suse') + + @mock.patch("cloudinit.distros.opensuse.Distro.install_packages") + @mock.patch("platform.linux_distribution") + def test_set_default_timesync_client_osl42( + self, + mock_distro, + mock_install + ): + mock_distro.return_value = ('SLES ', '12.3', 'x86_64') + mock_install.return_value = True + distro = _get_distro("sles") + distro._set_default_timesync_client() + self.assertEqual(distro.timesync_client, 'ntp') + self.assertEqual(distro.timesync_service_name, 'ntpd') + + @mock.patch("cloudinit.distros.opensuse.Distro.install_packages") + @mock.patch("platform.linux_distribution") + def test_set_default_timesync_client_os13( + self, + mock_distro, + mock_install + ): + mock_distro.return_value = ('SLES ', '15', 'x86_64') + mock_install.return_value = True + distro = _get_distro("sles") + distro._set_default_timesync_client() + self.assertEqual(distro.timesync_client, 'chrony') + self.assertEqual(distro.timesync_service_name, 'chronyd') diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py index 28a8455d..33fab8c8 100644 --- a/tests/unittests/test_handler/test_handler_ntp.py +++ b/tests/unittests/test_handler/test_handler_ntp.py @@ -10,6 +10,20 @@ import os from os.path import dirname import shutil +CHRONY_TEMPLATE = b"""\ +## 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_TEMPLATE = b"""\ ## template: jinja servers {{servers}} @@ -79,7 +93,7 @@ class TestNtp(FilesystemMockingTestCase): """When NTP_CONF exists, rename_ntp moves it.""" ntpconf = self.tmp_path("ntp.conf", self.new_root) util.write_file(ntpconf, "") - with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf): + with mock.patch("cloudinit.config.cc_ntp.NTP_CONF_FILE", ntpconf): cc_ntp.rename_ntp_conf() self.assertFalse(os.path.exists(ntpconf)) self.assertTrue(os.path.exists("{0}.dist".format(ntpconf))) @@ -112,7 +126,7 @@ class TestNtp(FilesystemMockingTestCase): """When NTP_CONF doesn't exist rename_ntp doesn't create a file.""" ntpconf = self.tmp_path("ntp.conf", self.new_root) self.assertFalse(os.path.exists(ntpconf)) - with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf): + with mock.patch("cloudinit.config.cc_ntp.NTP_CONF_FILE", ntpconf): cc_ntp.rename_ntp_conf() self.assertFalse(os.path.exists("{0}.dist".format(ntpconf))) self.assertFalse(os.path.exists(ntpconf)) @@ -133,7 +147,7 @@ class TestNtp(FilesystemMockingTestCase): # Create ntp.conf.tmpl with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.write_ntp_config_template(cfg, mycloud, ntp_conf) content = util.read_file_or_url('file://' + ntp_conf).contents self.assertEqual( @@ -159,7 +173,7 @@ class TestNtp(FilesystemMockingTestCase): # Create ntp.conf.tmpl. with open('{0}.{1}.tmpl'.format(ntp_conf, distro), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.write_ntp_config_template(cfg, mycloud, ntp_conf) content = util.read_file_or_url('file://' + ntp_conf).contents self.assertEqual( @@ -178,7 +192,7 @@ class TestNtp(FilesystemMockingTestCase): # Create ntp.conf.tmpl with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf) content = util.read_file_or_url('file://' + ntp_conf).contents default_pools = [ @@ -210,7 +224,7 @@ class TestNtp(FilesystemMockingTestCase): # Create ntp.conf.tmpl with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): with mock.patch.object(util, 'which', return_value=None): cc_ntp.handle('notimportant', cfg, mycloud, None, None) @@ -239,7 +253,10 @@ class TestNtp(FilesystemMockingTestCase): with open(template, 'wb') as stream: stream.write(TIMESYNCD_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.TIMESYNCD_CONF', tsyncd_conf): + with mock.patch( + 'cloudinit.config.cc_ntp.TIMESYNCD_CONF_FILE', + tsyncd_conf + ): cc_ntp.handle('notimportant', cfg, mycloud, None, None) content = util.read_file_or_url('file://' + tsyncd_conf).contents @@ -267,7 +284,7 @@ class TestNtp(FilesystemMockingTestCase): shutil.copy( tmpl_file, os.path.join(self.new_root, 'ntp.conf.%s.tmpl' % distro)) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): with mock.patch.object(util, 'which', return_value=[True]): cc_ntp.handle('notimportant', cfg, mycloud, None, None) @@ -300,7 +317,7 @@ class TestNtp(FilesystemMockingTestCase): with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) for valid_empty_config in valid_empty_configs: - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.handle('cc_ntp', valid_empty_config, cc, None, []) with open(ntp_conf) as stream: content = stream.read() @@ -323,7 +340,7 @@ class TestNtp(FilesystemMockingTestCase): ntp_conf = os.path.join(self.new_root, 'ntp.conf') with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) self.assertIn( "Invalid config:\nntp.pools.0: 123 is not of type 'string'\n" @@ -344,7 +361,7 @@ class TestNtp(FilesystemMockingTestCase): ntp_conf = os.path.join(self.new_root, 'ntp.conf') with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) self.assertIn( "Invalid config:\nntp.pools: 123 is not of type 'array'\n" @@ -366,7 +383,7 @@ class TestNtp(FilesystemMockingTestCase): ntp_conf = os.path.join(self.new_root, 'ntp.conf') with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) self.assertIn( "Invalid config:\nntp: Additional properties are not allowed " @@ -391,7 +408,7 @@ class TestNtp(FilesystemMockingTestCase): ntp_conf = os.path.join(self.new_root, 'ntp.conf') with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) self.assertIn( "Invalid config:\nntp.pools: ['0.mypool.org', '0.mypool.org'] has " @@ -421,7 +438,10 @@ class TestNtp(FilesystemMockingTestCase): print(template) with open(template, 'wb') as stream: stream.write(TIMESYNCD_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.TIMESYNCD_CONF', tsyncd_conf): + with mock.patch( + 'cloudinit.config.cc_ntp.TIMESYNCD_CONF_FILE', + tsyncd_conf + ): cc_ntp.write_ntp_config_template(cfg, mycloud, tsyncd_conf, template='timesyncd.conf') @@ -442,7 +462,7 @@ class TestNtp(FilesystemMockingTestCase): # Create ntp.conf.tmpl with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf): cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf) content = util.read_file_or_url('file://' + ntp_conf).contents default_pools = [ @@ -456,5 +476,35 @@ class TestNtp(FilesystemMockingTestCase): ",".join(default_pools)), self.logs.getvalue()) + def test_ntp_handler_chrony(self): + """Test ntp handler configures chrony""" + distro = 'opensuse' + cfg = { + 'servers': ['192.168.2.1', '192.168.2.2'], + 'pools': ['0.mypool.org'], + } + mycloud = self._get_cloud(distro) + mycloud.timesync_client = 'chrony' + mycloud.timesync_service_name = 'chronyd' + chrony_conf = self.tmp_path("chrony.conf", self.new_root) + # Create chrony.conf.tmpl + template = '{0}.tmpl'.format(chrony_conf) + print(template) + with open(template, 'wb') as stream: + stream.write(CHRONY_TEMPLATE) + with mock.patch( + 'cloudinit.config.cc_ntp.CHRONY_CONF_FILE', + chrony_conf + ): + cc_ntp.write_ntp_config_template(cfg, mycloud, chrony_conf, + template='chrony.conf') + + content = util.read_file_or_url('file://' + chrony_conf).contents + expected = '# pools\n' + expected += 'pool 0.mypool.org iburst\n' + expected += '# servers\n' + expected += 'server 192.168.2.1 iburst\n' + expected += 'server 192.168.2.2 iburst\n\n' + self.assertEqual(expected, content.decode()) # vi: ts=4 expandtab -- 2.13.6