diff --git a/Makefile b/Makefile index 4bd53b50294..b8167a05346 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ render-template: # Our generator is a shell script. Make it easy to measure the # generator. This should be monitored for performance regressions benchmark-generator: FILE=$(GENERATOR_F).tmpl +benchmark-generator: VARIANT="benchmark" benchmark-generator: export ITER=$(NUM_ITER) benchmark-generator: render-template $(BENCHMARK) $(GENERATOR_F) diff --git a/cloudinit/cmd/status.py b/cloudinit/cmd/status.py index e0db81c8fd9..3abc8d4c453 100644 --- a/cloudinit/cmd/status.py +++ b/cloudinit/cmd/status.py @@ -43,6 +43,7 @@ class UXAppBootStatusCode(enum.Enum): DISABLED_BY_GENERATOR = "disabled-by-generator" DISABLED_BY_KERNEL_CMDLINE = "disabled-by-kernel-cmdline" DISABLED_BY_MARKER_FILE = "disabled-by-marker-file" + DISABLED_BY_ENV_VARIABLE = "disabled-by-environment-variable" ENABLED_BY_GENERATOR = "enabled-by-generator" ENABLED_BY_KERNEL_CMDLINE = "enabled-by-kernel-cmdline" ENABLED_BY_SYSVINIT = "enabled-by-sysvinit" @@ -54,6 +55,7 @@ class UXAppBootStatusCode(enum.Enum): UXAppBootStatusCode.DISABLED_BY_GENERATOR, UXAppBootStatusCode.DISABLED_BY_KERNEL_CMDLINE, UXAppBootStatusCode.DISABLED_BY_MARKER_FILE, + UXAppBootStatusCode.DISABLED_BY_ENV_VARIABLE, ] ) @@ -184,6 +186,16 @@ def get_bootstatus(disable_file, paths) -> Tuple[UXAppBootStatusCode, str]: elif "cloud-init=disabled" in cmdline_parts: bootstatus_code = UXAppBootStatusCode.DISABLED_BY_KERNEL_CMDLINE reason = "Cloud-init disabled by kernel parameter cloud-init=disabled" + elif "cloud-init=disabled" in os.environ.get("KERNEL_CMDLINE", "") or ( + uses_systemd() + and "cloud-init=disabled" + in subp.subp(["systemctl", "show-environment"]).stdout + ): + bootstatus_code = UXAppBootStatusCode.DISABLED_BY_ENV_VARIABLE + reason = ( + "Cloud-init disabled by environment variable " + "KERNEL_CMDLINE=cloud-init=disabled" + ) elif os.path.exists(os.path.join(paths.run_dir, "disabled")): bootstatus_code = UXAppBootStatusCode.DISABLED_BY_GENERATOR reason = "Cloud-init disabled by cloud-init-generator" diff --git a/doc/rtd/howto/disable_cloud_init.rst b/doc/rtd/howto/disable_cloud_init.rst index f1cc394f7ed..721e20c2c06 100644 --- a/doc/rtd/howto/disable_cloud_init.rst +++ b/doc/rtd/howto/disable_cloud_init.rst @@ -6,7 +6,7 @@ How to disable cloud-init One may wish to disable cloud-init to ensure that it doesn't do anything on subsequent boots. Some parts of cloud-init may run once per boot otherwise. -There are two cross-platform methods of disabling ``cloud-init``. +There are three cross-platform methods of disabling ``cloud-init``. Method 1: text file ==================== @@ -34,6 +34,15 @@ Example (using GRUB2 with Ubuntu): $ echo 'GRUB_CMDLINE_LINUX="cloud-init=disabled"' >> /etc/default/grub $ grub-mkconfig -o /boot/efi/EFI/ubuntu/grub.cfg -.. note:: - When running in containers, ``cloud-init`` will read an environment - variable named ``KERNEL_CMDLINE`` in place of a kernel commandline. +Method 3: environment variable +============================== + +To disable cloud-init, pass the environment variable +``KERNEL_CMDLINE=cloud-init=disabled`` into each of cloud-init's +processes. + +Example (using systemd): + +.. code-block:: + + $ echo "DefaultEnvironment=KERNEL_CMDLINE=cloud-init=disabled" >> /etc/systemd/system.conf diff --git a/systemd/cloud-config.service.tmpl b/systemd/cloud-config.service.tmpl index 76e50ae180c..31d9d983e13 100644 --- a/systemd/cloud-config.service.tmpl +++ b/systemd/cloud-config.service.tmpl @@ -5,10 +5,9 @@ After=network-online.target cloud-config.target After=snapd.seeded.service Before=systemd-user-sessions.service Wants=network-online.target cloud-config.target -{% if variant == "rhel" %} ConditionPathExists=!/etc/cloud/cloud-init.disabled ConditionKernelCommandLine=!cloud-init=disabled -{% endif %} +ConditionEnvironment=!KERNEL_CMDLINE=cloud-init=disabled [Service] Type=oneshot diff --git a/systemd/cloud-final.service.tmpl b/systemd/cloud-final.service.tmpl index 85f423ac345..bcf8b009419 100644 --- a/systemd/cloud-final.service.tmpl +++ b/systemd/cloud-final.service.tmpl @@ -7,10 +7,9 @@ After=multi-user.target Before=apt-daily.service {% endif %} Wants=network-online.target cloud-config.service -{% if variant == "rhel" %} ConditionPathExists=!/etc/cloud/cloud-init.disabled ConditionKernelCommandLine=!cloud-init=disabled -{% endif %} +ConditionEnvironment=!KERNEL_CMDLINE=cloud-init=disabled [Service] diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl index 5b3dcd68fdf..3c9ca16958c 100644 --- a/systemd/cloud-init-generator.tmpl +++ b/systemd/cloud-init-generator.tmpl @@ -23,6 +23,8 @@ CLOUD_SYSTEM_TARGET="/lib/systemd/system/cloud-init.target" {% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", "miraclelinux", "openeuler", "OpenCloudOS", "openmandriva", "rhel", "rocky", "TencentOS", "virtuozzo"] %} dsidentify="/usr/libexec/cloud-init/ds-identify" +{% elif variant == "benchmark" %} + dsidentify="/bin/true" {% else %} dsidentify="/usr/lib/cloud-init/ds-identify" {% endif %} @@ -40,105 +42,51 @@ debug() { echo "$@" >> "$LOG" } -etc_file() { - local pprefix="${1:-/etc/cloud/cloud-init.}" - _RET="unset" - [ -f "${pprefix}$ENABLE" ] && _RET="$ENABLE" && return 0 - [ -f "${pprefix}$DISABLE" ] && _RET="$DISABLE" && return 0 - return 0 -} - -read_proc_cmdline() { - # return /proc/cmdline for non-container, and /proc/1/cmdline for container - local ctname="systemd" - if [ -n "$CONTAINER" ] && ctname=$CONTAINER || - systemd-detect-virt --container --quiet; then - if { _RET=$(tr '\0' ' ' < /proc/1/cmdline); } 2>/dev/null; then - _RET_MSG="container[$ctname]: pid 1 cmdline" - return - fi - _RET="" - _RET_MSG="container[$ctname]: pid 1 cmdline not available" - return 0 - fi - - _RET_MSG="/proc/cmdline" - read _RET < /proc/cmdline -} - -kernel_cmdline() { - local cmdline="" tok="" - if [ -n "${KERNEL_CMDLINE+x}" ]; then - # use KERNEL_CMDLINE if present in environment even if empty - cmdline=${KERNEL_CMDLINE} - debug 1 "kernel command line from env KERNEL_CMDLINE: $cmdline" - else - read_proc_cmdline && cmdline="$_RET" && - debug 1 "kernel command line ($_RET_MSG): $cmdline" - fi - _RET="unset" - cmdline=" $cmdline " - tok=${cmdline##* cloud-init=} - [ "$tok" = "$cmdline" ] && _RET="unset" - tok=${tok%% *} - [ "$tok" = "$ENABLE" -o "$tok" = "$DISABLE" ] && _RET="$tok" - return 0 -} - -default() { - _RET="$ENABLE" -} - -check_for_datasource() { - local ds_rc="" - if [ ! -x "$dsidentify" ]; then - debug 1 "no ds-identify in $dsidentify" - return 0 - fi - $dsidentify - ds_rc=$? - debug 1 "ds-identify rc=$ds_rc" - if [ "$ds_rc" = "0" ]; then - return 0 - fi - return 1 -} - main() { local normal_d="$1" early_d="$2" late_d="$3" local target_name="multi-user.target" gen_d="$early_d" local link_path="$gen_d/${target_name}.wants/${CLOUD_TARGET_NAME}" - local ds="" + local ds="" ret="" debug 1 "$0 normal=$normal_d early=$early_d late=$late_d" debug 2 "$0 $*" - local search result="error" ret="" - for search in kernel_cmdline etc_file default; do - if $search; then - debug 1 "$search found $_RET" - [ "$_RET" = "$ENABLE" -o "$_RET" = "$DISABLE" ] && - result=$_RET && break - else - ret=$? - debug 0 "search $search returned $ret" - fi - done + # ds=found => enable + # ds=notfound => disable + # => disable + debug 1 "checking for datasource" - # enable AND ds=found == enable - # enable AND ds=notfound == disable - # disable || == disabled - if [ "$result" = "$ENABLE" ]; then - debug 1 "checking for datasource" - check_for_datasource - ds=$? + if [ ! -x "$dsidentify" ]; then + debug 1 "no ds-identify in $dsidentify" + ds=0 + fi + $dsidentify + ds=$? + debug 1 "ds-identify rc=$ds" + + if [ "$ds" = "1" -o "$ds" = "2" ]; then if [ "$ds" = "1" ]; then debug 1 "cloud-init is enabled but no datasource found, disabling" - result="$DISABLE" + else + debug 1 "cloud-init is disabled by kernel commandline or etc_file" fi - fi + if [ -f "$link_path" ]; then + if rm -f "$link_path"; then + debug 1 "disabled. removed existing $link_path" + else + ret=$? + debug 0 "[$ret] disable failed, remove $link_path" + fi + else + debug 1 "already disabled: no change needed [no $link_path]" + fi + if [ -e "$RUN_ENABLED_FILE" ]; then + debug 1 "removing $RUN_ENABLED_FILE and creating $RUN_DISABLED_FILE" + rm -f "$RUN_ENABLED_FILE" + fi + : > "$RUN_DISABLED_FILE" - if [ "$result" = "$ENABLE" ]; then + elif [ "$ds" = "0" ]; then if [ -e "$link_path" ]; then debug 1 "already enabled: no change needed" else @@ -157,22 +105,6 @@ main() { rm -f $RUN_DISABLED_FILE fi : > "$RUN_ENABLED_FILE" - elif [ "$result" = "$DISABLE" ]; then - if [ -f "$link_path" ]; then - if rm -f "$link_path"; then - debug 1 "disabled. removed existing $link_path" - else - ret=$? - debug 0 "[$ret] disable failed, remove $link_path" - fi - else - debug 1 "already disabled: no change needed [no $link_path]" - fi - if [ -e "$RUN_ENABLED_FILE" ]; then - debug 1 "removing $RUN_ENABLED_FILE and creating $RUN_DISABLED_FILE" - rm -f "$RUN_ENABLED_FILE" - fi - : > "$RUN_DISABLED_FILE" else debug 0 "unexpected result '$result' 'ds=$ds'" ret=3 diff --git a/systemd/cloud-init-local.service.tmpl b/systemd/cloud-init-local.service.tmpl index 6f3f9d8d09e..3a1ca7fa253 100644 --- a/systemd/cloud-init-local.service.tmpl +++ b/systemd/cloud-init-local.service.tmpl @@ -26,10 +26,9 @@ Before=sysinit.target Conflicts=shutdown.target {% endif %} RequiresMountsFor=/var/lib/cloud -{% if variant == "rhel" %} ConditionPathExists=!/etc/cloud/cloud-init.disabled ConditionKernelCommandLine=!cloud-init=disabled -{% endif %} +ConditionEnvironment=!KERNEL_CMDLINE=cloud-init=disabled [Service] Type=oneshot diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index 26d2e39c559..bf91164a4bb 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -38,10 +38,9 @@ Conflicts=shutdown.target Before=shutdown.target Conflicts=shutdown.target {% endif %} -{% if variant == "rhel" %} ConditionPathExists=!/etc/cloud/cloud-init.disabled ConditionKernelCommandLine=!cloud-init=disabled -{% endif %} +ConditionEnvironment=!KERNEL_CMDLINE=cloud-init=disabled [Service] Type=oneshot diff --git a/systemd/cloud-init.target b/systemd/cloud-init.target index 760dfee5e4e..30450f7ff23 100644 --- a/systemd/cloud-init.target +++ b/systemd/cloud-init.target @@ -10,3 +10,6 @@ [Unit] Description=Cloud-init target After=multi-user.target +ConditionPathExists=!/etc/cloud/cloud-init.disabled +ConditionKernelCommandLine=!cloud-init=disabled +ConditionEnvironment=!KERNEL_CMDLINE=cloud-init=disabled diff --git a/tests/integration_tests/test_kernel_commandline_match.py b/tests/integration_tests/test_kernel_commandline_match.py index c042e428ec2..8a7d74f8b0c 100644 --- a/tests/integration_tests/test_kernel_commandline_match.py +++ b/tests/integration_tests/test_kernel_commandline_match.py @@ -11,7 +11,24 @@ log = logging.getLogger("integration_testing") -def override_kernel_cmdline(ds_str: str, c: IntegrationInstance) -> str: +def restart_cloud_init(c): + client = c + client.instance.shutdown(wait=False) + try: + client.instance.wait_for_state("STOPPED", num_retries=20) + except RuntimeError as e: + log.warning( + "Retrying shutdown due to timeout on initial shutdown request %s", + str(e), + ) + client.instance.shutdown() + + client.instance.execute_via_ssh = False + client.instance.start() + client.execute("cloud-init status --wait") + + +def override_kernel_cmdline(ds_str: str, c: IntegrationInstance): """ Configure grub's kernel command line to tell cloud-init to use OpenStack - even though LXD should naturally be detected. @@ -44,20 +61,7 @@ def override_kernel_cmdline(ds_str: str, c: IntegrationInstance) -> str: # most likely be as simple as updating the output path for grub-mkconfig client.execute("grub-mkconfig -o /boot/efi/EFI/ubuntu/grub.cfg") client.execute("cloud-init clean --logs") - client.instance.shutdown(wait=False) - try: - client.instance.wait_for_state("STOPPED", num_retries=20) - except RuntimeError as e: - log.warning( - "Retrying shutdown due to timeout on initial shutdown request %s", - str(e), - ) - client.instance.shutdown() - - client.instance.execute_via_ssh = False - client.instance.start() - client.execute("cloud-init status --wait") - return client.execute("cat /var/log/cloud-init.log") + restart_cloud_init(client) @pytest.mark.skipif(PLATFORM != "lxd_vm", reason="Modifies grub config") @@ -85,10 +89,11 @@ def test_lxd_datasource_kernel_override( kernel commandline in Python code is required. """ + override_kernel_cmdline(ds_str, client) assert ( "Machine is configured by the kernel commandline to run on single " f"datasource {configured}" - ) in override_kernel_cmdline(ds_str, client) + ) in client.execute("cat /var/log/cloud-init.log") GH_REPO_PATH = "https://raw.githubusercontent.com/canonical/cloud-init/main/" @@ -129,7 +134,9 @@ def test_lxd_datasource_kernel_override_nocloud_net( client.install_new_cloud_init( source, take_snapshot=False, clean=False ) - logs = override_kernel_cmdline(ds_str, client) + override_kernel_cmdline(ds_str, client) + + logs = client.execute("cat /var/log/cloud-init.log") assert ( "nocloud" == client.execute("cloud-init query platform").stdout.strip() @@ -139,3 +146,40 @@ def test_lxd_datasource_kernel_override_nocloud_net( "Detected platform: DataSourceNoCloudNet [seed=None]" "[dsmode=net]. Checking for active instance data" ) in logs + + +@pytest.mark.skipif(PLATFORM != "lxd_vm", reason="Modifies grub config") +@pytest.mark.lxd_use_exec +def test_lxd_disable_cloud_init_cmdline(client: IntegrationInstance): + """Verify cloud-init disablement via kernel commandline works.""" + + override_kernel_cmdline("cloud-init=disabled", client) + assert "Active: inactive (dead)" in client.execute( + "systemctl status cloud-init" + ) + + +@pytest.mark.lxd_use_exec +def test_lxd_disable_cloud_init_file(client: IntegrationInstance): + """Verify cloud-init disablement via file works.""" + + client.execute("touch /etc/cloud/cloud-init.disabled") + client.execute("cloud-init --clean") + restart_cloud_init(client) + assert "Active: inactive (dead)" in client.execute( + "systemctl status cloud-init" + ) + + +@pytest.mark.lxd_use_exec +def test_lxd_disable_cloud_init_env(client: IntegrationInstance): + """Verify cloud-init disablement via environment variable works.""" + env = """DefaultEnvironment=KERNEL_CMDLINE=cloud-init=disabled""" + + client.execute(f'echo "{env}" >> /etc/systemd/system.conf') + + client.execute("cloud-init --clean") + restart_cloud_init(client) + assert "Active: inactive (dead)" in client.execute( + "systemctl status cloud-init" + ) diff --git a/tests/unittests/cmd/test_status.py b/tests/unittests/cmd/test_status.py index 994209a1b9a..76fdb510708 100644 --- a/tests/unittests/cmd/test_status.py +++ b/tests/unittests/cmd/test_status.py @@ -164,16 +164,26 @@ def test_get_bootstatus( ): if ensured_file is not None: ensure_file(ensured_file(config)) - (code, reason) = wrap_and_call( - M_NAME, - { - "uses_systemd": uses_systemd, - "get_cmdline": get_cmdline, - }, - status.get_bootstatus, - config.disable_file, - config.paths, - ) + with mock.patch( + f"{M_PATH}subp.subp", + return_value=SubpResult( + """\ +LANG=en_US.UTF-8 +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin +""", + stderr=None, + ), + ): + code, reason = wrap_and_call( + M_NAME, + { + "uses_systemd": uses_systemd, + "get_cmdline": get_cmdline, + }, + status.get_bootstatus, + config.disable_file, + config.paths, + ) assert code == expected_bootstatus, failure_msg if isinstance(expected_reason, str): assert reason == expected_reason diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 66dae60e4a1..200c488c651 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -329,6 +329,10 @@ def write_mock(data): "ret": 1, "err": "No dmidecode program. ERROR.", }, + { + "name": "is_disabled", + "ret": 1, + }, { "name": "get_kenv_field", "ret": 1, diff --git a/tools/ds-identify b/tools/ds-identify index 8b5229d8ebf..051bc2376a5 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -1039,6 +1039,23 @@ has_ovf_cdrom() { return 1 } +is_disabled() { + if [ -f /etc/cloud/cloud-init.disabled ]; then + debug 1 "disabled by marker file /etc/cloud-init.disabled" + return 0 + fi + if [ "${KERNEL_CMDLINE:-}" = "cloud-init=disabled" ]; then + debug 1 "disabled by KERNEL_CMDLINE environment variable" + return 0 + fi + case "$DI_KERNEL_CMDLINE" in + *cloud-init=disabled*) + debug 1 "disabled by kernel command line cloud-init=disabled" + return 0 + esac + return 1 +} + dscheck_OVF() { check_seed_dir ovf ovf-env.xml && return "${DS_FOUND}" @@ -1517,10 +1534,7 @@ dscheck_VMware() { } collect_info() { - read_uname_info - read_virt read_pid1_product_name - read_kernel_cmdline read_config read_datasource_list read_dmi_sys_vendor @@ -1795,6 +1809,12 @@ _main() { read_uptime debug 1 "[up ${_RET}s]" "ds-identify $*" + read_uname_info + read_virt + read_kernel_cmdline + if is_disabled; then + return 2 + fi collect_info if [ "$DI_LOG" = "stderr" ]; then diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg index fac63f81367..f5f9e05f074 100755 --- a/tools/render-cloudcfg +++ b/tools/render-cloudcfg @@ -15,6 +15,7 @@ def main(): "alpine", "amazon", "arch", + "benchmark", "centos", "cloudlinux", "debian",