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 --- cloudinit/config/cc_ntp.py.orig +++ 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'] @@ -49,6 +50,7 @@ schema = { 'examples': [ dedent("""\ ntp: + enabled: true pools: [0.int.pool.ntp.org, 1.int.pool.ntp.org, ntp.myorg.org] servers: - ntp.server.local @@ -60,6 +62,9 @@ schema = { 'ntp': { 'type': ['object', 'null'], 'properties': { + 'enabled': { + "type": "boolean" + }, 'pools': { 'type': 'array', 'items': { @@ -110,26 +115,48 @@ def handle(name, cfg, cloud, log, _args) "'ntp' key existed in config, but not a dictionary type," " is a {_type} instead".format(_type=type_utils.obj_name(ntp_cfg))) + if ntp_cfg.get('enabled'): + cloud.distro.set_timesync_client() + else: + # When all distro implementations are switched return here + pass + validate_cloudconfig_schema(cfg, schema) - if ntp_installable(): - service_name = 'ntp' - confpath = NTP_CONF - template_name = None - packages = ['ntp'] - check_exe = 'ntpd' + 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.%s' % cloud.distro.name else: - service_name = 'systemd-timesyncd' - confpath = TIMESYNCD_CONF - template_name = 'timesyncd.conf' - packages = [] - check_exe = '/lib/systemd/systemd-timesyncd' + 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() + 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 +194,7 @@ def install_ntp(install_func, packages=N 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") --- cloudinit/distros/__init__.py.orig +++ cloudinit/distros/__init__.py @@ -61,6 +61,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 @@ -90,6 +93,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): --- /dev/null +++ templates/chrony.conf.tmpl @@ -0,0 +1,24 @@ +## 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 --- cloudinit/distros/opensuse.py.orig +++ cloudinit/distros/opensuse.py @@ -36,6 +36,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 +162,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 = util.get_linux_distro() + 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)