From 41152f10ddbd8681cdac44b408038a4f23ab02df Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 17 Oct 2017 16:12:59 -0400 Subject: schema: Log debug instead of warning when jsonschema is not available. When operating in expected path, cloud-init should avoid logging with warning. That causes 'WARNING' messages in /var/log/cloud-init.log. By default, warnings also go to the console. Since jsonschema is a optional dependency, and not present on xenial and zesty, cloud-init should not warn there. Also here: * Add a test to integration tests to assert that there are no warnings in /var/log/cloud-init.log. * Update one integration test that did show warning and the related documentation and examples. LP: #1724354 --- tests/cloud_tests/testcases/base.py | 4 ++++ tests/cloud_tests/testcases/examples/including_user_groups.py | 6 ++++++ tests/cloud_tests/testcases/examples/including_user_groups.yaml | 7 +++++-- tests/cloud_tests/testcases/modules/user_groups.py | 6 ++++++ tests/cloud_tests/testcases/modules/user_groups.yaml | 7 +++++-- 5 files changed, 26 insertions(+), 4 deletions(-) (limited to 'tests/cloud_tests') diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py index bb545ab9..b2b5b4b1 100644 --- a/tests/cloud_tests/testcases/base.py +++ b/tests/cloud_tests/testcases/base.py @@ -72,6 +72,10 @@ class CloudTestCase(unittest.TestCase): result = self.get_status_data(self.get_data_file('result.json')) self.assertEqual(len(result['errors']), 0) + def test_no_warnings_in_log(self): + """Warnings should not be found in the log.""" + self.assertNotIn("WARN", self.get_data_file('cloud-init.log')) + class PasswordListTest(CloudTestCase): """Base password test case class.""" diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.py b/tests/cloud_tests/testcases/examples/including_user_groups.py index 67af527b..93b7a82d 100644 --- a/tests/cloud_tests/testcases/examples/including_user_groups.py +++ b/tests/cloud_tests/testcases/examples/including_user_groups.py @@ -40,4 +40,10 @@ class TestUserGroups(base.CloudTestCase): out = self.get_data_file('user_cloudy') self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:') + def test_user_root_in_secret(self): + """Test root user is in 'secret' group.""" + user, _, groups = self.get_data_file('root_groups').partition(":") + self.assertIn("secret", groups.split(), + msg="User root is not in group 'secret'") + # vi: ts=4 expandtab diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.yaml b/tests/cloud_tests/testcases/examples/including_user_groups.yaml index 0aa7ad21..469d03c3 100644 --- a/tests/cloud_tests/testcases/examples/including_user_groups.yaml +++ b/tests/cloud_tests/testcases/examples/including_user_groups.yaml @@ -8,7 +8,7 @@ cloud_config: | #cloud-config # Add groups to the system groups: - - secret: [foobar,barfoo] + - secret: [root] - cloud-users # Add users to the system. Users are added after groups are added. @@ -24,7 +24,7 @@ cloud_config: | - name: barfoo gecos: Bar B. Foo sudo: ALL=(ALL) NOPASSWD:ALL - groups: cloud-users + groups: [cloud-users, secret] lock_passwd: true - name: cloudy gecos: Magic Cloud App Daemon User @@ -49,5 +49,8 @@ collect_scripts: user_cloudy: | #!/bin/bash getent passwd cloudy + root_groups: | + #!/bin/bash + groups root # vi: ts=4 expandtab diff --git a/tests/cloud_tests/testcases/modules/user_groups.py b/tests/cloud_tests/testcases/modules/user_groups.py index 67af527b..93b7a82d 100644 --- a/tests/cloud_tests/testcases/modules/user_groups.py +++ b/tests/cloud_tests/testcases/modules/user_groups.py @@ -40,4 +40,10 @@ class TestUserGroups(base.CloudTestCase): out = self.get_data_file('user_cloudy') self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:') + def test_user_root_in_secret(self): + """Test root user is in 'secret' group.""" + user, _, groups = self.get_data_file('root_groups').partition(":") + self.assertIn("secret", groups.split(), + msg="User root is not in group 'secret'") + # vi: ts=4 expandtab diff --git a/tests/cloud_tests/testcases/modules/user_groups.yaml b/tests/cloud_tests/testcases/modules/user_groups.yaml index 71cc9da3..22b5d706 100644 --- a/tests/cloud_tests/testcases/modules/user_groups.yaml +++ b/tests/cloud_tests/testcases/modules/user_groups.yaml @@ -7,7 +7,7 @@ cloud_config: | #cloud-config # Add groups to the system groups: - - secret: [foobar,barfoo] + - secret: [root] - cloud-users # Add users to the system. Users are added after groups are added. @@ -23,7 +23,7 @@ cloud_config: | - name: barfoo gecos: Bar B. Foo sudo: ALL=(ALL) NOPASSWD:ALL - groups: cloud-users + groups: [cloud-users, secret] lock_passwd: true - name: cloudy gecos: Magic Cloud App Daemon User @@ -48,5 +48,8 @@ collect_scripts: user_cloudy: | #!/bin/bash getent passwd cloudy + root_groups: | + #!/bin/bash + groups root # vi: ts=4 expandtab -- cgit v1.2.3 From 6bc504e41666329631cdfd5b947ed5b0e2529a76 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 20 Oct 2017 13:24:22 -0600 Subject: ntp: fix config module schema to allow empty ntp config Fix three things related to the ntp module: 1. Fix invalid cloud-config schema in the integration test which provided empty dicts instead of emptylists for pools and servers 2. Correct logic in the ntp module to allow support for the minimal cloud-config 'ntp:' without raising a RuntimeError. Docs and schema definitions already describe that cloud-config's ntp can be empty. An ntp configuration with neither pools nor servers will be configured with a default set of ntp pools. As such, the ntp module now officially allows the following ntp cloud-configs: - ntp: - ntp: {} - ntp: servers: [] pools: [] 3. Add a simple unit test which validates all cloud-config provided to our integration tests to ensure it adheres to any defined module schema so as more jsonschema definitions are added, we validate our integration test configs. LP: #1724951 --- cloudinit/config/cc_ntp.py | 4 ++- tests/cloud_tests/testcases/modules/ntp.yaml | 4 +-- tests/unittests/test_handler/test_handler_ntp.py | 23 ++++++++------- tests/unittests/test_handler/test_schema.py | 37 +++++++++++++++++++++++- 4 files changed, 53 insertions(+), 15 deletions(-) (limited to 'tests/cloud_tests') diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index 15ae1ecd..d43d060c 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -100,7 +100,9 @@ def handle(name, cfg, cloud, log, _args): LOG.debug( "Skipping module named %s, not present or disabled by cfg", name) return - ntp_cfg = cfg.get('ntp', {}) + ntp_cfg = cfg['ntp'] + if ntp_cfg is None: + ntp_cfg = {} # Allow empty config which will install the package # TODO drop this when validate_cloudconfig_schema is strict=True if not isinstance(ntp_cfg, (dict)): diff --git a/tests/cloud_tests/testcases/modules/ntp.yaml b/tests/cloud_tests/testcases/modules/ntp.yaml index fbef431b..2530d72e 100644 --- a/tests/cloud_tests/testcases/modules/ntp.yaml +++ b/tests/cloud_tests/testcases/modules/ntp.yaml @@ -4,8 +4,8 @@ cloud_config: | #cloud-config ntp: - pools: {} - servers: {} + pools: [] + servers: [] collect_scripts: ntp_installed: | #!/bin/bash diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py index 4f291248..3abe5786 100644 --- a/tests/unittests/test_handler/test_handler_ntp.py +++ b/tests/unittests/test_handler/test_handler_ntp.py @@ -293,23 +293,24 @@ class TestNtp(FilesystemMockingTestCase): def test_ntp_handler_schema_validation_allows_empty_ntp_config(self): """Ntp schema validation allows for an empty ntp: configuration.""" - invalid_config = {'ntp': {}} + valid_empty_configs = [{'ntp': {}}, {'ntp': None}] distro = 'ubuntu' cc = self._get_cloud(distro) ntp_conf = os.path.join(self.new_root, 'ntp.conf') with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: stream.write(NTP_TEMPLATE) - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): - cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) + for valid_empty_config in valid_empty_configs: + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): + cc_ntp.handle('cc_ntp', valid_empty_config, cc, None, []) + with open(ntp_conf) as stream: + content = stream.read() + default_pools = [ + "{0}.{1}.pool.ntp.org".format(x, distro) + for x in range(0, cc_ntp.NR_POOL_SERVERS)] + self.assertEqual( + "servers []\npools {0}\n".format(default_pools), + content) self.assertNotIn('Invalid config:', self.logs.getvalue()) - with open(ntp_conf) as stream: - content = stream.read() - default_pools = [ - "{0}.{1}.pool.ntp.org".format(x, distro) - for x in range(0, cc_ntp.NR_POOL_SERVERS)] - self.assertEqual( - "servers []\npools {0}\n".format(default_pools), - content) @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") def test_ntp_handler_schema_validation_warns_non_string_item_type(self): diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py index b8fc8930..648573f6 100644 --- a/tests/unittests/test_handler/test_schema.py +++ b/tests/unittests/test_handler/test_schema.py @@ -4,11 +4,12 @@ from cloudinit.config.schema import ( CLOUD_CONFIG_HEADER, SchemaValidationError, annotated_cloudconfig_file, get_schema_doc, get_schema, validate_cloudconfig_file, validate_cloudconfig_schema, main) -from cloudinit.util import write_file +from cloudinit.util import subp, write_file from cloudinit.tests.helpers import CiTestCase, mock, skipIf from copy import copy +import os from six import StringIO from textwrap import dedent from yaml import safe_load @@ -364,4 +365,38 @@ class MainTest(CiTestCase): self.assertIn( 'Valid cloud-config file {0}'.format(myyaml), m_stdout.getvalue()) + +class CloudTestsIntegrationTest(CiTestCase): + """Validate all cloud-config yaml schema provided in integration tests. + + It is less expensive to have unittests validate schema of all cloud-config + yaml provided to integration tests, than to run an integration test which + raises Warnings or errors on invalid cloud-config schema. + """ + + @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") + def test_all_integration_test_cloud_config_schema(self): + """Validate schema of cloud_tests yaml files looking for warnings.""" + schema = get_schema() + testsdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + integration_testdir = os.path.sep.join( + [testsdir, 'cloud_tests', 'testcases']) + errors = [] + out, _ = subp(['find', integration_testdir, '-name', '*yaml']) + for filename in out.splitlines(): + test_cfg = safe_load(open(filename)) + cloud_config = test_cfg.get('cloud_config') + if cloud_config: + cloud_config = safe_load( + cloud_config.replace("#cloud-config\n", "")) + try: + validate_cloudconfig_schema( + cloud_config, schema, strict=True) + except SchemaValidationError as e: + errors.append( + '{0}: {1}'.format( + filename, e)) + if errors: + raise AssertionError(', '.join(errors)) + # vi: ts=4 expandtab syntax=python -- cgit v1.2.3 From a51968d50e7cdecabaf255c26386fd82f6640cc3 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Fri, 20 Oct 2017 14:58:24 -0400 Subject: citest: show the class actual class name in results. Tests are currently run by creating a temporary subclass of each class and then executing it (in get_suites). When running the tests suite the output would contain the temporary class name. That was less than useful, and made batch runs almost impossible to identify which test case had an error. This change goes from output of: FAIL: test_no_warnings_in_log \ (tests.cloud_tests.testcases.get_suite..tmp) To FAIL: test_no_warnings_in_log \ (tests.cloud_tests.testcases.modules.ntp.TestNtp) --- tests/cloud_tests/testcases/__init__.py | 7 +++++++ tests/cloud_tests/testcases/base.py | 4 ---- 2 files changed, 7 insertions(+), 4 deletions(-) (limited to 'tests/cloud_tests') diff --git a/tests/cloud_tests/testcases/__init__.py b/tests/cloud_tests/testcases/__init__.py index 47217ce6..a29a0928 100644 --- a/tests/cloud_tests/testcases/__init__.py +++ b/tests/cloud_tests/testcases/__init__.py @@ -5,6 +5,7 @@ import importlib import inspect import unittest +from unittest.util import strclass from tests.cloud_tests import config from tests.cloud_tests.testcases.base import CloudTestCase as base_test @@ -37,6 +38,12 @@ def get_suite(test_name, data, conf): class tmp(test_class): + _realclass = test_class + + def __str__(self): + return "%s (%s)" % (self._testMethodName, + strclass(self._realclass)) + @classmethod def setUpClass(cls): cls.data = data diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py index b2b5b4b1..d3586e3c 100644 --- a/tests/cloud_tests/testcases/base.py +++ b/tests/cloud_tests/testcases/base.py @@ -16,10 +16,6 @@ class CloudTestCase(unittest.TestCase): conf = None _cloud_config = None - def shortDescription(self): - """Prevent nose from using docstrings.""" - return None - @property def cloud_config(self): """Get the cloud-config used by the test.""" -- cgit v1.2.3 From c06eea972eb4b7bfa68f4f542f2fb67ea1d455ac Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Fri, 20 Oct 2017 16:05:04 -0600 Subject: citest: fix remaining warnings raised by integration tests. There was fallout in a full integration test run from my adding of test_no_warnings_in_log which asserted that there could not be a WARNING found in the /var/log/cloud-init.log This fixes 2 of the cases: * TestCommandOutputSimple had a valid WARNING written, so adjust its test case to allow for that. * TestLxdDir had a valid config in the test but the module would log a WARNING, so fix the module. Also updates lxd unit tests to look for WARN themselves. --- cloudinit/config/cc_lxd.py | 2 +- tests/cloud_tests/testcases/base.py | 6 +++++- .../cloud_tests/testcases/main/command_output_simple.py | 16 ++++++++++++++++ tests/unittests/test_handler/test_handler_lxd.py | 16 ++++++++-------- 4 files changed, 30 insertions(+), 10 deletions(-) (limited to 'tests/cloud_tests') diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py index e6262f8c..09374d2e 100644 --- a/cloudinit/config/cc_lxd.py +++ b/cloudinit/config/cc_lxd.py @@ -72,7 +72,7 @@ def handle(name, cfg, cloud, log, args): type(init_cfg)) init_cfg = {} - bridge_cfg = lxd_cfg.get('bridge') + bridge_cfg = lxd_cfg.get('bridge', {}) if not isinstance(bridge_cfg, dict): log.warn("lxd/bridge config must be a dictionary. found a '%s'", type(bridge_cfg)) diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py index d3586e3c..1706f59b 100644 --- a/tests/cloud_tests/testcases/base.py +++ b/tests/cloud_tests/testcases/base.py @@ -70,7 +70,11 @@ class CloudTestCase(unittest.TestCase): def test_no_warnings_in_log(self): """Warnings should not be found in the log.""" - self.assertNotIn("WARN", self.get_data_file('cloud-init.log')) + self.assertEqual( + [], + [l for l in self.get_data_file('cloud-init.log').splitlines() + if 'WARN' in l], + msg="'WARN' found inside cloud-init.log") class PasswordListTest(CloudTestCase): diff --git a/tests/cloud_tests/testcases/main/command_output_simple.py b/tests/cloud_tests/testcases/main/command_output_simple.py index fe4c7670..857881cb 100644 --- a/tests/cloud_tests/testcases/main/command_output_simple.py +++ b/tests/cloud_tests/testcases/main/command_output_simple.py @@ -15,4 +15,20 @@ class TestCommandOutputSimple(base.CloudTestCase): data.splitlines()[-1].strip()) # TODO: need to test that all stages redirected here + def test_no_warnings_in_log(self): + """Warnings should not be found in the log. + + This class redirected stderr and stdout, so it expects to find + a warning in cloud-init.log to that effect.""" + redirect_msg = 'Stdout, stderr changing to' + warnings = [ + l for l in self.get_data_file('cloud-init.log').splitlines() + if 'WARN' in l] + self.assertEqual( + [], [w for w in warnings if redirect_msg not in w], + msg="'WARN' found inside cloud-init.log") + self.assertEqual( + 1, len(warnings), + msg="Did not find %s in cloud-init.log" % redirect_msg) + # vi: ts=4 expandtab diff --git a/tests/unittests/test_handler/test_handler_lxd.py b/tests/unittests/test_handler/test_handler_lxd.py index f132a778..e0d9ab6c 100644 --- a/tests/unittests/test_handler/test_handler_lxd.py +++ b/tests/unittests/test_handler/test_handler_lxd.py @@ -5,17 +5,16 @@ from cloudinit.sources import DataSourceNoCloud from cloudinit import (distros, helpers, cloud) from cloudinit.tests import helpers as t_help -import logging - try: from unittest import mock except ImportError: import mock -LOG = logging.getLogger(__name__) +class TestLxd(t_help.CiTestCase): + + with_logs = True -class TestLxd(t_help.TestCase): lxd_cfg = { 'lxd': { 'init': { @@ -41,7 +40,7 @@ class TestLxd(t_help.TestCase): def test_lxd_init(self, mock_util): cc = self._get_cloud('ubuntu') mock_util.which.return_value = True - cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, LOG, []) + cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, []) self.assertTrue(mock_util.which.called) init_call = mock_util.subp.call_args_list[0][0][0] self.assertEqual(init_call, @@ -55,7 +54,8 @@ class TestLxd(t_help.TestCase): cc = self._get_cloud('ubuntu') cc.distro = mock.MagicMock() mock_util.which.return_value = None - cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, LOG, []) + cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, []) + self.assertNotIn('WARN', self.logs.getvalue()) self.assertTrue(cc.distro.install_packages.called) install_pkg = cc.distro.install_packages.call_args_list[0][0][0] self.assertEqual(sorted(install_pkg), ['lxd', 'zfs']) @@ -64,7 +64,7 @@ class TestLxd(t_help.TestCase): def test_no_init_does_nothing(self, mock_util): cc = self._get_cloud('ubuntu') cc.distro = mock.MagicMock() - cc_lxd.handle('cc_lxd', {'lxd': {}}, cc, LOG, []) + cc_lxd.handle('cc_lxd', {'lxd': {}}, cc, self.logger, []) self.assertFalse(cc.distro.install_packages.called) self.assertFalse(mock_util.subp.called) @@ -72,7 +72,7 @@ class TestLxd(t_help.TestCase): def test_no_lxd_does_nothing(self, mock_util): cc = self._get_cloud('ubuntu') cc.distro = mock.MagicMock() - cc_lxd.handle('cc_lxd', {'package_update': True}, cc, LOG, []) + cc_lxd.handle('cc_lxd', {'package_update': True}, cc, self.logger, []) self.assertFalse(cc.distro.install_packages.called) self.assertFalse(mock_util.subp.called) -- cgit v1.2.3