Merge ~rjschwei/cloud-init:chrony into cloud-init:master

Proposed by Robert Schweikert on 2017-12-08
Status: Needs review
Proposed branch: ~rjschwei/cloud-init:chrony
Merge into: cloud-init:master
Diff against target: 812 lines (+439/-40) (has conflicts)
14 files modified
cloudinit/config/cc_ntp.py (+51/-18)
cloudinit/distros/__init__.py (+40/-0)
cloudinit/distros/arch.py (+4/-0)
cloudinit/distros/debian.py (+4/-0)
cloudinit/distros/freebsd.py (+4/-0)
cloudinit/distros/gentoo.py (+4/-0)
cloudinit/distros/opensuse.py (+45/-0)
cloudinit/distros/rhel.py (+4/-0)
templates/chrony.conf.opensuse.tmpl (+25/-0)
templates/chrony.conf.sles.tmpl (+25/-0)
tests/unittests/test_distros/test_generic.py (+96/-5)
tests/unittests/test_distros/test_opensuse.py (+43/-1)
tests/unittests/test_distros/test_sles.py (+29/-1)
tests/unittests/test_handler/test_handler_ntp.py (+65/-15)
Conflict in cloudinit/config/cc_ntp.py
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing on 2017-12-18
Ryan Harper 2017-12-08 Needs Fixing on 2017-12-18
Review via email: mp+334992@code.launchpad.net

Description of the Change

 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

Follows along the ideas discussed here: https://hackmd.io/CwEwHARgbAzDDsBaArPOjhQMYEZEEMBOHQjQkeAUxgCYb9h8og==

To post a comment you must log in.

FAILED: Continuous integration, rev:23f976be51ba9ad6e1e173f23c7220144beb942a
https://jenkins.ubuntu.com/server/job/cloud-init-ci/603/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/603/rebuild

review: Needs Fixing (continuous-integration)
~rjschwei/cloud-init:chrony updated on 2017-12-08
d94392b... by Robert Schweikert on 2017-12-08

- Disable method deprecation warning for pylint

PASSED: Continuous integration, rev:d94392bb6e54a6860c8b6ea7967e853d8e263d7a
https://jenkins.ubuntu.com/server/job/cloud-init-ci/605/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/605/rebuild

review: Approve (continuous-integration)
Ryan Harper (raharper) wrote :

Thanks for getting this started. For reference more of these changes were
discussed here: https://hackmd.io/s/r1qN6HNbz

I'd like to drop the bulk of the logic in cc_ntp and push those into either the base distro class or specific distro classes as needed. cc_ntp would look much simpler, basically, checking the configuration dictionary/validate-schema and call into the distro with the config.

  def handle(name, cfg, cloud, log, _args)
     # check for 'ntp' in config and bail if not present
     valid_cloudconfig_schema(cfg, schema)
     distro.configure_ntp(cfg)

We won't need to check for attributes, introduce the configure_ntp as part of the base
class were we handle any cross-distro actions and then call into per-distro methods

     DistroBase

       def configure_ntp(cfg)
         client_name = _select_ntp_client(cfg) # abstract in base class
         _write_ntp_config(client_name, cfg)
         _restart_ntp_service(client_name, cfg)

The _select_ntp_client() is where each distro will examine the
ntp_cfg, systemcfg and state to determine the correct client.

The rest I think can be common given the client selection and configuration
since we're rendering configs with templates (or the config format is common
like in systemd-timesyncd).

The schema/example configuration needs an update, specifically we need to reference that
we not have:
  ntp:
    enabled: true

To allow users to explicitly enable/disable, specifically for the case of using the
distro default ntp configuration.

review: Needs Fixing
Robert Schweikert (rjschwei) wrote :

> Thanks for getting this started. For reference more of these changes were
> discussed here: https://hackmd.io/s/r1qN6HNbz
>
> I'd like to drop the bulk of the logic in cc_ntp and push those into either
> the base distro class or specific distro classes as needed. cc_ntp would look
> much simpler, basically, checking the configuration dictionary/validate-schema
> and call into the distro with the config.
>
> def handle(name, cfg, cloud, log, _args)
> # check for 'ntp' in config and bail if not present
> valid_cloudconfig_schema(cfg, schema)
> distro.configure_ntp(cfg)
>
> We won't need to check for attributes, introduce the configure_ntp as part of
> the base
> class were we handle any cross-distro actions and then call into per-distro
> methods
>
> DistroBase
>
> def configure_ntp(cfg)
> client_name = _select_ntp_client(cfg) # abstract in base class
> _write_ntp_config(client_name, cfg)
> _restart_ntp_service(client_name, cfg)
>
> The _select_ntp_client() is where each distro will examine the
> ntp_cfg, systemcfg and state to determine the correct client.
>
> The rest I think can be common given the client selection and configuration
> since we're rendering configs with templates (or the config format is common
> like in systemd-timesyncd).
>
>
> The schema/example configuration needs an update, specifically we need to
> reference that
> we not have:
> ntp:
> enabled: true
>
> To allow users to explicitly enable/disable, specifically for the case of
> using the
> distro default ntp configuration.

Two concerns here with doing all of this at once, although I do have to agree that we'll probably end up with cleaner code earlier.

1.) Doing all of this at once potentially causes lots of upheaval as the core logic of cc_ntp would change. Unless there is some other mechanism, other than checking attributes to split the code path between old behavior and new behavior.

2.) Config compatibility, if we enforce

ntp:
    enabled: true

which I think that is being suggested then basically we are making an incompatible config file change, meaning any one who has

ntp:
  .....

in their config at this point needs to add "enabled: true"

That's fair enough, just want to make sure everyone is on the same page.

Ryan Harper (raharper) wrote :
Download full text (3.5 KiB)

On Mon, Dec 18, 2017 at 12:50 PM, Robert Schweikert <email address hidden>
wrote:

> > Thanks for getting this started. For reference more of these changes
> were
> > discussed here: https://hackmd.io/s/r1qN6HNbz
> >
> > I'd like to drop the bulk of the logic in cc_ntp and push those into
> either
> > the base distro class or specific distro classes as needed. cc_ntp
> would look
> > much simpler, basically, checking the configuration
> dictionary/validate-schema
> > and call into the distro with the config.
> >
> > def handle(name, cfg, cloud, log, _args)
> > # check for 'ntp' in config and bail if not present
> > valid_cloudconfig_schema(cfg, schema)
> > distro.configure_ntp(cfg)
> >
> > We won't need to check for attributes, introduce the configure_ntp as
> part of
> > the base
> > class were we handle any cross-distro actions and then call into
> per-distro
> > methods
> >
> > DistroBase
> >
> > def configure_ntp(cfg)
> > client_name = _select_ntp_client(cfg) # abstract in base class
> > _write_ntp_config(client_name, cfg)
> > _restart_ntp_service(client_name, cfg)
> >
> > The _select_ntp_client() is where each distro will examine the
> > ntp_cfg, systemcfg and state to determine the correct client.
> >
> > The rest I think can be common given the client selection and
> configuration
> > since we're rendering configs with templates (or the config format is
> common
> > like in systemd-timesyncd).
> >
> >
> > The schema/example configuration needs an update, specifically we need to
> > reference that
> > we not have:
> > ntp:
> > enabled: true
> >
> > To allow users to explicitly enable/disable, specifically for the case of
> > using the
> > distro default ntp configuration.
>
> Two concerns here with doing all of this at once, although I do have to
> agree that we'll probably end up with cleaner code earlier.
>
> 1.) Doing all of this at once potentially causes lots of upheaval as the
> core logic of cc_ntp would change. Unless there is some other mechanism,
> other than checking attributes to split the code path between old behavior
> and new behavior.
>

The "old" path is maintained by including a patch to configure the "legacy"
client name during packaging. Even if the code moves from cc_ntp into
distro; the input/output remains the same, and can be exercised with
a system config that specifies the legacy ntp_client value.

That is, the existing unittests which take the config dictionary of
servers/pools would also need a system_config which sets ntp_client =
'isc-ntp' and validates the 'old behavior'.
We'd also add new tests which leave system_config unset (using code
defaults, or set system config to other values) and will validate the new
behavior.

> 2.) Config compatibility, if we enforce
>
> ntp:
> enabled: true
>
> which I think that is being suggested then basically we are making an
> incompatible config file change, meaning any one who has
>
> ntp:
> .....
>
> in their config at this point needs to add "enabled: true"
>

This will need compat patches included in previous release updates to
maintain behavior (and likely emitting a warning).
On master, such a...

Read more...

~rjschwei/cloud-init:chrony updated on 2017-12-18
42cb184... by Robert Schweikert on 2017-12-18

- Distro dependent chrony config file
  + We all like to stor ethe drift file in different places and name it
    differently :(

FAILED: Continuous integration, rev:42cb1841035befa5b5823b3321c8fe92f2cb9087
https://jenkins.ubuntu.com/server/job/cloud-init-ci/641/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/641/rebuild

review: Needs Fixing (continuous-integration)

Unmerged commits

42cb184... by Robert Schweikert on 2017-12-18

- Distro dependent chrony config file
  + We all like to stor ethe drift file in different places and name it
    differently :(

d94392b... by Robert Schweikert on 2017-12-08

- Disable method deprecation warning for pylint

23f976b... by Robert Schweikert on 2017-11-14

- 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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
2index cbd0237..747b6f2 100644
3--- a/cloudinit/config/cc_ntp.py
4+++ b/cloudinit/config/cc_ntp.py
5@@ -20,8 +20,9 @@ from textwrap import dedent
6 LOG = logging.getLogger(__name__)
7
8 frequency = PER_INSTANCE
9-NTP_CONF = '/etc/ntp.conf'
10-TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf'
11+CHRONY_CONF_FILE = '/etc/chrony.conf'
12+NTP_CONF_FILE = '/etc/ntp.conf'
13+TIMESYNCD_CONF_FILE = '/etc/systemd/timesyncd.conf.d/cloud-init.conf'
14 NR_POOL_SERVERS = 4
15 distros = ['centos', 'debian', 'fedora', 'opensuse', 'sles', 'ubuntu']
16
17@@ -49,6 +50,7 @@ schema = {
18 'examples': [
19 dedent("""\
20 ntp:
21+ enabled: true
22 pools: [0.int.pool.ntp.org, 1.int.pool.ntp.org, ntp.myorg.org]
23 servers:
24 - ntp.server.local
25@@ -60,6 +62,9 @@ schema = {
26 'ntp': {
27 'type': ['object', 'null'],
28 'properties': {
29+ 'enabled': {
30+ "type": "boolean"
31+ },
32 'pools': {
33 'type': 'array',
34 'items': {
35@@ -106,30 +111,58 @@ def handle(name, cfg, cloud, log, _args):
36
37 # TODO drop this when validate_cloudconfig_schema is strict=True
38 if not isinstance(ntp_cfg, (dict)):
39+<<<<<<< cloudinit/config/cc_ntp.py
40 raise RuntimeError(
41 "'ntp' key existed in config, but not a dictionary type,"
42 " is a {_type} instead".format(_type=type_utils.obj_name(ntp_cfg)))
43+=======
44+ raise RuntimeError(("'ntp' key existed in config,"
45+ " but not a dictionary type,"
46+ " is a %s instead"), type_utils.obj_name(ntp_cfg))
47+>>>>>>> cloudinit/config/cc_ntp.py
48+
49+ if ntp_cfg.get('enabled') and ntp_cfg.get('enabled') == 'true':
50+ cloud.distro.set_timesync_client()
51+ else:
52+ # When all distro implementations are switched return here
53+ pass
54
55 validate_cloudconfig_schema(cfg, schema)
56- if ntp_installable():
57- service_name = 'ntp'
58- confpath = NTP_CONF
59- template_name = None
60- packages = ['ntp']
61- check_exe = 'ntpd'
62+ if hasattr(cloud.distro, 'timesync_client'):
63+ client_name = cloud.distro.timesync_client
64+ service_name = cloud.distro.timesync_service_name
65+ if client_name == 'ntp':
66+ confpath = NTP_CONF_FILE
67+ template_name = 'ntp.conf.%s' % cloud.distro.name
68+ elif client_name == 'systemd-timesyncd':
69+ confpath = TIMESYNCD_CONF_FILE
70+ template_name = 'timesyncd.conf'
71+ elif client_name == 'chrony':
72+ confpath = CHRONY_CONF_FILE
73+ template_name = 'chrony.conf.%s' % cloud.distro.name
74 else:
75- service_name = 'systemd-timesyncd'
76- confpath = TIMESYNCD_CONF
77- template_name = 'timesyncd.conf'
78- packages = []
79- check_exe = '/lib/systemd/systemd-timesyncd'
80-
81- rename_ntp_conf()
82+ if ntp_installable():
83+ service_name = 'ntp'
84+ confpath = NTP_CONF_FILE
85+ template_name = None
86+ packages = ['ntp']
87+ check_exe = 'ntpd'
88+ else:
89+ service_name = 'systemd-timesyncd'
90+ confpath = TIMESYNCD_CONF_FILE
91+ template_name = 'timesyncd.conf'
92+ packages = []
93+ check_exe = '/lib/systemd/systemd-timesyncd'
94+
95+ rename_ntp_conf(confpath)
96 # ensure when ntp is installed it has a configuration file
97 # to use instead of starting up with packaged defaults
98 write_ntp_config_template(ntp_cfg, cloud, confpath, template=template_name)
99- install_ntp(cloud.distro.install_packages, packages=packages,
100- check_exe=check_exe)
101+ if not hasattr(cloud.distro, 'timesync_client'):
102+ # Updated implementation installs a package is missing in
103+ # distro._set_default_timesync_client
104+ install_ntp(cloud.distro.install_packages, packages=packages,
105+ check_exe=check_exe)
106
107 try:
108 reload_ntp(service_name, systemd=cloud.distro.uses_systemd())
109@@ -167,7 +200,7 @@ def install_ntp(install_func, packages=None, check_exe="ntpd"):
110 def rename_ntp_conf(config=None):
111 """Rename any existing ntp.conf file"""
112 if config is None: # For testing
113- config = NTP_CONF
114+ config = NTP_CONF_FILE
115 if os.path.exists(config):
116 util.rename(config, config + ".dist")
117
118diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
119index 55260ea..26b75c4 100755
120--- a/cloudinit/distros/__init__.py
121+++ b/cloudinit/distros/__init__.py
122@@ -61,6 +61,9 @@ class Distro(object):
123 init_cmd = ['service'] # systemctl, service etc
124 renderer_configs = {}
125
126+ __timesync_client_map = {}
127+ __ntp_client_execs = []
128+
129 def __init__(self, name, cfg, paths):
130 self._paths = paths
131 self._cfg = cfg
132@@ -90,6 +93,43 @@ class Distro(object):
133 renderer.render_network_config(network_config=network_config)
134 return []
135
136+ def set_timesync_client(self):
137+ system_info = self._cfg.get('system_info')
138+ if system_info and isinstance(system_info, (dict)):
139+ ntp_client = system_info.get('ntp_client')
140+ if ntp_client and ntp_client in self.__timesync_client_map:
141+ self.timesync_client, self.timesync_service_name = \
142+ self.__timesync_client_map.get(ntp_client)
143+ LOG.debug('Using "%s" for timesync client per configuration',
144+ ntp_client)
145+ return
146+
147+ found = False
148+ for ntp_client in self.__ntp_client_execs:
149+ ntp_exec = util.which(ntp_client)
150+ if ntp_exec and not found:
151+ found = ntp_client
152+ # systemd-timesyncd is part of systemd and thus is probably
153+ # always installed, do not consider it as a conflict
154+ elif ntp_exec and found and 'systemd-timesyncd' not in ntp_exec:
155+ msg = 'Found multiple timesync clients installed. Resolve '
156+ msg += 'ambigutity by falling back to distro default'
157+ LOG.debug(msg)
158+ found = False
159+ break
160+
161+ if found and found in self.__timesync_client_map:
162+ self.timesync_client, self.timesync_service_name = \
163+ self.__timesync_client_map.get(found)
164+ LOG.debug('Using "%s" for timesync based on installed exec',
165+ ntp_client)
166+ return
167+
168+ self._set_default_timesync_client()
169+
170+ def _set_default_timesync_client(self):
171+ raise NotImplementedError()
172+
173 def _find_tz_file(self, tz):
174 tz_file = os.path.join(self.tz_zone_dir, str(tz))
175 if not os.path.isfile(tz_file):
176diff --git a/cloudinit/distros/arch.py b/cloudinit/distros/arch.py
177index f87a343..fffc1c9 100644
178--- a/cloudinit/distros/arch.py
179+++ b/cloudinit/distros/arch.py
180@@ -153,6 +153,10 @@ class Distro(distros.Distro):
181 self._runner.run("update-sources", self.package_command,
182 ["-y"], freq=PER_INSTANCE)
183
184+ def _set_default_timesync_client(self):
185+ # Fall back to previous implementation
186+ return
187+
188
189 def _render_network(entries, target="/", conf_dir="etc/netctl",
190 resolv_conf="etc/resolv.conf", enable_func=None):
191diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
192index 33cc0bf..46dd417 100644
193--- a/cloudinit/distros/debian.py
194+++ b/cloudinit/distros/debian.py
195@@ -212,6 +212,10 @@ class Distro(distros.Distro):
196 (arch, _err) = util.subp(['dpkg', '--print-architecture'])
197 return str(arch).strip()
198
199+ def _set_default_timesync_client(self):
200+ # Fall back to previous implementation
201+ return
202+
203
204 def _get_wrapper_prefix(cmd, mode):
205 if isinstance(cmd, str):
206diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
207index bad112f..00b3891 100644
208--- a/cloudinit/distros/freebsd.py
209+++ b/cloudinit/distros/freebsd.py
210@@ -649,4 +649,8 @@ class Distro(distros.Distro):
211 self._runner.run("update-sources", self.package_command,
212 ["update"], freq=PER_INSTANCE)
213
214+ def _set_default_timesync_client(self):
215+ # Fall back to previous implementation
216+ return
217+
218 # vi: ts=4 expandtab
219diff --git a/cloudinit/distros/gentoo.py b/cloudinit/distros/gentoo.py
220index dc57717..5685b05 100644
221--- a/cloudinit/distros/gentoo.py
222+++ b/cloudinit/distros/gentoo.py
223@@ -214,6 +214,10 @@ class Distro(distros.Distro):
224 self._runner.run("update-sources", self.package_command,
225 ["-u", "world"], freq=PER_INSTANCE)
226
227+ def _set_default_timesync_client(self):
228+ # Fall back to previous implementation
229+ return
230+
231
232 def convert_resolv_conf(settings):
233 """Returns a settings string formatted for resolv.conf."""
234diff --git a/cloudinit/distros/opensuse.py b/cloudinit/distros/opensuse.py
235index a219e9f..86318ea 100644
236--- a/cloudinit/distros/opensuse.py
237+++ b/cloudinit/distros/opensuse.py
238@@ -8,6 +8,12 @@
239 #
240 # This file is part of cloud-init. See LICENSE file for license information.
241
242+# pylint: disable=W1505
243+# platform.linux_distribution is deprecated (W1505) we need to decide if
244+# cloud-init will implement it's own or add a new dependency on the
245+# distro module
246+import platform
247+
248 from cloudinit import distros
249
250 from cloudinit.distros.parsers.hostname import HostnameConf
251@@ -36,6 +42,23 @@ class Distro(distros.Distro):
252 systemd_locale_conf_fn = '/etc/locale.conf'
253 tz_local_fn = '/etc/localtime'
254
255+ __timesync_client_map = {
256+ # Map the system_info supported values
257+ 'chrony': ('chrony', 'chronyd'),
258+ 'isc-ntp': ('ntp', 'ntpd'),
259+ 'systemd-timesyncd': ('systemd-timesyncd', 'systemd-timesyncd'),
260+ # Map the common names if different from system_info
261+ 'chronyd': ('chrony', 'chronyd'),
262+ 'ntpd': ('ntp', 'ntpd'),
263+ '/usr/lib/systemd/systemd-timesyncd':
264+ ('systemd-timesyncd', 'systemd-timesyncd')
265+ }
266+ __ntp_client_execs = [
267+ 'chronyd',
268+ 'ntpd',
269+ '/usr/lib/systemd/systemd-timesyncd'
270+ ]
271+
272 def __init__(self, name, cfg, paths):
273 distros.Distro.__init__(self, name, cfg, paths)
274 self._runner = helpers.Runners(paths)
275@@ -145,6 +168,28 @@ class Distro(distros.Distro):
276 host_fn = self.hostname_conf_fn
277 return (host_fn, self._read_hostname(host_fn))
278
279+ def _set_default_timesync_client(self):
280+ """The default timesync client is dependent on the distribution."""
281+ # When we get here the user has configured ntp to be enabled but
282+ # no client is installed
283+ distro_info = platform.linux_distribution()
284+ name = distro_info[0]
285+ major_ver = int(distro_info[1].split('.')[0])
286+
287+ # This is horribly complicated because of a case of
288+ # "we do not care if versions should be increasing syndrome"
289+ if (
290+ (major_ver >= 15 and 'openSUSE' not in name) or
291+ (major_ver >= 15 and 'openSUSE' in name and major_ver != 42)
292+ ):
293+ self.timesync_client = 'chrony'
294+ self.timesync_service_name = 'chronyd'
295+ self.install_packages(['chrony'])
296+ else:
297+ self.timesync_client = 'ntp'
298+ self.timesync_service_name = 'ntpd'
299+ self.install_packages(['ntp'])
300+
301 def _write_hostname(self, hostname, out_fn):
302 if self.uses_systemd() and out_fn.endswith('/previous-hostname'):
303 util.write_file(out_fn, hostname)
304diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py
305index 1fecb61..6d9c9f6 100644
306--- a/cloudinit/distros/rhel.py
307+++ b/cloudinit/distros/rhel.py
308@@ -218,4 +218,8 @@ class Distro(distros.Distro):
309 self._runner.run("update-sources", self.package_command,
310 ["makecache"], freq=PER_INSTANCE)
311
312+ def _set_default_timesync_client(self):
313+ # Fall back to previous implementation
314+ return
315+
316 # vi: ts=4 expandtab
317diff --git a/templates/chrony.conf.opensuse.tmpl b/templates/chrony.conf.opensuse.tmpl
318new file mode 100644
319index 0000000..38e84d8
320--- /dev/null
321+++ b/templates/chrony.conf.opensuse.tmpl
322@@ -0,0 +1,25 @@
323+## template:jinja
324+# cloud-init generated file
325+# See chrony.conf(5)
326+
327+{% if pools %}# pools
328+{% endif %}
329+{% for pool in pools -%}
330+pool {{pool}} iburst
331+{% endfor %}
332+{%- if servers %}# servers
333+{% endif %}
334+{% for server in servers -%}
335+server {{server}} iburst
336+{% endfor %}
337+
338+# Record the rate at which the the system clock gains/losses time
339+driftfile /var/lib/chrony/drift
340+
341+# Allow the system clock to be stepped in the first three updates
342+# if its offset is larger than 1 second.
343+makestep 1.0 3
344+
345+# Enable kernel synchronization of the real-time clock (RTC).
346+rtcsync
347+
348diff --git a/templates/chrony.conf.sles.tmpl b/templates/chrony.conf.sles.tmpl
349new file mode 100644
350index 0000000..38e84d8
351--- /dev/null
352+++ b/templates/chrony.conf.sles.tmpl
353@@ -0,0 +1,25 @@
354+## template:jinja
355+# cloud-init generated file
356+# See chrony.conf(5)
357+
358+{% if pools %}# pools
359+{% endif %}
360+{% for pool in pools -%}
361+pool {{pool}} iburst
362+{% endfor %}
363+{%- if servers %}# servers
364+{% endif %}
365+{% for server in servers -%}
366+server {{server}} iburst
367+{% endfor %}
368+
369+# Record the rate at which the the system clock gains/losses time
370+driftfile /var/lib/chrony/drift
371+
372+# Allow the system clock to be stepped in the first three updates
373+# if its offset is larger than 1 second.
374+makestep 1.0 3
375+
376+# Enable kernel synchronization of the real-time clock (RTC).
377+rtcsync
378+
379diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py
380index 791fe61..cdee4b1 100644
381--- a/tests/unittests/test_distros/test_generic.py
382+++ b/tests/unittests/test_distros/test_generic.py
383@@ -4,16 +4,12 @@ from cloudinit import distros
384 from cloudinit import util
385
386 from cloudinit.tests import helpers
387+from cloudinit.tests.helpers import mock
388
389 import os
390 import shutil
391 import tempfile
392
393-try:
394- from unittest import mock
395-except ImportError:
396- import mock
397-
398 unknown_arch_info = {
399 'arches': ['default'],
400 'failsafe': {'primary': 'http://fs-primary-default',
401@@ -35,6 +31,24 @@ package_mirrors = [
402 unknown_arch_info
403 ]
404
405+timesync_user_cfg_chrony = {
406+ 'system_info': {
407+ 'ntp_client': 'chrony'
408+ }
409+}
410+
411+timesync_user_cfg_ntp = {
412+ 'system_info': {
413+ 'ntp_client': 'isc-ntp'
414+ }
415+}
416+
417+timesync_user_cfg_systemd = {
418+ 'system_info': {
419+ 'ntp_client': 'systemd-timesyncd'
420+ }
421+}
422+
423 gpmi = distros._get_package_mirror_info
424 gapmi = distros._get_arch_package_mirror_info
425
426@@ -244,5 +258,82 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase):
427 with self.assertRaises(NotImplementedError):
428 d.get_locale()
429
430+ def test_set_timesync_client_user_config_chrony_sles(self):
431+ """Test sles distro sets proper values for chrony"""
432+ cls = distros.fetch("sles")
433+ d = cls("sles", timesync_user_cfg_chrony, None)
434+ d.set_timesync_client()
435+ self.assertEqual(d.timesync_client, 'chrony')
436+ self.assertEqual(d.timesync_service_name, 'chronyd')
437+
438+ def test_set_timesync_client_user_config_ntp_sles(self):
439+ """Test sles distro sets proper values for ntp"""
440+ cls = distros.fetch("sles")
441+ d = cls("sles", timesync_user_cfg_ntp, None)
442+ d.set_timesync_client()
443+ self.assertEqual(d.timesync_client, 'ntp')
444+ self.assertEqual(d.timesync_service_name, 'ntpd')
445+
446+ def test_set_timesync_client_user_config_timesyncd_sles(self):
447+ """Test sles distro sets proper values for timesyncd"""
448+ cls = distros.fetch("sles")
449+ d = cls("sles", timesync_user_cfg_systemd, None)
450+ d.set_timesync_client()
451+ self.assertEqual(d.timesync_client, 'systemd-timesyncd')
452+ self.assertEqual(d.timesync_service_name, 'systemd-timesyncd')
453+
454+ @mock.patch("cloudinit.distros.util")
455+ def test_set_timesync_client_chrony_installed_sles(self, mock_util):
456+ """Test sles distro sets proper values for chrony if chrony is
457+ installed"""
458+ mock_util.which.side_effect = side_effect_client_is_chrony
459+ cls = distros.fetch("sles")
460+ d = cls("sles", {}, None)
461+ d.set_timesync_client()
462+ self.assertEqual(d.timesync_client, 'chrony')
463+ self.assertEqual(d.timesync_service_name, 'chronyd')
464+
465+ @mock.patch("cloudinit.distros.util")
466+ def test_set_timesync_client_ntp_installed_sles(self, mock_util):
467+ """Test sles distro sets proper values for ntp if ntpd is
468+ installed"""
469+ mock_util.which.side_effect = side_effect_client_is_ntp
470+ cls = distros.fetch("sles")
471+ d = cls("sles", {}, None)
472+ d.set_timesync_client()
473+ self.assertEqual(d.timesync_client, 'ntp')
474+ self.assertEqual(d.timesync_service_name, 'ntpd')
475+
476+ @mock.patch("cloudinit.distros.util")
477+ def test_set_timesync_client_timesycd_installed_sles(self, mock_util):
478+ """Test sles distro sets proper values for timesycd if timesyncd is
479+ installed"""
480+ mock_util.which.side_effect = side_effect_client_is_timesyncd
481+ cls = distros.fetch("sles")
482+ d = cls("sles", {}, None)
483+ d.set_timesync_client()
484+ self.assertEqual(d.timesync_client, 'systemd-timesyncd')
485+ self.assertEqual(d.timesync_service_name, 'systemd-timesyncd')
486+
487+
488+def side_effect_client_is_chrony(ntp_client):
489+ if 'chrony' in ntp_client:
490+ return '/usr/sbin/chronyd'
491+ else:
492+ return False
493+
494+
495+def side_effect_client_is_ntp(ntp_client):
496+ if 'ntp' in ntp_client:
497+ return '/usr/sbin/ntpd'
498+ else:
499+ return False
500+
501+
502+def side_effect_client_is_timesyncd(ntp_client):
503+ if 'timesyncd' in ntp_client:
504+ return ntp_client
505+ else:
506+ return False
507
508 # vi: ts=4 expandtab
509diff --git a/tests/unittests/test_distros/test_opensuse.py b/tests/unittests/test_distros/test_opensuse.py
510index b9bb9b3..9ed10af 100644
511--- a/tests/unittests/test_distros/test_opensuse.py
512+++ b/tests/unittests/test_distros/test_opensuse.py
513@@ -1,6 +1,6 @@
514 # This file is part of cloud-init. See LICENSE file for license information.
515
516-from cloudinit.tests.helpers import CiTestCase
517+from cloudinit.tests.helpers import CiTestCase, mock
518
519 from . import _get_distro
520
521@@ -10,3 +10,45 @@ class TestopenSUSE(CiTestCase):
522 def test_get_distro(self):
523 distro = _get_distro("opensuse")
524 self.assertEqual(distro.osfamily, 'suse')
525+
526+ @mock.patch("cloudinit.distros.opensuse.Distro.install_packages")
527+ @mock.patch("platform.linux_distribution")
528+ def test_set_default_timesync_client_osl42(
529+ self,
530+ mock_distro,
531+ mock_install
532+ ):
533+ mock_distro.return_value = ('openSUSE ', '42.3', 'x86_64')
534+ mock_install.return_value = True
535+ distro = _get_distro("opensuse")
536+ distro._set_default_timesync_client()
537+ self.assertEqual(distro.timesync_client, 'ntp')
538+ self.assertEqual(distro.timesync_service_name, 'ntpd')
539+
540+ @mock.patch("cloudinit.distros.opensuse.Distro.install_packages")
541+ @mock.patch("platform.linux_distribution")
542+ def test_set_default_timesync_client_os13(
543+ self,
544+ mock_distro,
545+ mock_install
546+ ):
547+ mock_distro.return_value = ('openSUSE ', '13.1', 'x86_64')
548+ mock_install.return_value = True
549+ distro = _get_distro("opensuse")
550+ distro._set_default_timesync_client()
551+ self.assertEqual(distro.timesync_client, 'ntp')
552+ self.assertEqual(distro.timesync_service_name, 'ntpd')
553+
554+ @mock.patch("cloudinit.distros.opensuse.Distro.install_packages")
555+ @mock.patch("platform.linux_distribution")
556+ def test_set_default_timesync_client_osl15(
557+ self,
558+ mock_distro,
559+ mock_install
560+ ):
561+ mock_distro.return_value = ('openSUSE ', '15.1', 'x86_64')
562+ mock_install.return_value = True
563+ distro = _get_distro("opensuse")
564+ distro._set_default_timesync_client()
565+ self.assertEqual(distro.timesync_client, 'chrony')
566+ self.assertEqual(distro.timesync_service_name, 'chronyd')
567diff --git a/tests/unittests/test_distros/test_sles.py b/tests/unittests/test_distros/test_sles.py
568index 33e3c45..13237a2 100644
569--- a/tests/unittests/test_distros/test_sles.py
570+++ b/tests/unittests/test_distros/test_sles.py
571@@ -1,6 +1,6 @@
572 # This file is part of cloud-init. See LICENSE file for license information.
573
574-from cloudinit.tests.helpers import CiTestCase
575+from cloudinit.tests.helpers import CiTestCase, mock
576
577 from . import _get_distro
578
579@@ -10,3 +10,31 @@ class TestSLES(CiTestCase):
580 def test_get_distro(self):
581 distro = _get_distro("sles")
582 self.assertEqual(distro.osfamily, 'suse')
583+
584+ @mock.patch("cloudinit.distros.opensuse.Distro.install_packages")
585+ @mock.patch("platform.linux_distribution")
586+ def test_set_default_timesync_client_osl42(
587+ self,
588+ mock_distro,
589+ mock_install
590+ ):
591+ mock_distro.return_value = ('SLES ', '12.3', 'x86_64')
592+ mock_install.return_value = True
593+ distro = _get_distro("sles")
594+ distro._set_default_timesync_client()
595+ self.assertEqual(distro.timesync_client, 'ntp')
596+ self.assertEqual(distro.timesync_service_name, 'ntpd')
597+
598+ @mock.patch("cloudinit.distros.opensuse.Distro.install_packages")
599+ @mock.patch("platform.linux_distribution")
600+ def test_set_default_timesync_client_os13(
601+ self,
602+ mock_distro,
603+ mock_install
604+ ):
605+ mock_distro.return_value = ('SLES ', '15', 'x86_64')
606+ mock_install.return_value = True
607+ distro = _get_distro("sles")
608+ distro._set_default_timesync_client()
609+ self.assertEqual(distro.timesync_client, 'chrony')
610+ self.assertEqual(distro.timesync_service_name, 'chronyd')
611diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
612index 28a8455..33fab8c 100644
613--- a/tests/unittests/test_handler/test_handler_ntp.py
614+++ b/tests/unittests/test_handler/test_handler_ntp.py
615@@ -10,6 +10,20 @@ import os
616 from os.path import dirname
617 import shutil
618
619+CHRONY_TEMPLATE = b"""\
620+## template: jinja
621+{% if pools %}# pools
622+{% endif %}
623+{% for pool in pools -%}
624+pool {{pool}} iburst
625+{% endfor %}
626+{%- if servers %}# servers
627+{% endif %}
628+{% for server in servers -%}
629+server {{server}} iburst
630+{% endfor %}
631+"""
632+
633 NTP_TEMPLATE = b"""\
634 ## template: jinja
635 servers {{servers}}
636@@ -79,7 +93,7 @@ class TestNtp(FilesystemMockingTestCase):
637 """When NTP_CONF exists, rename_ntp moves it."""
638 ntpconf = self.tmp_path("ntp.conf", self.new_root)
639 util.write_file(ntpconf, "")
640- with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf):
641+ with mock.patch("cloudinit.config.cc_ntp.NTP_CONF_FILE", ntpconf):
642 cc_ntp.rename_ntp_conf()
643 self.assertFalse(os.path.exists(ntpconf))
644 self.assertTrue(os.path.exists("{0}.dist".format(ntpconf)))
645@@ -112,7 +126,7 @@ class TestNtp(FilesystemMockingTestCase):
646 """When NTP_CONF doesn't exist rename_ntp doesn't create a file."""
647 ntpconf = self.tmp_path("ntp.conf", self.new_root)
648 self.assertFalse(os.path.exists(ntpconf))
649- with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf):
650+ with mock.patch("cloudinit.config.cc_ntp.NTP_CONF_FILE", ntpconf):
651 cc_ntp.rename_ntp_conf()
652 self.assertFalse(os.path.exists("{0}.dist".format(ntpconf)))
653 self.assertFalse(os.path.exists(ntpconf))
654@@ -133,7 +147,7 @@ class TestNtp(FilesystemMockingTestCase):
655 # Create ntp.conf.tmpl
656 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
657 stream.write(NTP_TEMPLATE)
658- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
659+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
660 cc_ntp.write_ntp_config_template(cfg, mycloud, ntp_conf)
661 content = util.read_file_or_url('file://' + ntp_conf).contents
662 self.assertEqual(
663@@ -159,7 +173,7 @@ class TestNtp(FilesystemMockingTestCase):
664 # Create ntp.conf.tmpl.<distro>
665 with open('{0}.{1}.tmpl'.format(ntp_conf, distro), 'wb') as stream:
666 stream.write(NTP_TEMPLATE)
667- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
668+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
669 cc_ntp.write_ntp_config_template(cfg, mycloud, ntp_conf)
670 content = util.read_file_or_url('file://' + ntp_conf).contents
671 self.assertEqual(
672@@ -178,7 +192,7 @@ class TestNtp(FilesystemMockingTestCase):
673 # Create ntp.conf.tmpl
674 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
675 stream.write(NTP_TEMPLATE)
676- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
677+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
678 cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf)
679 content = util.read_file_or_url('file://' + ntp_conf).contents
680 default_pools = [
681@@ -210,7 +224,7 @@ class TestNtp(FilesystemMockingTestCase):
682 # Create ntp.conf.tmpl
683 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
684 stream.write(NTP_TEMPLATE)
685- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
686+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
687 with mock.patch.object(util, 'which', return_value=None):
688 cc_ntp.handle('notimportant', cfg, mycloud, None, None)
689
690@@ -239,7 +253,10 @@ class TestNtp(FilesystemMockingTestCase):
691 with open(template, 'wb') as stream:
692 stream.write(TIMESYNCD_TEMPLATE)
693
694- with mock.patch('cloudinit.config.cc_ntp.TIMESYNCD_CONF', tsyncd_conf):
695+ with mock.patch(
696+ 'cloudinit.config.cc_ntp.TIMESYNCD_CONF_FILE',
697+ tsyncd_conf
698+ ):
699 cc_ntp.handle('notimportant', cfg, mycloud, None, None)
700
701 content = util.read_file_or_url('file://' + tsyncd_conf).contents
702@@ -267,7 +284,7 @@ class TestNtp(FilesystemMockingTestCase):
703 shutil.copy(
704 tmpl_file,
705 os.path.join(self.new_root, 'ntp.conf.%s.tmpl' % distro))
706- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
707+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
708 with mock.patch.object(util, 'which', return_value=[True]):
709 cc_ntp.handle('notimportant', cfg, mycloud, None, None)
710
711@@ -300,7 +317,7 @@ class TestNtp(FilesystemMockingTestCase):
712 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
713 stream.write(NTP_TEMPLATE)
714 for valid_empty_config in valid_empty_configs:
715- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
716+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
717 cc_ntp.handle('cc_ntp', valid_empty_config, cc, None, [])
718 with open(ntp_conf) as stream:
719 content = stream.read()
720@@ -323,7 +340,7 @@ class TestNtp(FilesystemMockingTestCase):
721 ntp_conf = os.path.join(self.new_root, 'ntp.conf')
722 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
723 stream.write(NTP_TEMPLATE)
724- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
725+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
726 cc_ntp.handle('cc_ntp', invalid_config, cc, None, [])
727 self.assertIn(
728 "Invalid config:\nntp.pools.0: 123 is not of type 'string'\n"
729@@ -344,7 +361,7 @@ class TestNtp(FilesystemMockingTestCase):
730 ntp_conf = os.path.join(self.new_root, 'ntp.conf')
731 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
732 stream.write(NTP_TEMPLATE)
733- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
734+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
735 cc_ntp.handle('cc_ntp', invalid_config, cc, None, [])
736 self.assertIn(
737 "Invalid config:\nntp.pools: 123 is not of type 'array'\n"
738@@ -366,7 +383,7 @@ class TestNtp(FilesystemMockingTestCase):
739 ntp_conf = os.path.join(self.new_root, 'ntp.conf')
740 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
741 stream.write(NTP_TEMPLATE)
742- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
743+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
744 cc_ntp.handle('cc_ntp', invalid_config, cc, None, [])
745 self.assertIn(
746 "Invalid config:\nntp: Additional properties are not allowed "
747@@ -391,7 +408,7 @@ class TestNtp(FilesystemMockingTestCase):
748 ntp_conf = os.path.join(self.new_root, 'ntp.conf')
749 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
750 stream.write(NTP_TEMPLATE)
751- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
752+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
753 cc_ntp.handle('cc_ntp', invalid_config, cc, None, [])
754 self.assertIn(
755 "Invalid config:\nntp.pools: ['0.mypool.org', '0.mypool.org'] has "
756@@ -421,7 +438,10 @@ class TestNtp(FilesystemMockingTestCase):
757 print(template)
758 with open(template, 'wb') as stream:
759 stream.write(TIMESYNCD_TEMPLATE)
760- with mock.patch('cloudinit.config.cc_ntp.TIMESYNCD_CONF', tsyncd_conf):
761+ with mock.patch(
762+ 'cloudinit.config.cc_ntp.TIMESYNCD_CONF_FILE',
763+ tsyncd_conf
764+ ):
765 cc_ntp.write_ntp_config_template(cfg, mycloud, tsyncd_conf,
766 template='timesyncd.conf')
767
768@@ -442,7 +462,7 @@ class TestNtp(FilesystemMockingTestCase):
769 # Create ntp.conf.tmpl
770 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
771 stream.write(NTP_TEMPLATE)
772- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
773+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF_FILE', ntp_conf):
774 cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf)
775 content = util.read_file_or_url('file://' + ntp_conf).contents
776 default_pools = [
777@@ -456,5 +476,35 @@ class TestNtp(FilesystemMockingTestCase):
778 ",".join(default_pools)),
779 self.logs.getvalue())
780
781+ def test_ntp_handler_chrony(self):
782+ """Test ntp handler configures chrony"""
783+ distro = 'opensuse'
784+ cfg = {
785+ 'servers': ['192.168.2.1', '192.168.2.2'],
786+ 'pools': ['0.mypool.org'],
787+ }
788+ mycloud = self._get_cloud(distro)
789+ mycloud.timesync_client = 'chrony'
790+ mycloud.timesync_service_name = 'chronyd'
791+ chrony_conf = self.tmp_path("chrony.conf", self.new_root)
792+ # Create chrony.conf.tmpl
793+ template = '{0}.tmpl'.format(chrony_conf)
794+ print(template)
795+ with open(template, 'wb') as stream:
796+ stream.write(CHRONY_TEMPLATE)
797+ with mock.patch(
798+ 'cloudinit.config.cc_ntp.CHRONY_CONF_FILE',
799+ chrony_conf
800+ ):
801+ cc_ntp.write_ntp_config_template(cfg, mycloud, chrony_conf,
802+ template='chrony.conf')
803+
804+ content = util.read_file_or_url('file://' + chrony_conf).contents
805+ expected = '# pools\n'
806+ expected += 'pool 0.mypool.org iburst\n'
807+ expected += '# servers\n'
808+ expected += 'server 192.168.2.1 iburst\n'
809+ expected += 'server 192.168.2.2 iburst\n\n'
810+ self.assertEqual(expected, content.decode())
811
812 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches