cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #02274
Re: [Merge] ~chad.smith/cloud-init:cc-ntp-testing into cloud-init:master
@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?
Diff comments:
> diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
> index 225f898..257bb40 100644
> --- a/cloudinit/config/cc_ntp.py
> +++ b/cloudinit/config/cc_ntp.py
> @@ -53,14 +53,12 @@ distros = ['centos', 'debian', 'fedora', 'opensuse', 'ubuntu']
>
>
> def handle(name, cfg, cloud, log, _args):
> - """
> - Enable and configure ntp
> + """Enable and configure ntp."""
>
> - ntp:
> - pools: ['0.{{distro}}.pool.ntp.org', '1.{{distro}}.pool.ntp.org']
> - servers: ['192.168.2.1']
> -
> - """
> + if 'ntp' not in cfg:
This is the same logic unchanged which was below, just hoisted up to the top of the module. It would process {"ntp": {}} and install ntp. The ntp_cfg local variable we create just below isn't actually referenced by the original check.
In this branch, and the followup cc-ntp-schema branch, we are allowing any "ntp" key of any value in cfg. I understood from Ryan a couple days ago in IRC that we actually want to allow for an empty ntp: configuration. The expected behavior in this case would be to install ntp and leave it with the default 4 pool servers. This is also defined in the schema more strictly in the cc-ntp-schema followup branch which declares "ntp" key as:
'ntp': {
'type': ['object', 'null'], # allow for a defined map or no definition.
> + LOG.debug(
> + "Skipping module named %s, not present or disabled by cfg", name)
> + return
>
> ntp_cfg = cfg.get('ntp', {})
>
> @@ -69,18 +67,12 @@ def handle(name, cfg, cloud, log, _args):
> " but not a dictionary type,"
> " is a %s %instead"), type_utils.obj_name(ntp_cfg))
>
> - if 'ntp' not in cfg:
> - LOG.debug("Skipping module named %s,"
> - "not present or disabled by cfg", name)
> - return True
> -
> rename_ntp_conf()
> # ensure when ntp is installed it has a configuration file
> # to use instead of starting up with packaged defaults
> - write_ntp_config_template(ntp_cfg, cloud)
> + write_ntp_config_template(cfg['ntp'], cloud)
fixed.
> install_ntp(cloud.distro.install_packages, packages=['ntp'],
> check_exe="ntpd")
> -
> # if ntp was already installed, it may not have started
> try:
> reload_ntp(systemd=cloud.distro.uses_systemd())
> 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
> @@ -87,6 +92,27 @@ class TestCase(unittest2.TestCase):
> class CiTestCase(TestCase):
> """This is the preferred test case base class unless user
> needs other test case classes below."""
> +
> + # Subclass overrides for specific test behavior
> + # Whether or not a unit test needs logfile setup
> + with_logs = False
> +
> + def setUp(self):
> + super(CiTestCase, self).setUp()
> + if self.with_logs:
> + # Create a log handler so unit tests can search expected logs.
> + logger = logging.getLogger()
> + self.logs = StringIO()
> + handler = logging.StreamHandler(self.logs)
> + self.old_handlers = logger.handlers
> + logger.handlers = [handler]
2m20 seconds on avg for tox vs 1m30 seconds to add logs to all CiTestCase subclasses
> +
> + def tearDown(self):
> + if self.with_logs:
> + # Remove the handler we setup
> + logging.getLogger().handlers = self.old_handlers
> + super(CiTestCase, self).tearDown()
> +
> def tmp_dir(self, dir=None, cleanup=True):
> # return a full path to a temporary directory that will be cleaned up.
> 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..aa6c8ae 100644
> --- a/tests/unittests/test_handler/test_handler_ntp.py
> +++ b/tests/unittests/test_handler/test_handler_ntp.py
> @@ -59,294 +36,148 @@ class TestNtp(FilesystemMockingTestCase):
>
> @mock.patch("cloudinit.config.cc_ntp.util")
> def test_ntp_install(self, mock_util):
> - cc = self._get_cloud('ubuntu')
> - cc.distro = mock.MagicMock()
> - cc.distro.name = 'ubuntu'
> - mock_util.which.return_value = None
> + """ntp_install installs via install_func when check_exe is absent."""
> + mock_util.which.return_value = None # check_exe not found.
> install_func = mock.MagicMock()
> -
> cc_ntp.install_ntp(install_func, packages=['ntpx'], check_exe='ntpdx')
>
> - self.assertTrue(install_func.called)
> mock_util.which.assert_called_with('ntpdx')
> - install_pkg = install_func.call_args_list[0][0][0]
> - self.assertEqual(sorted(install_pkg), ['ntpx'])
> + install_func.assert_called_once_with(['ntpx'])
>
> @mock.patch("cloudinit.config.cc_ntp.util")
> def test_ntp_install_not_needed(self, mock_util):
> - cc = self._get_cloud('ubuntu')
> - cc.distro = mock.MagicMock()
> - cc.distro.name = 'ubuntu'
> - mock_util.which.return_value = ["/usr/sbin/ntpd"]
> - cc_ntp.install_ntp(cc)
> - self.assertFalse(cc.distro.install_packages.called)
> + """ntp_install doesn't attempt install when check_exe is found."""
> + mock_util.which.return_value = ["/usr/sbin/ntpd"] # check_exe found.
> + install_func = mock.MagicMock()
> + cc_ntp.install_ntp(install_func, packages=['ntp'], check_exe='ntpd')
> + install_func.assert_not_called()
>
> def test_ntp_rename_ntp_conf(self):
> - with mock.patch.object(os.path, 'exists',
> - return_value=True) as mockpath:
> - with mock.patch.object(util, 'rename') as mockrename:
> - cc_ntp.rename_ntp_conf()
> -
> - mockpath.assert_called_with('/etc/ntp.conf')
> - mockrename.assert_called_with('/etc/ntp.conf', '/etc/ntp.conf.dist')
> + """When NTP_CONF exists, rename_ntp moves it."""
> + ntpconf = self.tmp_path("ntp.conf", self.new_root)
> + os.mknod(ntpconf)
> + with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf):
> + cc_ntp.rename_ntp_conf()
> + self.assertFalse(os.path.exists(ntpconf))
> + self.assertTrue(os.path.exists("{}.dist".format(ntpconf)))
>
> def test_ntp_rename_ntp_conf_skip_missing(self):
> - with mock.patch.object(os.path, 'exists',
> - return_value=False) as mockpath:
> - with mock.patch.object(util, 'rename') as mockrename:
> - cc_ntp.rename_ntp_conf()
> -
> - mockpath.assert_called_with('/etc/ntp.conf')
> - mockrename.assert_not_called()
> -
> - def ntp_conf_render(self, distro):
> - """ntp_conf_render
> - Test rendering of a ntp.conf from template for a given distro
> + """When NTP_CONF doesn't exist rename_ntp doesn't create a file."""
> + ntpconf = self.tmp_path("ntp.conf", self.new_root)
> + self.assertFalse(os.path.exists(ntpconf))
> + with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf):
> + cc_ntp.rename_ntp_conf()
> + self.assertFalse(os.path.exists("{}.dist".format(ntpconf)))
> + self.assertFalse(os.path.exists(ntpconf))
> +
> + def test_write_ntp_config_template_from_ntp_conf_tmpl_with_servers(self):
> + """write_ntp_config_template reads content from ntp.conf.tmpl.
> +
> + It reads ntp.conf.tmpl if present and renders the value from servers
> + key. When no pools key is defined, template is rendered using an empty
> + list for pools.
> """
> -
> - cfg = {'ntp': {}}
> - mycloud = self._get_cloud(distro)
> - distro_names = cc_ntp.generate_server_names(distro)
> -
> - with mock.patch.object(templater, 'render_to_file') as mocktmpl:
> - with mock.patch.object(os.path, 'isfile', return_value=True):
> - with mock.patch.object(util, 'rename'):
> - cc_ntp.write_ntp_config_template(cfg, mycloud)
> -
> - mocktmpl.assert_called_once_with(
> - ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
> - '/etc/ntp.conf',
> - {'servers': [], 'pools': distro_names})
> -
> - def test_ntp_conf_render_rhel(self):
> - """Test templater.render_to_file() for rhel"""
> - self.ntp_conf_render('rhel')
> -
> - def test_ntp_conf_render_debian(self):
> - """Test templater.render_to_file() for debian"""
> - self.ntp_conf_render('debian')
> -
> - def test_ntp_conf_render_fedora(self):
> - """Test templater.render_to_file() for fedora"""
> - self.ntp_conf_render('fedora')
> -
> - def test_ntp_conf_render_sles(self):
> - """Test templater.render_to_file() for sles"""
> - self.ntp_conf_render('sles')
> -
> - def test_ntp_conf_render_ubuntu(self):
> - """Test templater.render_to_file() for ubuntu"""
> - self.ntp_conf_render('ubuntu')
> -
> - def test_ntp_conf_servers_no_pools(self):
> distro = 'ubuntu'
> - pools = []
> - servers = ['192.168.2.1']
> cfg = {
> - 'ntp': {
> - 'pools': pools,
> - 'servers': servers,
> - }
> + 'servers': ['192.168.2.1', '192.168.2.2']
> }
> mycloud = self._get_cloud(distro)
> -
> - with mock.patch.object(templater, 'render_to_file') as mocktmpl:
> - with mock.patch.object(os.path, 'isfile', return_value=True):
> - with mock.patch.object(util, 'rename'):
> - cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)
> -
> - mocktmpl.assert_called_once_with(
> - ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
> - '/etc/ntp.conf',
> - {'servers': servers, 'pools': pools})
> -
> - def test_ntp_conf_custom_pools_no_server(self):
> + ntp_conf = self.tmp_path("ntp.conf", self.new_root) # Doesn't exist
> + # Create ntp.conf.tmpl
> + with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
> + stream.write(NTP_TEMPLATE)
> + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
> + cc_ntp.write_ntp_config_template(cfg, mycloud)
> + with open(ntp_conf) as stream:
> + content = stream.read()
> + self.assertEqual(
> + "servers ['192.168.2.1', '192.168.2.2']\npools []\n",
> + content)
> +
> + def test_write_ntp_config_template_uses_ntp_conf_distro_no_servers(self):
> + """write_ntp_config_template reads content from ntp.conf.distro.tmpl.
> +
> + It reads ntp.conf.<distro>.tmpl before attempting ntp.conf.tmpl. It
> + renders the value from the keys servers and pools. When no
> + servers value is present, template is rendered using an empty list.
> + """
> distro = 'ubuntu'
> - pools = ['0.mycompany.pool.ntp.org']
> - servers = []
> cfg = {
> - 'ntp': {
> - 'pools': pools,
> - 'servers': servers,
> - }
> + 'pools': ['10.0.0.1', '10.0.0.2']
> }
> mycloud = self._get_cloud(distro)
> -
> - with mock.patch.object(templater, 'render_to_file') as mocktmpl:
> - with mock.patch.object(os.path, 'isfile', return_value=True):
> - with mock.patch.object(util, 'rename'):
> - cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)
> -
> - mocktmpl.assert_called_once_with(
> - ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
> - '/etc/ntp.conf',
> - {'servers': servers, 'pools': pools})
> -
> - def test_ntp_conf_custom_pools_and_server(self):
> + ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
> + # Create ntp.conf.tmpl which isn't read
> + with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
> + stream.write(b'NOT READ: ntp.conf.<distro>.tmpl is primary')
> + # Create ntp.conf.tmpl.<distro>
> + with open('{}.{}.tmpl'.format(ntp_conf, distro), 'wb') as stream:
> + stream.write(NTP_TEMPLATE)
> + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
> + cc_ntp.write_ntp_config_template(cfg, mycloud)
> + with open(ntp_conf) as stream:
> + content = stream.read()
> + self.assertEqual(
> + "servers []\npools ['10.0.0.1', '10.0.0.2']\n",
> + content)
> +
> + def test_write_ntp_config_template_defaults_pools_when_empty_lists(self):
> + """write_ntp_config_template defaults pools servers upon empty config.
> +
> + When both pools and servers are empty, default NR_POOL_SERVERS get
> + configured.
> + """
> distro = 'ubuntu'
> - pools = ['0.mycompany.pool.ntp.org']
> - servers = ['192.168.23.3']
> - cfg = {
> - 'ntp': {
> - 'pools': pools,
> - 'servers': servers,
> - }
> - }
> mycloud = self._get_cloud(distro)
> -
> - with mock.patch.object(templater, 'render_to_file') as mocktmpl:
> - with mock.patch.object(os.path, 'isfile', return_value=True):
> - with mock.patch.object(util, 'rename'):
> - cc_ntp.write_ntp_config_template(cfg.get('ntp'), mycloud)
> -
> - mocktmpl.assert_called_once_with(
> - ('/etc/cloud/templates/ntp.conf.%s.tmpl' % distro),
> - '/etc/ntp.conf',
> - {'servers': servers, 'pools': pools})
> -
> - def test_ntp_conf_contents_match(self):
> - """Test rendered contents of /etc/ntp.conf for ubuntu"""
> - pools = ['0.mycompany.pool.ntp.org']
> - servers = ['192.168.23.3']
> - cfg = {
> - 'ntp': {
> - 'pools': pools,
> - 'servers': servers,
> - }
> - }
> - mycloud = self._get_cloud('ubuntu')
> - side_effect = [NTP_TEMPLATE.lstrip()]
> -
> - # work backwards from util.write_file and mock out call path
> - # write_ntp_config_template()
> - # cloud.get_template_filename()
> - # os.path.isfile()
> - # templater.render_to_file()
> - # templater.render_from_file()
> - # util.load_file()
> - # util.write_file()
> - #
> - with mock.patch.object(util, 'write_file') as mockwrite:
> - with mock.patch.object(util, 'load_file', side_effect=side_effect):
> - with mock.patch.object(os.path, 'isfile', return_value=True):
> - with mock.patch.object(util, 'rename'):
> - cc_ntp.write_ntp_config_template(cfg.get('ntp'),
> - mycloud)
> -
> - mockwrite.assert_called_once_with(
> - '/etc/ntp.conf',
> - NTP_EXPECTED_UBUNTU,
> - mode=420)
> + ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
> + # Create ntp.conf.tmpl
> + with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
> + stream.write(NTP_TEMPLATE)
> + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
> + cc_ntp.write_ntp_config_template({}, mycloud)
> + with open(ntp_conf) as stream:
> + content = stream.read()
used read_file_or_url for these. thanks for that.
> + default_pools = [
> + "{}.{}.pool.ntp.org".format(x, distro)
> + for x in range(0, cc_ntp.NR_POOL_SERVERS)]
> + self.assertEqual(
> + "servers []\npools {}\n".format(default_pools),
> + content)
> + self.assertIn(
> + "Adding distro default ntp pool servers: {}".format(
> + ",".join(default_pools)),
> + self.logs.getvalue())
>
> def test_ntp_handler(self):
> - """Test ntp handler renders ubuntu ntp.conf template"""
> - pools = ['0.mycompany.pool.ntp.org']
> - servers = ['192.168.23.3']
> + """Test ntp handler renders ubuntu ntp.conf template."""
> + pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org']
> + servers = ['192.168.23.3', '192.168.23.4']
> cfg = {
> 'ntp': {
> 'pools': pools,
> - 'servers': servers,
> + 'servers': servers
> }
> }
> mycloud = self._get_cloud('ubuntu')
> - mycloud.distro = mock.MagicMock()
> - mycloud.distro.uses_systemd.return_value = True
> - side_effect = [NTP_TEMPLATE.lstrip()]
> -
> - with mock.patch.object(util, 'which', return_value=None):
> - with mock.patch.object(os.path, 'exists'):
> - with mock.patch.object(util, 'write_file') as mockwrite:
> - with mock.patch.object(util, 'load_file',
> - side_effect=side_effect):
> - with mock.patch.object(os.path, 'isfile',
> - return_value=True):
> - with mock.patch.object(util, 'rename'):
> - with mock.patch.object(util, 'subp') as msubp:
> - cc_ntp.handle("notimportant", cfg,
> - mycloud, LOG, None)
> -
> - mockwrite.assert_called_once_with(
> - '/etc/ntp.conf',
> - NTP_EXPECTED_UBUNTU,
> - mode=420)
> - msubp.assert_any_call(['systemctl', 'reload-or-restart', 'ntp'],
> - capture=True)
> -
> - @mock.patch("cloudinit.config.cc_ntp.install_ntp")
> - @mock.patch("cloudinit.config.cc_ntp.write_ntp_config_template")
> - @mock.patch("cloudinit.config.cc_ntp.rename_ntp_conf")
> - def test_write_config_before_install(self, mock_ntp_rename,
> - mock_ntp_write_config,
> - mock_install_ntp):
> - pools = ['0.mycompany.pool.ntp.org']
> - servers = ['192.168.23.3']
> - cfg = {
> - 'ntp': {
> - 'pools': pools,
> - 'servers': servers,
> - }
> - }
> - cc = self._get_cloud('ubuntu')
> - cc.distro = mock.MagicMock()
> - mock_parent = mock.MagicMock()
> - mock_parent.attach_mock(mock_ntp_rename, 'mock_ntp_rename')
> - mock_parent.attach_mock(mock_ntp_write_config, 'mock_ntp_write_config')
> - mock_parent.attach_mock(mock_install_ntp, 'mock_install_ntp')
> -
> - cc_ntp.handle('cc_ntp', cfg, cc, LOG, None)
> -
> - """Check call order"""
> - mock_parent.assert_has_calls([
> - mock.call.mock_ntp_rename(),
> - mock.call.mock_ntp_write_config(cfg.get('ntp'), cc),
> - mock.call.mock_install_ntp(cc.distro.install_packages,
> - packages=['ntp'], check_exe="ntpd")])
> -
> - @mock.patch("cloudinit.config.cc_ntp.reload_ntp")
> - @mock.patch("cloudinit.config.cc_ntp.install_ntp")
> - @mock.patch("cloudinit.config.cc_ntp.write_ntp_config_template")
> - @mock.patch("cloudinit.config.cc_ntp.rename_ntp_conf")
> - def test_reload_ntp_fail_raises_exception(self, mock_rename,
> - mock_write_conf,
> - mock_install,
> - mock_reload):
> - pools = ['0.mycompany.pool.ntp.org']
> - servers = ['192.168.23.3']
> - cfg = {
> - 'ntp': {
> - 'pools': pools,
> - 'servers': servers,
> - }
> - }
> - cc = self._get_cloud('ubuntu')
> - cc.distro = mock.MagicMock()
> -
> - mock_reload.side_effect = [util.ProcessExecutionError]
> - self.assertRaises(util.ProcessExecutionError,
> - cc_ntp.handle, 'cc_ntp',
> - cfg, cc, LOG, None)
> -
> - @mock.patch("cloudinit.config.cc_ntp.util")
> - def test_no_ntpcfg_does_nothing(self, mock_util):
> - cc = self._get_cloud('ubuntu')
> - cc.distro = mock.MagicMock()
> - cc_ntp.handle('cc_ntp', {}, cc, LOG, [])
> - self.assertFalse(cc.distro.install_packages.called)
> - self.assertFalse(mock_util.subp.called)
> -
> - @mock.patch("cloudinit.config.cc_ntp.util")
> - def test_reload_ntp_systemd(self, mock_util):
> - cc_ntp.reload_ntp(systemd=True)
> - self.assertTrue(mock_util.subp.called)
> - mock_util.subp.assert_called_with(
> - ['systemctl', 'reload-or-restart', 'ntp'], capture=True)
> -
> - @mock.patch("cloudinit.config.cc_ntp.util")
> - def test_reload_ntp_service(self, mock_util):
> - cc_ntp.reload_ntp(systemd=False)
> - self.assertTrue(mock_util.subp.called)
> - mock_util.subp.assert_called_with(
> - ['service', 'ntp', 'restart'], capture=True)
> -
> + ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
> + # Create ntp.conf.tmpl
> + with open('{}.tmpl'.format(ntp_conf), 'wb') as stream:
> + stream.write(NTP_TEMPLATE)
> + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
> + with mock.patch.object(util, 'which', return_value=None):
> + cc_ntp.handle('notimportant', cfg, mycloud, None, None)
> +
> + with open(ntp_conf) as stream:
> + content = stream.read()
> + self.assertEqual(
> + 'servers {}\npools {}\n'.format(servers, pools),
> + content)
> +
> + def test_no_ntpcfg_does_nothing(self):
> + """When no ntp section is defined handler logs a warning and noops."""
> + cc_ntp.handle('cc_ntp', {}, None, None, [])
> + self.assertEqual(
> + 'Skipping module named cc_ntp, not present or disabled by cfg\n',
> + self.logs.getvalue())
>
> # vi: ts=4 expandtab
--
https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/324450
Your team cloud-init commiters is requested to review the proposed merge of ~chad.smith/cloud-init:cc-ntp-testing into cloud-init:master.
References