Merge ~chad.smith/cloud-init:cc-ntp-testing into cloud-init:master

Proposed by Chad Smith
Status: Merged
Approved by: Scott Moser
Approved revision: 29d0bb1cffd00ec3df0c2effd4a649af07390911
Merged at revision: fd0c88cf8e8aa015eadb5ab842e872cb627197ec
Proposed branch: ~chad.smith/cloud-init:cc-ntp-testing
Merge into: cloud-init:master
Diff against target: 625 lines (+188/-304)
3 files modified
cloudinit/config/cc_ntp.py (+10/-15)
tests/unittests/helpers.py (+26/-0)
tests/unittests/test_handler/test_handler_ntp.py (+152/-289)
Reviewer Review Type Date Requested Status
Scott Moser Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+324450@code.launchpad.net

Commit message

cc_ntp: Restructure cc_ntp unit tests add with_logs class attr to unit tests.

Any CiTestCase subclass can now set a class attribute with_logs = True and tests can now make assertions on self.logs.getvalue(). This branch restructures a bit of cc_ntp module to get better test coverage of the module. It also restructures the handler_cc_ntp unit tests to avoid nested mocks where possible. Deeply nested mocks cause a couple of issues:
  - greater risk: mocks are permanent within the scope, so multiple call-sites could be affected by package mocks
  - less legible tests: each mock doesn't advertise the actual call-site
  - tight coupling: the unit test logic to tightly bound to the actual implementation in remote (unrelated) modules which makes it more costly to maintain code
  - false success: we should be testing the expected behavior not specific remote method names as we want to know if that underlying behavior changes and breaks us.

LP: #1692794

Description of the change

cc_ntp: Restructure cc_ntp unit tests add with_logs class attr to unit tests.

Any CiTestCase subclass can now set a class attribute with_logs = True and tests can now make assertions on self.logs.getvalue(). This branch restructures a bit of cc_ntp module to get better test coverage of the module. It also restructures the handler_cc_ntp unit tests to avoid nested mocks where possible. Deeply nested mocks cause a couple of issues:
  - greater risk: mocks are permanent within the scope, so multiple call-sites could be affected by package mocks
  - less legible tests: each mock doesn't advertise the actual call-site
  - tight coupling: the unit test logic to tightly bound to the actual implementation in remote (unrelated) modules which makes it more costly to maintain code
  - false success: we should be testing the expected behavior not specific remote method names as we want to know if that underlying behavior changes and breaks us.

LP: #1692794

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
e4f1154... by Chad Smith

lints

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Scott Moser (smoser) wrote :

some comments in line.

I'm generally in agreement here.

Revision history for this message
Chad Smith (chad.smith) :
49c4eb8... by Chad Smith

use util.read_file_or_url instead of open()read(). replace ntp_conf in write_.._template

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
15a8bc4... by Chad Smith

add unit test for the real ntp templates for each distro. drop unused metadata param

Revision history for this message
Chad Smith (chad.smith) wrote :

@scott Addressed your review comments thanks a lot. So unittest runtimes 1m30sec versus 2m20sec? Do we want to make the switch to have all CiTestCase subclasses logged?

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
29d0bb1... by Chad Smith

calculate root_dir path for templates directory from util import as sys.path will be different on test environments

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

I'm happy with this if you are.
You mentioned some improvement on the root_dir path calculation. You can do that here or in a follow on.
Just say which you want.

review: Approve
Revision history for this message
Chad Smith (chad.smith) wrote :

+1 let's land this as it's getting bigger. I've added a card/bug to work on a better root_dir path calculation in a followup.

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 225f898..5cc5453 100644
3--- a/cloudinit/config/cc_ntp.py
4+++ b/cloudinit/config/cc_ntp.py
5@@ -53,14 +53,12 @@ distros = ['centos', 'debian', 'fedora', 'opensuse', 'ubuntu']
6
7
8 def handle(name, cfg, cloud, log, _args):
9- """
10- Enable and configure ntp
11+ """Enable and configure ntp."""
12
13- ntp:
14- pools: ['0.{{distro}}.pool.ntp.org', '1.{{distro}}.pool.ntp.org']
15- servers: ['192.168.2.1']
16-
17- """
18+ if 'ntp' not in cfg:
19+ LOG.debug(
20+ "Skipping module named %s, not present or disabled by cfg", name)
21+ return
22
23 ntp_cfg = cfg.get('ntp', {})
24
25@@ -69,18 +67,12 @@ def handle(name, cfg, cloud, log, _args):
26 " but not a dictionary type,"
27 " is a %s %instead"), type_utils.obj_name(ntp_cfg))
28
29- if 'ntp' not in cfg:
30- LOG.debug("Skipping module named %s,"
31- "not present or disabled by cfg", name)
32- return True
33-
34 rename_ntp_conf()
35 # ensure when ntp is installed it has a configuration file
36 # to use instead of starting up with packaged defaults
37 write_ntp_config_template(ntp_cfg, cloud)
38 install_ntp(cloud.distro.install_packages, packages=['ntp'],
39 check_exe="ntpd")
40-
41 # if ntp was already installed, it may not have started
42 try:
43 reload_ntp(systemd=cloud.distro.uses_systemd())
44@@ -98,8 +90,10 @@ def install_ntp(install_func, packages=None, check_exe="ntpd"):
45 install_func(packages)
46
47
48-def rename_ntp_conf(config=NTP_CONF):
49+def rename_ntp_conf(config=None):
50 """Rename any existing ntp.conf file and render from template"""
51+ if config is None: # For testing
52+ config = NTP_CONF
53 if os.path.exists(config):
54 util.rename(config, config + ".dist")
55
56@@ -117,8 +111,9 @@ def write_ntp_config_template(cfg, cloud):
57 pools = cfg.get('pools', [])
58
59 if len(servers) == 0 and len(pools) == 0:
60- LOG.debug('Adding distro default ntp pool servers')
61 pools = generate_server_names(cloud.distro.name)
62+ LOG.debug(
63+ 'Adding distro default ntp pool servers: %s', ','.join(pools))
64
65 params = {
66 'servers': servers,
67diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
68index d24f817..9ff1599 100644
69--- a/tests/unittests/helpers.py
70+++ b/tests/unittests/helpers.py
71@@ -4,6 +4,7 @@ from __future__ import print_function
72
73 import functools
74 import json
75+import logging
76 import os
77 import shutil
78 import sys
79@@ -18,6 +19,10 @@ try:
80 from contextlib import ExitStack
81 except ImportError:
82 from contextlib2 import ExitStack
83+try:
84+ from cStringIO import StringIO
85+except ImportError:
86+ from io import StringIO
87
88 from cloudinit import helpers as ch
89 from cloudinit import util
90@@ -87,6 +92,27 @@ class TestCase(unittest2.TestCase):
91 class CiTestCase(TestCase):
92 """This is the preferred test case base class unless user
93 needs other test case classes below."""
94+
95+ # Subclass overrides for specific test behavior
96+ # Whether or not a unit test needs logfile setup
97+ with_logs = False
98+
99+ def setUp(self):
100+ super(CiTestCase, self).setUp()
101+ if self.with_logs:
102+ # Create a log handler so unit tests can search expected logs.
103+ logger = logging.getLogger()
104+ self.logs = StringIO()
105+ handler = logging.StreamHandler(self.logs)
106+ self.old_handlers = logger.handlers
107+ logger.handlers = [handler]
108+
109+ def tearDown(self):
110+ if self.with_logs:
111+ # Remove the handler we setup
112+ logging.getLogger().handlers = self.old_handlers
113+ super(CiTestCase, self).tearDown()
114+
115 def tmp_dir(self, dir=None, cleanup=True):
116 # return a full path to a temporary directory that will be cleaned up.
117 if dir is None:
118diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
119index 02bd304..21f2ab1 100644
120--- a/tests/unittests/test_handler/test_handler_ntp.py
121+++ b/tests/unittests/test_handler/test_handler_ntp.py
122@@ -2,351 +2,214 @@
123
124 from cloudinit.config import cc_ntp
125 from cloudinit.sources import DataSourceNone
126-from cloudinit import templater
127 from cloudinit import (distros, helpers, cloud, util)
128 from ..helpers import FilesystemMockingTestCase, mock
129
130-import logging
131+
132 import os
133+from os.path import dirname
134 import shutil
135-import tempfile
136-
137-LOG = logging.getLogger(__name__)
138
139-NTP_TEMPLATE = """
140+NTP_TEMPLATE = b"""\
141 ## template: jinja
142-
143-{% if pools %}# pools
144-{% endif %}
145-{% for pool in pools -%}
146-pool {{pool}} iburst
147-{% endfor %}
148-{%- if servers %}# servers
149-{% endif %}
150-{% for server in servers -%}
151-server {{server}} iburst
152-{% endfor %}
153-
154-"""
155-
156-
157-NTP_EXPECTED_UBUNTU = """
158-# pools
159-pool 0.mycompany.pool.ntp.org iburst
160-# servers
161-server 192.168.23.3 iburst
162-
163+servers {{servers}}
164+pools {{pools}}
165 """
166
167
168 class TestNtp(FilesystemMockingTestCase):
169
170+ with_logs = True
171+
172 def setUp(self):
173 super(TestNtp, self).setUp()
174 self.subp = util.subp
175- self.new_root = tempfile.mkdtemp()
176- self.addCleanup(shutil.rmtree, self.new_root)
177+ self.new_root = self.tmp_dir()
178
179- def _get_cloud(self, distro, metadata=None):
180+ def _get_cloud(self, distro):
181 self.patchUtils(self.new_root)
182- paths = helpers.Paths({})
183+ paths = helpers.Paths({'templates_dir': self.new_root})
184 cls = distros.fetch(distro)
185 mydist = cls(distro, {}, paths)
186 myds = DataSourceNone.DataSourceNone({}, mydist, paths)
187- if metadata:
188- myds.metadata.update(metadata)
189 return cloud.Cloud(myds, paths, {}, mydist, None)
190
191 @mock.patch("cloudinit.config.cc_ntp.util")
192 def test_ntp_install(self, mock_util):
193- cc = self._get_cloud('ubuntu')
194- cc.distro = mock.MagicMock()
195- cc.distro.name = 'ubuntu'
196- mock_util.which.return_value = None
197+ """ntp_install installs via install_func when check_exe is absent."""
198+ mock_util.which.return_value = None # check_exe not found.
199 install_func = mock.MagicMock()
200-
201 cc_ntp.install_ntp(install_func, packages=['ntpx'], check_exe='ntpdx')
202
203- self.assertTrue(install_func.called)
204 mock_util.which.assert_called_with('ntpdx')
205- install_pkg = install_func.call_args_list[0][0][0]
206- self.assertEqual(sorted(install_pkg), ['ntpx'])
207+ install_func.assert_called_once_with(['ntpx'])
208
209 @mock.patch("cloudinit.config.cc_ntp.util")
210 def test_ntp_install_not_needed(self, mock_util):
211- cc = self._get_cloud('ubuntu')
212- cc.distro = mock.MagicMock()
213- cc.distro.name = 'ubuntu'
214- mock_util.which.return_value = ["/usr/sbin/ntpd"]
215- cc_ntp.install_ntp(cc)
216- self.assertFalse(cc.distro.install_packages.called)
217+ """ntp_install doesn't attempt install when check_exe is found."""
218+ mock_util.which.return_value = ["/usr/sbin/ntpd"] # check_exe found.
219+ install_func = mock.MagicMock()
220+ cc_ntp.install_ntp(install_func, packages=['ntp'], check_exe='ntpd')
221+ install_func.assert_not_called()
222
223 def test_ntp_rename_ntp_conf(self):
224- with mock.patch.object(os.path, 'exists',
225- return_value=True) as mockpath:
226- with mock.patch.object(util, 'rename') as mockrename:
227- cc_ntp.rename_ntp_conf()
228-
229- mockpath.assert_called_with('/etc/ntp.conf')
230- mockrename.assert_called_with('/etc/ntp.conf', '/etc/ntp.conf.dist')
231+ """When NTP_CONF exists, rename_ntp moves it."""
232+ ntpconf = self.tmp_path("ntp.conf", self.new_root)
233+ os.mknod(ntpconf)
234+ with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf):
235+ cc_ntp.rename_ntp_conf()
236+ self.assertFalse(os.path.exists(ntpconf))
237+ self.assertTrue(os.path.exists("{}.dist".format(ntpconf)))
238
239 def test_ntp_rename_ntp_conf_skip_missing(self):
240- with mock.patch.object(os.path, 'exists',
241- return_value=False) as mockpath:
242- with mock.patch.object(util, 'rename') as mockrename:
243- cc_ntp.rename_ntp_conf()
244-
245- mockpath.assert_called_with('/etc/ntp.conf')
246- mockrename.assert_not_called()
247-
248- def ntp_conf_render(self, distro):
249- """ntp_conf_render
250- Test rendering of a ntp.conf from template for a given distro
251+ """When NTP_CONF doesn't exist rename_ntp doesn't create a file."""
252+ ntpconf = self.tmp_path("ntp.conf", self.new_root)
253+ self.assertFalse(os.path.exists(ntpconf))
254+ with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf):
255+ cc_ntp.rename_ntp_conf()
256+ self.assertFalse(os.path.exists("{}.dist".format(ntpconf)))
257+ self.assertFalse(os.path.exists(ntpconf))
258+
259+ def test_write_ntp_config_template_from_ntp_conf_tmpl_with_servers(self):
260+ """write_ntp_config_template reads content from ntp.conf.tmpl.
261+
262+ It reads ntp.conf.tmpl if present and renders the value from servers
263+ key. When no pools key is defined, template is rendered using an empty
264+ list for pools.
265 """
266-
267- cfg = {'ntp': {}}
268- mycloud = self._get_cloud(distro)
269- distro_names = cc_ntp.generate_server_names(distro)
270-
271- with mock.patch.object(templater, 'render_to_file') as mocktmpl:
272- with mock.patch.object(os.path, 'isfile', return_value=True):
273- with mock.patch.object(util, 'rename'):
274- cc_ntp.write_ntp_config_template(cfg, mycloud)
275-
276- mocktmpl.assert_called_once_with(
277- ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
278- '/etc/ntp.conf',
279- {'servers': [], 'pools': distro_names})
280-
281- def test_ntp_conf_render_rhel(self):
282- """Test templater.render_to_file() for rhel"""
283- self.ntp_conf_render('rhel')
284-
285- def test_ntp_conf_render_debian(self):
286- """Test templater.render_to_file() for debian"""
287- self.ntp_conf_render('debian')
288-
289- def test_ntp_conf_render_fedora(self):
290- """Test templater.render_to_file() for fedora"""
291- self.ntp_conf_render('fedora')
292-
293- def test_ntp_conf_render_sles(self):
294- """Test templater.render_to_file() for sles"""
295- self.ntp_conf_render('sles')
296-
297- def test_ntp_conf_render_ubuntu(self):
298- """Test templater.render_to_file() for ubuntu"""
299- self.ntp_conf_render('ubuntu')
300-
301- def test_ntp_conf_servers_no_pools(self):
302 distro = 'ubuntu'
303- pools = []
304- servers = ['192.168.2.1']
305 cfg = {
306- 'ntp': {
307- 'pools': pools,
308- 'servers': servers,
309- }
310+ 'servers': ['192.168.2.1', '192.168.2.2']
311 }
312 mycloud = self._get_cloud(distro)
313-
314- with mock.patch.object(templater, 'render_to_file') as mocktmpl:
315- with mock.patch.object(os.path, 'isfile', return_value=True):
316- with mock.patch.object(util, 'rename'):
317- cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)
318-
319- mocktmpl.assert_called_once_with(
320- ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
321- '/etc/ntp.conf',
322- {'servers': servers, 'pools': pools})
323-
324- def test_ntp_conf_custom_pools_no_server(self):
325+ ntp_conf = self.tmp_path("ntp.conf", self.new_root) # Doesn't exist
326+ # Create ntp.conf.tmpl
327+ with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
328+ stream.write(NTP_TEMPLATE)
329+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
330+ cc_ntp.write_ntp_config_template(cfg, mycloud)
331+ content = util.read_file_or_url('file://' + ntp_conf).contents
332+ self.assertEqual(
333+ "servers ['192.168.2.1', '192.168.2.2']\npools []\n",
334+ content.decode())
335+
336+ def test_write_ntp_config_template_uses_ntp_conf_distro_no_servers(self):
337+ """write_ntp_config_template reads content from ntp.conf.distro.tmpl.
338+
339+ It reads ntp.conf.<distro>.tmpl before attempting ntp.conf.tmpl. It
340+ renders the value from the keys servers and pools. When no
341+ servers value is present, template is rendered using an empty list.
342+ """
343 distro = 'ubuntu'
344- pools = ['0.mycompany.pool.ntp.org']
345- servers = []
346 cfg = {
347- 'ntp': {
348- 'pools': pools,
349- 'servers': servers,
350- }
351+ 'pools': ['10.0.0.1', '10.0.0.2']
352 }
353 mycloud = self._get_cloud(distro)
354-
355- with mock.patch.object(templater, 'render_to_file') as mocktmpl:
356- with mock.patch.object(os.path, 'isfile', return_value=True):
357- with mock.patch.object(util, 'rename'):
358- cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)
359-
360- mocktmpl.assert_called_once_with(
361- ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
362- '/etc/ntp.conf',
363- {'servers': servers, 'pools': pools})
364-
365- def test_ntp_conf_custom_pools_and_server(self):
366+ ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
367+ # Create ntp.conf.tmpl which isn't read
368+ with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
369+ stream.write(b'NOT READ: ntp.conf.<distro>.tmpl is primary')
370+ # Create ntp.conf.tmpl.<distro>
371+ with open('{}.{}.tmpl'.format(ntp_conf, distro), 'wb') as stream:
372+ stream.write(NTP_TEMPLATE)
373+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
374+ cc_ntp.write_ntp_config_template(cfg, mycloud)
375+ content = util.read_file_or_url('file://' + ntp_conf).contents
376+ self.assertEqual(
377+ "servers []\npools ['10.0.0.1', '10.0.0.2']\n",
378+ content.decode())
379+
380+ def test_write_ntp_config_template_defaults_pools_when_empty_lists(self):
381+ """write_ntp_config_template defaults pools servers upon empty config.
382+
383+ When both pools and servers are empty, default NR_POOL_SERVERS get
384+ configured.
385+ """
386 distro = 'ubuntu'
387- pools = ['0.mycompany.pool.ntp.org']
388- servers = ['192.168.23.3']
389- cfg = {
390- 'ntp': {
391- 'pools': pools,
392- 'servers': servers,
393- }
394- }
395 mycloud = self._get_cloud(distro)
396-
397- with mock.patch.object(templater, 'render_to_file') as mocktmpl:
398- with mock.patch.object(os.path, 'isfile', return_value=True):
399- with mock.patch.object(util, 'rename'):
400- cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)
401-
402- mocktmpl.assert_called_once_with(
403- ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
404- '/etc/ntp.conf',
405- {'servers': servers, 'pools': pools})
406-
407- def test_ntp_conf_contents_match(self):
408- """Test rendered contents of /etc/ntp.conf for ubuntu"""
409- pools = ['0.mycompany.pool.ntp.org']
410- servers = ['192.168.23.3']
411+ ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
412+ # Create ntp.conf.tmpl
413+ with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
414+ stream.write(NTP_TEMPLATE)
415+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
416+ cc_ntp.write_ntp_config_template({}, mycloud)
417+ content = util.read_file_or_url('file://' + ntp_conf).contents
418+ default_pools = [
419+ "{}.{}.pool.ntp.org".format(x, distro)
420+ for x in range(0, cc_ntp.NR_POOL_SERVERS)]
421+ self.assertEqual(
422+ "servers []\npools {}\n".format(default_pools),
423+ content.decode())
424+ self.assertIn(
425+ "Adding distro default ntp pool servers: {}".format(
426+ ",".join(default_pools)),
427+ self.logs.getvalue())
428+
429+ def test_ntp_handler_mocked_template(self):
430+ """Test ntp handler renders ubuntu ntp.conf template."""
431+ pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org']
432+ servers = ['192.168.23.3', '192.168.23.4']
433 cfg = {
434 'ntp': {
435 'pools': pools,
436- 'servers': servers,
437+ 'servers': servers
438 }
439 }
440 mycloud = self._get_cloud('ubuntu')
441- side_effect = [NTP_TEMPLATE.lstrip()]
442-
443- # work backwards from util.write_file and mock out call path
444- # write_ntp_config_template()
445- # cloud.get_template_filename()
446- # os.path.isfile()
447- # templater.render_to_file()
448- # templater.render_from_file()
449- # util.load_file()
450- # util.write_file()
451- #
452- with mock.patch.object(util, 'write_file') as mockwrite:
453- with mock.patch.object(util, 'load_file', side_effect=side_effect):
454- with mock.patch.object(os.path, 'isfile', return_value=True):
455- with mock.patch.object(util, 'rename'):
456- cc_ntp.write_ntp_config_template(cfg.get('ntp'),
457- mycloud)
458-
459- mockwrite.assert_called_once_with(
460- '/etc/ntp.conf',
461- NTP_EXPECTED_UBUNTU,
462- mode=420)
463-
464- def test_ntp_handler(self):
465- """Test ntp handler renders ubuntu ntp.conf template"""
466- pools = ['0.mycompany.pool.ntp.org']
467- servers = ['192.168.23.3']
468+ ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
469+ # Create ntp.conf.tmpl
470+ with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
471+ stream.write(NTP_TEMPLATE)
472+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
473+ with mock.patch.object(util, 'which', return_value=None):
474+ cc_ntp.handle('notimportant', cfg, mycloud, None, None)
475+
476+ content = util.read_file_or_url('file://' + ntp_conf).contents
477+ self.assertEqual(
478+ 'servers {}\npools {}\n'.format(servers, pools),
479+ content.decode())
480+
481+ def test_ntp_handler_real_distro_templates(self):
482+ """Test ntp handler renders the shipped distro ntp.conf templates."""
483+ pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org']
484+ servers = ['192.168.23.3', '192.168.23.4']
485 cfg = {
486 'ntp': {
487 'pools': pools,
488- 'servers': servers,
489+ 'servers': servers
490 }
491 }
492- mycloud = self._get_cloud('ubuntu')
493- mycloud.distro = mock.MagicMock()
494- mycloud.distro.uses_systemd.return_value = True
495- side_effect = [NTP_TEMPLATE.lstrip()]
496-
497- with mock.patch.object(util, 'which', return_value=None):
498- with mock.patch.object(os.path, 'exists'):
499- with mock.patch.object(util, 'write_file') as mockwrite:
500- with mock.patch.object(util, 'load_file',
501- side_effect=side_effect):
502- with mock.patch.object(os.path, 'isfile',
503- return_value=True):
504- with mock.patch.object(util, 'rename'):
505- with mock.patch.object(util, 'subp') as msubp:
506- cc_ntp.handle("notimportant", cfg,
507- mycloud, LOG, None)
508-
509- mockwrite.assert_called_once_with(
510- '/etc/ntp.conf',
511- NTP_EXPECTED_UBUNTU,
512- mode=420)
513- msubp.assert_any_call(['systemctl', 'reload-or-restart', 'ntp'],
514- capture=True)
515-
516- @mock.patch("cloudinit.config.cc_ntp.install_ntp")
517- @mock.patch("cloudinit.config.cc_ntp.write_ntp_config_template")
518- @mock.patch("cloudinit.config.cc_ntp.rename_ntp_conf")
519- def test_write_config_before_install(self, mock_ntp_rename,
520- mock_ntp_write_config,
521- mock_install_ntp):
522- pools = ['0.mycompany.pool.ntp.org']
523- servers = ['192.168.23.3']
524- cfg = {
525- 'ntp': {
526- 'pools': pools,
527- 'servers': servers,
528- }
529- }
530- cc = self._get_cloud('ubuntu')
531- cc.distro = mock.MagicMock()
532- mock_parent = mock.MagicMock()
533- mock_parent.attach_mock(mock_ntp_rename, 'mock_ntp_rename')
534- mock_parent.attach_mock(mock_ntp_write_config, 'mock_ntp_write_config')
535- mock_parent.attach_mock(mock_install_ntp, 'mock_install_ntp')
536-
537- cc_ntp.handle('cc_ntp', cfg, cc, LOG, None)
538-
539- """Check call order"""
540- mock_parent.assert_has_calls([
541- mock.call.mock_ntp_rename(),
542- mock.call.mock_ntp_write_config(cfg.get('ntp'), cc),
543- mock.call.mock_install_ntp(cc.distro.install_packages,
544- packages=['ntp'], check_exe="ntpd")])
545-
546- @mock.patch("cloudinit.config.cc_ntp.reload_ntp")
547- @mock.patch("cloudinit.config.cc_ntp.install_ntp")
548- @mock.patch("cloudinit.config.cc_ntp.write_ntp_config_template")
549- @mock.patch("cloudinit.config.cc_ntp.rename_ntp_conf")
550- def test_reload_ntp_fail_raises_exception(self, mock_rename,
551- mock_write_conf,
552- mock_install,
553- mock_reload):
554- pools = ['0.mycompany.pool.ntp.org']
555- servers = ['192.168.23.3']
556- cfg = {
557- 'ntp': {
558- 'pools': pools,
559- 'servers': servers,
560- }
561- }
562- cc = self._get_cloud('ubuntu')
563- cc.distro = mock.MagicMock()
564-
565- mock_reload.side_effect = [util.ProcessExecutionError]
566- self.assertRaises(util.ProcessExecutionError,
567- cc_ntp.handle, 'cc_ntp',
568- cfg, cc, LOG, None)
569-
570- @mock.patch("cloudinit.config.cc_ntp.util")
571- def test_no_ntpcfg_does_nothing(self, mock_util):
572- cc = self._get_cloud('ubuntu')
573- cc.distro = mock.MagicMock()
574- cc_ntp.handle('cc_ntp', {}, cc, LOG, [])
575- self.assertFalse(cc.distro.install_packages.called)
576- self.assertFalse(mock_util.subp.called)
577-
578- @mock.patch("cloudinit.config.cc_ntp.util")
579- def test_reload_ntp_systemd(self, mock_util):
580- cc_ntp.reload_ntp(systemd=True)
581- self.assertTrue(mock_util.subp.called)
582- mock_util.subp.assert_called_with(
583- ['systemctl', 'reload-or-restart', 'ntp'], capture=True)
584-
585- @mock.patch("cloudinit.config.cc_ntp.util")
586- def test_reload_ntp_service(self, mock_util):
587- cc_ntp.reload_ntp(systemd=False)
588- self.assertTrue(mock_util.subp.called)
589- mock_util.subp.assert_called_with(
590- ['service', 'ntp', 'restart'], capture=True)
591-
592+ ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
593+ for distro in ('debian', 'ubuntu', 'fedora', 'rhel', 'sles'):
594+ mycloud = self._get_cloud(distro)
595+ root_dir = dirname(dirname(os.path.realpath(util.__file__)))
596+ tmpl_file = os.path.join(
597+ '{}/templates/ntp.conf.{}.tmpl'.format(root_dir, distro))
598+ # Create a copy in our tmp_dir
599+ shutil.copy(
600+ tmpl_file,
601+ os.path.join(self.new_root, 'ntp.conf.%s.tmpl' % distro))
602+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
603+ with mock.patch.object(util, 'which', return_value=[True]):
604+ cc_ntp.handle('notimportant', cfg, mycloud, None, None)
605+
606+ content = util.read_file_or_url('file://' + ntp_conf).contents
607+ expected_servers = '\n'.join([
608+ 'server {} iburst'.format(server) for server in servers])
609+ self.assertIn(
610+ expected_servers, content.decode(),
611+ 'failed to render ntp.conf for distro:{}'.format(distro))
612+ expected_pools = '\n'.join([
613+ 'pool {} iburst'.format(pool) for pool in pools])
614+ self.assertIn(
615+ expected_pools, content.decode(),
616+ 'failed to render ntp.conf for distro:{}'.format(distro))
617+
618+ def test_no_ntpcfg_does_nothing(self):
619+ """When no ntp section is defined handler logs a warning and noops."""
620+ cc_ntp.handle('cc_ntp', {}, None, None, [])
621+ self.assertEqual(
622+ 'Skipping module named cc_ntp, not present or disabled by cfg\n',
623+ self.logs.getvalue())
624
625 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches