diff --git a/cloudinit/config/modules.py b/cloudinit/config/modules.py index ab3a0e130a2..efb7a5a4343 100644 --- a/cloudinit/config/modules.py +++ b/cloudinit/config/modules.py @@ -172,7 +172,8 @@ def _fixup_modules(self, raw_mods) -> List[ModuleDetails]: raw_name, freq, ) - # Reset it so when ran it will get set to a known value + # Misconfigured in /etc/cloud/cloud.cfg. Reset so cc_* module + # default meta attribute "frequency" value is used. freq = None mod_locs, looked_locs = importer.find_module( mod_name, ["", type_utils.obj_name(config)], ["handle"] @@ -186,6 +187,9 @@ def _fixup_modules(self, raw_mods) -> List[ModuleDetails]: continue mod = importer.import_module(mod_locs[0]) validate_module(mod, raw_name) + if freq is None: + # Use cc_* module default setting since no cloud.cfg overrides + freq = mod.meta["frequency"] mostly_mods.append( ModuleDetails( module=mod, diff --git a/tests/integration_tests/modules/test_combined.py b/tests/integration_tests/modules/test_combined.py index eb45062eff2..70850fd9038 100644 --- a/tests/integration_tests/modules/test_combined.py +++ b/tests/integration_tests/modules/test_combined.py @@ -5,12 +5,16 @@ the same instance launch. Most independent module coherence tests can go here. """ +import glob +import importlib import json import re import uuid +from pathlib import Path import pytest +import cloudinit.config from tests.integration_tests.clouds import ImageSpecification from tests.integration_tests.decorators import retry from tests.integration_tests.instances import IntegrationInstance @@ -223,6 +227,26 @@ def test_cloud_id_file_symlink(self, class_client: IntegrationInstance): class_client.execute("stat -c %N /run/cloud-init/cloud-id") ) + def test_run_frequency(self, class_client: IntegrationInstance): + log = class_client.read_from_file("/var/log/cloud-init.log") + config_dir = Path(cloudinit.config.__file__).parent + module_paths = glob.glob(str(config_dir / "cc*.py")) + module_names = [Path(x).stem for x in module_paths] + found_count = 0 + for name in module_names: + mod = importlib.import_module(f"cloudinit.config.{name}") + frequency = mod.meta["frequency"] + # cc_ gets replaced with config- in logs + log_name = name.replace("cc_", "config-") + # Some modules have been filtered out in /etc/cloud/cloud.cfg, + if f"running {log_name}" in log: + found_count += 1 # Ensure we're matching on the right text + assert f"running {log_name} with frequency {frequency}" in log + assert ( + found_count > 10 + ), "Not enough modules found in log. Did the log message change?" + assert "with frequency None" not in log + def _check_common_metadata(self, data): assert data["base64_encoded_keys"] == [] assert data["merged_cfg"] == "redacted for non-root user" diff --git a/tests/integration_tests/modules/test_frequency_override.py b/tests/integration_tests/modules/test_frequency_override.py new file mode 100644 index 00000000000..0cefadcc574 --- /dev/null +++ b/tests/integration_tests/modules/test_frequency_override.py @@ -0,0 +1,33 @@ +import pytest + +from tests.integration_tests.instances import IntegrationInstance + +USER_DATA = """\ +#cloud-config +runcmd: + - echo "hi" >> /var/tmp/hi +""" + + +@pytest.mark.user_data(USER_DATA) +def test_frequency_override(client: IntegrationInstance): + # Some pre-checks + assert ( + "running config-scripts-user with frequency once-per-instance" + in client.read_from_file("/var/log/cloud-init.log") + ) + assert client.read_from_file("/var/tmp/hi").strip().count("hi") == 1 + + # Change frequency of scripts-user to always + config = client.read_from_file("/etc/cloud/cloud.cfg") + new_config = config.replace("- scripts-user", "- [ scripts-user, always ]") + client.write_to_file("/etc/cloud/cloud.cfg", new_config) + + client.restart() + + # Ensure the script was run again + assert ( + "running config-scripts-user with frequency always" + in client.read_from_file("/var/log/cloud-init.log") + ) + assert client.read_from_file("/var/tmp/hi").strip().count("hi") == 2 diff --git a/tests/integration_tests/modules/test_write_files.py b/tests/integration_tests/modules/test_write_files.py index 1eb7e9454cf..a713b9c5cf2 100644 --- a/tests/integration_tests/modules/test_write_files.py +++ b/tests/integration_tests/modules/test_write_files.py @@ -50,6 +50,7 @@ defer: true owner: 'myuser' permissions: '0644' + append: true """.format( B64_CONTENT.decode("ascii") ) @@ -91,3 +92,8 @@ def test_write_files_deferred(self, class_client): class_client.execute('stat -c "%U %a" /home/testuser/my-file') == "myuser 644" ) + # Assert write_files per-instance is honored and run only once. + # Given append: true multiple runs across would append new content. + class_client.restart() + out = class_client.read_from_file("/home/testuser/my-file") + assert "echo 'hello world!'" == out