Merge ~raharper/cloud-init:ntp-configure-timesyncd-fallback-lp-1686485 into cloud-init:master

Proposed by Ryan Harper
Status: Merged
Approved by: Scott Moser
Approved revision: 32ec08e88d0dc01f70b600860c65ae258fb39e52
Merged at revision: 5bba5db2655d88b8aba8fa06b30f8e91e2ca6836
Proposed branch: ~raharper/cloud-init:ntp-configure-timesyncd-fallback-lp-1686485
Merge into: cloud-init:master
Diff against target: 304 lines (+154/-17)
3 files modified
cloudinit/config/cc_ntp.py (+45/-13)
templates/timesyncd.conf.tmpl (+8/-0)
tests/unittests/test_handler/test_handler_ntp.py (+101/-4)
Reviewer Review Type Date Requested Status
Scott Moser Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+328427@code.launchpad.net

Description of the change

cc_ntp: fallback on timesyncd configuration if ntp is not installable

Some systems like Ubuntu-Core do not provide an ntp package for
installation but do include systemd-timesyncd (an ntp client).
On such systems cloud-init will generate a timesyncd configuration
using the 'servers' and 'pools' values as ntp hosts for timesyncd to use.

LP: #1686485

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:2a2e3a9ea8b6e3dd84e523943ee663eaf42eb6bf
https://jenkins.ubuntu.com/server/job/cloud-init-ci/109/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:baadcc70ca7d4785768e01b0958dffc4378f217c
https://jenkins.ubuntu.com/server/job/cloud-init-ci/110/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:b1a0b8848d8cb7cc3f8dea7b45c319ef28e97db8
https://jenkins.ubuntu.com/server/job/cloud-init-ci/111/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

some small things. i assume you were as much trying to avoid test changes as anything else, but the defaults we have there seem simplistic.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the review, I'll fix the issues you raised and push in an update.

Revision history for this message
Ryan Harper (raharper) wrote :

Fix suggestions, rebased and updated.

Revision history for this message
Scott Moser (smoser) :
review: Approve

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 31ed64e..a02b4bf 100644
3--- a/cloudinit/config/cc_ntp.py
4+++ b/cloudinit/config/cc_ntp.py
5@@ -50,6 +50,7 @@ LOG = logging.getLogger(__name__)
6
7 frequency = PER_INSTANCE
8 NTP_CONF = '/etc/ntp.conf'
9+TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf'
10 NR_POOL_SERVERS = 4
11 distros = ['centos', 'debian', 'fedora', 'opensuse', 'ubuntu']
12
13@@ -132,20 +133,50 @@ def handle(name, cfg, cloud, log, _args):
14 " is a %s %instead"), type_utils.obj_name(ntp_cfg))
15
16 validate_cloudconfig_schema(cfg, schema)
17+ if ntp_installable():
18+ service_name = 'ntp'
19+ confpath = NTP_CONF
20+ template_name = None
21+ packages = ['ntp']
22+ check_exe = 'ntpd'
23+ else:
24+ service_name = 'systemd-timesyncd'
25+ confpath = TIMESYNCD_CONF
26+ template_name = 'timesyncd.conf'
27+ packages = []
28+ check_exe = '/lib/systemd/systemd-timesyncd'
29+
30 rename_ntp_conf()
31 # ensure when ntp is installed it has a configuration file
32 # to use instead of starting up with packaged defaults
33- write_ntp_config_template(ntp_cfg, cloud)
34- install_ntp(cloud.distro.install_packages, packages=['ntp'],
35- check_exe="ntpd")
36- # if ntp was already installed, it may not have started
37+ write_ntp_config_template(ntp_cfg, cloud, confpath, template=template_name)
38+ install_ntp(cloud.distro.install_packages, packages=packages,
39+ check_exe=check_exe)
40+
41 try:
42- reload_ntp(systemd=cloud.distro.uses_systemd())
43+ reload_ntp(service_name, systemd=cloud.distro.uses_systemd())
44 except util.ProcessExecutionError as e:
45 LOG.exception("Failed to reload/start ntp service: %s", e)
46 raise
47
48
49+def ntp_installable():
50+ """Check if we can install ntp package
51+
52+ Ubuntu-Core systems do not have an ntp package available, so
53+ we always return False. Other systems require package managers to install
54+ the ntp package If we fail to find one of the package managers, then we
55+ cannot install ntp.
56+ """
57+ if util.system_is_snappy():
58+ return False
59+
60+ if any(map(util.which, ['apt-get', 'dnf', 'yum', 'zypper'])):
61+ return True
62+
63+ return False
64+
65+
66 def install_ntp(install_func, packages=None, check_exe="ntpd"):
67 if util.which(check_exe):
68 return
69@@ -156,7 +187,7 @@ def install_ntp(install_func, packages=None, check_exe="ntpd"):
70
71
72 def rename_ntp_conf(config=None):
73- """Rename any existing ntp.conf file and render from template"""
74+ """Rename any existing ntp.conf file"""
75 if config is None: # For testing
76 config = NTP_CONF
77 if os.path.exists(config):
78@@ -171,7 +202,7 @@ def generate_server_names(distro):
79 return names
80
81
82-def write_ntp_config_template(cfg, cloud):
83+def write_ntp_config_template(cfg, cloud, path, template=None):
84 servers = cfg.get('servers', [])
85 pools = cfg.get('pools', [])
86
87@@ -185,19 +216,20 @@ def write_ntp_config_template(cfg, cloud):
88 'pools': pools,
89 }
90
91- template_fn = cloud.get_template_filename('ntp.conf.%s' %
92- (cloud.distro.name))
93+ if template is None:
94+ template = 'ntp.conf.%s' % cloud.distro.name
95+
96+ template_fn = cloud.get_template_filename(template)
97 if not template_fn:
98 template_fn = cloud.get_template_filename('ntp.conf')
99 if not template_fn:
100 raise RuntimeError(("No template found, "
101- "not rendering %s"), NTP_CONF)
102+ "not rendering %s"), path)
103
104- templater.render_to_file(template_fn, NTP_CONF, params)
105+ templater.render_to_file(template_fn, path, params)
106
107
108-def reload_ntp(systemd=False):
109- service = 'ntp'
110+def reload_ntp(service, systemd=False):
111 if systemd:
112 cmd = ['systemctl', 'reload-or-restart', service]
113 else:
114diff --git a/templates/timesyncd.conf.tmpl b/templates/timesyncd.conf.tmpl
115new file mode 100644
116index 0000000..6b98301
117--- /dev/null
118+++ b/templates/timesyncd.conf.tmpl
119@@ -0,0 +1,8 @@
120+## template:jinja
121+# cloud-init generated file
122+# See timesyncd.conf(5) for details.
123+
124+[Time]
125+{% if servers or pools -%}
126+NTP={% for host in servers|list + pools|list %}{{ host }} {% endfor -%}
127+{% endif -%}
128diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
129index 7f27864..83d5faa 100644
130--- a/tests/unittests/test_handler/test_handler_ntp.py
131+++ b/tests/unittests/test_handler/test_handler_ntp.py
132@@ -16,6 +16,14 @@ servers {{servers}}
133 pools {{pools}}
134 """
135
136+TIMESYNCD_TEMPLATE = b"""\
137+## template:jinja
138+[Time]
139+{% if servers or pools -%}
140+NTP={% for host in servers|list + pools|list %}{{ host }} {% endfor -%}
141+{% endif -%}
142+"""
143+
144 try:
145 import jsonschema
146 assert jsonschema # avoid pyflakes error F401: import unused
147@@ -59,6 +67,14 @@ class TestNtp(FilesystemMockingTestCase):
148 cc_ntp.install_ntp(install_func, packages=['ntp'], check_exe='ntpd')
149 install_func.assert_not_called()
150
151+ @mock.patch("cloudinit.config.cc_ntp.util")
152+ def test_ntp_install_no_op_with_empty_pkg_list(self, mock_util):
153+ """ntp_install calls install_func with empty list"""
154+ mock_util.which.return_value = None # check_exe not found
155+ install_func = mock.MagicMock()
156+ cc_ntp.install_ntp(install_func, packages=[], check_exe='timesyncd')
157+ install_func.assert_called_once_with([])
158+
159 def test_ntp_rename_ntp_conf(self):
160 """When NTP_CONF exists, rename_ntp moves it."""
161 ntpconf = self.tmp_path("ntp.conf", self.new_root)
162@@ -68,6 +84,30 @@ class TestNtp(FilesystemMockingTestCase):
163 self.assertFalse(os.path.exists(ntpconf))
164 self.assertTrue(os.path.exists("{0}.dist".format(ntpconf)))
165
166+ @mock.patch("cloudinit.config.cc_ntp.util")
167+ def test_reload_ntp_defaults(self, mock_util):
168+ """Test service is restarted/reloaded (defaults)"""
169+ service = 'ntp'
170+ cmd = ['service', service, 'restart']
171+ cc_ntp.reload_ntp(service)
172+ mock_util.subp.assert_called_with(cmd, capture=True)
173+
174+ @mock.patch("cloudinit.config.cc_ntp.util")
175+ def test_reload_ntp_systemd(self, mock_util):
176+ """Test service is restarted/reloaded (systemd)"""
177+ service = 'ntp'
178+ cmd = ['systemctl', 'reload-or-restart', service]
179+ cc_ntp.reload_ntp(service, systemd=True)
180+ mock_util.subp.assert_called_with(cmd, capture=True)
181+
182+ @mock.patch("cloudinit.config.cc_ntp.util")
183+ def test_reload_ntp_systemd_timesycnd(self, mock_util):
184+ """Test service is restarted/reloaded (systemd/timesyncd)"""
185+ service = 'systemd-timesycnd'
186+ cmd = ['systemctl', 'reload-or-restart', service]
187+ cc_ntp.reload_ntp(service, systemd=True)
188+ mock_util.subp.assert_called_with(cmd, capture=True)
189+
190 def test_ntp_rename_ntp_conf_skip_missing(self):
191 """When NTP_CONF doesn't exist rename_ntp doesn't create a file."""
192 ntpconf = self.tmp_path("ntp.conf", self.new_root)
193@@ -94,7 +134,7 @@ class TestNtp(FilesystemMockingTestCase):
194 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
195 stream.write(NTP_TEMPLATE)
196 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
197- cc_ntp.write_ntp_config_template(cfg, mycloud)
198+ cc_ntp.write_ntp_config_template(cfg, mycloud, ntp_conf)
199 content = util.read_file_or_url('file://' + ntp_conf).contents
200 self.assertEqual(
201 "servers ['192.168.2.1', '192.168.2.2']\npools []\n",
202@@ -120,7 +160,7 @@ class TestNtp(FilesystemMockingTestCase):
203 with open('{0}.{1}.tmpl'.format(ntp_conf, distro), 'wb') as stream:
204 stream.write(NTP_TEMPLATE)
205 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
206- cc_ntp.write_ntp_config_template(cfg, mycloud)
207+ cc_ntp.write_ntp_config_template(cfg, mycloud, ntp_conf)
208 content = util.read_file_or_url('file://' + ntp_conf).contents
209 self.assertEqual(
210 "servers []\npools ['10.0.0.1', '10.0.0.2']\n",
211@@ -139,7 +179,7 @@ class TestNtp(FilesystemMockingTestCase):
212 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
213 stream.write(NTP_TEMPLATE)
214 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
215- cc_ntp.write_ntp_config_template({}, mycloud)
216+ cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf)
217 content = util.read_file_or_url('file://' + ntp_conf).contents
218 default_pools = [
219 "{0}.{1}.pool.ntp.org".format(x, distro)
220@@ -152,7 +192,8 @@ class TestNtp(FilesystemMockingTestCase):
221 ",".join(default_pools)),
222 self.logs.getvalue())
223
224- def test_ntp_handler_mocked_template(self):
225+ @mock.patch("cloudinit.config.cc_ntp.ntp_installable")
226+ def test_ntp_handler_mocked_template(self, m_ntp_install):
227 """Test ntp handler renders ubuntu ntp.conf template."""
228 pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org']
229 servers = ['192.168.23.3', '192.168.23.4']
230@@ -164,6 +205,8 @@ class TestNtp(FilesystemMockingTestCase):
231 }
232 mycloud = self._get_cloud('ubuntu')
233 ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
234+ m_ntp_install.return_value = True
235+
236 # Create ntp.conf.tmpl
237 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
238 stream.write(NTP_TEMPLATE)
239@@ -176,6 +219,34 @@ class TestNtp(FilesystemMockingTestCase):
240 'servers {0}\npools {1}\n'.format(servers, pools),
241 content.decode())
242
243+ @mock.patch("cloudinit.config.cc_ntp.util")
244+ def test_ntp_handler_mocked_template_snappy(self, m_util):
245+ """Test ntp handler renders timesycnd.conf template on snappy."""
246+ pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org']
247+ servers = ['192.168.23.3', '192.168.23.4']
248+ cfg = {
249+ 'ntp': {
250+ 'pools': pools,
251+ 'servers': servers
252+ }
253+ }
254+ mycloud = self._get_cloud('ubuntu')
255+ m_util.system_is_snappy.return_value = True
256+
257+ # Create timesyncd.conf.tmpl
258+ tsyncd_conf = self.tmp_path("timesyncd.conf", self.new_root)
259+ template = '{0}.tmpl'.format(tsyncd_conf)
260+ with open(template, 'wb') as stream:
261+ stream.write(TIMESYNCD_TEMPLATE)
262+
263+ with mock.patch('cloudinit.config.cc_ntp.TIMESYNCD_CONF', tsyncd_conf):
264+ cc_ntp.handle('notimportant', cfg, mycloud, None, None)
265+
266+ content = util.read_file_or_url('file://' + tsyncd_conf).contents
267+ self.assertEqual(
268+ "[Time]\nNTP=%s %s \n" % (" ".join(servers), " ".join(pools)),
269+ content.decode())
270+
271 def test_ntp_handler_real_distro_templates(self):
272 """Test ntp handler renders the shipped distro ntp.conf templates."""
273 pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org']
274@@ -333,4 +404,30 @@ class TestNtp(FilesystemMockingTestCase):
275 "pools ['0.mypool.org', '0.mypool.org']\n",
276 content)
277
278+ @mock.patch("cloudinit.config.cc_ntp.ntp_installable")
279+ def test_ntp_handler_timesyncd(self, m_ntp_install):
280+ """Test ntp handler configures timesyncd"""
281+ m_ntp_install.return_value = False
282+ distro = 'ubuntu'
283+ cfg = {
284+ 'servers': ['192.168.2.1', '192.168.2.2'],
285+ 'pools': ['0.mypool.org'],
286+ }
287+ mycloud = self._get_cloud(distro)
288+ tsyncd_conf = self.tmp_path("timesyncd.conf", self.new_root)
289+ # Create timesyncd.conf.tmpl
290+ template = '{0}.tmpl'.format(tsyncd_conf)
291+ print(template)
292+ with open(template, 'wb') as stream:
293+ stream.write(TIMESYNCD_TEMPLATE)
294+ with mock.patch('cloudinit.config.cc_ntp.TIMESYNCD_CONF', tsyncd_conf):
295+ cc_ntp.write_ntp_config_template(cfg, mycloud, tsyncd_conf,
296+ template='timesyncd.conf')
297+
298+ content = util.read_file_or_url('file://' + tsyncd_conf).contents
299+ self.assertEqual(
300+ "[Time]\nNTP=192.168.2.1 192.168.2.2 0.mypool.org \n",
301+ content.decode())
302+
303+
304 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches