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
diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
index 225f898..5cc5453 100644
--- a/cloudinit/config/cc_ntp.py
+++ b/cloudinit/config/cc_ntp.py
@@ -53,14 +53,12 @@ distros = ['centos', 'debian', 'fedora', 'opensuse', 'ubuntu']
5353
5454
55def handle(name, cfg, cloud, log, _args):55def handle(name, cfg, cloud, log, _args):
56 """56 """Enable and configure ntp."""
57 Enable and configure ntp
5857
59 ntp:58 if 'ntp' not in cfg:
60 pools: ['0.{{distro}}.pool.ntp.org', '1.{{distro}}.pool.ntp.org']59 LOG.debug(
61 servers: ['192.168.2.1']60 "Skipping module named %s, not present or disabled by cfg", name)
6261 return
63 """
6462
65 ntp_cfg = cfg.get('ntp', {})63 ntp_cfg = cfg.get('ntp', {})
6664
@@ -69,18 +67,12 @@ def handle(name, cfg, cloud, log, _args):
69 " but not a dictionary type,"67 " but not a dictionary type,"
70 " is a %s %instead"), type_utils.obj_name(ntp_cfg))68 " is a %s %instead"), type_utils.obj_name(ntp_cfg))
7169
72 if 'ntp' not in cfg:
73 LOG.debug("Skipping module named %s,"
74 "not present or disabled by cfg", name)
75 return True
76
77 rename_ntp_conf()70 rename_ntp_conf()
78 # ensure when ntp is installed it has a configuration file71 # ensure when ntp is installed it has a configuration file
79 # to use instead of starting up with packaged defaults72 # to use instead of starting up with packaged defaults
80 write_ntp_config_template(ntp_cfg, cloud)73 write_ntp_config_template(ntp_cfg, cloud)
81 install_ntp(cloud.distro.install_packages, packages=['ntp'],74 install_ntp(cloud.distro.install_packages, packages=['ntp'],
82 check_exe="ntpd")75 check_exe="ntpd")
83
84 # if ntp was already installed, it may not have started76 # if ntp was already installed, it may not have started
85 try:77 try:
86 reload_ntp(systemd=cloud.distro.uses_systemd())78 reload_ntp(systemd=cloud.distro.uses_systemd())
@@ -98,8 +90,10 @@ def install_ntp(install_func, packages=None, check_exe="ntpd"):
98 install_func(packages)90 install_func(packages)
9991
10092
101def rename_ntp_conf(config=NTP_CONF):93def rename_ntp_conf(config=None):
102 """Rename any existing ntp.conf file and render from template"""94 """Rename any existing ntp.conf file and render from template"""
95 if config is None: # For testing
96 config = NTP_CONF
103 if os.path.exists(config):97 if os.path.exists(config):
104 util.rename(config, config + ".dist")98 util.rename(config, config + ".dist")
10599
@@ -117,8 +111,9 @@ def write_ntp_config_template(cfg, cloud):
117 pools = cfg.get('pools', [])111 pools = cfg.get('pools', [])
118112
119 if len(servers) == 0 and len(pools) == 0:113 if len(servers) == 0 and len(pools) == 0:
120 LOG.debug('Adding distro default ntp pool servers')
121 pools = generate_server_names(cloud.distro.name)114 pools = generate_server_names(cloud.distro.name)
115 LOG.debug(
116 'Adding distro default ntp pool servers: %s', ','.join(pools))
122117
123 params = {118 params = {
124 'servers': servers,119 'servers': servers,
diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
index d24f817..9ff1599 100644
--- a/tests/unittests/helpers.py
+++ b/tests/unittests/helpers.py
@@ -4,6 +4,7 @@ from __future__ import print_function
44
5import functools5import functools
6import json6import json
7import logging
7import os8import os
8import shutil9import shutil
9import sys10import sys
@@ -18,6 +19,10 @@ try:
18 from contextlib import ExitStack19 from contextlib import ExitStack
19except ImportError:20except ImportError:
20 from contextlib2 import ExitStack21 from contextlib2 import ExitStack
22try:
23 from cStringIO import StringIO
24except ImportError:
25 from io import StringIO
2126
22from cloudinit import helpers as ch27from cloudinit import helpers as ch
23from cloudinit import util28from cloudinit import util
@@ -87,6 +92,27 @@ class TestCase(unittest2.TestCase):
87class CiTestCase(TestCase):92class CiTestCase(TestCase):
88 """This is the preferred test case base class unless user93 """This is the preferred test case base class unless user
89 needs other test case classes below."""94 needs other test case classes below."""
95
96 # Subclass overrides for specific test behavior
97 # Whether or not a unit test needs logfile setup
98 with_logs = False
99
100 def setUp(self):
101 super(CiTestCase, self).setUp()
102 if self.with_logs:
103 # Create a log handler so unit tests can search expected logs.
104 logger = logging.getLogger()
105 self.logs = StringIO()
106 handler = logging.StreamHandler(self.logs)
107 self.old_handlers = logger.handlers
108 logger.handlers = [handler]
109
110 def tearDown(self):
111 if self.with_logs:
112 # Remove the handler we setup
113 logging.getLogger().handlers = self.old_handlers
114 super(CiTestCase, self).tearDown()
115
90 def tmp_dir(self, dir=None, cleanup=True):116 def tmp_dir(self, dir=None, cleanup=True):
91 # return a full path to a temporary directory that will be cleaned up.117 # return a full path to a temporary directory that will be cleaned up.
92 if dir is None:118 if dir is None:
diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
index 02bd304..21f2ab1 100644
--- a/tests/unittests/test_handler/test_handler_ntp.py
+++ b/tests/unittests/test_handler/test_handler_ntp.py
@@ -2,351 +2,214 @@
22
3from cloudinit.config import cc_ntp3from cloudinit.config import cc_ntp
4from cloudinit.sources import DataSourceNone4from cloudinit.sources import DataSourceNone
5from cloudinit import templater
6from cloudinit import (distros, helpers, cloud, util)5from cloudinit import (distros, helpers, cloud, util)
7from ..helpers import FilesystemMockingTestCase, mock6from ..helpers import FilesystemMockingTestCase, mock
87
9import logging8
10import os9import os
10from os.path import dirname
11import shutil11import shutil
12import tempfile
13
14LOG = logging.getLogger(__name__)
1512
16NTP_TEMPLATE = """13NTP_TEMPLATE = b"""\
17## template: jinja14## template: jinja
1815servers {{servers}}
19{% if pools %}# pools16pools {{pools}}
20{% endif %}
21{% for pool in pools -%}
22pool {{pool}} iburst
23{% endfor %}
24{%- if servers %}# servers
25{% endif %}
26{% for server in servers -%}
27server {{server}} iburst
28{% endfor %}
29
30"""
31
32
33NTP_EXPECTED_UBUNTU = """
34# pools
35pool 0.mycompany.pool.ntp.org iburst
36# servers
37server 192.168.23.3 iburst
38
39"""17"""
4018
4119
42class TestNtp(FilesystemMockingTestCase):20class TestNtp(FilesystemMockingTestCase):
4321
22 with_logs = True
23
44 def setUp(self):24 def setUp(self):
45 super(TestNtp, self).setUp()25 super(TestNtp, self).setUp()
46 self.subp = util.subp26 self.subp = util.subp
47 self.new_root = tempfile.mkdtemp()27 self.new_root = self.tmp_dir()
48 self.addCleanup(shutil.rmtree, self.new_root)
4928
50 def _get_cloud(self, distro, metadata=None):29 def _get_cloud(self, distro):
51 self.patchUtils(self.new_root)30 self.patchUtils(self.new_root)
52 paths = helpers.Paths({})31 paths = helpers.Paths({'templates_dir': self.new_root})
53 cls = distros.fetch(distro)32 cls = distros.fetch(distro)
54 mydist = cls(distro, {}, paths)33 mydist = cls(distro, {}, paths)
55 myds = DataSourceNone.DataSourceNone({}, mydist, paths)34 myds = DataSourceNone.DataSourceNone({}, mydist, paths)
56 if metadata:
57 myds.metadata.update(metadata)
58 return cloud.Cloud(myds, paths, {}, mydist, None)35 return cloud.Cloud(myds, paths, {}, mydist, None)
5936
60 @mock.patch("cloudinit.config.cc_ntp.util")37 @mock.patch("cloudinit.config.cc_ntp.util")
61 def test_ntp_install(self, mock_util):38 def test_ntp_install(self, mock_util):
62 cc = self._get_cloud('ubuntu')39 """ntp_install installs via install_func when check_exe is absent."""
63 cc.distro = mock.MagicMock()40 mock_util.which.return_value = None # check_exe not found.
64 cc.distro.name = 'ubuntu'
65 mock_util.which.return_value = None
66 install_func = mock.MagicMock()41 install_func = mock.MagicMock()
67
68 cc_ntp.install_ntp(install_func, packages=['ntpx'], check_exe='ntpdx')42 cc_ntp.install_ntp(install_func, packages=['ntpx'], check_exe='ntpdx')
6943
70 self.assertTrue(install_func.called)
71 mock_util.which.assert_called_with('ntpdx')44 mock_util.which.assert_called_with('ntpdx')
72 install_pkg = install_func.call_args_list[0][0][0]45 install_func.assert_called_once_with(['ntpx'])
73 self.assertEqual(sorted(install_pkg), ['ntpx'])
7446
75 @mock.patch("cloudinit.config.cc_ntp.util")47 @mock.patch("cloudinit.config.cc_ntp.util")
76 def test_ntp_install_not_needed(self, mock_util):48 def test_ntp_install_not_needed(self, mock_util):
77 cc = self._get_cloud('ubuntu')49 """ntp_install doesn't attempt install when check_exe is found."""
78 cc.distro = mock.MagicMock()50 mock_util.which.return_value = ["/usr/sbin/ntpd"] # check_exe found.
79 cc.distro.name = 'ubuntu'51 install_func = mock.MagicMock()
80 mock_util.which.return_value = ["/usr/sbin/ntpd"]52 cc_ntp.install_ntp(install_func, packages=['ntp'], check_exe='ntpd')
81 cc_ntp.install_ntp(cc)53 install_func.assert_not_called()
82 self.assertFalse(cc.distro.install_packages.called)
8354
84 def test_ntp_rename_ntp_conf(self):55 def test_ntp_rename_ntp_conf(self):
85 with mock.patch.object(os.path, 'exists',56 """When NTP_CONF exists, rename_ntp moves it."""
86 return_value=True) as mockpath:57 ntpconf = self.tmp_path("ntp.conf", self.new_root)
87 with mock.patch.object(util, 'rename') as mockrename:58 os.mknod(ntpconf)
88 cc_ntp.rename_ntp_conf()59 with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf):
8960 cc_ntp.rename_ntp_conf()
90 mockpath.assert_called_with('/etc/ntp.conf')61 self.assertFalse(os.path.exists(ntpconf))
91 mockrename.assert_called_with('/etc/ntp.conf', '/etc/ntp.conf.dist')62 self.assertTrue(os.path.exists("{}.dist".format(ntpconf)))
9263
93 def test_ntp_rename_ntp_conf_skip_missing(self):64 def test_ntp_rename_ntp_conf_skip_missing(self):
94 with mock.patch.object(os.path, 'exists',65 """When NTP_CONF doesn't exist rename_ntp doesn't create a file."""
95 return_value=False) as mockpath:66 ntpconf = self.tmp_path("ntp.conf", self.new_root)
96 with mock.patch.object(util, 'rename') as mockrename:67 self.assertFalse(os.path.exists(ntpconf))
97 cc_ntp.rename_ntp_conf()68 with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf):
9869 cc_ntp.rename_ntp_conf()
99 mockpath.assert_called_with('/etc/ntp.conf')70 self.assertFalse(os.path.exists("{}.dist".format(ntpconf)))
100 mockrename.assert_not_called()71 self.assertFalse(os.path.exists(ntpconf))
10172
102 def ntp_conf_render(self, distro):73 def test_write_ntp_config_template_from_ntp_conf_tmpl_with_servers(self):
103 """ntp_conf_render74 """write_ntp_config_template reads content from ntp.conf.tmpl.
104 Test rendering of a ntp.conf from template for a given distro75
76 It reads ntp.conf.tmpl if present and renders the value from servers
77 key. When no pools key is defined, template is rendered using an empty
78 list for pools.
105 """79 """
106
107 cfg = {'ntp': {}}
108 mycloud = self._get_cloud(distro)
109 distro_names = cc_ntp.generate_server_names(distro)
110
111 with mock.patch.object(templater, 'render_to_file') as mocktmpl:
112 with mock.patch.object(os.path, 'isfile', return_value=True):
113 with mock.patch.object(util, 'rename'):
114 cc_ntp.write_ntp_config_template(cfg, mycloud)
115
116 mocktmpl.assert_called_once_with(
117 ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
118 '/etc/ntp.conf',
119 {'servers': [], 'pools': distro_names})
120
121 def test_ntp_conf_render_rhel(self):
122 """Test templater.render_to_file() for rhel"""
123 self.ntp_conf_render('rhel')
124
125 def test_ntp_conf_render_debian(self):
126 """Test templater.render_to_file() for debian"""
127 self.ntp_conf_render('debian')
128
129 def test_ntp_conf_render_fedora(self):
130 """Test templater.render_to_file() for fedora"""
131 self.ntp_conf_render('fedora')
132
133 def test_ntp_conf_render_sles(self):
134 """Test templater.render_to_file() for sles"""
135 self.ntp_conf_render('sles')
136
137 def test_ntp_conf_render_ubuntu(self):
138 """Test templater.render_to_file() for ubuntu"""
139 self.ntp_conf_render('ubuntu')
140
141 def test_ntp_conf_servers_no_pools(self):
142 distro = 'ubuntu'80 distro = 'ubuntu'
143 pools = []
144 servers = ['192.168.2.1']
145 cfg = {81 cfg = {
146 'ntp': {82 'servers': ['192.168.2.1', '192.168.2.2']
147 'pools': pools,
148 'servers': servers,
149 }
150 }83 }
151 mycloud = self._get_cloud(distro)84 mycloud = self._get_cloud(distro)
15285 ntp_conf = self.tmp_path("ntp.conf", self.new_root) # Doesn't exist
153 with mock.patch.object(templater, 'render_to_file') as mocktmpl:86 # Create ntp.conf.tmpl
154 with mock.patch.object(os.path, 'isfile', return_value=True):87 with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
155 with mock.patch.object(util, 'rename'):88 stream.write(NTP_TEMPLATE)
156 cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)89 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
15790 cc_ntp.write_ntp_config_template(cfg, mycloud)
158 mocktmpl.assert_called_once_with(91 content = util.read_file_or_url('file://' + ntp_conf).contents
159 ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),92 self.assertEqual(
160 '/etc/ntp.conf',93 "servers ['192.168.2.1', '192.168.2.2']\npools []\n",
161 {'servers': servers, 'pools': pools})94 content.decode())
16295
163 def test_ntp_conf_custom_pools_no_server(self):96 def test_write_ntp_config_template_uses_ntp_conf_distro_no_servers(self):
97 """write_ntp_config_template reads content from ntp.conf.distro.tmpl.
98
99 It reads ntp.conf.<distro>.tmpl before attempting ntp.conf.tmpl. It
100 renders the value from the keys servers and pools. When no
101 servers value is present, template is rendered using an empty list.
102 """
164 distro = 'ubuntu'103 distro = 'ubuntu'
165 pools = ['0.mycompany.pool.ntp.org']
166 servers = []
167 cfg = {104 cfg = {
168 'ntp': {105 'pools': ['10.0.0.1', '10.0.0.2']
169 'pools': pools,
170 'servers': servers,
171 }
172 }106 }
173 mycloud = self._get_cloud(distro)107 mycloud = self._get_cloud(distro)
174108 ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
175 with mock.patch.object(templater, 'render_to_file') as mocktmpl:109 # Create ntp.conf.tmpl which isn't read
176 with mock.patch.object(os.path, 'isfile', return_value=True):110 with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
177 with mock.patch.object(util, 'rename'):111 stream.write(b'NOT READ: ntp.conf.<distro>.tmpl is primary')
178 cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)112 # Create ntp.conf.tmpl.<distro>
179113 with open('{}.{}.tmpl'.format(ntp_conf, distro), 'wb') as stream:
180 mocktmpl.assert_called_once_with(114 stream.write(NTP_TEMPLATE)
181 ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),115 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
182 '/etc/ntp.conf',116 cc_ntp.write_ntp_config_template(cfg, mycloud)
183 {'servers': servers, 'pools': pools})117 content = util.read_file_or_url('file://' + ntp_conf).contents
184118 self.assertEqual(
185 def test_ntp_conf_custom_pools_and_server(self):119 "servers []\npools ['10.0.0.1', '10.0.0.2']\n",
120 content.decode())
121
122 def test_write_ntp_config_template_defaults_pools_when_empty_lists(self):
123 """write_ntp_config_template defaults pools servers upon empty config.
124
125 When both pools and servers are empty, default NR_POOL_SERVERS get
126 configured.
127 """
186 distro = 'ubuntu'128 distro = 'ubuntu'
187 pools = ['0.mycompany.pool.ntp.org']
188 servers = ['192.168.23.3']
189 cfg = {
190 'ntp': {
191 'pools': pools,
192 'servers': servers,
193 }
194 }
195 mycloud = self._get_cloud(distro)129 mycloud = self._get_cloud(distro)
196130 ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
197 with mock.patch.object(templater, 'render_to_file') as mocktmpl:131 # Create ntp.conf.tmpl
198 with mock.patch.object(os.path, 'isfile', return_value=True):132 with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
199 with mock.patch.object(util, 'rename'):133 stream.write(NTP_TEMPLATE)
200 cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)134 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
201135 cc_ntp.write_ntp_config_template({}, mycloud)
202 mocktmpl.assert_called_once_with(136 content = util.read_file_or_url('file://' + ntp_conf).contents
203 ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),137 default_pools = [
204 '/etc/ntp.conf',138 "{}.{}.pool.ntp.org".format(x, distro)
205 {'servers': servers, 'pools': pools})139 for x in range(0, cc_ntp.NR_POOL_SERVERS)]
206140 self.assertEqual(
207 def test_ntp_conf_contents_match(self):141 "servers []\npools {}\n".format(default_pools),
208 """Test rendered contents of /etc/ntp.conf for ubuntu"""142 content.decode())
209 pools = ['0.mycompany.pool.ntp.org']143 self.assertIn(
210 servers = ['192.168.23.3']144 "Adding distro default ntp pool servers: {}".format(
145 ",".join(default_pools)),
146 self.logs.getvalue())
147
148 def test_ntp_handler_mocked_template(self):
149 """Test ntp handler renders ubuntu ntp.conf template."""
150 pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org']
151 servers = ['192.168.23.3', '192.168.23.4']
211 cfg = {152 cfg = {
212 'ntp': {153 'ntp': {
213 'pools': pools,154 'pools': pools,
214 'servers': servers,155 'servers': servers
215 }156 }
216 }157 }
217 mycloud = self._get_cloud('ubuntu')158 mycloud = self._get_cloud('ubuntu')
218 side_effect = [NTP_TEMPLATE.lstrip()]159 ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
219160 # Create ntp.conf.tmpl
220 # work backwards from util.write_file and mock out call path161 with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
221 # write_ntp_config_template()162 stream.write(NTP_TEMPLATE)
222 # cloud.get_template_filename()163 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
223 # os.path.isfile()164 with mock.patch.object(util, 'which', return_value=None):
224 # templater.render_to_file()165 cc_ntp.handle('notimportant', cfg, mycloud, None, None)
225 # templater.render_from_file()166
226 # util.load_file()167 content = util.read_file_or_url('file://' + ntp_conf).contents
227 # util.write_file()168 self.assertEqual(
228 #169 'servers {}\npools {}\n'.format(servers, pools),
229 with mock.patch.object(util, 'write_file') as mockwrite:170 content.decode())
230 with mock.patch.object(util, 'load_file', side_effect=side_effect):171
231 with mock.patch.object(os.path, 'isfile', return_value=True):172 def test_ntp_handler_real_distro_templates(self):
232 with mock.patch.object(util, 'rename'):173 """Test ntp handler renders the shipped distro ntp.conf templates."""
233 cc_ntp.write_ntp_config_template(cfg.get('ntp'),174 pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org']
234 mycloud)175 servers = ['192.168.23.3', '192.168.23.4']
235
236 mockwrite.assert_called_once_with(
237 '/etc/ntp.conf',
238 NTP_EXPECTED_UBUNTU,
239 mode=420)
240
241 def test_ntp_handler(self):
242 """Test ntp handler renders ubuntu ntp.conf template"""
243 pools = ['0.mycompany.pool.ntp.org']
244 servers = ['192.168.23.3']
245 cfg = {176 cfg = {
246 'ntp': {177 'ntp': {
247 'pools': pools,178 'pools': pools,
248 'servers': servers,179 'servers': servers
249 }180 }
250 }181 }
251 mycloud = self._get_cloud('ubuntu')182 ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
252 mycloud.distro = mock.MagicMock()183 for distro in ('debian', 'ubuntu', 'fedora', 'rhel', 'sles'):
253 mycloud.distro.uses_systemd.return_value = True184 mycloud = self._get_cloud(distro)
254 side_effect = [NTP_TEMPLATE.lstrip()]185 root_dir = dirname(dirname(os.path.realpath(util.__file__)))
255186 tmpl_file = os.path.join(
256 with mock.patch.object(util, 'which', return_value=None):187 '{}/templates/ntp.conf.{}.tmpl'.format(root_dir, distro))
257 with mock.patch.object(os.path, 'exists'):188 # Create a copy in our tmp_dir
258 with mock.patch.object(util, 'write_file') as mockwrite:189 shutil.copy(
259 with mock.patch.object(util, 'load_file',190 tmpl_file,
260 side_effect=side_effect):191 os.path.join(self.new_root, 'ntp.conf.%s.tmpl' % distro))
261 with mock.patch.object(os.path, 'isfile',192 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
262 return_value=True):193 with mock.patch.object(util, 'which', return_value=[True]):
263 with mock.patch.object(util, 'rename'):194 cc_ntp.handle('notimportant', cfg, mycloud, None, None)
264 with mock.patch.object(util, 'subp') as msubp:195
265 cc_ntp.handle("notimportant", cfg,196 content = util.read_file_or_url('file://' + ntp_conf).contents
266 mycloud, LOG, None)197 expected_servers = '\n'.join([
267198 'server {} iburst'.format(server) for server in servers])
268 mockwrite.assert_called_once_with(199 self.assertIn(
269 '/etc/ntp.conf',200 expected_servers, content.decode(),
270 NTP_EXPECTED_UBUNTU,201 'failed to render ntp.conf for distro:{}'.format(distro))
271 mode=420)202 expected_pools = '\n'.join([
272 msubp.assert_any_call(['systemctl', 'reload-or-restart', 'ntp'],203 'pool {} iburst'.format(pool) for pool in pools])
273 capture=True)204 self.assertIn(
274205 expected_pools, content.decode(),
275 @mock.patch("cloudinit.config.cc_ntp.install_ntp")206 'failed to render ntp.conf for distro:{}'.format(distro))
276 @mock.patch("cloudinit.config.cc_ntp.write_ntp_config_template")207
277 @mock.patch("cloudinit.config.cc_ntp.rename_ntp_conf")208 def test_no_ntpcfg_does_nothing(self):
278 def test_write_config_before_install(self, mock_ntp_rename,209 """When no ntp section is defined handler logs a warning and noops."""
279 mock_ntp_write_config,210 cc_ntp.handle('cc_ntp', {}, None, None, [])
280 mock_install_ntp):211 self.assertEqual(
281 pools = ['0.mycompany.pool.ntp.org']212 'Skipping module named cc_ntp, not present or disabled by cfg\n',
282 servers = ['192.168.23.3']213 self.logs.getvalue())
283 cfg = {
284 'ntp': {
285 'pools': pools,
286 'servers': servers,
287 }
288 }
289 cc = self._get_cloud('ubuntu')
290 cc.distro = mock.MagicMock()
291 mock_parent = mock.MagicMock()
292 mock_parent.attach_mock(mock_ntp_rename, 'mock_ntp_rename')
293 mock_parent.attach_mock(mock_ntp_write_config, 'mock_ntp_write_config')
294 mock_parent.attach_mock(mock_install_ntp, 'mock_install_ntp')
295
296 cc_ntp.handle('cc_ntp', cfg, cc, LOG, None)
297
298 """Check call order"""
299 mock_parent.assert_has_calls([
300 mock.call.mock_ntp_rename(),
301 mock.call.mock_ntp_write_config(cfg.get('ntp'), cc),
302 mock.call.mock_install_ntp(cc.distro.install_packages,
303 packages=['ntp'], check_exe="ntpd")])
304
305 @mock.patch("cloudinit.config.cc_ntp.reload_ntp")
306 @mock.patch("cloudinit.config.cc_ntp.install_ntp")
307 @mock.patch("cloudinit.config.cc_ntp.write_ntp_config_template")
308 @mock.patch("cloudinit.config.cc_ntp.rename_ntp_conf")
309 def test_reload_ntp_fail_raises_exception(self, mock_rename,
310 mock_write_conf,
311 mock_install,
312 mock_reload):
313 pools = ['0.mycompany.pool.ntp.org']
314 servers = ['192.168.23.3']
315 cfg = {
316 'ntp': {
317 'pools': pools,
318 'servers': servers,
319 }
320 }
321 cc = self._get_cloud('ubuntu')
322 cc.distro = mock.MagicMock()
323
324 mock_reload.side_effect = [util.ProcessExecutionError]
325 self.assertRaises(util.ProcessExecutionError,
326 cc_ntp.handle, 'cc_ntp',
327 cfg, cc, LOG, None)
328
329 @mock.patch("cloudinit.config.cc_ntp.util")
330 def test_no_ntpcfg_does_nothing(self, mock_util):
331 cc = self._get_cloud('ubuntu')
332 cc.distro = mock.MagicMock()
333 cc_ntp.handle('cc_ntp', {}, cc, LOG, [])
334 self.assertFalse(cc.distro.install_packages.called)
335 self.assertFalse(mock_util.subp.called)
336
337 @mock.patch("cloudinit.config.cc_ntp.util")
338 def test_reload_ntp_systemd(self, mock_util):
339 cc_ntp.reload_ntp(systemd=True)
340 self.assertTrue(mock_util.subp.called)
341 mock_util.subp.assert_called_with(
342 ['systemctl', 'reload-or-restart', 'ntp'], capture=True)
343
344 @mock.patch("cloudinit.config.cc_ntp.util")
345 def test_reload_ntp_service(self, mock_util):
346 cc_ntp.reload_ntp(systemd=False)
347 self.assertTrue(mock_util.subp.called)
348 mock_util.subp.assert_called_with(
349 ['service', 'ntp', 'restart'], capture=True)
350
351214
352# vi: ts=4 expandtab215# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches