commit 7f8bf27d61bb4c667bc9ec54060a93236d8ef7de Author: CentOS Sources Date: Tue Nov 15 01:34:59 2022 -0500 import cloud-init-22.1-5.el9 diff --git a/.cloud-init.metadata b/.cloud-init.metadata new file mode 100644 index 0000000..0356978 --- /dev/null +++ b/.cloud-init.metadata @@ -0,0 +1 @@ +830185bb5ce87ad86e4d1c0c62329bb255ec1648 SOURCES/cloud-init-22.1.tar.gz diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf19bdd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +SOURCES/cloud-init-22.1.tar.gz diff --git a/SOURCES/0001-Add-initial-redhat-changes.patch b/SOURCES/0001-Add-initial-redhat-changes.patch new file mode 100644 index 0000000..d9ae844 --- /dev/null +++ b/SOURCES/0001-Add-initial-redhat-changes.patch @@ -0,0 +1,621 @@ +From 7daf47d1ee60a132244f2a9ec4e89ad496c32c3e Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 28 Mar 2022 14:08:05 +0200 +Subject: Add initial redhat setup + +Merged patches (22.1): +- 70f80f82 Use _systemdgeneratordir macro for cloud-init-generator handling +- 0f74e7b8 Add dhcp-client as a dependency +- 791eb2b6 Report full specific version with "cloud-init --version" +- 865805d6 source-git: Use dynamic SRPM_NAME +- 45ddc666 add the drop-in also in the %files section of cloud-init.spec +- 08d939b2 Add gdisk and openssl as deps to fix UEFI / Azure initialization +- 0531cd68 Add netifaces package as a Requires in cloud-init.spec.template + +Conflicts: +- .gitignore +- cloudinit/config/cc_chef.py = use double quotes instead of single quotes +- cloudinit/settings.py = use rhel settings +- cloudinit/sources/DataSourceAzure.py = remove temporary_hostname and helper +- requirements.txt = just add netifaces and not jsonschema dep +- setup.py = use double quotes instead of single quotes + +Signed-off-by: Emanuele Giuseppe Esposito + +Merged patches (RHEL-9/21.1): +- 5688a1d0 Removing python-nose and python-tox as dependency +- 237d57f9 Removing mock dependency +- d1c2f496 Removing python-jsonschema dependency +- 0d1cd14c Don't override default network configuration + +Merged patches (21.1): +- 915d30ad Change gating file to correct rhel version +- 311f318d Removing net-tools dependency +- 74731806 Adding man pages to Red Hat spec file +- 758d333d Removing blocking test from yaml configuration file +- c7e7c59c Changing permission of cloud-init-generator to 755 +- 8b85abbb Installing man pages in the correct place with correct permissions +- c6808d8d Fix unit failure of cloud-final.service if NetworkManager was not present. +- 11866ef6 Report full specific version with "cloud-init --version" + +Rebase notes (18.5): +- added bash_completition file +- added cloud-id file + +Merged patches (20.3): +- 01900d0 changing ds-identify patch from /usr/lib to /usr/libexec +- 7f47ca3 Render the generator from template instead of cp + +Merged patches (19.4): +- 4ab5a61 Fix for network configuration not persisting after reboot +- 84cf125 Removing cloud-user from wheel +- 31290ab Adding gating tests for Azure, ESXi and AWS + +Merged patches (18.5): +- 2d6b469 add power-state-change module to cloud_final_modules +- 764159f Adding systemd mount options to wait for cloud-init +- da4d99e Adding disk_setup to rhel/cloud.cfg +- f5c6832 Enable cloud-init by default on vmware + +Conflicts: +cloudinit/config/cc_chef.py: +- Updated header documentation text +- Replacing double quotes by simple quotes + +setup.py: +- Adding missing cmdclass info + +Signed-off-by: Eduardo Otubo + +Changes: +- move redhat to .distro to use new build script structure +- Fixing changelog for RHEL 9 + +Merged patches (21.1): +- 69bd7f71 DataSourceAzure.py: use hostnamectl to set hostname +- 0407867e Remove race condition between cloud-init and NetworkManager + +Signed-off-by: Miroslav Rezanina +--- + .distro/.gitignore | 1 + + .distro/Makefile | 74 ++++ + .distro/Makefile.common | 20 + + .distro/cloud-init-tmpfiles.conf | 1 + + .distro/cloud-init.spec.template | 505 ++++++++++++++++++++++++++ + .distro/gating.yaml | 8 + + .distro/rpmbuild/BUILD/.gitignore | 3 + + .distro/rpmbuild/RPMS/.gitignore | 3 + + .distro/rpmbuild/SOURCES/.gitignore | 3 + + .distro/rpmbuild/SPECS/.gitignore | 3 + + .distro/rpmbuild/SRPMS/.gitignore | 3 + + .distro/scripts/frh.py | 27 ++ + .distro/scripts/git-backport-diff | 327 +++++++++++++++++ + .distro/scripts/git-compile-check | 215 +++++++++++ + .distro/scripts/process-patches.sh | 88 +++++ + .distro/scripts/tarball_checksum.sh | 3 + + .gitignore | 1 + + cloudinit/config/cc_chef.py | 65 +++- + cloudinit/settings.py | 21 +- + requirements.txt | 3 - + rhel/README.rhel | 5 + + rhel/cloud-init-tmpfiles.conf | 1 + + rhel/cloud.cfg | 69 ++++ + rhel/systemd/cloud-config.service | 18 + + rhel/systemd/cloud-config.target | 11 + + rhel/systemd/cloud-final.service | 24 ++ + rhel/systemd/cloud-init-local.service | 31 ++ + rhel/systemd/cloud-init.service | 26 ++ + rhel/systemd/cloud-init.target | 7 + + setup.py | 28 +- + tools/read-version | 28 +- + 31 files changed, 1557 insertions(+), 65 deletions(-) + create mode 100644 .distro/.gitignore + create mode 100644 .distro/Makefile + create mode 100644 .distro/Makefile.common + create mode 100644 .distro/cloud-init-tmpfiles.conf + create mode 100644 .distro/cloud-init.spec.template + create mode 100644 .distro/gating.yaml + create mode 100644 .distro/rpmbuild/BUILD/.gitignore + create mode 100644 .distro/rpmbuild/RPMS/.gitignore + create mode 100644 .distro/rpmbuild/SOURCES/.gitignore + create mode 100644 .distro/rpmbuild/SPECS/.gitignore + create mode 100644 .distro/rpmbuild/SRPMS/.gitignore + create mode 100755 .distro/scripts/frh.py + create mode 100755 .distro/scripts/git-backport-diff + create mode 100755 .distro/scripts/git-compile-check + create mode 100755 .distro/scripts/process-patches.sh + create mode 100755 .distro/scripts/tarball_checksum.sh + create mode 100644 rhel/README.rhel + create mode 100644 rhel/cloud-init-tmpfiles.conf + create mode 100644 rhel/cloud.cfg + create mode 100644 rhel/systemd/cloud-config.service + create mode 100644 rhel/systemd/cloud-config.target + create mode 100644 rhel/systemd/cloud-final.service + create mode 100644 rhel/systemd/cloud-init-local.service + create mode 100644 rhel/systemd/cloud-init.service + create mode 100644 rhel/systemd/cloud-init.target + +diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py +index fdb3a6e3..d028c548 100644 +--- a/cloudinit/config/cc_chef.py ++++ b/cloudinit/config/cc_chef.py +@@ -6,7 +6,70 @@ + # + # This file is part of cloud-init. See LICENSE file for license information. + +-"""Chef: module that configures, starts and installs chef.""" ++""" ++Chef ++---- ++**Summary:** module that configures, starts and installs chef. ++ ++This module enables chef to be installed (from packages or ++from gems, or from omnibus). Before this occurs chef configurations are ++written to disk (validation.pem, client.pem, firstboot.json, client.rb), ++and needed chef folders/directories are created (/etc/chef and /var/log/chef ++and so-on). Then once installing proceeds correctly if configured chef will ++be started (in daemon mode or in non-daemon mode) and then once that has ++finished (if ran in non-daemon mode this will be when chef finishes ++converging, if ran in daemon mode then no further actions are possible since ++chef will have forked into its own process) then a post run function can ++run that can do finishing activities (such as removing the validation pem ++file). ++ ++**Internal name:** ``cc_chef`` ++ ++**Module frequency:** per always ++ ++**Supported distros:** all ++ ++**Config keys**:: ++ ++ chef: ++ directories: (defaulting to /etc/chef, /var/log/chef, /var/lib/chef, ++ /var/cache/chef, /var/backups/chef, /run/chef) ++ validation_cert: (optional string to be written to file validation_key) ++ special value 'system' means set use existing file ++ validation_key: (optional the path for validation_cert. default ++ /etc/chef/validation.pem) ++ firstboot_path: (path to write run_list and initial_attributes keys that ++ should also be present in this configuration, defaults ++ to /etc/chef/firstboot.json) ++ exec: boolean to run or not run chef (defaults to false, unless ++ a gem installed is requested ++ where this will then default ++ to true) ++ ++ chef.rb template keys (if falsey, then will be skipped and not ++ written to /etc/chef/client.rb) ++ ++ chef: ++ client_key: ++ encrypted_data_bag_secret: ++ environment: ++ file_backup_path: ++ file_cache_path: ++ json_attribs: ++ log_level: ++ log_location: ++ node_name: ++ omnibus_url: ++ omnibus_url_retries: ++ omnibus_version: ++ pid_file: ++ server_url: ++ show_time: ++ ssl_verify_mode: ++ validation_cert: ++ validation_key: ++ validation_name: ++""" + + import itertools + import json +diff --git a/cloudinit/settings.py b/cloudinit/settings.py +index ecc1403b..a780e21e 100644 +--- a/cloudinit/settings.py ++++ b/cloudinit/settings.py +@@ -48,16 +48,19 @@ CFG_BUILTIN = { + # At the end to act as a 'catch' when none of the above work... + "None", + ], +- "def_log_file": "/var/log/cloud-init.log", +- "log_cfgs": [], +- "syslog_fix_perms": ["syslog:adm", "root:adm", "root:wheel", "root:root"], +- "system_info": { +- "paths": { +- "cloud_dir": "/var/lib/cloud", +- "templates_dir": "/etc/cloud/templates/", ++ 'def_log_file': '/var/log/cloud-init.log', ++ 'log_cfgs': [], ++ 'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'], ++ 'ssh_deletekeys': False, ++ 'ssh_genkeytypes': [], ++ 'syslog_fix_perms': [], ++ 'system_info': { ++ 'paths': { ++ 'cloud_dir': '/var/lib/cloud', ++ 'templates_dir': '/etc/cloud/templates/', + }, +- "distro": "ubuntu", +- "network": {"renderers": None}, ++ 'distro': 'rhel', ++ 'network': {'renderers': None}, + }, + "vendor_data": {"enabled": True, "prefix": []}, + "vendor_data2": {"enabled": True, "prefix": []}, +diff --git a/requirements.txt b/requirements.txt +index c4adc455..f77f1d73 100644 +--- a/requirements.txt ++++ b/requirements.txt +@@ -30,9 +30,6 @@ requests + # For patching pieces of cloud-config together + jsonpatch + +-# For validating cloud-config sections per schema definitions +-jsonschema +- + # Used by DataSourceVMware to inspect the host's network configuration during + # the "setup()" function. + # +diff --git a/rhel/README.rhel b/rhel/README.rhel +new file mode 100644 +index 00000000..aa29630d +--- /dev/null ++++ b/rhel/README.rhel +@@ -0,0 +1,5 @@ ++The following cloud-init modules are currently unsupported on this OS: ++ - apt_update_upgrade ('apt_update', 'apt_upgrade', 'apt_mirror', 'apt_preserve_sources_list', 'apt_old_mirror', 'apt_sources', 'debconf_selections', 'packages' options) ++ - byobu ('byobu_by_default' option) ++ - chef ++ - grub_dpkg +diff --git a/rhel/cloud-init-tmpfiles.conf b/rhel/cloud-init-tmpfiles.conf +new file mode 100644 +index 00000000..0c6d2a3b +--- /dev/null ++++ b/rhel/cloud-init-tmpfiles.conf +@@ -0,0 +1 @@ ++d /run/cloud-init 0700 root root - - +diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg +new file mode 100644 +index 00000000..9ecba215 +--- /dev/null ++++ b/rhel/cloud.cfg +@@ -0,0 +1,69 @@ ++users: ++ - default ++ ++disable_root: 1 ++ssh_pwauth: 0 ++ ++mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2'] ++resize_rootfs_tmp: /dev ++ssh_deletekeys: 1 ++ssh_genkeytypes: ~ ++syslog_fix_perms: ~ ++disable_vmware_customization: false ++ ++cloud_init_modules: ++ - disk_setup ++ - migrator ++ - bootcmd ++ - write-files ++ - growpart ++ - resizefs ++ - set_hostname ++ - update_hostname ++ - update_etc_hosts ++ - rsyslog ++ - users-groups ++ - ssh ++ ++cloud_config_modules: ++ - mounts ++ - locale ++ - set-passwords ++ - rh_subscription ++ - yum-add-repo ++ - package-update-upgrade-install ++ - timezone ++ - puppet ++ - chef ++ - salt-minion ++ - mcollective ++ - disable-ec2-metadata ++ - runcmd ++ ++cloud_final_modules: ++ - rightscale_userdata ++ - scripts-per-once ++ - scripts-per-boot ++ - scripts-per-instance ++ - scripts-user ++ - ssh-authkey-fingerprints ++ - keys-to-console ++ - phone-home ++ - final-message ++ - power-state-change ++ ++system_info: ++ default_user: ++ name: cloud-user ++ lock_passwd: true ++ gecos: Cloud User ++ groups: [adm, systemd-journal] ++ sudo: ["ALL=(ALL) NOPASSWD:ALL"] ++ shell: /bin/bash ++ distro: rhel ++ paths: ++ cloud_dir: /var/lib/cloud ++ templates_dir: /etc/cloud/templates ++ ssh_svcname: sshd ++ ++# vim:syntax=yaml +diff --git a/rhel/systemd/cloud-config.service b/rhel/systemd/cloud-config.service +new file mode 100644 +index 00000000..f3dcd4be +--- /dev/null ++++ b/rhel/systemd/cloud-config.service +@@ -0,0 +1,18 @@ ++[Unit] ++Description=Apply the settings specified in cloud-config ++After=network-online.target cloud-config.target ++Wants=network-online.target cloud-config.target ++ConditionPathExists=!/etc/cloud/cloud-init.disabled ++ConditionKernelCommandLine=!cloud-init=disabled ++ ++[Service] ++Type=oneshot ++ExecStart=/usr/bin/cloud-init modules --mode=config ++RemainAfterExit=yes ++TimeoutSec=0 ++ ++# Output needs to appear in instance console output ++StandardOutput=journal+console ++ ++[Install] ++WantedBy=cloud-init.target +diff --git a/rhel/systemd/cloud-config.target b/rhel/systemd/cloud-config.target +new file mode 100644 +index 00000000..ae9b7d02 +--- /dev/null ++++ b/rhel/systemd/cloud-config.target +@@ -0,0 +1,11 @@ ++# cloud-init normally emits a "cloud-config" upstart event to inform third ++# parties that cloud-config is available, which does us no good when we're ++# using systemd. cloud-config.target serves as this synchronization point ++# instead. Services that would "start on cloud-config" with upstart can ++# instead use "After=cloud-config.target" and "Wants=cloud-config.target" ++# as appropriate. ++ ++[Unit] ++Description=Cloud-config availability ++Wants=cloud-init-local.service cloud-init.service ++After=cloud-init-local.service cloud-init.service +diff --git a/rhel/systemd/cloud-final.service b/rhel/systemd/cloud-final.service +new file mode 100644 +index 00000000..e281c0cf +--- /dev/null ++++ b/rhel/systemd/cloud-final.service +@@ -0,0 +1,24 @@ ++[Unit] ++Description=Execute cloud user/final scripts ++After=network-online.target cloud-config.service rc-local.service ++Wants=network-online.target cloud-config.service ++ConditionPathExists=!/etc/cloud/cloud-init.disabled ++ConditionKernelCommandLine=!cloud-init=disabled ++ ++[Service] ++Type=oneshot ++ExecStart=/usr/bin/cloud-init modules --mode=final ++RemainAfterExit=yes ++TimeoutSec=0 ++KillMode=process ++# Restart NetworkManager if it is present and running. ++ExecStartPost=/bin/sh -c 'u=NetworkManager.service; \ ++ out=$(systemctl show --property=SubState $u) || exit; \ ++ [ "$out" = "SubState=running" ] || exit 0; \ ++ systemctl reload-or-try-restart $u' ++ ++# Output needs to appear in instance console output ++StandardOutput=journal+console ++ ++[Install] ++WantedBy=cloud-init.target +diff --git a/rhel/systemd/cloud-init-local.service b/rhel/systemd/cloud-init-local.service +new file mode 100644 +index 00000000..8f9f6c9f +--- /dev/null ++++ b/rhel/systemd/cloud-init-local.service +@@ -0,0 +1,31 @@ ++[Unit] ++Description=Initial cloud-init job (pre-networking) ++DefaultDependencies=no ++Wants=network-pre.target ++After=systemd-remount-fs.service ++Requires=dbus.socket ++After=dbus.socket ++Before=NetworkManager.service network.service ++Before=network-pre.target ++Before=shutdown.target ++Before=firewalld.target ++Conflicts=shutdown.target ++RequiresMountsFor=/var/lib/cloud ++ConditionPathExists=!/etc/cloud/cloud-init.disabled ++ConditionKernelCommandLine=!cloud-init=disabled ++ ++[Service] ++Type=oneshot ++ExecStartPre=/bin/mkdir -p /run/cloud-init ++ExecStartPre=/sbin/restorecon /run/cloud-init ++ExecStartPre=/usr/bin/touch /run/cloud-init/enabled ++ExecStart=/usr/bin/cloud-init init --local ++ExecStart=/bin/touch /run/cloud-init/network-config-ready ++RemainAfterExit=yes ++TimeoutSec=0 ++ ++# Output needs to appear in instance console output ++StandardOutput=journal+console ++ ++[Install] ++WantedBy=cloud-init.target +diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service +new file mode 100644 +index 00000000..0b3d796d +--- /dev/null ++++ b/rhel/systemd/cloud-init.service +@@ -0,0 +1,26 @@ ++[Unit] ++Description=Initial cloud-init job (metadata service crawler) ++Wants=cloud-init-local.service ++Wants=sshd-keygen.service ++Wants=sshd.service ++After=cloud-init-local.service ++After=NetworkManager.service network.service ++After=NetworkManager-wait-online.service ++Before=network-online.target ++Before=sshd-keygen.service ++Before=sshd.service ++Before=systemd-user-sessions.service ++ConditionPathExists=!/etc/cloud/cloud-init.disabled ++ConditionKernelCommandLine=!cloud-init=disabled ++ ++[Service] ++Type=oneshot ++ExecStart=/usr/bin/cloud-init init ++RemainAfterExit=yes ++TimeoutSec=0 ++ ++# Output needs to appear in instance console output ++StandardOutput=journal+console ++ ++[Install] ++WantedBy=cloud-init.target +diff --git a/rhel/systemd/cloud-init.target b/rhel/systemd/cloud-init.target +new file mode 100644 +index 00000000..083c3b6f +--- /dev/null ++++ b/rhel/systemd/cloud-init.target +@@ -0,0 +1,7 @@ ++# cloud-init target is enabled by cloud-init-generator ++# To disable it you can either: ++# a.) boot with kernel cmdline of 'cloud-init=disabled' ++# b.) touch a file /etc/cloud/cloud-init.disabled ++[Unit] ++Description=Cloud-init target ++After=multi-user.target +diff --git a/setup.py b/setup.py +index a9132d2c..3c377eaa 100755 +--- a/setup.py ++++ b/setup.py +@@ -139,21 +139,6 @@ INITSYS_FILES = { + "sysvinit_deb": [f for f in glob("sysvinit/debian/*") if is_f(f)], + "sysvinit_openrc": [f for f in glob("sysvinit/gentoo/*") if is_f(f)], + "sysvinit_suse": [f for f in glob("sysvinit/suse/*") if is_f(f)], +- "systemd": [ +- render_tmpl(f) +- for f in ( +- glob("systemd/*.tmpl") +- + glob("systemd/*.service") +- + glob("systemd/*.socket") +- + glob("systemd/*.target") +- ) +- if (is_f(f) and not is_generator(f)) +- ], +- "systemd.generators": [ +- render_tmpl(f, mode=0o755) +- for f in glob("systemd/*") +- if is_f(f) and is_generator(f) +- ], + "upstart": [f for f in glob("upstart/*") if is_f(f)], + } + INITSYS_ROOTS = { +@@ -163,10 +148,6 @@ INITSYS_ROOTS = { + "sysvinit_deb": "etc/init.d", + "sysvinit_openrc": "etc/init.d", + "sysvinit_suse": "etc/init.d", +- "systemd": pkg_config_read("systemd", "systemdsystemunitdir"), +- "systemd.generators": pkg_config_read( +- "systemd", "systemdsystemgeneratordir" +- ), + "upstart": "etc/init/", + } + INITSYS_TYPES = sorted([f.partition(".")[0] for f in INITSYS_ROOTS.keys()]) +@@ -281,15 +262,13 @@ data_files = [ + ( + USR_LIB_EXEC + "/cloud-init", + [ +- "tools/ds-identify", + "tools/hook-hotplug", + "tools/uncloud-init", + "tools/write-ssh-key-fingerprints", + ], + ), + ( +- USR + "/share/bash-completion/completions", +- ["bash_completion/cloud-init"], ++ ETC + "/bash_completion.d", ["bash_completion/cloud-init"], + ), + (USR + "/share/doc/cloud-init", [f for f in glob("doc/*") if is_f(f)]), + ( +@@ -308,8 +287,7 @@ if not platform.system().endswith("BSD"): + ETC + "/NetworkManager/dispatcher.d/", + ["tools/hook-network-manager"], + ), +- (ETC + "/dhcp/dhclient-exit-hooks.d/", ["tools/hook-dhclient"]), +- (LIB + "/udev/rules.d", [f for f in glob("udev/*.rules")]), ++ ("/usr/lib/udev/rules.d", [f for f in glob("udev/*.rules")]), + ( + ETC + "/systemd/system/sshd-keygen@.service.d/", + ["systemd/disable-sshd-keygen-if-cloud-init-active.conf"], +@@ -339,8 +317,6 @@ setuptools.setup( + scripts=["tools/cloud-init-per"], + license="Dual-licensed under GPLv3 or Apache 2.0", + data_files=data_files, +- install_requires=requirements, +- cmdclass=cmdclass, + entry_points={ + "console_scripts": [ + "cloud-init = cloudinit.cmd.main:main", +diff --git a/tools/read-version b/tools/read-version +index 02c90643..79755f78 100755 +--- a/tools/read-version ++++ b/tools/read-version +@@ -71,32 +71,8 @@ version_long = None + is_release_branch_ci = ( + os.environ.get("TRAVIS_PULL_REQUEST_BRANCH", "").startswith("upstream/") + ) +-if is_gitdir(_tdir) and which("git") and not is_release_branch_ci: +- flags = [] +- if use_tags: +- flags = ['--tags'] +- cmd = ['git', 'describe', '--abbrev=8', '--match=[0-9]*'] + flags +- +- try: +- version = tiny_p(cmd).strip() +- except RuntimeError: +- version = None +- +- if version is None or not version.startswith(src_version): +- sys.stderr.write("git describe version (%s) differs from " +- "cloudinit.version (%s)\n" % (version, src_version)) +- sys.stderr.write( +- "Please get the latest upstream tags.\n" +- "As an example, this can be done with the following:\n" +- "$ git remote add upstream https://git.launchpad.net/cloud-init\n" +- "$ git fetch upstream --tags\n" +- ) +- sys.exit(1) +- +- version_long = tiny_p(cmd + ["--long"]).strip() +-else: +- version = src_version +- version_long = None ++version = src_version ++version_long = None + + # version is X.Y.Z[+xxx.gHASH] + # version_long is None or X.Y.Z-xxx-gHASH +-- +2.31.1 + diff --git a/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch b/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch new file mode 100644 index 0000000..a686f6d --- /dev/null +++ b/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch @@ -0,0 +1,282 @@ +From c521ec2ce5b1d9a7322ce152011b8792f121bf5c Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Fri, 7 May 2021 13:36:06 +0200 +Subject: Do not write NM_CONTROLLED=no in generated interface config files + +Conflicts 20.3: + - Not appplying patch on cloudinit/net/sysconfig.py since it now has a +mechanism to identify if cloud-init is running on RHEL, having the +correct settings for NM_CONTROLLED. + +Merged patches (21.1): +- ecbace48 sysconfig: Don't write BOOTPROTO=dhcp for ipv6 dhcp +- a1a00383 include 'NOZEROCONF=yes' in /etc/sysconfig/network +X-downstream-only: true +Signed-off-by: Eduardo Otubo +Signed-off-by: Ryan McCabe +--- + cloudinit/net/sysconfig.py | 12 ++++++++++-- + tests/unittests/test_net.py | 28 ---------------------------- + 2 files changed, 10 insertions(+), 30 deletions(-) + +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index ba85c4f6..d8c53312 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -336,7 +336,6 @@ class Renderer(renderer.Renderer): + "rhel": { + "ONBOOT": True, + "USERCTL": False, +- "NM_CONTROLLED": False, + "BOOTPROTO": "none", + }, + "suse": {"BOOTPROTO": "static", "STARTMODE": "auto"}, +@@ -1039,7 +1038,16 @@ class Renderer(renderer.Renderer): + # Distros configuring /etc/sysconfig/network as a file e.g. Centos + if sysconfig_path.endswith("network"): + util.ensure_dir(os.path.dirname(sysconfig_path)) +- netcfg = [_make_header(), "NETWORKING=yes"] ++ netcfg = [] ++ for line in util.load_file(sysconfig_path, quiet=True).split('\n'): ++ if 'cloud-init' in line: ++ break ++ if not line.startswith(('NETWORKING=', ++ 'IPV6_AUTOCONF=', ++ 'NETWORKING_IPV6=')): ++ netcfg.append(line) ++ # Now generate the cloud-init portion of sysconfig/network ++ netcfg.extend([_make_header(), 'NETWORKING=yes']) + if network_state.use_ipv6: + netcfg.append("NETWORKING_IPV6=yes") + netcfg.append("IPV6_AUTOCONF=no") +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 47e4ba00..591241b3 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -579,7 +579,6 @@ GATEWAY=172.19.3.254 + HWADDR=fa:16:3e:ed:9a:59 + IPADDR=172.19.1.34 + NETMASK=255.255.252.0 +-NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +@@ -712,7 +711,6 @@ IPADDR=172.19.1.34 + IPADDR1=10.0.0.10 + NETMASK=255.255.252.0 + NETMASK1=255.255.255.0 +-NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +@@ -874,7 +872,6 @@ IPV6_AUTOCONF=no + IPV6_DEFAULTGW=2001:DB8::1 + IPV6_FORCE_ACCEPT_RA=no + NETMASK=255.255.252.0 +-NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +@@ -1053,7 +1050,6 @@ NETWORK_CONFIGS = { + BOOTPROTO=none + DEVICE=eth1 + HWADDR=cf:d6:af:48:e8:80 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no""" +@@ -1072,7 +1068,6 @@ NETWORK_CONFIGS = { + IPADDR=192.168.21.3 + NETMASK=255.255.255.0 + METRIC=10000 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no""" +@@ -1244,7 +1239,6 @@ NETWORK_CONFIGS = { + IPV6_AUTOCONF=no + IPV6_FORCE_ACCEPT_RA=no + NETMASK=255.255.255.0 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +@@ -2093,7 +2087,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + DHCPV6C=yes + IPV6INIT=yes + MACADDR=aa:bb:cc:dd:ee:ff +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Bond + USERCTL=no""" +@@ -2103,7 +2096,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + BOOTPROTO=dhcp + DEVICE=bond0.200 + DHCLIENT_SET_DEFAULT_ROUTE=no +- NM_CONTROLLED=no + ONBOOT=yes + PHYSDEV=bond0 + USERCTL=no +@@ -2123,7 +2115,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + IPV6_DEFAULTGW=2001:4800:78ff:1b::1 + MACADDR=bb:bb:bb:bb:bb:aa + NETMASK=255.255.255.0 +- NM_CONTROLLED=no + ONBOOT=yes + PRIO=22 + STP=no +@@ -2135,7 +2126,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + BOOTPROTO=none + DEVICE=eth0 + HWADDR=c0:d6:9f:2c:e8:80 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no""" +@@ -2154,7 +2144,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + MTU=1500 + NETMASK=255.255.255.0 + NETMASK1=255.255.255.0 +- NM_CONTROLLED=no + ONBOOT=yes + PHYSDEV=eth0 + USERCTL=no +@@ -2166,7 +2155,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + DEVICE=eth1 + HWADDR=aa:d6:9f:2c:e8:80 + MASTER=bond0 +- NM_CONTROLLED=no + ONBOOT=yes + SLAVE=yes + TYPE=Ethernet +@@ -2178,7 +2166,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + DEVICE=eth2 + HWADDR=c0:bb:9f:2c:e8:80 + MASTER=bond0 +- NM_CONTROLLED=no + ONBOOT=yes + SLAVE=yes + TYPE=Ethernet +@@ -2190,7 +2177,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + BRIDGE=br0 + DEVICE=eth3 + HWADDR=66:bb:9f:2c:e8:80 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no""" +@@ -2201,7 +2187,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + BRIDGE=br0 + DEVICE=eth4 + HWADDR=98:bb:9f:2c:e8:80 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no""" +@@ -2212,7 +2197,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + DEVICE=eth5 + DHCLIENT_SET_DEFAULT_ROUTE=no + HWADDR=98:bb:9f:2c:e8:8a +- NM_CONTROLLED=no + ONBOOT=no + TYPE=Ethernet + USERCTL=no""" +@@ -2689,7 +2673,6 @@ iface bond0 inet6 static + MTU=9000 + NETMASK=255.255.255.0 + NETMASK1=255.255.255.0 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Bond + USERCTL=no +@@ -2701,7 +2684,6 @@ iface bond0 inet6 static + DEVICE=bond0s0 + HWADDR=aa:bb:cc:dd:e8:00 + MASTER=bond0 +- NM_CONTROLLED=no + ONBOOT=yes + SLAVE=yes + TYPE=Ethernet +@@ -2729,7 +2711,6 @@ iface bond0 inet6 static + DEVICE=bond0s1 + HWADDR=aa:bb:cc:dd:e8:01 + MASTER=bond0 +- NM_CONTROLLED=no + ONBOOT=yes + SLAVE=yes + TYPE=Ethernet +@@ -2794,7 +2775,6 @@ iface bond0 inet6 static + BOOTPROTO=none + DEVICE=en0 + HWADDR=aa:bb:cc:dd:e8:00 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no""" +@@ -2815,7 +2795,6 @@ iface bond0 inet6 static + MTU=2222 + NETMASK=255.255.255.0 + NETMASK1=255.255.255.0 +- NM_CONTROLLED=no + ONBOOT=yes + PHYSDEV=en0 + USERCTL=no +@@ -2890,7 +2869,6 @@ iface bond0 inet6 static + DEVICE=br0 + IPADDR=192.168.2.2 + NETMASK=255.255.255.0 +- NM_CONTROLLED=no + ONBOOT=yes + PRIO=22 + STP=no +@@ -3032,7 +3010,6 @@ iface bond0 inet6 static + HWADDR=52:54:00:12:34:00 + IPADDR=192.168.1.2 + NETMASK=255.255.255.0 +- NM_CONTROLLED=no + ONBOOT=no + TYPE=Ethernet + USERCTL=no +@@ -3044,7 +3021,6 @@ iface bond0 inet6 static + DEVICE=eth1 + HWADDR=52:54:00:12:34:aa + MTU=1480 +- NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +@@ -3055,7 +3031,6 @@ iface bond0 inet6 static + BOOTPROTO=none + DEVICE=eth2 + HWADDR=52:54:00:12:34:ff +- NM_CONTROLLED=no + ONBOOT=no + TYPE=Ethernet + USERCTL=no +@@ -3628,7 +3603,6 @@ class TestRhelSysConfigRendering(CiTestCase): + BOOTPROTO=dhcp + DEVICE=eth1000 + HWADDR=07-1c-c6-75-a4-be +-NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +@@ -3840,7 +3814,6 @@ GATEWAY=10.0.2.2 + HWADDR=52:54:00:12:34:00 + IPADDR=10.0.2.15 + NETMASK=255.255.255.0 +-NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +@@ -3910,7 +3883,6 @@ USERCTL=no + # + BOOTPROTO=dhcp + DEVICE=eth0 +-NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +-- +2.31.1 + diff --git a/SOURCES/0003-Adding-_netdev-to-the-default-mount-configuration.patch b/SOURCES/0003-Adding-_netdev-to-the-default-mount-configuration.patch new file mode 100644 index 0000000..0f57fed --- /dev/null +++ b/SOURCES/0003-Adding-_netdev-to-the-default-mount-configuration.patch @@ -0,0 +1,42 @@ +From b952fa472be3f417e0d857c8647a1b930624c247 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Fri, 25 Feb 2022 05:05:17 -0500 +Subject: Adding _netdev to the default mount configuration + +RH-Author: Eduardo Otubo +RH-MergeRequest: 21: Adding _netdev to the default mount configuration +RH-Commit: [1/1] 250860a24db396a5088d207d6526a0028ac73eb3 (otubo/cloud-init-src) +RH-Bugzilla: 1998445 +RH-Acked-by: Miroslav Rezanina +RH-Acked-by: Emanuele Giuseppe Esposito + +Adding _netdev option also to the default configuration for RHEL. + +rhbz: 1998445 +x-downstream-only: yes + +Signed-off-by: Eduardo Otubo + +patch_name: ci-Adding-_netdev-to-the-default-mount-configuration.patch +present_in_specfile: true +location_in_specfile: 29 +--- + rhel/cloud.cfg | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg +index 9ecba215..1ec1a6c6 100644 +--- a/rhel/cloud.cfg ++++ b/rhel/cloud.cfg +@@ -4,7 +4,7 @@ users: + disable_root: 1 + ssh_pwauth: 0 + +-mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2'] ++mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service,_netdev', '0', '2'] + resize_rootfs_tmp: /dev + ssh_deletekeys: 1 + ssh_genkeytypes: ~ +-- +2.31.1 + diff --git a/SOURCES/0004-Setting-highest-autoconnect-priority-for-network-scr.patch b/SOURCES/0004-Setting-highest-autoconnect-priority-for-network-scr.patch new file mode 100644 index 0000000..690a02f --- /dev/null +++ b/SOURCES/0004-Setting-highest-autoconnect-priority-for-network-scr.patch @@ -0,0 +1,49 @@ +From a14df44ffdc880ae16c691901e2671458ab234ff Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Thu, 17 Feb 2022 15:32:35 +0100 +Subject: Setting highest autoconnect priority for network-scripts + +RH-Author: Eduardo Otubo +RH-MergeRequest: 22: Setting highest autoconnect priority for network-scripts +RH-Commit: [1/1] 34f1d62f8934a983a124df95b861a1e448681d3b (otubo/cloud-init-src) +RH-Bugzilla: 2036060 +RH-Acked-by: Miroslav Rezanina +RH-Acked-by: Emanuele Giuseppe Esposito + +Set the highest autoconnect priority for network-scripts which is +loaded by NetworkManager ifcfg-rh plugin. Note that keyfile is the only +and default existing plugin on RHEL9, by setting the highest autoconnect +priority for network-scripts, NetworkManager will activate +network-scripts but keyfile. Network-scripts path: + +Since this is a blocking issue, we decided to have this one-liner +downstream-only patch so we can move forward and have a better +NetworkManager support later on the release. + +rhbz: 2036060 +x-downstream-only: yes + +Signed-off-by: Eduardo Otubo + +patch_name: ci-Setting-highest-autoconnect-priority-for-network-scr.patch +present_in_specfile: true +location_in_specfile: 30 +--- + cloudinit/net/sysconfig.py | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index d8c53312..b50035b5 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -337,6 +337,7 @@ class Renderer(renderer.Renderer): + "ONBOOT": True, + "USERCTL": False, + "BOOTPROTO": "none", ++ "AUTOCONNECT_PRIORITY": 999 + }, + "suse": {"BOOTPROTO": "static", "STARTMODE": "auto"}, + } +-- +2.31.1 + diff --git a/SOURCES/0005-limit-permissions-on-def_log_file.patch b/SOURCES/0005-limit-permissions-on-def_log_file.patch new file mode 100644 index 0000000..ed1072e --- /dev/null +++ b/SOURCES/0005-limit-permissions-on-def_log_file.patch @@ -0,0 +1,69 @@ +From 40ad855b883050069393b9c00db2a6d222d949db Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Fri, 7 May 2021 13:36:08 +0200 +Subject: limit permissions on def_log_file + +This sets a default mode of 0600 on def_log_file, and makes this +configurable via the def_log_file_mode option in cloud.cfg. + +LP: #1541196 +Resolves: rhbz#1424612 +X-approved-upstream: true + +Conflicts 21.1: + cloudinit/stages.py: adjusting call of ensure_file() to use more +recent version + +Signed-off-by: Eduardo Otubo +--- + cloudinit/settings.py | 1 + + cloudinit/stages.py | 1 + + doc/examples/cloud-config.txt | 4 ++++ + 3 files changed, 6 insertions(+) + +diff --git a/cloudinit/settings.py b/cloudinit/settings.py +index a780e21e..aa2d6b95 100644 +--- a/cloudinit/settings.py ++++ b/cloudinit/settings.py +@@ -49,6 +49,7 @@ CFG_BUILTIN = { + "None", + ], + 'def_log_file': '/var/log/cloud-init.log', ++ 'def_log_file_mode': 0o600, + 'log_cfgs': [], + 'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'], + 'ssh_deletekeys': False, +diff --git a/cloudinit/stages.py b/cloudinit/stages.py +index 3f17294b..61db1dbd 100644 +--- a/cloudinit/stages.py ++++ b/cloudinit/stages.py +@@ -205,6 +205,7 @@ class Init(object): + def _initialize_filesystem(self): + util.ensure_dirs(self._initial_subdirs()) + log_file = util.get_cfg_option_str(self.cfg, "def_log_file") ++ log_file_mode = util.get_cfg_option_int(self.cfg, "def_log_file_mode") + if log_file: + util.ensure_file(log_file, mode=0o640, preserve_mode=True) + perms = self.cfg.get("syslog_fix_perms") +diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt +index a2b4a3fa..0ccf3147 100644 +--- a/doc/examples/cloud-config.txt ++++ b/doc/examples/cloud-config.txt +@@ -414,10 +414,14 @@ timezone: US/Eastern + # if syslog_fix_perms is a list, it will iterate through and use the + # first pair that does not raise error. + # ++# 'def_log_file' will be created with mode 'def_log_file_mode', which ++# is specified as a numeric value and defaults to 0600. ++# + # the default values are '/var/log/cloud-init.log' and 'syslog:adm' + # the value of 'def_log_file' should match what is configured in logging + # if either is empty, then no change of ownership will be done + def_log_file: /var/log/my-logging-file.log ++def_log_file_mode: 0600 + syslog_fix_perms: syslog:root + + # you can set passwords for a user or multiple users +-- +2.31.1 + diff --git a/SOURCES/0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch b/SOURCES/0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch new file mode 100644 index 0000000..fb05778 --- /dev/null +++ b/SOURCES/0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch @@ -0,0 +1,65 @@ +From 00f1f910d8d166ebe2913c12549f212c2d666c11 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Thu, 20 May 2021 08:53:55 +0200 +Subject: rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and set in + cloud.cfg + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 10: rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and set in cloud.cfg +RH-Commit: [1/1] 6da989423b9b6e017afbac2f1af3649b0487310f +RH-Bugzilla: 1957532 +RH-Acked-by: Eduardo Otubo +RH-Acked-by: Cathy Avery +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Mohamed Gamal Morsy + +Currently genkeytypes in cloud.cfg is set to None, so together with +ssh_deletekeys=1 cloudinit on first boot it will just delete the existing +keys and not generate new ones. + +Just removing that property in cloud.cfg is not enough, because +settings.py provides another empty default value that will be used +instead, resulting to no key generated even when the property is not defined. + +Removing genkeytypes also in settings.py will default to GENERATE_KEY_NAMES, +but since we want only 'rsa', 'ecdsa' and 'ed25519', add back genkeytypes in +cloud.cfg with the above defaults. + +Also remove ssh_deletekeys in settings.py as we always need +to 1 (and it also defaults to 1). + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/settings.py | 2 -- + rhel/cloud.cfg | 2 +- + 2 files changed, 1 insertion(+), 3 deletions(-) + +diff --git a/cloudinit/settings.py b/cloudinit/settings.py +index aa2d6b95..38a90b70 100644 +--- a/cloudinit/settings.py ++++ b/cloudinit/settings.py +@@ -52,8 +52,6 @@ CFG_BUILTIN = { + 'def_log_file_mode': 0o600, + 'log_cfgs': [], + 'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'], +- 'ssh_deletekeys': False, +- 'ssh_genkeytypes': [], + 'syslog_fix_perms': [], + 'system_info': { + 'paths': { +diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg +index 1ec1a6c6..75d5c84b 100644 +--- a/rhel/cloud.cfg ++++ b/rhel/cloud.cfg +@@ -7,7 +7,7 @@ ssh_pwauth: 0 + mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service,_netdev', '0', '2'] + resize_rootfs_tmp: /dev + ssh_deletekeys: 1 +-ssh_genkeytypes: ~ ++ssh_genkeytypes: ['rsa', 'ecdsa', 'ed25519'] + syslog_fix_perms: ~ + disable_vmware_customization: false + +-- +2.31.1 + diff --git a/SOURCES/ci-Add-native-NetworkManager-support-1224.patch b/SOURCES/ci-Add-native-NetworkManager-support-1224.patch new file mode 100644 index 0000000..8bcbcc7 --- /dev/null +++ b/SOURCES/ci-Add-native-NetworkManager-support-1224.patch @@ -0,0 +1,2299 @@ +From f0ae77cbf4a5e269da54fc2783a2a836023bbd86 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 2 May 2022 14:42:52 +0200 +Subject: [PATCH 1/5] Add native NetworkManager support (#1224) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 24: Add native NetworkManager support (#1224) +RH-Commit: [1/3] 65231ba68460c505646807faf186c704d67678b5 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2056964 +RH-Acked-by: Miroslav Rezanina +RH-Acked-by: Vitaly Kuznetsov + +commit feda344e6cf9d37b09bc13cf333a717d1654c26c +Author: Lubomir Rintel +Date: Fri Feb 25 23:33:20 2022 +0100 + + Add native NetworkManager support (#1224) + + Fedora currently relies on sysconfig/ifcfg renderer. This is not too great, + because Fedora (also RHEL since version 8) dropped support for the legacy + network service that uses ifcfg files long ago. + + In turn, Fedora ended up patching cloud-init downstream to utilize + NetworkManager's ifcfg compatibility mode [1]. This seems to have worked + for a while, nevertheless the NetworkManager's ifcfg backend is reaching + the end of its useful life too [2]. + + [1] https://src.fedoraproject.org/rpms/cloud-init/blob/rawhide/f/cloud-init-21.3-nm-controlled.patch + [2] https://fedoraproject.org/wiki/Changes/NoIfcfgFiles + + Let's not mangle things downstream and make vanilla cloud-init work great + on Fedora instead. + + This also means that the sysconfig compatibility with + Network Manager was removed. + + Firstly, this relies upon the fact that you can get ifcfg support by adding + it to NetworkManager.conf. That is not guaranteed and certainly will not + be case in future. + + Secondly, cloud-init always generates configuration with + NM_CONTROLLED=no, so the generated ifcfg files are no good for + NetworkManager. Fedora patches around this by just removing those lines + in their cloud-init package. + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/cmd/devel/net_convert.py | 14 +- + cloudinit/net/activators.py | 25 +- + cloudinit/net/network_manager.py | 377 +++++++ + cloudinit/net/renderers.py | 3 + + cloudinit/net/sysconfig.py | 37 +- + tests/unittests/test_net.py | 1270 +++++++++++++++++++++--- + tests/unittests/test_net_activators.py | 93 +- + 7 files changed, 1625 insertions(+), 194 deletions(-) + create mode 100644 cloudinit/net/network_manager.py + +diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py +index 18b1e7ff..647fe07b 100755 +--- a/cloudinit/cmd/devel/net_convert.py ++++ b/cloudinit/cmd/devel/net_convert.py +@@ -7,7 +7,14 @@ import os + import sys + + from cloudinit import distros, log, safeyaml +-from cloudinit.net import eni, netplan, network_state, networkd, sysconfig ++from cloudinit.net import ( ++ eni, ++ netplan, ++ network_manager, ++ network_state, ++ networkd, ++ sysconfig, ++) + from cloudinit.sources import DataSourceAzure as azure + from cloudinit.sources import DataSourceOVF as ovf + from cloudinit.sources.helpers import openstack +@@ -74,7 +81,7 @@ def get_parser(parser=None): + parser.add_argument( + "-O", + "--output-kind", +- choices=["eni", "netplan", "networkd", "sysconfig"], ++ choices=["eni", "netplan", "networkd", "sysconfig", "network-manager"], + required=True, + help="The network config format to emit", + ) +@@ -148,6 +155,9 @@ def handle_args(name, args): + elif args.output_kind == "sysconfig": + r_cls = sysconfig.Renderer + config = distro.renderer_configs.get("sysconfig") ++ elif args.output_kind == "network-manager": ++ r_cls = network_manager.Renderer ++ config = distro.renderer_configs.get("network-manager") + else: + raise RuntimeError("Invalid output_kind") + +diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py +index e80c26df..edbc0c06 100644 +--- a/cloudinit/net/activators.py ++++ b/cloudinit/net/activators.py +@@ -1,15 +1,14 @@ + # This file is part of cloud-init. See LICENSE file for license information. + import logging +-import os + from abc import ABC, abstractmethod + from typing import Iterable, List, Type + + from cloudinit import subp, util + from cloudinit.net.eni import available as eni_available + from cloudinit.net.netplan import available as netplan_available ++from cloudinit.net.network_manager import available as nm_available + from cloudinit.net.network_state import NetworkState + from cloudinit.net.networkd import available as networkd_available +-from cloudinit.net.sysconfig import NM_CFG_FILE + + LOG = logging.getLogger(__name__) + +@@ -124,20 +123,24 @@ class IfUpDownActivator(NetworkActivator): + class NetworkManagerActivator(NetworkActivator): + @staticmethod + def available(target=None) -> bool: +- """Return true if network manager can be used on this system.""" +- config_present = os.path.isfile( +- subp.target_path(target, path=NM_CFG_FILE) +- ) +- nmcli_present = subp.which("nmcli", target=target) +- return config_present and bool(nmcli_present) ++ """Return true if NetworkManager can be used on this system.""" ++ return nm_available(target=target) + + @staticmethod + def bring_up_interface(device_name: str) -> bool: +- """Bring up interface using nmcli. ++ """Bring up connection using nmcli. + + Return True is successful, otherwise return False + """ +- cmd = ["nmcli", "connection", "up", "ifname", device_name] ++ from cloudinit.net.network_manager import conn_filename ++ ++ filename = conn_filename(device_name) ++ cmd = ["nmcli", "connection", "load", filename] ++ if _alter_interface(cmd, device_name): ++ cmd = ["nmcli", "connection", "up", "filename", filename] ++ else: ++ _alter_interface(["nmcli", "connection", "reload"], device_name) ++ cmd = ["nmcli", "connection", "up", "ifname", device_name] + return _alter_interface(cmd, device_name) + + @staticmethod +@@ -146,7 +149,7 @@ class NetworkManagerActivator(NetworkActivator): + + Return True is successful, otherwise return False + """ +- cmd = ["nmcli", "connection", "down", device_name] ++ cmd = ["nmcli", "device", "disconnect", device_name] + return _alter_interface(cmd, device_name) + + +diff --git a/cloudinit/net/network_manager.py b/cloudinit/net/network_manager.py +new file mode 100644 +index 00000000..79b0fe0b +--- /dev/null ++++ b/cloudinit/net/network_manager.py +@@ -0,0 +1,377 @@ ++# Copyright 2022 Red Hat, Inc. ++# ++# Author: Lubomir Rintel ++# Fixes and suggestions contributed by James Falcon, Neal Gompa, ++# Zbigniew Jędrzejewski-Szmek and Emanuele Giuseppe Esposito. ++# ++# This file is part of cloud-init. See LICENSE file for license information. ++ ++import configparser ++import io ++import itertools ++import os ++import uuid ++ ++from cloudinit import log as logging ++from cloudinit import subp, util ++ ++from . import renderer ++from .network_state import is_ipv6_addr, subnet_is_ipv6 ++ ++NM_RUN_DIR = "/etc/NetworkManager" ++NM_LIB_DIR = "/usr/lib/NetworkManager" ++LOG = logging.getLogger(__name__) ++ ++ ++class NMConnection: ++ """Represents a NetworkManager connection profile.""" ++ ++ def __init__(self, con_id): ++ """ ++ Initializes the connection with some very basic properties, ++ notably the UUID so that the connection can be referred to. ++ """ ++ ++ # Chosen by fair dice roll ++ CI_NM_UUID = uuid.UUID("a3924cb8-09e0-43e9-890b-77972a800108") ++ ++ self.config = configparser.ConfigParser() ++ # Identity option name mapping, to achieve case sensitivity ++ self.config.optionxform = str ++ ++ self.config["connection"] = { ++ "id": f"cloud-init {con_id}", ++ "uuid": str(uuid.uuid5(CI_NM_UUID, con_id)), ++ } ++ ++ # This is not actually used anywhere, but may be useful in future ++ self.config["user"] = { ++ "org.freedesktop.NetworkManager.origin": "cloud-init" ++ } ++ ++ def _set_default(self, section, option, value): ++ """ ++ Sets a property unless it's already set, ensuring the section ++ exists. ++ """ ++ ++ if not self.config.has_section(section): ++ self.config[section] = {} ++ if not self.config.has_option(section, option): ++ self.config[section][option] = value ++ ++ def _set_ip_method(self, family, subnet_type): ++ """ ++ Ensures there's appropriate [ipv4]/[ipv6] for given family ++ appropriate for given configuration type ++ """ ++ ++ method_map = { ++ "static": "manual", ++ "dhcp6": "dhcp", ++ "ipv6_slaac": "auto", ++ "ipv6_dhcpv6-stateless": "auto", ++ "ipv6_dhcpv6-stateful": "auto", ++ "dhcp4": "auto", ++ "dhcp": "auto", ++ } ++ ++ # Ensure we got an [ipvX] section ++ self._set_default(family, "method", "disabled") ++ ++ try: ++ method = method_map[subnet_type] ++ except KeyError: ++ # What else can we do ++ method = "auto" ++ self.config[family]["may-fail"] = "true" ++ ++ # Make sure we don't "downgrade" the method in case ++ # we got conflicting subnets (e.g. static along with dhcp) ++ if self.config[family]["method"] == "dhcp": ++ return ++ if self.config[family]["method"] == "auto" and method == "manual": ++ return ++ ++ self.config[family]["method"] = method ++ self._set_default(family, "may-fail", "false") ++ if family == "ipv6": ++ self._set_default(family, "addr-gen-mode", "stable-privacy") ++ ++ def _add_numbered(self, section, key_prefix, value): ++ """ ++ Adds a numbered property, such as address or route, ensuring ++ the appropriate value gets used for . ++ """ ++ ++ for index in itertools.count(1): ++ key = f"{key_prefix}{index}" ++ if not self.config.has_option(section, key): ++ self.config[section][key] = value ++ break ++ ++ def _add_address(self, family, subnet): ++ """ ++ Adds an ipv[46]address property. ++ """ ++ ++ value = subnet["address"] + "/" + str(subnet["prefix"]) ++ self._add_numbered(family, "address", value) ++ ++ def _add_route(self, family, route): ++ """ ++ Adds a ipv[46].route property. ++ """ ++ ++ value = route["network"] + "/" + str(route["prefix"]) ++ if "gateway" in route: ++ value = value + "," + route["gateway"] ++ self._add_numbered(family, "route", value) ++ ++ def _add_nameserver(self, dns): ++ """ ++ Extends the ipv[46].dns property with a name server. ++ """ ++ ++ # FIXME: the subnet contains IPv4 and IPv6 name server mixed ++ # together. We might be getting an IPv6 name server while ++ # we're dealing with an IPv4 subnet. Sort this out by figuring ++ # out the correct family and making sure a valid section exist. ++ family = "ipv6" if is_ipv6_addr(dns) else "ipv4" ++ self._set_default(family, "method", "disabled") ++ ++ self._set_default(family, "dns", "") ++ self.config[family]["dns"] = self.config[family]["dns"] + dns + ";" ++ ++ def _add_dns_search(self, family, dns_search): ++ """ ++ Extends the ipv[46].dns-search property with a name server. ++ """ ++ ++ self._set_default(family, "dns-search", "") ++ self.config[family]["dns-search"] = ( ++ self.config[family]["dns-search"] + ";".join(dns_search) + ";" ++ ) ++ ++ def con_uuid(self): ++ """ ++ Returns the connection UUID ++ """ ++ return self.config["connection"]["uuid"] ++ ++ def valid(self): ++ """ ++ Can this be serialized into a meaningful connection profile? ++ """ ++ return self.config.has_option("connection", "type") ++ ++ @staticmethod ++ def mac_addr(addr): ++ """ ++ Sanitize a MAC address. ++ """ ++ return addr.replace("-", ":").upper() ++ ++ def render_interface(self, iface, renderer): ++ """ ++ Integrate information from network state interface information ++ into the connection. Most of the work is done here. ++ """ ++ ++ # Initialize type & connectivity ++ _type_map = { ++ "physical": "ethernet", ++ "vlan": "vlan", ++ "bond": "bond", ++ "bridge": "bridge", ++ "infiniband": "infiniband", ++ "loopback": None, ++ } ++ ++ if_type = _type_map[iface["type"]] ++ if if_type is None: ++ return ++ if "bond-master" in iface: ++ slave_type = "bond" ++ else: ++ slave_type = None ++ ++ self.config["connection"]["type"] = if_type ++ if slave_type is not None: ++ self.config["connection"]["slave-type"] = slave_type ++ self.config["connection"]["master"] = renderer.con_ref( ++ iface[slave_type + "-master"] ++ ) ++ ++ # Add type specific-section ++ self.config[if_type] = {} ++ ++ # These are the interface properties that map nicely ++ # to NetworkManager properties ++ _prop_map = { ++ "bond": { ++ "mode": "bond-mode", ++ "miimon": "bond_miimon", ++ "xmit_hash_policy": "bond-xmit-hash-policy", ++ "num_grat_arp": "bond-num-grat-arp", ++ "downdelay": "bond-downdelay", ++ "updelay": "bond-updelay", ++ "fail_over_mac": "bond-fail-over-mac", ++ "primary_reselect": "bond-primary-reselect", ++ "primary": "bond-primary", ++ }, ++ "bridge": { ++ "stp": "bridge_stp", ++ "priority": "bridge_bridgeprio", ++ }, ++ "vlan": { ++ "id": "vlan_id", ++ }, ++ "ethernet": {}, ++ "infiniband": {}, ++ } ++ ++ device_mtu = iface["mtu"] ++ ipv4_mtu = None ++ ++ # Deal with Layer 3 configuration ++ for subnet in iface["subnets"]: ++ family = "ipv6" if subnet_is_ipv6(subnet) else "ipv4" ++ ++ self._set_ip_method(family, subnet["type"]) ++ if "address" in subnet: ++ self._add_address(family, subnet) ++ if "gateway" in subnet: ++ self.config[family]["gateway"] = subnet["gateway"] ++ for route in subnet["routes"]: ++ self._add_route(family, route) ++ if "dns_nameservers" in subnet: ++ for nameserver in subnet["dns_nameservers"]: ++ self._add_nameserver(nameserver) ++ if "dns_search" in subnet: ++ self._add_dns_search(family, subnet["dns_search"]) ++ if family == "ipv4" and "mtu" in subnet: ++ ipv4_mtu = subnet["mtu"] ++ ++ if ipv4_mtu is None: ++ ipv4_mtu = device_mtu ++ if not ipv4_mtu == device_mtu: ++ LOG.warning( ++ "Network config: ignoring %s device-level mtu:%s" ++ " because ipv4 subnet-level mtu:%s provided.", ++ iface["name"], ++ device_mtu, ++ ipv4_mtu, ++ ) ++ ++ # Parse type-specific properties ++ for nm_prop, key in _prop_map[if_type].items(): ++ if key not in iface: ++ continue ++ if iface[key] is None: ++ continue ++ if isinstance(iface[key], bool): ++ self.config[if_type][nm_prop] = ( ++ "true" if iface[key] else "false" ++ ) ++ else: ++ self.config[if_type][nm_prop] = str(iface[key]) ++ ++ # These ones need special treatment ++ if if_type == "ethernet": ++ if iface["wakeonlan"] is True: ++ # NM_SETTING_WIRED_WAKE_ON_LAN_MAGIC ++ self.config["ethernet"]["wake-on-lan"] = str(0x40) ++ if ipv4_mtu is not None: ++ self.config["ethernet"]["mtu"] = str(ipv4_mtu) ++ if iface["mac_address"] is not None: ++ self.config["ethernet"]["mac-address"] = self.mac_addr( ++ iface["mac_address"] ++ ) ++ if if_type == "vlan" and "vlan-raw-device" in iface: ++ self.config["vlan"]["parent"] = renderer.con_ref( ++ iface["vlan-raw-device"] ++ ) ++ if if_type == "bridge": ++ # Bridge is ass-backwards compared to bond ++ for port in iface["bridge_ports"]: ++ port = renderer.get_conn(port) ++ port._set_default("connection", "slave-type", "bridge") ++ port._set_default("connection", "master", self.con_uuid()) ++ if iface["mac_address"] is not None: ++ self.config["bridge"]["mac-address"] = self.mac_addr( ++ iface["mac_address"] ++ ) ++ if if_type == "infiniband" and ipv4_mtu is not None: ++ self.config["infiniband"]["transport-mode"] = "datagram" ++ self.config["infiniband"]["mtu"] = str(ipv4_mtu) ++ if iface["mac_address"] is not None: ++ self.config["infiniband"]["mac-address"] = self.mac_addr( ++ iface["mac_address"] ++ ) ++ ++ # Finish up ++ if if_type == "bridge" or not self.config.has_option( ++ if_type, "mac-address" ++ ): ++ self.config["connection"]["interface-name"] = iface["name"] ++ ++ def dump(self): ++ """ ++ Stringify. ++ """ ++ ++ buf = io.StringIO() ++ self.config.write(buf, space_around_delimiters=False) ++ header = "# Generated by cloud-init. Changes will be lost.\n\n" ++ return header + buf.getvalue() ++ ++ ++class Renderer(renderer.Renderer): ++ """Renders network information in a NetworkManager keyfile format.""" ++ ++ def __init__(self, config=None): ++ self.connections = {} ++ ++ def get_conn(self, con_id): ++ return self.connections[con_id] ++ ++ def con_ref(self, con_id): ++ if con_id in self.connections: ++ return self.connections[con_id].con_uuid() ++ else: ++ # Well, what can we do... ++ return con_id ++ ++ def render_network_state(self, network_state, templates=None, target=None): ++ # First pass makes sure there's NMConnections for all known ++ # interfaces that have UUIDs that can be linked to from related ++ # interfaces ++ for iface in network_state.iter_interfaces(): ++ self.connections[iface["name"]] = NMConnection(iface["name"]) ++ ++ # Now render the actual interface configuration ++ for iface in network_state.iter_interfaces(): ++ conn = self.connections[iface["name"]] ++ conn.render_interface(iface, self) ++ ++ # And finally write the files ++ for con_id, conn in self.connections.items(): ++ if not conn.valid(): ++ continue ++ name = conn_filename(con_id, target) ++ util.write_file(name, conn.dump(), 0o600) ++ ++ ++def conn_filename(con_id, target=None): ++ target_con_dir = subp.target_path(target, NM_RUN_DIR) ++ con_file = f"cloud-init-{con_id}.nmconnection" ++ return f"{target_con_dir}/system-connections/{con_file}" ++ ++ ++def available(target=None): ++ target_nm_dir = subp.target_path(target, NM_LIB_DIR) ++ return os.path.exists(target_nm_dir) ++ ++ ++# vi: ts=4 expandtab +diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py +index c755f04c..7edc34b5 100644 +--- a/cloudinit/net/renderers.py ++++ b/cloudinit/net/renderers.py +@@ -8,6 +8,7 @@ from . import ( + freebsd, + netbsd, + netplan, ++ network_manager, + networkd, + openbsd, + renderer, +@@ -19,6 +20,7 @@ NAME_TO_RENDERER = { + "freebsd": freebsd, + "netbsd": netbsd, + "netplan": netplan, ++ "network-manager": network_manager, + "networkd": networkd, + "openbsd": openbsd, + "sysconfig": sysconfig, +@@ -28,6 +30,7 @@ DEFAULT_PRIORITY = [ + "eni", + "sysconfig", + "netplan", ++ "network-manager", + "freebsd", + "netbsd", + "openbsd", +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index b50035b5..2a45a4fa 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -5,8 +5,6 @@ import io + import os + import re + +-from configobj import ConfigObj +- + from cloudinit import log as logging + from cloudinit import subp, util + from cloudinit.distros.parsers import networkmanager_conf, resolv_conf +@@ -66,24 +64,6 @@ def _quote_value(value): + return value + + +-def enable_ifcfg_rh(path): +- """Add ifcfg-rh to NetworkManager.cfg plugins if main section is present""" +- config = ConfigObj(path) +- if "main" in config: +- if "plugins" in config["main"]: +- if "ifcfg-rh" in config["main"]["plugins"]: +- return +- else: +- config["main"]["plugins"] = [] +- +- if isinstance(config["main"]["plugins"], list): +- config["main"]["plugins"].append("ifcfg-rh") +- else: +- config["main"]["plugins"] = [config["main"]["plugins"], "ifcfg-rh"] +- config.write() +- LOG.debug("Enabled ifcfg-rh NetworkManager plugins") +- +- + class ConfigMap(object): + """Sysconfig like dictionary object.""" + +@@ -1032,8 +1012,6 @@ class Renderer(renderer.Renderer): + netrules_content = self._render_persistent_net(network_state) + netrules_path = subp.target_path(target, self.netrules_path) + util.write_file(netrules_path, netrules_content, file_mode) +- if available_nm(target=target): +- enable_ifcfg_rh(subp.target_path(target, path=NM_CFG_FILE)) + + sysconfig_path = subp.target_path(target, templates.get("control")) + # Distros configuring /etc/sysconfig/network as a file e.g. Centos +@@ -1072,14 +1050,9 @@ def _supported_vlan_names(rdev, vid): + + + def available(target=None): +- sysconfig = available_sysconfig(target=target) +- nm = available_nm(target=target) +- return util.system_info()["variant"] in KNOWN_DISTROS and any( +- [nm, sysconfig] +- ) +- ++ if not util.system_info()["variant"] in KNOWN_DISTROS: ++ return False + +-def available_sysconfig(target=None): + expected = ["ifup", "ifdown"] + search = ["/sbin", "/usr/sbin"] + for p in expected: +@@ -1096,10 +1069,4 @@ def available_sysconfig(target=None): + return False + + +-def available_nm(target=None): +- if not os.path.isfile(subp.target_path(target, path=NM_CFG_FILE)): +- return False +- return True +- +- + # vi: ts=4 expandtab +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 591241b3..ef21ad76 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -21,6 +21,7 @@ from cloudinit.net import ( + interface_has_own_mac, + natural_sort_key, + netplan, ++ network_manager, + network_state, + networkd, + renderers, +@@ -611,6 +612,37 @@ dns = none + ), + ), + ], ++ "expected_network_manager": [ ++ ( ++ "".join( ++ [ ++ "etc/NetworkManager/system-connections", ++ "/cloud-init-eth0.nmconnection", ++ ] ++ ), ++ """ ++# Generated by cloud-init. Changes will be lost. ++ ++[connection] ++id=cloud-init eth0 ++uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++type=ethernet ++ ++[user] ++org.freedesktop.NetworkManager.origin=cloud-init ++ ++[ethernet] ++mac-address=FA:16:3E:ED:9A:59 ++ ++[ipv4] ++method=manual ++may-fail=false ++address1=172.19.1.34/22 ++route1=0.0.0.0/0,172.19.3.254 ++ ++""".lstrip(), ++ ), ++ ], + }, + { + "in_data": { +@@ -1073,6 +1105,50 @@ NETWORK_CONFIGS = { + USERCTL=no""" + ), + }, ++ "expected_network_manager": { ++ "cloud-init-eth1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1 ++ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=CF:D6:AF:48:E8:80 ++ ++ """ ++ ), ++ "cloud-init-eth99.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth99 ++ uuid=b1b88000-1f03-5360-8377-1a2205efffb4 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=C0:D6:9F:2C:E8:80 ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ address1=192.168.21.3/24 ++ route1=0.0.0.0/0,65.61.151.37 ++ dns=8.8.8.8;8.8.4.4; ++ dns-search=barley.maas;sach.maas; ++ ++ """ ++ ), ++ }, + "yaml": textwrap.dedent( + """ + version: 1 +@@ -1145,6 +1221,34 @@ NETWORK_CONFIGS = { + STARTMODE=auto""" + ) + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ [ipv6] ++ method=dhcp ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ }, + "yaml": textwrap.dedent( + """\ + version: 1 +@@ -1247,6 +1351,37 @@ NETWORK_CONFIGS = { + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mtu=9000 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.14.2/24 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::1/64 ++ ++ """ ++ ), ++ }, + }, + "v6_and_v4": { + "expected_sysconfig_opensuse": { +@@ -1257,6 +1392,34 @@ NETWORK_CONFIGS = { + STARTMODE=auto""" + ) + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv6] ++ method=dhcp ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, + "yaml": textwrap.dedent( + """\ + version: 1 +@@ -1330,6 +1493,30 @@ NETWORK_CONFIGS = { + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv6] ++ method=dhcp ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ }, + }, + "dhcpv6_accept_ra": { + "expected_eni": textwrap.dedent( +@@ -1537,6 +1724,30 @@ NETWORK_CONFIGS = { + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv6] ++ method=auto ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ }, + }, + "static6": { + "yaml": textwrap.dedent( +@@ -1625,6 +1836,30 @@ NETWORK_CONFIGS = { + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv6] ++ method=auto ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ }, + }, + "dhcpv6_stateful": { + "expected_eni": textwrap.dedent( +@@ -1724,6 +1959,29 @@ NETWORK_CONFIGS = { + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, + "yaml_v2": textwrap.dedent( + """\ + version: 2 +@@ -1777,6 +2035,30 @@ NETWORK_CONFIGS = { + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ wake-on-lan=64 ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, + "yaml_v2": textwrap.dedent( + """\ + version: 2 +@@ -2215,6 +2497,254 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + USERCTL=no""" + ), + }, ++ "expected_network_manager": { ++ "cloud-init-eth3.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth3 ++ uuid=b7e95dda-7746-5bf8-bf33-6e5f3c926790 ++ type=ethernet ++ slave-type=bridge ++ master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=66:BB:9F:2C:E8:80 ++ ++ """ ++ ), ++ "cloud-init-eth5.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth5 ++ uuid=5fda13c7-9942-5e90-a41b-1d043bd725dc ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=98:BB:9F:2C:E8:8A ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ "cloud-init-ib0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init ib0 ++ uuid=11a1dda7-78b4-5529-beba-d9b5f549ad7b ++ type=infiniband ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [infiniband] ++ transport-mode=datagram ++ mtu=9000 ++ mac-address=A0:00:02:20:FE:80:00:00:00:00:00:00:EC:0D:9A:03:00:15:E2:C1 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.200.7/24 ++ ++ """ ++ ), ++ "cloud-init-bond0.200.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0.200 ++ uuid=88984a9c-ff22-5233-9267-86315e0acaa7 ++ type=vlan ++ interface-name=bond0.200 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [vlan] ++ id=200 ++ parent=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ "cloud-init-eth0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0 ++ uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=C0:D6:9F:2C:E8:80 ++ ++ """ ++ ), ++ "cloud-init-eth4.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth4 ++ uuid=e27e4959-fb50-5580-b9a4-2073554627b9 ++ type=ethernet ++ slave-type=bridge ++ master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=98:BB:9F:2C:E8:80 ++ ++ """ ++ ), ++ "cloud-init-eth1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1 ++ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 ++ type=ethernet ++ slave-type=bond ++ master=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=AA:D6:9F:2C:E8:80 ++ ++ """ ++ ), ++ "cloud-init-br0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init br0 ++ uuid=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ type=bridge ++ interface-name=br0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [bridge] ++ stp=false ++ priority=22 ++ mac-address=BB:BB:BB:BB:BB:AA ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.14.2/24 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::1/64 ++ route1=::/0,2001:4800:78ff:1b::1 ++ ++ """ ++ ), ++ "cloud-init-eth0.101.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0.101 ++ uuid=b5acec5e-db80-5935-8b02-0d5619fc42bf ++ type=vlan ++ interface-name=eth0.101 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [vlan] ++ id=101 ++ parent=1dd9a779-d327-56e1-8454-c65e2556c12c ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.0.2/24 ++ gateway=192.168.0.1 ++ dns=192.168.0.10;10.23.23.134; ++ dns-search=barley.maas;sacchromyces.maas;brettanomyces.maas; ++ address2=192.168.2.10/24 ++ ++ """ ++ ), ++ "cloud-init-bond0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0 ++ uuid=54317911-f840-516b-a10d-82cb4c1f075c ++ type=bond ++ interface-name=bond0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [bond] ++ mode=active-backup ++ miimon=100 ++ xmit_hash_policy=layer3+4 ++ ++ [ipv6] ++ method=dhcp ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ "cloud-init-eth2.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth2 ++ uuid=5559a242-3421-5fdd-896e-9cb8313d5804 ++ type=ethernet ++ slave-type=bond ++ master=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=C0:BB:9F:2C:E8:80 ++ ++ """ ++ ), ++ }, + "yaml": textwrap.dedent( + """ + version: 1 +@@ -2403,10 +2933,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + - type: static + address: 2001:1::1/92 + routes: +- - gateway: 2001:67c:1562:1 ++ - gateway: 2001:67c:1562::1 + network: 2001:67c:1 + netmask: "ffff:ffff::" +- - gateway: 3001:67c:1562:1 ++ - gateway: 3001:67c:15::1 + network: 3001:67c:1 + netmask: "ffff:ffff::" + metric: 10000 +@@ -2451,10 +2981,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + - to: 10.1.3.0/24 + via: 192.168.0.3 + - to: 2001:67c:1/32 +- via: 2001:67c:1562:1 ++ via: 2001:67c:1562::1 + - metric: 10000 + to: 3001:67c:1/32 +- via: 3001:67c:1562:1 ++ via: 3001:67c:15::1 + """ + ), + "expected_eni": textwrap.dedent( +@@ -2514,11 +3044,11 @@ iface bond0 inet static + # control-alias bond0 + iface bond0 inet6 static + address 2001:1::1/92 +- post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true +- pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true +- post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \ ++ post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true ++ pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true ++ post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:15::1 metric 10000 \ + || true +- pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \ ++ pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:15::1 metric 10000 \ + || true + """ + ), +@@ -2561,8 +3091,8 @@ iface bond0 inet6 static + - to: 2001:67c:1562:8007::1/64 + via: 2001:67c:1562:8007::aac:40b2 + - metric: 10000 +- to: 3001:67c:1562:8007::1/64 +- via: 3001:67c:1562:8007::aac:40b2 ++ to: 3001:67c:15:8007::1/64 ++ via: 3001:67c:15:8007::aac:40b2 + """ + ), + "expected_netplan-v2": textwrap.dedent( +@@ -2594,8 +3124,8 @@ iface bond0 inet6 static + - to: 2001:67c:1562:8007::1/64 + via: 2001:67c:1562:8007::aac:40b2 + - metric: 10000 +- to: 3001:67c:1562:8007::1/64 +- via: 3001:67c:1562:8007::aac:40b2 ++ to: 3001:67c:15:8007::1/64 ++ via: 3001:67c:15:8007::aac:40b2 + ethernets: + eth0: + match: +@@ -2694,8 +3224,8 @@ iface bond0 inet6 static + """\ + # Created by cloud-init on instance boot automatically, do not edit. + # +- 2001:67c:1/32 via 2001:67c:1562:1 dev bond0 +- 3001:67c:1/32 via 3001:67c:1562:1 metric 10000 dev bond0 ++ 2001:67c:1/32 via 2001:67c:1562::1 dev bond0 ++ 3001:67c:1/32 via 3001:67c:15::1 metric 10000 dev bond0 + """ + ), + "route-bond0": textwrap.dedent( +@@ -2718,6 +3248,88 @@ iface bond0 inet6 static + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-bond0s0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0s0 ++ uuid=09d0b5b9-67e7-5577-a1af-74d1cf17a71e ++ type=ethernet ++ slave-type=bond ++ master=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=AA:BB:CC:DD:E8:00 ++ ++ """ ++ ), ++ "cloud-init-bond0s1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0s1 ++ uuid=4d9aca96-b515-5630-ad83-d13daac7f9d0 ++ type=ethernet ++ slave-type=bond ++ master=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=AA:BB:CC:DD:E8:01 ++ ++ """ ++ ), ++ "cloud-init-bond0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0 ++ uuid=54317911-f840-516b-a10d-82cb4c1f075c ++ type=bond ++ interface-name=bond0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [bond] ++ mode=active-backup ++ miimon=100 ++ xmit_hash_policy=layer3+4 ++ num_grat_arp=5 ++ downdelay=10 ++ updelay=20 ++ fail_over_mac=active ++ primary_reselect=always ++ primary=bond0s0 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.0.2/24 ++ gateway=192.168.0.1 ++ route1=10.1.3.0/24,192.168.0.3 ++ address2=192.168.1.2/24 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::1/92 ++ route1=2001:67c:1/32,2001:67c:1562::1 ++ route2=3001:67c:1/32,3001:67c:15::1 ++ ++ """ ++ ), ++ }, + }, + "vlan": { + "yaml": textwrap.dedent( +@@ -2801,6 +3413,58 @@ iface bond0 inet6 static + VLAN=yes""" + ), + }, ++ "expected_network_manager": { ++ "cloud-init-en0.99.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init en0.99 ++ uuid=f594e2ed-f107-51df-b225-1dc530a5356b ++ type=vlan ++ interface-name=en0.99 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [vlan] ++ id=99 ++ parent=e0ca478b-8d84-52ab-8fae-628482c629b5 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.2.2/24 ++ address2=192.168.1.2/24 ++ gateway=192.168.1.1 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::bbbb/96 ++ route1=::/0,2001:1::1 ++ ++ """ ++ ), ++ "cloud-init-en0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init en0 ++ uuid=e0ca478b-8d84-52ab-8fae-628482c629b5 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=AA:BB:CC:DD:E8:00 ++ ++ """ ++ ), ++ }, + }, + "bridge": { + "yaml": textwrap.dedent( +@@ -2909,6 +3573,82 @@ iface bond0 inet6 static + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-br0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init br0 ++ uuid=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ type=bridge ++ interface-name=br0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [bridge] ++ stp=false ++ priority=22 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.2.2/24 ++ ++ """ ++ ), ++ "cloud-init-eth0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0 ++ uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++ type=ethernet ++ slave-type=bridge ++ master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:00 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::100/96 ++ ++ """ ++ ), ++ "cloud-init-eth1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1 ++ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 ++ type=ethernet ++ slave-type=bridge ++ master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:01 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::101/96 ++ ++ """ ++ ), ++ }, + }, + "manual": { + "yaml": textwrap.dedent( +@@ -3037,28 +3777,95 @@ iface bond0 inet6 static + """ + ), + }, +- }, +-} ++ "expected_network_manager": { ++ "cloud-init-eth0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. + ++ [connection] ++ id=cloud-init eth0 ++ uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++ type=ethernet + +-CONFIG_V1_EXPLICIT_LOOPBACK = { +- "version": 1, +- "config": [ +- { +- "name": "eth0", +- "type": "physical", +- "subnets": [{"control": "auto", "type": "dhcp"}], +- }, +- { +- "name": "lo", +- "type": "loopback", +- "subnets": [{"control": "auto", "type": "loopback"}], +- }, +- ], +-} ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init + ++ [ethernet] ++ mac-address=52:54:00:12:34:00 + +-CONFIG_V1_SIMPLE_SUBNET = { ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.1.2/24 ++ ++ """ ++ ), ++ "cloud-init-eth1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1 ++ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mtu=1480 ++ mac-address=52:54:00:12:34:AA ++ ++ [ipv4] ++ method=auto ++ may-fail=true ++ ++ """ ++ ), ++ "cloud-init-eth2.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth2 ++ uuid=5559a242-3421-5fdd-896e-9cb8313d5804 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:FF ++ ++ [ipv4] ++ method=auto ++ may-fail=true ++ ++ """ ++ ), ++ }, ++ }, ++} ++ ++ ++CONFIG_V1_EXPLICIT_LOOPBACK = { ++ "version": 1, ++ "config": [ ++ { ++ "name": "eth0", ++ "type": "physical", ++ "subnets": [{"control": "auto", "type": "dhcp"}], ++ }, ++ { ++ "name": "lo", ++ "type": "loopback", ++ "subnets": [{"control": "auto", "type": "loopback"}], ++ }, ++ ], ++} ++ ++ ++CONFIG_V1_SIMPLE_SUBNET = { + "version": 1, + "config": [ + { +@@ -3497,7 +4304,6 @@ class TestRhelSysConfigRendering(CiTestCase): + + with_logs = True + +- nm_cfg_file = "/etc/NetworkManager/NetworkManager.conf" + scripts_dir = "/etc/sysconfig/network-scripts" + header = ( + "# Created by cloud-init on instance boot automatically, " +@@ -4072,78 +4878,6 @@ USERCTL=no + self._compare_files_to_expected(entry[self.expected_name], found) + self._assert_headers(found) + +- def test_check_ifcfg_rh(self): +- """ifcfg-rh plugin is added NetworkManager.conf if conf present.""" +- render_dir = self.tmp_dir() +- nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) +- util.ensure_dir(os.path.dirname(nm_cfg)) +- +- # write a template nm.conf, note plugins is a list here +- with open(nm_cfg, "w") as fh: +- fh.write("# test_check_ifcfg_rh\n[main]\nplugins=foo,bar\n") +- self.assertTrue(os.path.exists(nm_cfg)) +- +- # render and read +- entry = NETWORK_CONFIGS["small"] +- found = self._render_and_read( +- network_config=yaml.load(entry["yaml"]), dir=render_dir +- ) +- self._compare_files_to_expected(entry[self.expected_name], found) +- self._assert_headers(found) +- +- # check ifcfg-rh is in the 'plugins' list +- config = sysconfig.ConfigObj(nm_cfg) +- self.assertIn("ifcfg-rh", config["main"]["plugins"]) +- +- def test_check_ifcfg_rh_plugins_string(self): +- """ifcfg-rh plugin is append when plugins is a string.""" +- render_dir = self.tmp_path("render") +- os.makedirs(render_dir) +- nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) +- util.ensure_dir(os.path.dirname(nm_cfg)) +- +- # write a template nm.conf, note plugins is a value here +- util.write_file(nm_cfg, "# test_check_ifcfg_rh\n[main]\nplugins=foo\n") +- +- # render and read +- entry = NETWORK_CONFIGS["small"] +- found = self._render_and_read( +- network_config=yaml.load(entry["yaml"]), dir=render_dir +- ) +- self._compare_files_to_expected(entry[self.expected_name], found) +- self._assert_headers(found) +- +- # check raw content has plugin +- nm_file_content = util.load_file(nm_cfg) +- self.assertIn("ifcfg-rh", nm_file_content) +- +- # check ifcfg-rh is in the 'plugins' list +- config = sysconfig.ConfigObj(nm_cfg) +- self.assertIn("ifcfg-rh", config["main"]["plugins"]) +- +- def test_check_ifcfg_rh_plugins_no_plugins(self): +- """enable_ifcfg_plugin creates plugins value if missing.""" +- render_dir = self.tmp_path("render") +- os.makedirs(render_dir) +- nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) +- util.ensure_dir(os.path.dirname(nm_cfg)) +- +- # write a template nm.conf, note plugins is missing +- util.write_file(nm_cfg, "# test_check_ifcfg_rh\n[main]\n") +- self.assertTrue(os.path.exists(nm_cfg)) +- +- # render and read +- entry = NETWORK_CONFIGS["small"] +- found = self._render_and_read( +- network_config=yaml.load(entry["yaml"]), dir=render_dir +- ) +- self._compare_files_to_expected(entry[self.expected_name], found) +- self._assert_headers(found) +- +- # check ifcfg-rh is in the 'plugins' list +- config = sysconfig.ConfigObj(nm_cfg) +- self.assertIn("ifcfg-rh", config["main"]["plugins"]) +- + def test_netplan_dhcp_false_disable_dhcp_in_state(self): + """netplan config with dhcp[46]: False should not add dhcp in state""" + net_config = yaml.load(NETPLAN_DHCP_FALSE) +@@ -4699,6 +5433,281 @@ STARTMODE=auto + self._assert_headers(found) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False), ++) ++class TestNetworkManagerRendering(CiTestCase): ++ ++ with_logs = True ++ ++ scripts_dir = "/etc/NetworkManager/system-connections" ++ ++ expected_name = "expected_network_manager" ++ ++ def _get_renderer(self): ++ return network_manager.Renderer() ++ ++ def _render_and_read(self, network_config=None, state=None, dir=None): ++ if dir is None: ++ dir = self.tmp_dir() ++ ++ if network_config: ++ ns = network_state.parse_net_config_data(network_config) ++ elif state: ++ ns = state ++ else: ++ raise ValueError("Expected data or state, got neither") ++ ++ renderer = self._get_renderer() ++ renderer.render_network_state(ns, target=dir) ++ return dir2dict(dir) ++ ++ def _compare_files_to_expected(self, expected, found): ++ orig_maxdiff = self.maxDiff ++ expected_d = dict( ++ (os.path.join(self.scripts_dir, k), v) for k, v in expected.items() ++ ) ++ ++ try: ++ self.maxDiff = None ++ self.assertEqual(expected_d, found) ++ finally: ++ self.maxDiff = orig_maxdiff ++ ++ @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot") ++ @mock.patch("cloudinit.net.sys_dev_path") ++ @mock.patch("cloudinit.net.read_sys_net") ++ @mock.patch("cloudinit.net.get_devicelist") ++ def test_default_generation( ++ self, ++ mock_get_devicelist, ++ mock_read_sys_net, ++ mock_sys_dev_path, ++ m_get_cmdline, ++ ): ++ tmp_dir = self.tmp_dir() ++ _setup_test( ++ tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path ++ ) ++ ++ network_cfg = net.generate_fallback_config() ++ ns = network_state.parse_net_config_data( ++ network_cfg, skip_broken=False ++ ) ++ ++ render_dir = os.path.join(tmp_dir, "render") ++ os.makedirs(render_dir) ++ ++ renderer = self._get_renderer() ++ renderer.render_network_state(ns, target=render_dir) ++ ++ found = dir2dict(render_dir) ++ self._compare_files_to_expected( ++ { ++ "cloud-init-eth1000.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1000 ++ uuid=8c517500-0c95-5308-9c8a-3092eebc44eb ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=07:1C:C6:75:A4:BE ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, ++ found, ++ ) ++ ++ def test_openstack_rendering_samples(self): ++ for os_sample in OS_SAMPLES: ++ render_dir = self.tmp_dir() ++ ex_input = os_sample["in_data"] ++ ex_mac_addrs = os_sample["in_macs"] ++ network_cfg = openstack.convert_net_json( ++ ex_input, known_macs=ex_mac_addrs ++ ) ++ ns = network_state.parse_net_config_data( ++ network_cfg, skip_broken=False ++ ) ++ renderer = self._get_renderer() ++ # render a multiple times to simulate reboots ++ renderer.render_network_state(ns, target=render_dir) ++ renderer.render_network_state(ns, target=render_dir) ++ renderer.render_network_state(ns, target=render_dir) ++ for fn, expected_content in os_sample.get(self.expected_name, []): ++ with open(os.path.join(render_dir, fn)) as fh: ++ self.assertEqual(expected_content, fh.read()) ++ ++ def test_network_config_v1_samples(self): ++ ns = network_state.parse_net_config_data(CONFIG_V1_SIMPLE_SUBNET) ++ render_dir = self.tmp_path("render") ++ os.makedirs(render_dir) ++ renderer = self._get_renderer() ++ renderer.render_network_state(ns, target=render_dir) ++ found = dir2dict(render_dir) ++ self._compare_files_to_expected( ++ { ++ "cloud-init-interface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init interface0 ++ uuid=8b6862ed-dbd6-5830-93f7-a91451c13828 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:00 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=10.0.2.15/24 ++ gateway=10.0.2.2 ++ ++ """ ++ ), ++ }, ++ found, ++ ) ++ ++ def test_config_with_explicit_loopback(self): ++ render_dir = self.tmp_path("render") ++ os.makedirs(render_dir) ++ ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK) ++ renderer = self._get_renderer() ++ renderer.render_network_state(ns, target=render_dir) ++ found = dir2dict(render_dir) ++ self._compare_files_to_expected( ++ { ++ "cloud-init-eth0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0 ++ uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++ type=ethernet ++ interface-name=eth0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, ++ found, ++ ) ++ ++ def test_bond_config(self): ++ entry = NETWORK_CONFIGS["bond"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_vlan_config(self): ++ entry = NETWORK_CONFIGS["vlan"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_bridge_config(self): ++ entry = NETWORK_CONFIGS["bridge"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_manual_config(self): ++ entry = NETWORK_CONFIGS["manual"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_all_config(self): ++ entry = NETWORK_CONFIGS["all"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self.assertNotIn( ++ "WARNING: Network config: ignoring eth0.101 device-level mtu", ++ self.logs.getvalue(), ++ ) ++ ++ def test_small_config(self): ++ entry = NETWORK_CONFIGS["small"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_v4_and_v6_static_config(self): ++ entry = NETWORK_CONFIGS["v4_and_v6_static"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ expected_msg = ( ++ "WARNING: Network config: ignoring iface0 device-level mtu:8999" ++ " because ipv4 subnet-level mtu:9000 provided." ++ ) ++ self.assertIn(expected_msg, self.logs.getvalue()) ++ ++ def test_dhcpv6_only_config(self): ++ entry = NETWORK_CONFIGS["dhcpv6_only"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_simple_render_ipv6_slaac(self): ++ entry = NETWORK_CONFIGS["ipv6_slaac"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_dhcpv6_stateless_config(self): ++ entry = NETWORK_CONFIGS["dhcpv6_stateless"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_wakeonlan_disabled_config_v2(self): ++ entry = NETWORK_CONFIGS["wakeonlan_disabled"] ++ found = self._render_and_read( ++ network_config=yaml.load(entry["yaml_v2"]) ++ ) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_wakeonlan_enabled_config_v2(self): ++ entry = NETWORK_CONFIGS["wakeonlan_enabled"] ++ found = self._render_and_read( ++ network_config=yaml.load(entry["yaml_v2"]) ++ ) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_render_v4_and_v6(self): ++ entry = NETWORK_CONFIGS["v4_and_v6"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_render_v6_and_v4(self): ++ entry = NETWORK_CONFIGS["v6_and_v4"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False), ++) + class TestEniNetRendering(CiTestCase): + @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot") + @mock.patch("cloudinit.net.sys_dev_path") +@@ -6136,9 +7145,9 @@ class TestNetworkdRoundTrip(CiTestCase): + + class TestRenderersSelect: + @pytest.mark.parametrize( +- "renderer_selected,netplan,eni,nm,scfg,sys,networkd", ++ "renderer_selected,netplan,eni,sys,network_manager,networkd", + ( +- # -netplan -ifupdown -nm -scfg -sys raises error ++ # -netplan -ifupdown -sys -network-manager -networkd raises error + ( + net.RendererNotFoundError, + False, +@@ -6146,52 +7155,51 @@ class TestRenderersSelect: + False, + False, + False, +- False, + ), +- # -netplan +ifupdown -nm -scfg -sys selects eni +- ("eni", False, True, False, False, False, False), +- # +netplan +ifupdown -nm -scfg -sys selects eni +- ("eni", True, True, False, False, False, False), +- # +netplan -ifupdown -nm -scfg -sys selects netplan +- ("netplan", True, False, False, False, False, False), +- # Ubuntu with Network-Manager installed +- # +netplan -ifupdown +nm -scfg -sys selects netplan +- ("netplan", True, False, True, False, False, False), +- # Centos/OpenSuse with Network-Manager installed selects sysconfig +- # -netplan -ifupdown +nm -scfg +sys selects netplan +- ("sysconfig", False, False, True, False, True, False), +- # -netplan -ifupdown -nm -scfg -sys +networkd selects networkd +- ("networkd", False, False, False, False, False, True), ++ # -netplan +ifupdown -sys -nm -networkd selects eni ++ ("eni", False, True, False, False, False), ++ # +netplan +ifupdown -sys -nm -networkd selects eni ++ ("eni", True, True, False, False, False), ++ # +netplan -ifupdown -sys -nm -networkd selects netplan ++ ("netplan", True, False, False, False, False), ++ # +netplan -ifupdown -sys -nm -networkd selects netplan ++ ("netplan", True, False, False, False, False), ++ # -netplan -ifupdown +sys -nm -networkd selects sysconfig ++ ("sysconfig", False, False, True, False, False), ++ # -netplan -ifupdown +sys +nm -networkd selects sysconfig ++ ("sysconfig", False, False, True, True, False), ++ # -netplan -ifupdown -sys +nm -networkd selects nm ++ ("network-manager", False, False, False, True, False), ++ # -netplan -ifupdown -sys +nm +networkd selects nm ++ ("network-manager", False, False, False, True, True), ++ # -netplan -ifupdown -sys -nm +networkd selects networkd ++ ("networkd", False, False, False, False, True), + ), + ) + @mock.patch("cloudinit.net.renderers.networkd.available") ++ @mock.patch("cloudinit.net.renderers.network_manager.available") + @mock.patch("cloudinit.net.renderers.netplan.available") + @mock.patch("cloudinit.net.renderers.sysconfig.available") +- @mock.patch("cloudinit.net.renderers.sysconfig.available_sysconfig") +- @mock.patch("cloudinit.net.renderers.sysconfig.available_nm") + @mock.patch("cloudinit.net.renderers.eni.available") + def test_valid_renderer_from_defaults_depending_on_availability( + self, + m_eni_avail, +- m_nm_avail, +- m_scfg_avail, + m_sys_avail, + m_netplan_avail, ++ m_network_manager_avail, + m_networkd_avail, + renderer_selected, + netplan, + eni, +- nm, +- scfg, + sys, ++ network_manager, + networkd, + ): + """Assert proper renderer per DEFAULT_PRIORITY given availability.""" + m_eni_avail.return_value = eni # ifupdown pkg presence +- m_nm_avail.return_value = nm # network-manager presence +- m_scfg_avail.return_value = scfg # sysconfig presence + m_sys_avail.return_value = sys # sysconfig/ifup/down presence + m_netplan_avail.return_value = netplan # netplan presence ++ m_network_manager_avail.return_value = network_manager # NM presence + m_networkd_avail.return_value = networkd # networkd presence + if isinstance(renderer_selected, str): + (renderer_name, _rnd_class) = renderers.select( +@@ -6249,7 +7257,7 @@ class TestNetRenderers(CiTestCase): + priority=["sysconfig", "eni"], + ) + +- @mock.patch("cloudinit.net.sysconfig.available_sysconfig") ++ @mock.patch("cloudinit.net.sysconfig.available") + @mock.patch("cloudinit.util.system_info") + def test_sysconfig_available_uses_variant_mapping(self, m_info, m_avail): + m_avail.return_value = True +diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py +index 3c29e2f7..4525c49c 100644 +--- a/tests/unittests/test_net_activators.py ++++ b/tests/unittests/test_net_activators.py +@@ -41,18 +41,20 @@ NETPLAN_CALL_LIST = [ + + @pytest.fixture + def available_mocks(): +- mocks = namedtuple("Mocks", "m_which, m_file") ++ mocks = namedtuple("Mocks", "m_which, m_file, m_exists") + with patch("cloudinit.subp.which", return_value=True) as m_which: + with patch("os.path.isfile", return_value=True) as m_file: +- yield mocks(m_which, m_file) ++ with patch("os.path.exists", return_value=True) as m_exists: ++ yield mocks(m_which, m_file, m_exists) + + + @pytest.fixture + def unavailable_mocks(): +- mocks = namedtuple("Mocks", "m_which, m_file") ++ mocks = namedtuple("Mocks", "m_which, m_file, m_exists") + with patch("cloudinit.subp.which", return_value=False) as m_which: + with patch("os.path.isfile", return_value=False) as m_file: +- yield mocks(m_which, m_file) ++ with patch("os.path.exists", return_value=False) as m_exists: ++ yield mocks(m_which, m_file, m_exists) + + + class TestSearchAndSelect: +@@ -113,10 +115,6 @@ NETPLAN_AVAILABLE_CALLS = [ + (("netplan",), {"search": ["/usr/sbin", "/sbin"], "target": None}), + ] + +-NETWORK_MANAGER_AVAILABLE_CALLS = [ +- (("nmcli",), {"target": None}), +-] +- + NETWORKD_AVAILABLE_CALLS = [ + (("ip",), {"search": ["/usr/sbin", "/bin"], "target": None}), + (("systemctl",), {"search": ["/usr/sbin", "/bin"], "target": None}), +@@ -128,7 +126,6 @@ NETWORKD_AVAILABLE_CALLS = [ + [ + (IfUpDownActivator, IF_UP_DOWN_AVAILABLE_CALLS), + (NetplanActivator, NETPLAN_AVAILABLE_CALLS), +- (NetworkManagerActivator, NETWORK_MANAGER_AVAILABLE_CALLS), + (NetworkdActivator, NETWORKD_AVAILABLE_CALLS), + ], + ) +@@ -144,8 +141,72 @@ IF_UP_DOWN_BRING_UP_CALL_LIST = [ + ] + + NETWORK_MANAGER_BRING_UP_CALL_LIST = [ +- ((["nmcli", "connection", "up", "ifname", "eth0"],), {}), +- ((["nmcli", "connection", "up", "ifname", "eth1"],), {}), ++ ( ++ ( ++ [ ++ "nmcli", ++ "connection", ++ "load", ++ "".join( ++ [ ++ "/etc/NetworkManager/system-connections", ++ "/cloud-init-eth0.nmconnection", ++ ] ++ ), ++ ], ++ ), ++ {}, ++ ), ++ ( ++ ( ++ [ ++ "nmcli", ++ "connection", ++ "up", ++ "filename", ++ "".join( ++ [ ++ "/etc/NetworkManager/system-connections", ++ "/cloud-init-eth0.nmconnection", ++ ] ++ ), ++ ], ++ ), ++ {}, ++ ), ++ ( ++ ( ++ [ ++ "nmcli", ++ "connection", ++ "load", ++ "".join( ++ [ ++ "/etc/NetworkManager/system-connections", ++ "/cloud-init-eth1.nmconnection", ++ ] ++ ), ++ ], ++ ), ++ {}, ++ ), ++ ( ++ ( ++ [ ++ "nmcli", ++ "connection", ++ "up", ++ "filename", ++ "".join( ++ [ ++ "/etc/NetworkManager/system-connections", ++ "/cloud-init-eth1.nmconnection", ++ ] ++ ), ++ ], ++ ), ++ {}, ++ ), + ] + + NETWORKD_BRING_UP_CALL_LIST = [ +@@ -169,9 +230,11 @@ class TestActivatorsBringUp: + def test_bring_up_interface( + self, m_subp, activator, expected_call_list, available_mocks + ): ++ index = 0 + activator.bring_up_interface("eth0") +- assert len(m_subp.call_args_list) == 1 +- assert m_subp.call_args_list[0] == expected_call_list[0] ++ for call in m_subp.call_args_list: ++ assert call == expected_call_list[index] ++ index += 1 + + @patch("cloudinit.subp.subp", return_value=("", "")) + def test_bring_up_interfaces( +@@ -208,8 +271,8 @@ IF_UP_DOWN_BRING_DOWN_CALL_LIST = [ + ] + + NETWORK_MANAGER_BRING_DOWN_CALL_LIST = [ +- ((["nmcli", "connection", "down", "eth0"],), {}), +- ((["nmcli", "connection", "down", "eth1"],), {}), ++ ((["nmcli", "device", "disconnect", "eth0"],), {}), ++ ((["nmcli", "device", "disconnect", "eth1"],), {}), + ] + + NETWORKD_BRING_DOWN_CALL_LIST = [ +-- +2.31.1 + diff --git a/SOURCES/ci-Align-rhel-custom-files-with-upstream-1431.patch b/SOURCES/ci-Align-rhel-custom-files-with-upstream-1431.patch new file mode 100644 index 0000000..ec79047 --- /dev/null +++ b/SOURCES/ci-Align-rhel-custom-files-with-upstream-1431.patch @@ -0,0 +1,256 @@ +From 8e599c618ba33f7ed572f752fc9201ca44e41868 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Wed, 18 May 2022 16:21:45 +0200 +Subject: [PATCH 4/5] Align rhel custom files with upstream (#1431) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 26: Align rhel custom files with upstream (#1431) +RH-Commit: [1/2] edac80c4fa3a11d093ee0e7260796566a7eb141e (eesposit/cloud-init-centos-) +RH-Bugzilla: 2088448 +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Miroslav Rezanina + +commit 9624758f91b61f4711e8d7b5c83075b5d23e0c43 +Author: Emanuele Giuseppe Esposito +Date: Wed May 18 15:18:04 2022 +0200 + + Align rhel custom files with upstream (#1431) + + So far RHEL had its own custom .service and cloud.cfg files, + that diverged from upstream. We always replaced the generated files + with the ones we had. + + This caused only confusion and made it harder to rebase and backport + patches targeting these files. + At the same time, we are going to delete our custom downstream-only files + and use the ones generated by .tmpl. + + The mapping is: + config/cloud.cfg.tmpl -> rhel/cloud.cfg + systemd/* -> rhel/systemd/* + + Such rhel-specific files are open and available in the Centos repo: + https://gitlab.com/redhat/centos-stream/src/cloud-init + + With this commit, we are also introducing modules in cloud.cfg that + were not in the default rhel cfg file, even though they should already + have been there with previous rebases and releases. + Anyways such modules support rhel as distro, and + therefore should cause no harm. + + Signed-off-by: Emanuele Giuseppe Esposito + + RHBZ: 2082071 + +Signed-off-by: Emanuele Giuseppe Esposito +--- + config/cloud.cfg.tmpl | 23 +++++++++++++++++++++++ + systemd/cloud-config.service.tmpl | 4 ++++ + systemd/cloud-final.service.tmpl | 13 +++++++++++++ + systemd/cloud-init-local.service.tmpl | 22 +++++++++++++++++++++- + systemd/cloud-init.service.tmpl | 6 +++++- + tests/unittests/test_render_cloudcfg.py | 1 + + 6 files changed, 67 insertions(+), 2 deletions(-) + +diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl +index 86beee3c..f4d2fd14 100644 +--- a/config/cloud.cfg.tmpl ++++ b/config/cloud.cfg.tmpl +@@ -34,7 +34,11 @@ disable_root: true + + {% if variant in ["almalinux", "alpine", "amazon", "centos", "cloudlinux", "eurolinux", + "fedora", "miraclelinux", "openEuler", "rhel", "rocky", "virtuozzo"] %} ++{% if variant == "rhel" %} ++mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service,_netdev', '0', '2'] ++{% else %} + mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2'] ++{% endif %} + {% if variant == "amazon" %} + resize_rootfs: noblock + {% endif %} +@@ -66,6 +70,14 @@ network: + config: disabled + {% endif %} + ++{% if variant == "rhel" %} ++# Default redhat settings: ++ssh_deletekeys: true ++ssh_genkeytypes: ['rsa', 'ecdsa', 'ed25519'] ++syslog_fix_perms: ~ ++disable_vmware_customization: false ++{% endif %} ++ + # The modules that run in the 'init' stage + cloud_init_modules: + - migrator +@@ -107,10 +119,15 @@ cloud_config_modules: + {% endif %} + {% if variant not in ["photon"] %} + - ssh-import-id ++{% if variant not in ["rhel"] %} + - keyboard ++{% endif %} + - locale + {% endif %} + - set-passwords ++{% if variant in ["rhel"] %} ++ - rh_subscription ++{% endif %} + {% if variant in ["rhel", "fedora", "photon"] %} + {% if variant not in ["photon"] %} + - spacewalk +@@ -239,6 +256,10 @@ system_info: + name: ec2-user + lock_passwd: True + gecos: EC2 Default User ++{% elif variant == "rhel" %} ++ name: cloud-user ++ lock_passwd: true ++ gecos: Cloud User + {% else %} + name: {{ variant }} + lock_passwd: True +@@ -254,6 +275,8 @@ system_info: + groups: [adm, sudo] + {% elif variant == "arch" %} + groups: [wheel, users] ++{% elif variant == "rhel" %} ++ groups: [adm, systemd-journal] + {% else %} + groups: [wheel, adm, systemd-journal] + {% endif %} +diff --git a/systemd/cloud-config.service.tmpl b/systemd/cloud-config.service.tmpl +index 9d928ca2..d5568a6e 100644 +--- a/systemd/cloud-config.service.tmpl ++++ b/systemd/cloud-config.service.tmpl +@@ -4,6 +4,10 @@ Description=Apply the settings specified in cloud-config + After=network-online.target cloud-config.target + After=snapd.seeded.service + Wants=network-online.target cloud-config.target ++{% if variant == "rhel" %} ++ConditionPathExists=!/etc/cloud/cloud-init.disabled ++ConditionKernelCommandLine=!cloud-init=disabled ++{% endif %} + + [Service] + Type=oneshot +diff --git a/systemd/cloud-final.service.tmpl b/systemd/cloud-final.service.tmpl +index 8207b18c..85f423ac 100644 +--- a/systemd/cloud-final.service.tmpl ++++ b/systemd/cloud-final.service.tmpl +@@ -7,6 +7,10 @@ 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 %} + + + [Service] +@@ -15,7 +19,16 @@ ExecStart=/usr/bin/cloud-init modules --mode=final + RemainAfterExit=yes + TimeoutSec=0 + KillMode=process ++{% if variant == "rhel" %} ++# Restart NetworkManager if it is present and running. ++ExecStartPost=/bin/sh -c 'u=NetworkManager.service; \ ++ out=$(systemctl show --property=SubState $u) || exit; \ ++ [ "$out" = "SubState=running" ] || exit 0; \ ++ systemctl reload-or-try-restart $u' ++{% else %} + TasksMax=infinity ++{% endif %} ++ + + # Output needs to appear in instance console output + StandardOutput=journal+console +diff --git a/systemd/cloud-init-local.service.tmpl b/systemd/cloud-init-local.service.tmpl +index 7166f640..a6b82650 100644 +--- a/systemd/cloud-init-local.service.tmpl ++++ b/systemd/cloud-init-local.service.tmpl +@@ -1,23 +1,43 @@ + ## template:jinja + [Unit] + Description=Initial cloud-init job (pre-networking) +-{% if variant in ["ubuntu", "unknown", "debian"] %} ++{% if variant in ["ubuntu", "unknown", "debian", "rhel" ] %} + DefaultDependencies=no + {% endif %} + Wants=network-pre.target + After=hv_kvp_daemon.service + After=systemd-remount-fs.service ++{% if variant == "rhel" %} ++Requires=dbus.socket ++After=dbus.socket ++{% endif %} + Before=NetworkManager.service ++{% if variant == "rhel" %} ++Before=network.service ++{% endif %} + Before=network-pre.target + Before=shutdown.target ++{% if variant == "rhel" %} ++Before=firewalld.target ++Conflicts=shutdown.target ++{% endif %} + {% if variant in ["ubuntu", "unknown", "debian"] %} + 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 %} + + [Service] + Type=oneshot ++{% if variant == "rhel" %} ++ExecStartPre=/bin/mkdir -p /run/cloud-init ++ExecStartPre=/sbin/restorecon /run/cloud-init ++ExecStartPre=/usr/bin/touch /run/cloud-init/enabled ++{% endif %} + ExecStart=/usr/bin/cloud-init init --local + ExecStart=/bin/touch /run/cloud-init/network-config-ready + RemainAfterExit=yes +diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl +index e71e5679..c170aef7 100644 +--- a/systemd/cloud-init.service.tmpl ++++ b/systemd/cloud-init.service.tmpl +@@ -1,7 +1,7 @@ + ## template:jinja + [Unit] + Description=Initial cloud-init job (metadata service crawler) +-{% if variant not in ["photon"] %} ++{% if variant not in ["photon", "rhel"] %} + DefaultDependencies=no + {% endif %} + Wants=cloud-init-local.service +@@ -36,6 +36,10 @@ Before=shutdown.target + Conflicts=shutdown.target + {% endif %} + Before=systemd-user-sessions.service ++{% if variant == "rhel" %} ++ConditionPathExists=!/etc/cloud/cloud-init.disabled ++ConditionKernelCommandLine=!cloud-init=disabled ++{% endif %} + + [Service] + Type=oneshot +diff --git a/tests/unittests/test_render_cloudcfg.py b/tests/unittests/test_render_cloudcfg.py +index 30fbd1a4..9f95d448 100644 +--- a/tests/unittests/test_render_cloudcfg.py ++++ b/tests/unittests/test_render_cloudcfg.py +@@ -68,6 +68,7 @@ class TestRenderCloudCfg: + default_user_exceptions = { + "amazon": "ec2-user", + "debian": "ubuntu", ++ "rhel": "cloud-user", + "unknown": "ubuntu", + } + default_user = system_cfg["system_info"]["default_user"]["name"] +-- +2.31.1 + diff --git a/SOURCES/ci-Honor-system-locale-for-RHEL-1355.patch b/SOURCES/ci-Honor-system-locale-for-RHEL-1355.patch new file mode 100644 index 0000000..c362160 --- /dev/null +++ b/SOURCES/ci-Honor-system-locale-for-RHEL-1355.patch @@ -0,0 +1,135 @@ +From 53e3f8ab9008fec8400f96918c2129f7defe6a70 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Fri, 10 Jun 2022 20:51:55 +0200 +Subject: [PATCH 1/3] Honor system locale for RHEL (#1355) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 29: Honor system locale for RHEL (#1355) +RH-Commit: [1/1] d571126fe6add8dc34a22c869d4e1a07a7373d8d (eesposit/cloud-init-centos-) +RH-Bugzilla: 2061604 +RH-Acked-by: Mohamed Gamal Morsy +RH-Acked-by: Vitaly Kuznetsov + +commit 58da7d856274e9ca2b507128d6f186e0e6abfe06 +Author: Wei Shi +Date: Wed Mar 30 23:55:30 2022 +0800 + + Honor system locale for RHEL (#1355) + + Make sure to use system locale as default on RHEL if locale is not + set in cloud-config. + + RHEL has a pre-installed cloud image using C.UTF-8 for system locale + just like ubuntu-minimal cloud image, without this patch, locale + module will set it to en_US.UTF-8 from ds default value during config + stage. + + Authored-by: Wei Shi + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/distros/rhel.py | 32 +++++++++++++++++++++++++ + tests/unittests/distros/test_generic.py | 10 ++++---- + tools/.github-cla-signers | 1 + + 3 files changed, 39 insertions(+), 4 deletions(-) + +diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py +index 84744ece..320f4ba1 100644 +--- a/cloudinit/distros/rhel.py ++++ b/cloudinit/distros/rhel.py +@@ -7,6 +7,7 @@ + # Author: Joshua Harlow + # + # This file is part of cloud-init. See LICENSE file for license information. ++import os + + from cloudinit import distros, helpers + from cloudinit import log as logging +@@ -57,11 +58,25 @@ class Distro(distros.Distro): + # should only happen say once per instance...) + self._runner = helpers.Runners(paths) + self.osfamily = "redhat" ++ self.default_locale = "en_US.UTF-8" ++ self.system_locale = None + cfg["ssh_svcname"] = "sshd" + + def install_packages(self, pkglist): + self.package_command("install", pkgs=pkglist) + ++ def get_locale(self): ++ """Return the default locale if set, else use system locale""" ++ ++ # read system locale value ++ if not self.system_locale: ++ self.system_locale = self._read_system_locale() ++ ++ # Return system_locale setting if valid, else use default locale ++ return ( ++ self.system_locale if self.system_locale else self.default_locale ++ ) ++ + def apply_locale(self, locale, out_fn=None): + if self.uses_systemd(): + if not out_fn: +@@ -75,6 +90,23 @@ class Distro(distros.Distro): + } + rhel_util.update_sysconfig_file(out_fn, locale_cfg) + ++ def _read_system_locale(self, keyname="LANG"): ++ """Read system default locale setting, if present""" ++ if self.uses_systemd(): ++ locale_fn = self.systemd_locale_conf_fn ++ else: ++ locale_fn = self.locale_conf_fn ++ ++ if not locale_fn: ++ raise ValueError("Invalid path: %s" % locale_fn) ++ ++ if os.path.exists(locale_fn): ++ (_exists, contents) = rhel_util.read_sysconfig_file(locale_fn) ++ if keyname in contents: ++ return contents[keyname] ++ else: ++ return None ++ + def _write_hostname(self, hostname, filename): + # systemd will never update previous-hostname for us, so + # we need to do it ourselves +diff --git a/tests/unittests/distros/test_generic.py b/tests/unittests/distros/test_generic.py +index 93c5395c..fedc7300 100644 +--- a/tests/unittests/distros/test_generic.py ++++ b/tests/unittests/distros/test_generic.py +@@ -187,12 +187,14 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase): + locale = d.get_locale() + self.assertEqual("C.UTF-8", locale) + +- def test_get_locale_rhel(self): +- """Test rhel distro returns NotImplementedError exception""" ++ @mock.patch("cloudinit.distros.rhel.Distro._read_system_locale") ++ def test_get_locale_rhel(self, m_locale): ++ """Test rhel distro returns locale set to C.UTF-8""" ++ m_locale.return_value = "C.UTF-8" + cls = distros.fetch("rhel") + d = cls("rhel", {}, None) +- with self.assertRaises(NotImplementedError): +- d.get_locale() ++ locale = d.get_locale() ++ self.assertEqual("C.UTF-8", locale) + + def test_expire_passwd_uses_chpasswd(self): + """Test ubuntu.expire_passwd uses the passwd command.""" +diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers +index 9f71ea0c..9eb2ae38 100644 +--- a/tools/.github-cla-signers ++++ b/tools/.github-cla-signers +@@ -70,6 +70,7 @@ renanrodrigo + rhansen + riedel + sarahwzadara ++shi2wei3 + slingamn + slyon + smoser +-- +2.35.1 + diff --git a/SOURCES/ci-Remove-rhel-specific-files.patch b/SOURCES/ci-Remove-rhel-specific-files.patch new file mode 100644 index 0000000..efce1f5 --- /dev/null +++ b/SOURCES/ci-Remove-rhel-specific-files.patch @@ -0,0 +1,370 @@ +From 0ae221fe8f29555370520d05f6558eb75fe3cd42 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Thu, 5 May 2022 11:31:33 +0200 +Subject: [PATCH 5/5] Remove rhel specific files + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 26: Align rhel custom files with upstream (#1431) +RH-Commit: [2/2] 0b9687ebab4f4039d5bbe05c00806ec7923e898d (eesposit/cloud-init-centos-) +RH-Bugzilla: 2088448 +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Miroslav Rezanina + +Remove all files in rhel/ directory and related commands that copy +and replace them with the generated ones. + +Also adjust setup.py, align it with upstream: +- by default, after rhel 8.3 ds-identify is in /usr/libexec, so no need to move it manually +- bash-completions work also in /usr/share, as upstream +- udev also works in /lib/udev + +Also remove rhel/README since it is outdated (chef is used in cloud.cfg) and cloud-init-tmpfiles.conf, +as it exists also in .distro. + +X-downstream-only: yes + +Signed-off-by: Emanuele Giuseppe Esposito +--- + rhel/README.rhel | 5 -- + rhel/cloud-init-tmpfiles.conf | 1 - + rhel/cloud.cfg | 69 --------------------------- + rhel/systemd/cloud-config.service | 18 ------- + rhel/systemd/cloud-config.target | 11 ----- + rhel/systemd/cloud-final.service | 24 ---------- + rhel/systemd/cloud-init-local.service | 31 ------------ + rhel/systemd/cloud-init.service | 26 ---------- + rhel/systemd/cloud-init.target | 7 --- + setup.py | 28 ++++++++++- + 11 files changed, 31 insertions(+), 210 deletions(-) + delete mode 100644 rhel/README.rhel + delete mode 100644 rhel/cloud-init-tmpfiles.conf + delete mode 100644 rhel/cloud.cfg + delete mode 100644 rhel/systemd/cloud-config.service + delete mode 100644 rhel/systemd/cloud-config.target + delete mode 100644 rhel/systemd/cloud-final.service + delete mode 100644 rhel/systemd/cloud-init-local.service + delete mode 100644 rhel/systemd/cloud-init.service + delete mode 100644 rhel/systemd/cloud-init.target + +diff --git a/rhel/README.rhel b/rhel/README.rhel +deleted file mode 100644 +index aa29630d..00000000 +--- a/rhel/README.rhel ++++ /dev/null +@@ -1,5 +0,0 @@ +-The following cloud-init modules are currently unsupported on this OS: +- - apt_update_upgrade ('apt_update', 'apt_upgrade', 'apt_mirror', 'apt_preserve_sources_list', 'apt_old_mirror', 'apt_sources', 'debconf_selections', 'packages' options) +- - byobu ('byobu_by_default' option) +- - chef +- - grub_dpkg +diff --git a/rhel/cloud-init-tmpfiles.conf b/rhel/cloud-init-tmpfiles.conf +deleted file mode 100644 +index 0c6d2a3b..00000000 +--- a/rhel/cloud-init-tmpfiles.conf ++++ /dev/null +@@ -1 +0,0 @@ +-d /run/cloud-init 0700 root root - - +diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg +deleted file mode 100644 +index 75d5c84b..00000000 +--- a/rhel/cloud.cfg ++++ /dev/null +@@ -1,69 +0,0 @@ +-users: +- - default +- +-disable_root: 1 +-ssh_pwauth: 0 +- +-mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service,_netdev', '0', '2'] +-resize_rootfs_tmp: /dev +-ssh_deletekeys: 1 +-ssh_genkeytypes: ['rsa', 'ecdsa', 'ed25519'] +-syslog_fix_perms: ~ +-disable_vmware_customization: false +- +-cloud_init_modules: +- - disk_setup +- - migrator +- - bootcmd +- - write-files +- - growpart +- - resizefs +- - set_hostname +- - update_hostname +- - update_etc_hosts +- - rsyslog +- - users-groups +- - ssh +- +-cloud_config_modules: +- - mounts +- - locale +- - set-passwords +- - rh_subscription +- - yum-add-repo +- - package-update-upgrade-install +- - timezone +- - puppet +- - chef +- - salt-minion +- - mcollective +- - disable-ec2-metadata +- - runcmd +- +-cloud_final_modules: +- - rightscale_userdata +- - scripts-per-once +- - scripts-per-boot +- - scripts-per-instance +- - scripts-user +- - ssh-authkey-fingerprints +- - keys-to-console +- - phone-home +- - final-message +- - power-state-change +- +-system_info: +- default_user: +- name: cloud-user +- lock_passwd: true +- gecos: Cloud User +- groups: [adm, systemd-journal] +- sudo: ["ALL=(ALL) NOPASSWD:ALL"] +- shell: /bin/bash +- distro: rhel +- paths: +- cloud_dir: /var/lib/cloud +- templates_dir: /etc/cloud/templates +- ssh_svcname: sshd +- +-# vim:syntax=yaml +diff --git a/rhel/systemd/cloud-config.service b/rhel/systemd/cloud-config.service +deleted file mode 100644 +index f3dcd4be..00000000 +--- a/rhel/systemd/cloud-config.service ++++ /dev/null +@@ -1,18 +0,0 @@ +-[Unit] +-Description=Apply the settings specified in cloud-config +-After=network-online.target cloud-config.target +-Wants=network-online.target cloud-config.target +-ConditionPathExists=!/etc/cloud/cloud-init.disabled +-ConditionKernelCommandLine=!cloud-init=disabled +- +-[Service] +-Type=oneshot +-ExecStart=/usr/bin/cloud-init modules --mode=config +-RemainAfterExit=yes +-TimeoutSec=0 +- +-# Output needs to appear in instance console output +-StandardOutput=journal+console +- +-[Install] +-WantedBy=cloud-init.target +diff --git a/rhel/systemd/cloud-config.target b/rhel/systemd/cloud-config.target +deleted file mode 100644 +index ae9b7d02..00000000 +--- a/rhel/systemd/cloud-config.target ++++ /dev/null +@@ -1,11 +0,0 @@ +-# cloud-init normally emits a "cloud-config" upstart event to inform third +-# parties that cloud-config is available, which does us no good when we're +-# using systemd. cloud-config.target serves as this synchronization point +-# instead. Services that would "start on cloud-config" with upstart can +-# instead use "After=cloud-config.target" and "Wants=cloud-config.target" +-# as appropriate. +- +-[Unit] +-Description=Cloud-config availability +-Wants=cloud-init-local.service cloud-init.service +-After=cloud-init-local.service cloud-init.service +diff --git a/rhel/systemd/cloud-final.service b/rhel/systemd/cloud-final.service +deleted file mode 100644 +index e281c0cf..00000000 +--- a/rhel/systemd/cloud-final.service ++++ /dev/null +@@ -1,24 +0,0 @@ +-[Unit] +-Description=Execute cloud user/final scripts +-After=network-online.target cloud-config.service rc-local.service +-Wants=network-online.target cloud-config.service +-ConditionPathExists=!/etc/cloud/cloud-init.disabled +-ConditionKernelCommandLine=!cloud-init=disabled +- +-[Service] +-Type=oneshot +-ExecStart=/usr/bin/cloud-init modules --mode=final +-RemainAfterExit=yes +-TimeoutSec=0 +-KillMode=process +-# Restart NetworkManager if it is present and running. +-ExecStartPost=/bin/sh -c 'u=NetworkManager.service; \ +- out=$(systemctl show --property=SubState $u) || exit; \ +- [ "$out" = "SubState=running" ] || exit 0; \ +- systemctl reload-or-try-restart $u' +- +-# Output needs to appear in instance console output +-StandardOutput=journal+console +- +-[Install] +-WantedBy=cloud-init.target +diff --git a/rhel/systemd/cloud-init-local.service b/rhel/systemd/cloud-init-local.service +deleted file mode 100644 +index 8f9f6c9f..00000000 +--- a/rhel/systemd/cloud-init-local.service ++++ /dev/null +@@ -1,31 +0,0 @@ +-[Unit] +-Description=Initial cloud-init job (pre-networking) +-DefaultDependencies=no +-Wants=network-pre.target +-After=systemd-remount-fs.service +-Requires=dbus.socket +-After=dbus.socket +-Before=NetworkManager.service network.service +-Before=network-pre.target +-Before=shutdown.target +-Before=firewalld.target +-Conflicts=shutdown.target +-RequiresMountsFor=/var/lib/cloud +-ConditionPathExists=!/etc/cloud/cloud-init.disabled +-ConditionKernelCommandLine=!cloud-init=disabled +- +-[Service] +-Type=oneshot +-ExecStartPre=/bin/mkdir -p /run/cloud-init +-ExecStartPre=/sbin/restorecon /run/cloud-init +-ExecStartPre=/usr/bin/touch /run/cloud-init/enabled +-ExecStart=/usr/bin/cloud-init init --local +-ExecStart=/bin/touch /run/cloud-init/network-config-ready +-RemainAfterExit=yes +-TimeoutSec=0 +- +-# Output needs to appear in instance console output +-StandardOutput=journal+console +- +-[Install] +-WantedBy=cloud-init.target +diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service +deleted file mode 100644 +index 0b3d796d..00000000 +--- a/rhel/systemd/cloud-init.service ++++ /dev/null +@@ -1,26 +0,0 @@ +-[Unit] +-Description=Initial cloud-init job (metadata service crawler) +-Wants=cloud-init-local.service +-Wants=sshd-keygen.service +-Wants=sshd.service +-After=cloud-init-local.service +-After=NetworkManager.service network.service +-After=NetworkManager-wait-online.service +-Before=network-online.target +-Before=sshd-keygen.service +-Before=sshd.service +-Before=systemd-user-sessions.service +-ConditionPathExists=!/etc/cloud/cloud-init.disabled +-ConditionKernelCommandLine=!cloud-init=disabled +- +-[Service] +-Type=oneshot +-ExecStart=/usr/bin/cloud-init init +-RemainAfterExit=yes +-TimeoutSec=0 +- +-# Output needs to appear in instance console output +-StandardOutput=journal+console +- +-[Install] +-WantedBy=cloud-init.target +diff --git a/rhel/systemd/cloud-init.target b/rhel/systemd/cloud-init.target +deleted file mode 100644 +index 083c3b6f..00000000 +--- a/rhel/systemd/cloud-init.target ++++ /dev/null +@@ -1,7 +0,0 @@ +-# cloud-init target is enabled by cloud-init-generator +-# To disable it you can either: +-# a.) boot with kernel cmdline of 'cloud-init=disabled' +-# b.) touch a file /etc/cloud/cloud-init.disabled +-[Unit] +-Description=Cloud-init target +-After=multi-user.target +diff --git a/setup.py b/setup.py +index 3c377eaa..a9132d2c 100755 +--- a/setup.py ++++ b/setup.py +@@ -139,6 +139,21 @@ INITSYS_FILES = { + "sysvinit_deb": [f for f in glob("sysvinit/debian/*") if is_f(f)], + "sysvinit_openrc": [f for f in glob("sysvinit/gentoo/*") if is_f(f)], + "sysvinit_suse": [f for f in glob("sysvinit/suse/*") if is_f(f)], ++ "systemd": [ ++ render_tmpl(f) ++ for f in ( ++ glob("systemd/*.tmpl") ++ + glob("systemd/*.service") ++ + glob("systemd/*.socket") ++ + glob("systemd/*.target") ++ ) ++ if (is_f(f) and not is_generator(f)) ++ ], ++ "systemd.generators": [ ++ render_tmpl(f, mode=0o755) ++ for f in glob("systemd/*") ++ if is_f(f) and is_generator(f) ++ ], + "upstart": [f for f in glob("upstart/*") if is_f(f)], + } + INITSYS_ROOTS = { +@@ -148,6 +163,10 @@ INITSYS_ROOTS = { + "sysvinit_deb": "etc/init.d", + "sysvinit_openrc": "etc/init.d", + "sysvinit_suse": "etc/init.d", ++ "systemd": pkg_config_read("systemd", "systemdsystemunitdir"), ++ "systemd.generators": pkg_config_read( ++ "systemd", "systemdsystemgeneratordir" ++ ), + "upstart": "etc/init/", + } + INITSYS_TYPES = sorted([f.partition(".")[0] for f in INITSYS_ROOTS.keys()]) +@@ -262,13 +281,15 @@ data_files = [ + ( + USR_LIB_EXEC + "/cloud-init", + [ ++ "tools/ds-identify", + "tools/hook-hotplug", + "tools/uncloud-init", + "tools/write-ssh-key-fingerprints", + ], + ), + ( +- ETC + "/bash_completion.d", ["bash_completion/cloud-init"], ++ USR + "/share/bash-completion/completions", ++ ["bash_completion/cloud-init"], + ), + (USR + "/share/doc/cloud-init", [f for f in glob("doc/*") if is_f(f)]), + ( +@@ -287,7 +308,8 @@ if not platform.system().endswith("BSD"): + ETC + "/NetworkManager/dispatcher.d/", + ["tools/hook-network-manager"], + ), +- ("/usr/lib/udev/rules.d", [f for f in glob("udev/*.rules")]), ++ (ETC + "/dhcp/dhclient-exit-hooks.d/", ["tools/hook-dhclient"]), ++ (LIB + "/udev/rules.d", [f for f in glob("udev/*.rules")]), + ( + ETC + "/systemd/system/sshd-keygen@.service.d/", + ["systemd/disable-sshd-keygen-if-cloud-init-active.conf"], +@@ -317,6 +339,8 @@ setuptools.setup( + scripts=["tools/cloud-init-per"], + license="Dual-licensed under GPLv3 or Apache 2.0", + data_files=data_files, ++ install_requires=requirements, ++ cmdclass=cmdclass, + entry_points={ + "console_scripts": [ + "cloud-init = cloudinit.cmd.main:main", +-- +2.31.1 + diff --git a/SOURCES/ci-Revert-Add-native-NetworkManager-support-1224.patch b/SOURCES/ci-Revert-Add-native-NetworkManager-support-1224.patch new file mode 100644 index 0000000..981473c --- /dev/null +++ b/SOURCES/ci-Revert-Add-native-NetworkManager-support-1224.patch @@ -0,0 +1,2267 @@ +From 35bd50e66f636a3f3923b6980bdee3ba33f7457d Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 8 Aug 2022 10:01:16 +0200 +Subject: [PATCH 1/3] Revert "Add native NetworkManager support (#1224)" + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 31: Revert "Revert "Setting highest autoconnect priority for network-scripts"" +RH-Commit: [1/3] 38dcbc9ec19412601e96305fcac09642c89d73b8 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2107463 2104389 2117532 2098501 +RH-Acked-by: Eduardo Otubo +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Mohamed Gamal Morsy + +NM is still not stable, revert it for now. + +This reverts commit 588deeb5b3f87ffe40d9ecaf6da3639176f806c4. + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/cmd/devel/net_convert.py | 14 +- + cloudinit/net/activators.py | 25 +- + cloudinit/net/network_manager.py | 377 ------- + cloudinit/net/renderers.py | 3 - + cloudinit/net/sysconfig.py | 37 +- + tests/unittests/test_net.py | 1268 +++--------------------- + tests/unittests/test_net_activators.py | 93 +- + 7 files changed, 193 insertions(+), 1624 deletions(-) + delete mode 100644 cloudinit/net/network_manager.py + +diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py +index 647fe07b..18b1e7ff 100755 +--- a/cloudinit/cmd/devel/net_convert.py ++++ b/cloudinit/cmd/devel/net_convert.py +@@ -7,14 +7,7 @@ import os + import sys + + from cloudinit import distros, log, safeyaml +-from cloudinit.net import ( +- eni, +- netplan, +- network_manager, +- network_state, +- networkd, +- sysconfig, +-) ++from cloudinit.net import eni, netplan, network_state, networkd, sysconfig + from cloudinit.sources import DataSourceAzure as azure + from cloudinit.sources import DataSourceOVF as ovf + from cloudinit.sources.helpers import openstack +@@ -81,7 +74,7 @@ def get_parser(parser=None): + parser.add_argument( + "-O", + "--output-kind", +- choices=["eni", "netplan", "networkd", "sysconfig", "network-manager"], ++ choices=["eni", "netplan", "networkd", "sysconfig"], + required=True, + help="The network config format to emit", + ) +@@ -155,9 +148,6 @@ def handle_args(name, args): + elif args.output_kind == "sysconfig": + r_cls = sysconfig.Renderer + config = distro.renderer_configs.get("sysconfig") +- elif args.output_kind == "network-manager": +- r_cls = network_manager.Renderer +- config = distro.renderer_configs.get("network-manager") + else: + raise RuntimeError("Invalid output_kind") + +diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py +index edbc0c06..e80c26df 100644 +--- a/cloudinit/net/activators.py ++++ b/cloudinit/net/activators.py +@@ -1,14 +1,15 @@ + # This file is part of cloud-init. See LICENSE file for license information. + import logging ++import os + from abc import ABC, abstractmethod + from typing import Iterable, List, Type + + from cloudinit import subp, util + from cloudinit.net.eni import available as eni_available + from cloudinit.net.netplan import available as netplan_available +-from cloudinit.net.network_manager import available as nm_available + from cloudinit.net.network_state import NetworkState + from cloudinit.net.networkd import available as networkd_available ++from cloudinit.net.sysconfig import NM_CFG_FILE + + LOG = logging.getLogger(__name__) + +@@ -123,24 +124,20 @@ class IfUpDownActivator(NetworkActivator): + class NetworkManagerActivator(NetworkActivator): + @staticmethod + def available(target=None) -> bool: +- """Return true if NetworkManager can be used on this system.""" +- return nm_available(target=target) ++ """Return true if network manager can be used on this system.""" ++ config_present = os.path.isfile( ++ subp.target_path(target, path=NM_CFG_FILE) ++ ) ++ nmcli_present = subp.which("nmcli", target=target) ++ return config_present and bool(nmcli_present) + + @staticmethod + def bring_up_interface(device_name: str) -> bool: +- """Bring up connection using nmcli. ++ """Bring up interface using nmcli. + + Return True is successful, otherwise return False + """ +- from cloudinit.net.network_manager import conn_filename +- +- filename = conn_filename(device_name) +- cmd = ["nmcli", "connection", "load", filename] +- if _alter_interface(cmd, device_name): +- cmd = ["nmcli", "connection", "up", "filename", filename] +- else: +- _alter_interface(["nmcli", "connection", "reload"], device_name) +- cmd = ["nmcli", "connection", "up", "ifname", device_name] ++ cmd = ["nmcli", "connection", "up", "ifname", device_name] + return _alter_interface(cmd, device_name) + + @staticmethod +@@ -149,7 +146,7 @@ class NetworkManagerActivator(NetworkActivator): + + Return True is successful, otherwise return False + """ +- cmd = ["nmcli", "device", "disconnect", device_name] ++ cmd = ["nmcli", "connection", "down", device_name] + return _alter_interface(cmd, device_name) + + +diff --git a/cloudinit/net/network_manager.py b/cloudinit/net/network_manager.py +deleted file mode 100644 +index 79b0fe0b..00000000 +--- a/cloudinit/net/network_manager.py ++++ /dev/null +@@ -1,377 +0,0 @@ +-# Copyright 2022 Red Hat, Inc. +-# +-# Author: Lubomir Rintel +-# Fixes and suggestions contributed by James Falcon, Neal Gompa, +-# Zbigniew Jędrzejewski-Szmek and Emanuele Giuseppe Esposito. +-# +-# This file is part of cloud-init. See LICENSE file for license information. +- +-import configparser +-import io +-import itertools +-import os +-import uuid +- +-from cloudinit import log as logging +-from cloudinit import subp, util +- +-from . import renderer +-from .network_state import is_ipv6_addr, subnet_is_ipv6 +- +-NM_RUN_DIR = "/etc/NetworkManager" +-NM_LIB_DIR = "/usr/lib/NetworkManager" +-LOG = logging.getLogger(__name__) +- +- +-class NMConnection: +- """Represents a NetworkManager connection profile.""" +- +- def __init__(self, con_id): +- """ +- Initializes the connection with some very basic properties, +- notably the UUID so that the connection can be referred to. +- """ +- +- # Chosen by fair dice roll +- CI_NM_UUID = uuid.UUID("a3924cb8-09e0-43e9-890b-77972a800108") +- +- self.config = configparser.ConfigParser() +- # Identity option name mapping, to achieve case sensitivity +- self.config.optionxform = str +- +- self.config["connection"] = { +- "id": f"cloud-init {con_id}", +- "uuid": str(uuid.uuid5(CI_NM_UUID, con_id)), +- } +- +- # This is not actually used anywhere, but may be useful in future +- self.config["user"] = { +- "org.freedesktop.NetworkManager.origin": "cloud-init" +- } +- +- def _set_default(self, section, option, value): +- """ +- Sets a property unless it's already set, ensuring the section +- exists. +- """ +- +- if not self.config.has_section(section): +- self.config[section] = {} +- if not self.config.has_option(section, option): +- self.config[section][option] = value +- +- def _set_ip_method(self, family, subnet_type): +- """ +- Ensures there's appropriate [ipv4]/[ipv6] for given family +- appropriate for given configuration type +- """ +- +- method_map = { +- "static": "manual", +- "dhcp6": "dhcp", +- "ipv6_slaac": "auto", +- "ipv6_dhcpv6-stateless": "auto", +- "ipv6_dhcpv6-stateful": "auto", +- "dhcp4": "auto", +- "dhcp": "auto", +- } +- +- # Ensure we got an [ipvX] section +- self._set_default(family, "method", "disabled") +- +- try: +- method = method_map[subnet_type] +- except KeyError: +- # What else can we do +- method = "auto" +- self.config[family]["may-fail"] = "true" +- +- # Make sure we don't "downgrade" the method in case +- # we got conflicting subnets (e.g. static along with dhcp) +- if self.config[family]["method"] == "dhcp": +- return +- if self.config[family]["method"] == "auto" and method == "manual": +- return +- +- self.config[family]["method"] = method +- self._set_default(family, "may-fail", "false") +- if family == "ipv6": +- self._set_default(family, "addr-gen-mode", "stable-privacy") +- +- def _add_numbered(self, section, key_prefix, value): +- """ +- Adds a numbered property, such as address or route, ensuring +- the appropriate value gets used for . +- """ +- +- for index in itertools.count(1): +- key = f"{key_prefix}{index}" +- if not self.config.has_option(section, key): +- self.config[section][key] = value +- break +- +- def _add_address(self, family, subnet): +- """ +- Adds an ipv[46]address property. +- """ +- +- value = subnet["address"] + "/" + str(subnet["prefix"]) +- self._add_numbered(family, "address", value) +- +- def _add_route(self, family, route): +- """ +- Adds a ipv[46].route property. +- """ +- +- value = route["network"] + "/" + str(route["prefix"]) +- if "gateway" in route: +- value = value + "," + route["gateway"] +- self._add_numbered(family, "route", value) +- +- def _add_nameserver(self, dns): +- """ +- Extends the ipv[46].dns property with a name server. +- """ +- +- # FIXME: the subnet contains IPv4 and IPv6 name server mixed +- # together. We might be getting an IPv6 name server while +- # we're dealing with an IPv4 subnet. Sort this out by figuring +- # out the correct family and making sure a valid section exist. +- family = "ipv6" if is_ipv6_addr(dns) else "ipv4" +- self._set_default(family, "method", "disabled") +- +- self._set_default(family, "dns", "") +- self.config[family]["dns"] = self.config[family]["dns"] + dns + ";" +- +- def _add_dns_search(self, family, dns_search): +- """ +- Extends the ipv[46].dns-search property with a name server. +- """ +- +- self._set_default(family, "dns-search", "") +- self.config[family]["dns-search"] = ( +- self.config[family]["dns-search"] + ";".join(dns_search) + ";" +- ) +- +- def con_uuid(self): +- """ +- Returns the connection UUID +- """ +- return self.config["connection"]["uuid"] +- +- def valid(self): +- """ +- Can this be serialized into a meaningful connection profile? +- """ +- return self.config.has_option("connection", "type") +- +- @staticmethod +- def mac_addr(addr): +- """ +- Sanitize a MAC address. +- """ +- return addr.replace("-", ":").upper() +- +- def render_interface(self, iface, renderer): +- """ +- Integrate information from network state interface information +- into the connection. Most of the work is done here. +- """ +- +- # Initialize type & connectivity +- _type_map = { +- "physical": "ethernet", +- "vlan": "vlan", +- "bond": "bond", +- "bridge": "bridge", +- "infiniband": "infiniband", +- "loopback": None, +- } +- +- if_type = _type_map[iface["type"]] +- if if_type is None: +- return +- if "bond-master" in iface: +- slave_type = "bond" +- else: +- slave_type = None +- +- self.config["connection"]["type"] = if_type +- if slave_type is not None: +- self.config["connection"]["slave-type"] = slave_type +- self.config["connection"]["master"] = renderer.con_ref( +- iface[slave_type + "-master"] +- ) +- +- # Add type specific-section +- self.config[if_type] = {} +- +- # These are the interface properties that map nicely +- # to NetworkManager properties +- _prop_map = { +- "bond": { +- "mode": "bond-mode", +- "miimon": "bond_miimon", +- "xmit_hash_policy": "bond-xmit-hash-policy", +- "num_grat_arp": "bond-num-grat-arp", +- "downdelay": "bond-downdelay", +- "updelay": "bond-updelay", +- "fail_over_mac": "bond-fail-over-mac", +- "primary_reselect": "bond-primary-reselect", +- "primary": "bond-primary", +- }, +- "bridge": { +- "stp": "bridge_stp", +- "priority": "bridge_bridgeprio", +- }, +- "vlan": { +- "id": "vlan_id", +- }, +- "ethernet": {}, +- "infiniband": {}, +- } +- +- device_mtu = iface["mtu"] +- ipv4_mtu = None +- +- # Deal with Layer 3 configuration +- for subnet in iface["subnets"]: +- family = "ipv6" if subnet_is_ipv6(subnet) else "ipv4" +- +- self._set_ip_method(family, subnet["type"]) +- if "address" in subnet: +- self._add_address(family, subnet) +- if "gateway" in subnet: +- self.config[family]["gateway"] = subnet["gateway"] +- for route in subnet["routes"]: +- self._add_route(family, route) +- if "dns_nameservers" in subnet: +- for nameserver in subnet["dns_nameservers"]: +- self._add_nameserver(nameserver) +- if "dns_search" in subnet: +- self._add_dns_search(family, subnet["dns_search"]) +- if family == "ipv4" and "mtu" in subnet: +- ipv4_mtu = subnet["mtu"] +- +- if ipv4_mtu is None: +- ipv4_mtu = device_mtu +- if not ipv4_mtu == device_mtu: +- LOG.warning( +- "Network config: ignoring %s device-level mtu:%s" +- " because ipv4 subnet-level mtu:%s provided.", +- iface["name"], +- device_mtu, +- ipv4_mtu, +- ) +- +- # Parse type-specific properties +- for nm_prop, key in _prop_map[if_type].items(): +- if key not in iface: +- continue +- if iface[key] is None: +- continue +- if isinstance(iface[key], bool): +- self.config[if_type][nm_prop] = ( +- "true" if iface[key] else "false" +- ) +- else: +- self.config[if_type][nm_prop] = str(iface[key]) +- +- # These ones need special treatment +- if if_type == "ethernet": +- if iface["wakeonlan"] is True: +- # NM_SETTING_WIRED_WAKE_ON_LAN_MAGIC +- self.config["ethernet"]["wake-on-lan"] = str(0x40) +- if ipv4_mtu is not None: +- self.config["ethernet"]["mtu"] = str(ipv4_mtu) +- if iface["mac_address"] is not None: +- self.config["ethernet"]["mac-address"] = self.mac_addr( +- iface["mac_address"] +- ) +- if if_type == "vlan" and "vlan-raw-device" in iface: +- self.config["vlan"]["parent"] = renderer.con_ref( +- iface["vlan-raw-device"] +- ) +- if if_type == "bridge": +- # Bridge is ass-backwards compared to bond +- for port in iface["bridge_ports"]: +- port = renderer.get_conn(port) +- port._set_default("connection", "slave-type", "bridge") +- port._set_default("connection", "master", self.con_uuid()) +- if iface["mac_address"] is not None: +- self.config["bridge"]["mac-address"] = self.mac_addr( +- iface["mac_address"] +- ) +- if if_type == "infiniband" and ipv4_mtu is not None: +- self.config["infiniband"]["transport-mode"] = "datagram" +- self.config["infiniband"]["mtu"] = str(ipv4_mtu) +- if iface["mac_address"] is not None: +- self.config["infiniband"]["mac-address"] = self.mac_addr( +- iface["mac_address"] +- ) +- +- # Finish up +- if if_type == "bridge" or not self.config.has_option( +- if_type, "mac-address" +- ): +- self.config["connection"]["interface-name"] = iface["name"] +- +- def dump(self): +- """ +- Stringify. +- """ +- +- buf = io.StringIO() +- self.config.write(buf, space_around_delimiters=False) +- header = "# Generated by cloud-init. Changes will be lost.\n\n" +- return header + buf.getvalue() +- +- +-class Renderer(renderer.Renderer): +- """Renders network information in a NetworkManager keyfile format.""" +- +- def __init__(self, config=None): +- self.connections = {} +- +- def get_conn(self, con_id): +- return self.connections[con_id] +- +- def con_ref(self, con_id): +- if con_id in self.connections: +- return self.connections[con_id].con_uuid() +- else: +- # Well, what can we do... +- return con_id +- +- def render_network_state(self, network_state, templates=None, target=None): +- # First pass makes sure there's NMConnections for all known +- # interfaces that have UUIDs that can be linked to from related +- # interfaces +- for iface in network_state.iter_interfaces(): +- self.connections[iface["name"]] = NMConnection(iface["name"]) +- +- # Now render the actual interface configuration +- for iface in network_state.iter_interfaces(): +- conn = self.connections[iface["name"]] +- conn.render_interface(iface, self) +- +- # And finally write the files +- for con_id, conn in self.connections.items(): +- if not conn.valid(): +- continue +- name = conn_filename(con_id, target) +- util.write_file(name, conn.dump(), 0o600) +- +- +-def conn_filename(con_id, target=None): +- target_con_dir = subp.target_path(target, NM_RUN_DIR) +- con_file = f"cloud-init-{con_id}.nmconnection" +- return f"{target_con_dir}/system-connections/{con_file}" +- +- +-def available(target=None): +- target_nm_dir = subp.target_path(target, NM_LIB_DIR) +- return os.path.exists(target_nm_dir) +- +- +-# vi: ts=4 expandtab +diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py +index 7edc34b5..c755f04c 100644 +--- a/cloudinit/net/renderers.py ++++ b/cloudinit/net/renderers.py +@@ -8,7 +8,6 @@ from . import ( + freebsd, + netbsd, + netplan, +- network_manager, + networkd, + openbsd, + renderer, +@@ -20,7 +19,6 @@ NAME_TO_RENDERER = { + "freebsd": freebsd, + "netbsd": netbsd, + "netplan": netplan, +- "network-manager": network_manager, + "networkd": networkd, + "openbsd": openbsd, + "sysconfig": sysconfig, +@@ -30,7 +28,6 @@ DEFAULT_PRIORITY = [ + "eni", + "sysconfig", + "netplan", +- "network-manager", + "freebsd", + "netbsd", + "openbsd", +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index dc401d78..d8c53312 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -5,6 +5,8 @@ import io + import os + import re + ++from configobj import ConfigObj ++ + from cloudinit import log as logging + from cloudinit import subp, util + from cloudinit.distros.parsers import networkmanager_conf, resolv_conf +@@ -64,6 +66,24 @@ def _quote_value(value): + return value + + ++def enable_ifcfg_rh(path): ++ """Add ifcfg-rh to NetworkManager.cfg plugins if main section is present""" ++ config = ConfigObj(path) ++ if "main" in config: ++ if "plugins" in config["main"]: ++ if "ifcfg-rh" in config["main"]["plugins"]: ++ return ++ else: ++ config["main"]["plugins"] = [] ++ ++ if isinstance(config["main"]["plugins"], list): ++ config["main"]["plugins"].append("ifcfg-rh") ++ else: ++ config["main"]["plugins"] = [config["main"]["plugins"], "ifcfg-rh"] ++ config.write() ++ LOG.debug("Enabled ifcfg-rh NetworkManager plugins") ++ ++ + class ConfigMap(object): + """Sysconfig like dictionary object.""" + +@@ -1011,6 +1031,8 @@ class Renderer(renderer.Renderer): + netrules_content = self._render_persistent_net(network_state) + netrules_path = subp.target_path(target, self.netrules_path) + util.write_file(netrules_path, netrules_content, file_mode) ++ if available_nm(target=target): ++ enable_ifcfg_rh(subp.target_path(target, path=NM_CFG_FILE)) + + sysconfig_path = subp.target_path(target, templates.get("control")) + # Distros configuring /etc/sysconfig/network as a file e.g. Centos +@@ -1049,9 +1071,14 @@ def _supported_vlan_names(rdev, vid): + + + def available(target=None): +- if not util.system_info()["variant"] in KNOWN_DISTROS: +- return False ++ sysconfig = available_sysconfig(target=target) ++ nm = available_nm(target=target) ++ return util.system_info()["variant"] in KNOWN_DISTROS and any( ++ [nm, sysconfig] ++ ) ++ + ++def available_sysconfig(target=None): + expected = ["ifup", "ifdown"] + search = ["/sbin", "/usr/sbin"] + for p in expected: +@@ -1068,4 +1095,10 @@ def available(target=None): + return False + + ++def available_nm(target=None): ++ if not os.path.isfile(subp.target_path(target, path=NM_CFG_FILE)): ++ return False ++ return True ++ ++ + # vi: ts=4 expandtab +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index ef21ad76..591241b3 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -21,7 +21,6 @@ from cloudinit.net import ( + interface_has_own_mac, + natural_sort_key, + netplan, +- network_manager, + network_state, + networkd, + renderers, +@@ -612,37 +611,6 @@ dns = none + ), + ), + ], +- "expected_network_manager": [ +- ( +- "".join( +- [ +- "etc/NetworkManager/system-connections", +- "/cloud-init-eth0.nmconnection", +- ] +- ), +- """ +-# Generated by cloud-init. Changes will be lost. +- +-[connection] +-id=cloud-init eth0 +-uuid=1dd9a779-d327-56e1-8454-c65e2556c12c +-type=ethernet +- +-[user] +-org.freedesktop.NetworkManager.origin=cloud-init +- +-[ethernet] +-mac-address=FA:16:3E:ED:9A:59 +- +-[ipv4] +-method=manual +-may-fail=false +-address1=172.19.1.34/22 +-route1=0.0.0.0/0,172.19.3.254 +- +-""".lstrip(), +- ), +- ], + }, + { + "in_data": { +@@ -1105,50 +1073,6 @@ NETWORK_CONFIGS = { + USERCTL=no""" + ), + }, +- "expected_network_manager": { +- "cloud-init-eth1.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth1 +- uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=CF:D6:AF:48:E8:80 +- +- """ +- ), +- "cloud-init-eth99.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth99 +- uuid=b1b88000-1f03-5360-8377-1a2205efffb4 +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=C0:D6:9F:2C:E8:80 +- +- [ipv4] +- method=auto +- may-fail=false +- address1=192.168.21.3/24 +- route1=0.0.0.0/0,65.61.151.37 +- dns=8.8.8.8;8.8.4.4; +- dns-search=barley.maas;sach.maas; +- +- """ +- ), +- }, + "yaml": textwrap.dedent( + """ + version: 1 +@@ -1221,34 +1145,6 @@ NETWORK_CONFIGS = { + STARTMODE=auto""" + ) + }, +- "expected_network_manager": { +- "cloud-init-iface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init iface0 +- uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 +- type=ethernet +- interface-name=iface0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- +- [ipv4] +- method=auto +- may-fail=false +- +- [ipv6] +- method=dhcp +- may-fail=false +- addr-gen-mode=stable-privacy +- +- """ +- ), +- }, + "yaml": textwrap.dedent( + """\ + version: 1 +@@ -1351,37 +1247,6 @@ NETWORK_CONFIGS = { + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-iface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init iface0 +- uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 +- type=ethernet +- interface-name=iface0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mtu=9000 +- +- [ipv4] +- method=manual +- may-fail=false +- address1=192.168.14.2/24 +- +- [ipv6] +- method=manual +- may-fail=false +- addr-gen-mode=stable-privacy +- address1=2001:1::1/64 +- +- """ +- ), +- }, + }, + "v6_and_v4": { + "expected_sysconfig_opensuse": { +@@ -1392,34 +1257,6 @@ NETWORK_CONFIGS = { + STARTMODE=auto""" + ) + }, +- "expected_network_manager": { +- "cloud-init-iface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init iface0 +- uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 +- type=ethernet +- interface-name=iface0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- +- [ipv6] +- method=dhcp +- may-fail=false +- addr-gen-mode=stable-privacy +- +- [ipv4] +- method=auto +- may-fail=false +- +- """ +- ), +- }, + "yaml": textwrap.dedent( + """\ + version: 1 +@@ -1493,30 +1330,6 @@ NETWORK_CONFIGS = { + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-iface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init iface0 +- uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 +- type=ethernet +- interface-name=iface0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- +- [ipv6] +- method=dhcp +- may-fail=false +- addr-gen-mode=stable-privacy +- +- """ +- ), +- }, + }, + "dhcpv6_accept_ra": { + "expected_eni": textwrap.dedent( +@@ -1724,30 +1537,6 @@ NETWORK_CONFIGS = { + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-iface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init iface0 +- uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 +- type=ethernet +- interface-name=iface0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- +- [ipv6] +- method=auto +- may-fail=false +- addr-gen-mode=stable-privacy +- +- """ +- ), +- }, + }, + "static6": { + "yaml": textwrap.dedent( +@@ -1836,30 +1625,6 @@ NETWORK_CONFIGS = { + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-iface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init iface0 +- uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 +- type=ethernet +- interface-name=iface0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- +- [ipv6] +- method=auto +- may-fail=false +- addr-gen-mode=stable-privacy +- +- """ +- ), +- }, + }, + "dhcpv6_stateful": { + "expected_eni": textwrap.dedent( +@@ -1959,29 +1724,6 @@ NETWORK_CONFIGS = { + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-iface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init iface0 +- uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 +- type=ethernet +- interface-name=iface0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- +- [ipv4] +- method=auto +- may-fail=false +- +- """ +- ), +- }, + "yaml_v2": textwrap.dedent( + """\ + version: 2 +@@ -2035,30 +1777,6 @@ NETWORK_CONFIGS = { + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-iface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init iface0 +- uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 +- type=ethernet +- interface-name=iface0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- wake-on-lan=64 +- +- [ipv4] +- method=auto +- may-fail=false +- +- """ +- ), +- }, + "yaml_v2": textwrap.dedent( + """\ + version: 2 +@@ -2497,254 +2215,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + USERCTL=no""" + ), + }, +- "expected_network_manager": { +- "cloud-init-eth3.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth3 +- uuid=b7e95dda-7746-5bf8-bf33-6e5f3c926790 +- type=ethernet +- slave-type=bridge +- master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=66:BB:9F:2C:E8:80 +- +- """ +- ), +- "cloud-init-eth5.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth5 +- uuid=5fda13c7-9942-5e90-a41b-1d043bd725dc +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=98:BB:9F:2C:E8:8A +- +- [ipv4] +- method=auto +- may-fail=false +- +- """ +- ), +- "cloud-init-ib0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init ib0 +- uuid=11a1dda7-78b4-5529-beba-d9b5f549ad7b +- type=infiniband +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [infiniband] +- transport-mode=datagram +- mtu=9000 +- mac-address=A0:00:02:20:FE:80:00:00:00:00:00:00:EC:0D:9A:03:00:15:E2:C1 +- +- [ipv4] +- method=manual +- may-fail=false +- address1=192.168.200.7/24 +- +- """ +- ), +- "cloud-init-bond0.200.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init bond0.200 +- uuid=88984a9c-ff22-5233-9267-86315e0acaa7 +- type=vlan +- interface-name=bond0.200 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [vlan] +- id=200 +- parent=54317911-f840-516b-a10d-82cb4c1f075c +- +- [ipv4] +- method=auto +- may-fail=false +- +- """ +- ), +- "cloud-init-eth0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth0 +- uuid=1dd9a779-d327-56e1-8454-c65e2556c12c +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=C0:D6:9F:2C:E8:80 +- +- """ +- ), +- "cloud-init-eth4.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth4 +- uuid=e27e4959-fb50-5580-b9a4-2073554627b9 +- type=ethernet +- slave-type=bridge +- master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=98:BB:9F:2C:E8:80 +- +- """ +- ), +- "cloud-init-eth1.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth1 +- uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 +- type=ethernet +- slave-type=bond +- master=54317911-f840-516b-a10d-82cb4c1f075c +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=AA:D6:9F:2C:E8:80 +- +- """ +- ), +- "cloud-init-br0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init br0 +- uuid=dee46ce4-af7a-5e7c-aa08-b25533ae9213 +- type=bridge +- interface-name=br0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [bridge] +- stp=false +- priority=22 +- mac-address=BB:BB:BB:BB:BB:AA +- +- [ipv4] +- method=manual +- may-fail=false +- address1=192.168.14.2/24 +- +- [ipv6] +- method=manual +- may-fail=false +- addr-gen-mode=stable-privacy +- address1=2001:1::1/64 +- route1=::/0,2001:4800:78ff:1b::1 +- +- """ +- ), +- "cloud-init-eth0.101.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth0.101 +- uuid=b5acec5e-db80-5935-8b02-0d5619fc42bf +- type=vlan +- interface-name=eth0.101 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [vlan] +- id=101 +- parent=1dd9a779-d327-56e1-8454-c65e2556c12c +- +- [ipv4] +- method=manual +- may-fail=false +- address1=192.168.0.2/24 +- gateway=192.168.0.1 +- dns=192.168.0.10;10.23.23.134; +- dns-search=barley.maas;sacchromyces.maas;brettanomyces.maas; +- address2=192.168.2.10/24 +- +- """ +- ), +- "cloud-init-bond0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init bond0 +- uuid=54317911-f840-516b-a10d-82cb4c1f075c +- type=bond +- interface-name=bond0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [bond] +- mode=active-backup +- miimon=100 +- xmit_hash_policy=layer3+4 +- +- [ipv6] +- method=dhcp +- may-fail=false +- addr-gen-mode=stable-privacy +- +- """ +- ), +- "cloud-init-eth2.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth2 +- uuid=5559a242-3421-5fdd-896e-9cb8313d5804 +- type=ethernet +- slave-type=bond +- master=54317911-f840-516b-a10d-82cb4c1f075c +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=C0:BB:9F:2C:E8:80 +- +- """ +- ), +- }, + "yaml": textwrap.dedent( + """ + version: 1 +@@ -2933,10 +2403,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + - type: static + address: 2001:1::1/92 + routes: +- - gateway: 2001:67c:1562::1 ++ - gateway: 2001:67c:1562:1 + network: 2001:67c:1 + netmask: "ffff:ffff::" +- - gateway: 3001:67c:15::1 ++ - gateway: 3001:67c:1562:1 + network: 3001:67c:1 + netmask: "ffff:ffff::" + metric: 10000 +@@ -2981,10 +2451,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + - to: 10.1.3.0/24 + via: 192.168.0.3 + - to: 2001:67c:1/32 +- via: 2001:67c:1562::1 ++ via: 2001:67c:1562:1 + - metric: 10000 + to: 3001:67c:1/32 +- via: 3001:67c:15::1 ++ via: 3001:67c:1562:1 + """ + ), + "expected_eni": textwrap.dedent( +@@ -3044,11 +2514,11 @@ iface bond0 inet static + # control-alias bond0 + iface bond0 inet6 static + address 2001:1::1/92 +- post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true +- pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true +- post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:15::1 metric 10000 \ ++ post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true ++ pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true ++ post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \ + || true +- pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:15::1 metric 10000 \ ++ pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \ + || true + """ + ), +@@ -3091,8 +2561,8 @@ iface bond0 inet6 static + - to: 2001:67c:1562:8007::1/64 + via: 2001:67c:1562:8007::aac:40b2 + - metric: 10000 +- to: 3001:67c:15:8007::1/64 +- via: 3001:67c:15:8007::aac:40b2 ++ to: 3001:67c:1562:8007::1/64 ++ via: 3001:67c:1562:8007::aac:40b2 + """ + ), + "expected_netplan-v2": textwrap.dedent( +@@ -3124,8 +2594,8 @@ iface bond0 inet6 static + - to: 2001:67c:1562:8007::1/64 + via: 2001:67c:1562:8007::aac:40b2 + - metric: 10000 +- to: 3001:67c:15:8007::1/64 +- via: 3001:67c:15:8007::aac:40b2 ++ to: 3001:67c:1562:8007::1/64 ++ via: 3001:67c:1562:8007::aac:40b2 + ethernets: + eth0: + match: +@@ -3224,8 +2694,8 @@ iface bond0 inet6 static + """\ + # Created by cloud-init on instance boot automatically, do not edit. + # +- 2001:67c:1/32 via 2001:67c:1562::1 dev bond0 +- 3001:67c:1/32 via 3001:67c:15::1 metric 10000 dev bond0 ++ 2001:67c:1/32 via 2001:67c:1562:1 dev bond0 ++ 3001:67c:1/32 via 3001:67c:1562:1 metric 10000 dev bond0 + """ + ), + "route-bond0": textwrap.dedent( +@@ -3248,88 +2718,6 @@ iface bond0 inet6 static + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-bond0s0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init bond0s0 +- uuid=09d0b5b9-67e7-5577-a1af-74d1cf17a71e +- type=ethernet +- slave-type=bond +- master=54317911-f840-516b-a10d-82cb4c1f075c +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=AA:BB:CC:DD:E8:00 +- +- """ +- ), +- "cloud-init-bond0s1.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init bond0s1 +- uuid=4d9aca96-b515-5630-ad83-d13daac7f9d0 +- type=ethernet +- slave-type=bond +- master=54317911-f840-516b-a10d-82cb4c1f075c +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=AA:BB:CC:DD:E8:01 +- +- """ +- ), +- "cloud-init-bond0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init bond0 +- uuid=54317911-f840-516b-a10d-82cb4c1f075c +- type=bond +- interface-name=bond0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [bond] +- mode=active-backup +- miimon=100 +- xmit_hash_policy=layer3+4 +- num_grat_arp=5 +- downdelay=10 +- updelay=20 +- fail_over_mac=active +- primary_reselect=always +- primary=bond0s0 +- +- [ipv4] +- method=manual +- may-fail=false +- address1=192.168.0.2/24 +- gateway=192.168.0.1 +- route1=10.1.3.0/24,192.168.0.3 +- address2=192.168.1.2/24 +- +- [ipv6] +- method=manual +- may-fail=false +- addr-gen-mode=stable-privacy +- address1=2001:1::1/92 +- route1=2001:67c:1/32,2001:67c:1562::1 +- route2=3001:67c:1/32,3001:67c:15::1 +- +- """ +- ), +- }, + }, + "vlan": { + "yaml": textwrap.dedent( +@@ -3413,58 +2801,6 @@ iface bond0 inet6 static + VLAN=yes""" + ), + }, +- "expected_network_manager": { +- "cloud-init-en0.99.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init en0.99 +- uuid=f594e2ed-f107-51df-b225-1dc530a5356b +- type=vlan +- interface-name=en0.99 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [vlan] +- id=99 +- parent=e0ca478b-8d84-52ab-8fae-628482c629b5 +- +- [ipv4] +- method=manual +- may-fail=false +- address1=192.168.2.2/24 +- address2=192.168.1.2/24 +- gateway=192.168.1.1 +- +- [ipv6] +- method=manual +- may-fail=false +- addr-gen-mode=stable-privacy +- address1=2001:1::bbbb/96 +- route1=::/0,2001:1::1 +- +- """ +- ), +- "cloud-init-en0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init en0 +- uuid=e0ca478b-8d84-52ab-8fae-628482c629b5 +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=AA:BB:CC:DD:E8:00 +- +- """ +- ), +- }, + }, + "bridge": { + "yaml": textwrap.dedent( +@@ -3573,82 +2909,6 @@ iface bond0 inet6 static + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-br0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init br0 +- uuid=dee46ce4-af7a-5e7c-aa08-b25533ae9213 +- type=bridge +- interface-name=br0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [bridge] +- stp=false +- priority=22 +- +- [ipv4] +- method=manual +- may-fail=false +- address1=192.168.2.2/24 +- +- """ +- ), +- "cloud-init-eth0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth0 +- uuid=1dd9a779-d327-56e1-8454-c65e2556c12c +- type=ethernet +- slave-type=bridge +- master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=52:54:00:12:34:00 +- +- [ipv6] +- method=manual +- may-fail=false +- addr-gen-mode=stable-privacy +- address1=2001:1::100/96 +- +- """ +- ), +- "cloud-init-eth1.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth1 +- uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 +- type=ethernet +- slave-type=bridge +- master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=52:54:00:12:34:01 +- +- [ipv6] +- method=manual +- may-fail=false +- addr-gen-mode=stable-privacy +- address1=2001:1::101/96 +- +- """ +- ), +- }, + }, + "manual": { + "yaml": textwrap.dedent( +@@ -3777,92 +3037,25 @@ iface bond0 inet6 static + """ + ), + }, +- "expected_network_manager": { +- "cloud-init-eth0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth0 +- uuid=1dd9a779-d327-56e1-8454-c65e2556c12c +- type=ethernet ++ }, ++} + +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init + +- [ethernet] +- mac-address=52:54:00:12:34:00 +- +- [ipv4] +- method=manual +- may-fail=false +- address1=192.168.1.2/24 +- +- """ +- ), +- "cloud-init-eth1.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth1 +- uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mtu=1480 +- mac-address=52:54:00:12:34:AA +- +- [ipv4] +- method=auto +- may-fail=true +- +- """ +- ), +- "cloud-init-eth2.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth2 +- uuid=5559a242-3421-5fdd-896e-9cb8313d5804 +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=52:54:00:12:34:FF +- +- [ipv4] +- method=auto +- may-fail=true +- +- """ +- ), +- }, +- }, +-} +- +- +-CONFIG_V1_EXPLICIT_LOOPBACK = { +- "version": 1, +- "config": [ +- { +- "name": "eth0", +- "type": "physical", +- "subnets": [{"control": "auto", "type": "dhcp"}], +- }, +- { +- "name": "lo", +- "type": "loopback", +- "subnets": [{"control": "auto", "type": "loopback"}], +- }, +- ], +-} ++CONFIG_V1_EXPLICIT_LOOPBACK = { ++ "version": 1, ++ "config": [ ++ { ++ "name": "eth0", ++ "type": "physical", ++ "subnets": [{"control": "auto", "type": "dhcp"}], ++ }, ++ { ++ "name": "lo", ++ "type": "loopback", ++ "subnets": [{"control": "auto", "type": "loopback"}], ++ }, ++ ], ++} + + + CONFIG_V1_SIMPLE_SUBNET = { +@@ -4304,6 +3497,7 @@ class TestRhelSysConfigRendering(CiTestCase): + + with_logs = True + ++ nm_cfg_file = "/etc/NetworkManager/NetworkManager.conf" + scripts_dir = "/etc/sysconfig/network-scripts" + header = ( + "# Created by cloud-init on instance boot automatically, " +@@ -4878,6 +4072,78 @@ USERCTL=no + self._compare_files_to_expected(entry[self.expected_name], found) + self._assert_headers(found) + ++ def test_check_ifcfg_rh(self): ++ """ifcfg-rh plugin is added NetworkManager.conf if conf present.""" ++ render_dir = self.tmp_dir() ++ nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) ++ util.ensure_dir(os.path.dirname(nm_cfg)) ++ ++ # write a template nm.conf, note plugins is a list here ++ with open(nm_cfg, "w") as fh: ++ fh.write("# test_check_ifcfg_rh\n[main]\nplugins=foo,bar\n") ++ self.assertTrue(os.path.exists(nm_cfg)) ++ ++ # render and read ++ entry = NETWORK_CONFIGS["small"] ++ found = self._render_and_read( ++ network_config=yaml.load(entry["yaml"]), dir=render_dir ++ ) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self._assert_headers(found) ++ ++ # check ifcfg-rh is in the 'plugins' list ++ config = sysconfig.ConfigObj(nm_cfg) ++ self.assertIn("ifcfg-rh", config["main"]["plugins"]) ++ ++ def test_check_ifcfg_rh_plugins_string(self): ++ """ifcfg-rh plugin is append when plugins is a string.""" ++ render_dir = self.tmp_path("render") ++ os.makedirs(render_dir) ++ nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) ++ util.ensure_dir(os.path.dirname(nm_cfg)) ++ ++ # write a template nm.conf, note plugins is a value here ++ util.write_file(nm_cfg, "# test_check_ifcfg_rh\n[main]\nplugins=foo\n") ++ ++ # render and read ++ entry = NETWORK_CONFIGS["small"] ++ found = self._render_and_read( ++ network_config=yaml.load(entry["yaml"]), dir=render_dir ++ ) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self._assert_headers(found) ++ ++ # check raw content has plugin ++ nm_file_content = util.load_file(nm_cfg) ++ self.assertIn("ifcfg-rh", nm_file_content) ++ ++ # check ifcfg-rh is in the 'plugins' list ++ config = sysconfig.ConfigObj(nm_cfg) ++ self.assertIn("ifcfg-rh", config["main"]["plugins"]) ++ ++ def test_check_ifcfg_rh_plugins_no_plugins(self): ++ """enable_ifcfg_plugin creates plugins value if missing.""" ++ render_dir = self.tmp_path("render") ++ os.makedirs(render_dir) ++ nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) ++ util.ensure_dir(os.path.dirname(nm_cfg)) ++ ++ # write a template nm.conf, note plugins is missing ++ util.write_file(nm_cfg, "# test_check_ifcfg_rh\n[main]\n") ++ self.assertTrue(os.path.exists(nm_cfg)) ++ ++ # render and read ++ entry = NETWORK_CONFIGS["small"] ++ found = self._render_and_read( ++ network_config=yaml.load(entry["yaml"]), dir=render_dir ++ ) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self._assert_headers(found) ++ ++ # check ifcfg-rh is in the 'plugins' list ++ config = sysconfig.ConfigObj(nm_cfg) ++ self.assertIn("ifcfg-rh", config["main"]["plugins"]) ++ + def test_netplan_dhcp_false_disable_dhcp_in_state(self): + """netplan config with dhcp[46]: False should not add dhcp in state""" + net_config = yaml.load(NETPLAN_DHCP_FALSE) +@@ -5433,281 +4699,6 @@ STARTMODE=auto + self._assert_headers(found) + + +-@mock.patch( +- "cloudinit.net.is_openvswitch_internal_interface", +- mock.Mock(return_value=False), +-) +-class TestNetworkManagerRendering(CiTestCase): +- +- with_logs = True +- +- scripts_dir = "/etc/NetworkManager/system-connections" +- +- expected_name = "expected_network_manager" +- +- def _get_renderer(self): +- return network_manager.Renderer() +- +- def _render_and_read(self, network_config=None, state=None, dir=None): +- if dir is None: +- dir = self.tmp_dir() +- +- if network_config: +- ns = network_state.parse_net_config_data(network_config) +- elif state: +- ns = state +- else: +- raise ValueError("Expected data or state, got neither") +- +- renderer = self._get_renderer() +- renderer.render_network_state(ns, target=dir) +- return dir2dict(dir) +- +- def _compare_files_to_expected(self, expected, found): +- orig_maxdiff = self.maxDiff +- expected_d = dict( +- (os.path.join(self.scripts_dir, k), v) for k, v in expected.items() +- ) +- +- try: +- self.maxDiff = None +- self.assertEqual(expected_d, found) +- finally: +- self.maxDiff = orig_maxdiff +- +- @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot") +- @mock.patch("cloudinit.net.sys_dev_path") +- @mock.patch("cloudinit.net.read_sys_net") +- @mock.patch("cloudinit.net.get_devicelist") +- def test_default_generation( +- self, +- mock_get_devicelist, +- mock_read_sys_net, +- mock_sys_dev_path, +- m_get_cmdline, +- ): +- tmp_dir = self.tmp_dir() +- _setup_test( +- tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path +- ) +- +- network_cfg = net.generate_fallback_config() +- ns = network_state.parse_net_config_data( +- network_cfg, skip_broken=False +- ) +- +- render_dir = os.path.join(tmp_dir, "render") +- os.makedirs(render_dir) +- +- renderer = self._get_renderer() +- renderer.render_network_state(ns, target=render_dir) +- +- found = dir2dict(render_dir) +- self._compare_files_to_expected( +- { +- "cloud-init-eth1000.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth1000 +- uuid=8c517500-0c95-5308-9c8a-3092eebc44eb +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=07:1C:C6:75:A4:BE +- +- [ipv4] +- method=auto +- may-fail=false +- +- """ +- ), +- }, +- found, +- ) +- +- def test_openstack_rendering_samples(self): +- for os_sample in OS_SAMPLES: +- render_dir = self.tmp_dir() +- ex_input = os_sample["in_data"] +- ex_mac_addrs = os_sample["in_macs"] +- network_cfg = openstack.convert_net_json( +- ex_input, known_macs=ex_mac_addrs +- ) +- ns = network_state.parse_net_config_data( +- network_cfg, skip_broken=False +- ) +- renderer = self._get_renderer() +- # render a multiple times to simulate reboots +- renderer.render_network_state(ns, target=render_dir) +- renderer.render_network_state(ns, target=render_dir) +- renderer.render_network_state(ns, target=render_dir) +- for fn, expected_content in os_sample.get(self.expected_name, []): +- with open(os.path.join(render_dir, fn)) as fh: +- self.assertEqual(expected_content, fh.read()) +- +- def test_network_config_v1_samples(self): +- ns = network_state.parse_net_config_data(CONFIG_V1_SIMPLE_SUBNET) +- render_dir = self.tmp_path("render") +- os.makedirs(render_dir) +- renderer = self._get_renderer() +- renderer.render_network_state(ns, target=render_dir) +- found = dir2dict(render_dir) +- self._compare_files_to_expected( +- { +- "cloud-init-interface0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init interface0 +- uuid=8b6862ed-dbd6-5830-93f7-a91451c13828 +- type=ethernet +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- mac-address=52:54:00:12:34:00 +- +- [ipv4] +- method=manual +- may-fail=false +- address1=10.0.2.15/24 +- gateway=10.0.2.2 +- +- """ +- ), +- }, +- found, +- ) +- +- def test_config_with_explicit_loopback(self): +- render_dir = self.tmp_path("render") +- os.makedirs(render_dir) +- ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK) +- renderer = self._get_renderer() +- renderer.render_network_state(ns, target=render_dir) +- found = dir2dict(render_dir) +- self._compare_files_to_expected( +- { +- "cloud-init-eth0.nmconnection": textwrap.dedent( +- """\ +- # Generated by cloud-init. Changes will be lost. +- +- [connection] +- id=cloud-init eth0 +- uuid=1dd9a779-d327-56e1-8454-c65e2556c12c +- type=ethernet +- interface-name=eth0 +- +- [user] +- org.freedesktop.NetworkManager.origin=cloud-init +- +- [ethernet] +- +- [ipv4] +- method=auto +- may-fail=false +- +- """ +- ), +- }, +- found, +- ) +- +- def test_bond_config(self): +- entry = NETWORK_CONFIGS["bond"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_vlan_config(self): +- entry = NETWORK_CONFIGS["vlan"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_bridge_config(self): +- entry = NETWORK_CONFIGS["bridge"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_manual_config(self): +- entry = NETWORK_CONFIGS["manual"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_all_config(self): +- entry = NETWORK_CONFIGS["all"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- self.assertNotIn( +- "WARNING: Network config: ignoring eth0.101 device-level mtu", +- self.logs.getvalue(), +- ) +- +- def test_small_config(self): +- entry = NETWORK_CONFIGS["small"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_v4_and_v6_static_config(self): +- entry = NETWORK_CONFIGS["v4_and_v6_static"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- expected_msg = ( +- "WARNING: Network config: ignoring iface0 device-level mtu:8999" +- " because ipv4 subnet-level mtu:9000 provided." +- ) +- self.assertIn(expected_msg, self.logs.getvalue()) +- +- def test_dhcpv6_only_config(self): +- entry = NETWORK_CONFIGS["dhcpv6_only"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_simple_render_ipv6_slaac(self): +- entry = NETWORK_CONFIGS["ipv6_slaac"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_dhcpv6_stateless_config(self): +- entry = NETWORK_CONFIGS["dhcpv6_stateless"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_wakeonlan_disabled_config_v2(self): +- entry = NETWORK_CONFIGS["wakeonlan_disabled"] +- found = self._render_and_read( +- network_config=yaml.load(entry["yaml_v2"]) +- ) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_wakeonlan_enabled_config_v2(self): +- entry = NETWORK_CONFIGS["wakeonlan_enabled"] +- found = self._render_and_read( +- network_config=yaml.load(entry["yaml_v2"]) +- ) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_render_v4_and_v6(self): +- entry = NETWORK_CONFIGS["v4_and_v6"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- def test_render_v6_and_v4(self): +- entry = NETWORK_CONFIGS["v6_and_v4"] +- found = self._render_and_read(network_config=yaml.load(entry["yaml"])) +- self._compare_files_to_expected(entry[self.expected_name], found) +- +- +-@mock.patch( +- "cloudinit.net.is_openvswitch_internal_interface", +- mock.Mock(return_value=False), +-) + class TestEniNetRendering(CiTestCase): + @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot") + @mock.patch("cloudinit.net.sys_dev_path") +@@ -7145,9 +6136,9 @@ class TestNetworkdRoundTrip(CiTestCase): + + class TestRenderersSelect: + @pytest.mark.parametrize( +- "renderer_selected,netplan,eni,sys,network_manager,networkd", ++ "renderer_selected,netplan,eni,nm,scfg,sys,networkd", + ( +- # -netplan -ifupdown -sys -network-manager -networkd raises error ++ # -netplan -ifupdown -nm -scfg -sys raises error + ( + net.RendererNotFoundError, + False, +@@ -7155,51 +6146,52 @@ class TestRenderersSelect: + False, + False, + False, ++ False, + ), +- # -netplan +ifupdown -sys -nm -networkd selects eni +- ("eni", False, True, False, False, False), +- # +netplan +ifupdown -sys -nm -networkd selects eni +- ("eni", True, True, False, False, False), +- # +netplan -ifupdown -sys -nm -networkd selects netplan +- ("netplan", True, False, False, False, False), +- # +netplan -ifupdown -sys -nm -networkd selects netplan +- ("netplan", True, False, False, False, False), +- # -netplan -ifupdown +sys -nm -networkd selects sysconfig +- ("sysconfig", False, False, True, False, False), +- # -netplan -ifupdown +sys +nm -networkd selects sysconfig +- ("sysconfig", False, False, True, True, False), +- # -netplan -ifupdown -sys +nm -networkd selects nm +- ("network-manager", False, False, False, True, False), +- # -netplan -ifupdown -sys +nm +networkd selects nm +- ("network-manager", False, False, False, True, True), +- # -netplan -ifupdown -sys -nm +networkd selects networkd +- ("networkd", False, False, False, False, True), ++ # -netplan +ifupdown -nm -scfg -sys selects eni ++ ("eni", False, True, False, False, False, False), ++ # +netplan +ifupdown -nm -scfg -sys selects eni ++ ("eni", True, True, False, False, False, False), ++ # +netplan -ifupdown -nm -scfg -sys selects netplan ++ ("netplan", True, False, False, False, False, False), ++ # Ubuntu with Network-Manager installed ++ # +netplan -ifupdown +nm -scfg -sys selects netplan ++ ("netplan", True, False, True, False, False, False), ++ # Centos/OpenSuse with Network-Manager installed selects sysconfig ++ # -netplan -ifupdown +nm -scfg +sys selects netplan ++ ("sysconfig", False, False, True, False, True, False), ++ # -netplan -ifupdown -nm -scfg -sys +networkd selects networkd ++ ("networkd", False, False, False, False, False, True), + ), + ) + @mock.patch("cloudinit.net.renderers.networkd.available") +- @mock.patch("cloudinit.net.renderers.network_manager.available") + @mock.patch("cloudinit.net.renderers.netplan.available") + @mock.patch("cloudinit.net.renderers.sysconfig.available") ++ @mock.patch("cloudinit.net.renderers.sysconfig.available_sysconfig") ++ @mock.patch("cloudinit.net.renderers.sysconfig.available_nm") + @mock.patch("cloudinit.net.renderers.eni.available") + def test_valid_renderer_from_defaults_depending_on_availability( + self, + m_eni_avail, ++ m_nm_avail, ++ m_scfg_avail, + m_sys_avail, + m_netplan_avail, +- m_network_manager_avail, + m_networkd_avail, + renderer_selected, + netplan, + eni, ++ nm, ++ scfg, + sys, +- network_manager, + networkd, + ): + """Assert proper renderer per DEFAULT_PRIORITY given availability.""" + m_eni_avail.return_value = eni # ifupdown pkg presence ++ m_nm_avail.return_value = nm # network-manager presence ++ m_scfg_avail.return_value = scfg # sysconfig presence + m_sys_avail.return_value = sys # sysconfig/ifup/down presence + m_netplan_avail.return_value = netplan # netplan presence +- m_network_manager_avail.return_value = network_manager # NM presence + m_networkd_avail.return_value = networkd # networkd presence + if isinstance(renderer_selected, str): + (renderer_name, _rnd_class) = renderers.select( +@@ -7257,7 +6249,7 @@ class TestNetRenderers(CiTestCase): + priority=["sysconfig", "eni"], + ) + +- @mock.patch("cloudinit.net.sysconfig.available") ++ @mock.patch("cloudinit.net.sysconfig.available_sysconfig") + @mock.patch("cloudinit.util.system_info") + def test_sysconfig_available_uses_variant_mapping(self, m_info, m_avail): + m_avail.return_value = True +diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py +index 4525c49c..3c29e2f7 100644 +--- a/tests/unittests/test_net_activators.py ++++ b/tests/unittests/test_net_activators.py +@@ -41,20 +41,18 @@ NETPLAN_CALL_LIST = [ + + @pytest.fixture + def available_mocks(): +- mocks = namedtuple("Mocks", "m_which, m_file, m_exists") ++ mocks = namedtuple("Mocks", "m_which, m_file") + with patch("cloudinit.subp.which", return_value=True) as m_which: + with patch("os.path.isfile", return_value=True) as m_file: +- with patch("os.path.exists", return_value=True) as m_exists: +- yield mocks(m_which, m_file, m_exists) ++ yield mocks(m_which, m_file) + + + @pytest.fixture + def unavailable_mocks(): +- mocks = namedtuple("Mocks", "m_which, m_file, m_exists") ++ mocks = namedtuple("Mocks", "m_which, m_file") + with patch("cloudinit.subp.which", return_value=False) as m_which: + with patch("os.path.isfile", return_value=False) as m_file: +- with patch("os.path.exists", return_value=False) as m_exists: +- yield mocks(m_which, m_file, m_exists) ++ yield mocks(m_which, m_file) + + + class TestSearchAndSelect: +@@ -115,6 +113,10 @@ NETPLAN_AVAILABLE_CALLS = [ + (("netplan",), {"search": ["/usr/sbin", "/sbin"], "target": None}), + ] + ++NETWORK_MANAGER_AVAILABLE_CALLS = [ ++ (("nmcli",), {"target": None}), ++] ++ + NETWORKD_AVAILABLE_CALLS = [ + (("ip",), {"search": ["/usr/sbin", "/bin"], "target": None}), + (("systemctl",), {"search": ["/usr/sbin", "/bin"], "target": None}), +@@ -126,6 +128,7 @@ NETWORKD_AVAILABLE_CALLS = [ + [ + (IfUpDownActivator, IF_UP_DOWN_AVAILABLE_CALLS), + (NetplanActivator, NETPLAN_AVAILABLE_CALLS), ++ (NetworkManagerActivator, NETWORK_MANAGER_AVAILABLE_CALLS), + (NetworkdActivator, NETWORKD_AVAILABLE_CALLS), + ], + ) +@@ -141,72 +144,8 @@ IF_UP_DOWN_BRING_UP_CALL_LIST = [ + ] + + NETWORK_MANAGER_BRING_UP_CALL_LIST = [ +- ( +- ( +- [ +- "nmcli", +- "connection", +- "load", +- "".join( +- [ +- "/etc/NetworkManager/system-connections", +- "/cloud-init-eth0.nmconnection", +- ] +- ), +- ], +- ), +- {}, +- ), +- ( +- ( +- [ +- "nmcli", +- "connection", +- "up", +- "filename", +- "".join( +- [ +- "/etc/NetworkManager/system-connections", +- "/cloud-init-eth0.nmconnection", +- ] +- ), +- ], +- ), +- {}, +- ), +- ( +- ( +- [ +- "nmcli", +- "connection", +- "load", +- "".join( +- [ +- "/etc/NetworkManager/system-connections", +- "/cloud-init-eth1.nmconnection", +- ] +- ), +- ], +- ), +- {}, +- ), +- ( +- ( +- [ +- "nmcli", +- "connection", +- "up", +- "filename", +- "".join( +- [ +- "/etc/NetworkManager/system-connections", +- "/cloud-init-eth1.nmconnection", +- ] +- ), +- ], +- ), +- {}, +- ), ++ ((["nmcli", "connection", "up", "ifname", "eth0"],), {}), ++ ((["nmcli", "connection", "up", "ifname", "eth1"],), {}), + ] + + NETWORKD_BRING_UP_CALL_LIST = [ +@@ -230,11 +169,9 @@ class TestActivatorsBringUp: + def test_bring_up_interface( + self, m_subp, activator, expected_call_list, available_mocks + ): +- index = 0 + activator.bring_up_interface("eth0") +- for call in m_subp.call_args_list: +- assert call == expected_call_list[index] +- index += 1 ++ assert len(m_subp.call_args_list) == 1 ++ assert m_subp.call_args_list[0] == expected_call_list[0] + + @patch("cloudinit.subp.subp", return_value=("", "")) + def test_bring_up_interfaces( +@@ -271,8 +208,8 @@ IF_UP_DOWN_BRING_DOWN_CALL_LIST = [ + ] + + NETWORK_MANAGER_BRING_DOWN_CALL_LIST = [ +- ((["nmcli", "device", "disconnect", "eth0"],), {}), +- ((["nmcli", "device", "disconnect", "eth1"],), {}), ++ ((["nmcli", "connection", "down", "eth0"],), {}), ++ ((["nmcli", "connection", "down", "eth1"],), {}), + ] + + NETWORKD_BRING_DOWN_CALL_LIST = [ +-- +2.31.1 + diff --git a/SOURCES/ci-Revert-Revert-Setting-highest-autoconnect-priority-f.patch b/SOURCES/ci-Revert-Revert-Setting-highest-autoconnect-priority-f.patch new file mode 100644 index 0000000..d919da9 --- /dev/null +++ b/SOURCES/ci-Revert-Revert-Setting-highest-autoconnect-priority-f.patch @@ -0,0 +1,37 @@ +From c843f5cf7152846da3422185a6ad344dd47604e6 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 8 Aug 2022 10:02:48 +0200 +Subject: [PATCH 3/3] Revert "Revert "Setting highest autoconnect priority for + network-scripts"" + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 31: Revert "Revert "Setting highest autoconnect priority for network-scripts"" +RH-Commit: [3/3] 09b83fb559675a2fcd4fc5394b251f034b92c1bc (eesposit/cloud-init-centos-) +RH-Bugzilla: 2107463 2104389 2117532 2098501 +RH-Acked-by: Eduardo Otubo +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Mohamed Gamal Morsy + +Reverting NM means we also need to restore this patch +This reverts commit 0eba5c6194017ef493a735cb24757c57d8af7b59. + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/net/sysconfig.py | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index d8c53312..b50035b5 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -337,6 +337,7 @@ class Renderer(renderer.Renderer): + "ONBOOT": True, + "USERCTL": False, + "BOOTPROTO": "none", ++ "AUTOCONNECT_PRIORITY": 999 + }, + "suse": {"BOOTPROTO": "static", "STARTMODE": "auto"}, + } +-- +2.31.1 + diff --git a/SOURCES/ci-Revert-Setting-highest-autoconnect-priority-for-netw.patch b/SOURCES/ci-Revert-Setting-highest-autoconnect-priority-for-netw.patch new file mode 100644 index 0000000..bd183a4 --- /dev/null +++ b/SOURCES/ci-Revert-Setting-highest-autoconnect-priority-for-netw.patch @@ -0,0 +1,37 @@ +From e5e2ba9e21ac40ec7b8fe1c1dcf501eaeb9f66bf Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 2 May 2022 14:43:17 +0200 +Subject: [PATCH 3/5] Revert "Setting highest autoconnect priority for + network-scripts" + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 24: Add native NetworkManager support (#1224) +RH-Commit: [3/3] 06c87581fc42b6827ad9df600f3dd76d51ced0d0 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2056964 +RH-Acked-by: Miroslav Rezanina +RH-Acked-by: Vitaly Kuznetsov + +This reverts commit 0ef0c86350aa5f800f8393a432f431e2818c8938, +since it was a temporary downstream-only patch while we waited for +"Add native NetworkManager support (#1224)". + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/net/sysconfig.py | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index 2a45a4fa..dc401d78 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -317,7 +317,6 @@ class Renderer(renderer.Renderer): + "ONBOOT": True, + "USERCTL": False, + "BOOTPROTO": "none", +- "AUTOCONNECT_PRIORITY": 999 + }, + "suse": {"BOOTPROTO": "static", "STARTMODE": "auto"}, + } +-- +2.31.1 + diff --git a/SOURCES/ci-Revert-Use-Network-Manager-and-Netplan-as-default-re.patch b/SOURCES/ci-Revert-Use-Network-Manager-and-Netplan-as-default-re.patch new file mode 100644 index 0000000..28c4151 --- /dev/null +++ b/SOURCES/ci-Revert-Use-Network-Manager-and-Netplan-as-default-re.patch @@ -0,0 +1,75 @@ +From c063021168dee7937281decd8f9b601f49a7d0f3 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 8 Aug 2022 10:02:17 +0200 +Subject: [PATCH 2/3] Revert "Use Network-Manager and Netplan as default + renderers for RHEL and Fedora (#1465)" + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 31: Revert "Revert "Setting highest autoconnect priority for network-scripts"" +RH-Commit: [2/3] bd662b768dc694c748cab9e36bc5ff0eb009e128 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2107463 2104389 2117532 2098501 +RH-Acked-by: Eduardo Otubo +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Mohamed Gamal Morsy + +As NM is reverted, remove also documentation and any trace of it. +This reverts commit bbd9f47a7988e15a2823b065cd539d7c9562d77e. + +Signed-off-by: Emanuele Giuseppe Esposito +--- + config/cloud.cfg.tmpl | 3 --- + doc/rtd/topics/network-config.rst | 12 +----------- + 2 files changed, 1 insertion(+), 14 deletions(-) + +diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl +index f4d2fd14..80ab4f96 100644 +--- a/config/cloud.cfg.tmpl ++++ b/config/cloud.cfg.tmpl +@@ -353,7 +353,4 @@ system_info: + {% elif variant in ["dragonfly"] %} + network: + renderers: ['freebsd'] +-{% elif variant in ["rhel", "fedora"] %} +- network: +- renderers: ['netplan', 'network-manager', 'networkd', 'sysconfig', 'eni'] + {% endif %} +diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst +index f503caab..c461a3fe 100644 +--- a/doc/rtd/topics/network-config.rst ++++ b/doc/rtd/topics/network-config.rst +@@ -188,15 +188,6 @@ generated configuration into an internal network configuration state. From + this state `Cloud-init`_ delegates rendering of the configuration to Distro + supported formats. The following ``renderers`` are supported in cloud-init: + +-- **NetworkManager** +- +-`NetworkManager `_ is the standard Linux network +-configuration tool suite. It supports a wide range of networking setups. +-Configuration is typically stored in ``/etc/NetworkManager``. +- +-It is the default for a number of Linux distributions, notably Fedora; +-CentOS/RHEL; and derivatives. +- + - **ENI** + + /etc/network/interfaces or ``ENI`` is supported by the ``ifupdown`` package +@@ -224,7 +215,6 @@ is as follows: + - ENI + - Sysconfig + - Netplan +-- NetworkManager + + When applying the policy, `Cloud-init`_ checks if the current instance has the + correct binaries and paths to support the renderer. The first renderer that +@@ -233,7 +223,7 @@ supplying an updated configuration in cloud-config. :: + + system_info: + network: +- renderers: ['netplan', 'network-manager', 'eni', 'sysconfig', 'freebsd', 'netbsd', 'openbsd'] ++ renderers: ['netplan', 'eni', 'sysconfig', 'freebsd', 'netbsd', 'openbsd'] + + + Network Configuration Tools +-- +2.31.1 + diff --git a/SOURCES/ci-Support-EC2-tags-in-instance-metadata-1309.patch b/SOURCES/ci-Support-EC2-tags-in-instance-metadata-1309.patch new file mode 100644 index 0000000..7e2c490 --- /dev/null +++ b/SOURCES/ci-Support-EC2-tags-in-instance-metadata-1309.patch @@ -0,0 +1,165 @@ +From f5e9ed6c698eddd30e8e97d6f71070e7b75b1381 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 30 May 2022 16:45:08 +0200 +Subject: [PATCH 1/2] Support EC2 tags in instance metadata (#1309) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 27: Support EC2 tags in instance metadata (#1309) +RH-Commit: [1/1] f6a03e1619316959d3cd1806981b0bebf12bd3b0 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2091640 +RH-Acked-by: Eduardo Otubo +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Mohamed Gamal Morsy + +commit 40c52ce1f4049449b04f93226721f63af874c5c7 +Author: Eduardo Dobay +Date: Wed Apr 6 01:28:01 2022 -0300 + + Support EC2 tags in instance metadata (#1309) + + Add support for newer EC2 metadata versions (up to 2021-03-23), so that + tags can be retrieved from the `ds.meta_data.tags` field, as well as + with any new fields that might have been added since the 2018-09-24 + version. + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/sources/DataSourceEc2.py | 5 +++-- + doc/rtd/topics/datasources/ec2.rst | 28 ++++++++++++++++++++++------ + tests/unittests/sources/test_ec2.py | 26 +++++++++++++++++++++++++- + tools/.github-cla-signers | 1 + + 4 files changed, 51 insertions(+), 9 deletions(-) + +diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py +index 03b3870c..a030b498 100644 +--- a/cloudinit/sources/DataSourceEc2.py ++++ b/cloudinit/sources/DataSourceEc2.py +@@ -61,8 +61,9 @@ class DataSourceEc2(sources.DataSource): + min_metadata_version = "2009-04-04" + + # Priority ordered list of additional metadata versions which will be tried +- # for extended metadata content. IPv6 support comes in 2016-09-02 +- extended_metadata_versions = ["2018-09-24", "2016-09-02"] ++ # for extended metadata content. IPv6 support comes in 2016-09-02. ++ # Tags support comes in 2021-03-23. ++ extended_metadata_versions = ["2021-03-23", "2018-09-24", "2016-09-02"] + + # Setup read_url parameters per get_url_params. + url_max_wait = 120 +diff --git a/doc/rtd/topics/datasources/ec2.rst b/doc/rtd/topics/datasources/ec2.rst +index 94e4158d..77232269 100644 +--- a/doc/rtd/topics/datasources/ec2.rst ++++ b/doc/rtd/topics/datasources/ec2.rst +@@ -38,11 +38,26 @@ Userdata is accessible via the following URL: + GET http://169.254.169.254/2009-04-04/user-data + 1234,fred,reboot,true | 4512,jimbo, | 173,,, + +-Note that there are multiple versions of this data provided, cloud-init +-by default uses **2009-04-04** but newer versions can be supported with +-relative ease (newer versions have more data exposed, while maintaining +-backward compatibility with the previous versions). +-Version **2016-09-02** is required for secondary IP address support. ++Note that there are multiple EC2 Metadata versions of this data provided ++to instances. cloud-init will attempt to use the most recent API version it ++supports in order to get latest API features and instance-data. If a given ++API version is not exposed to the instance, those API features will be ++unavailable to the instance. ++ ++ +++----------------+----------------------------------------------------------+ +++ EC2 version | supported instance-data/feature | +++================+==========================================================+ +++ **2021-03-23** | Required for Instance tag support. This feature must be | ++| | enabled individually on each instance. See the | ++| | `EC2 tags user guide`_. | +++----------------+----------------------------------------------------------+ ++| **2016-09-02** | Required for secondary IP address support. | +++----------------+----------------------------------------------------------+ ++| **2009-04-04** | Minimum supports EC2 API version for meta-data and | ++| | user-data. | +++----------------+----------------------------------------------------------+ ++ + + To see which versions are supported from your cloud provider use the following + URL: +@@ -71,7 +86,7 @@ configuration (in `/etc/cloud/cloud.cfg` or `/etc/cloud/cloud.cfg.d/`). + + The settings that may be configured are: + +- * **metadata_urls**: This list of urls will be searched for an Ec2 ++ * **metadata_urls**: This list of urls will be searched for an EC2 + metadata service. The first entry that successfully returns a 200 response + for //meta-data/instance-id will be selected. + (default: ['http://169.254.169.254', 'http://instance-data:8773']). +@@ -121,4 +136,5 @@ Notes + For example: the primary NIC will have a DHCP route-metric of 100, + the next NIC will be 200. + ++.. _EC2 tags user guide: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS + .. vi: textwidth=79 +diff --git a/tests/unittests/sources/test_ec2.py b/tests/unittests/sources/test_ec2.py +index b376660d..7c8a5ea5 100644 +--- a/tests/unittests/sources/test_ec2.py ++++ b/tests/unittests/sources/test_ec2.py +@@ -210,6 +210,17 @@ SECONDARY_IP_METADATA_2018_09_24 = { + + M_PATH_NET = "cloudinit.sources.DataSourceEc2.net." + ++TAGS_METADATA_2021_03_23 = { ++ **DEFAULT_METADATA, ++ "tags": { ++ "instance": { ++ "Environment": "production", ++ "Application": "test", ++ "TagWithoutValue": "", ++ } ++ }, ++} ++ + + def _register_ssh_keys(rfunc, base_url, keys_data): + """handle ssh key inconsistencies. +@@ -670,7 +681,7 @@ class TestEc2(test_helpers.HttprettyTestCase): + logs_with_redacted = [log for log in all_logs if REDACT_TOK in log] + logs_with_token = [log for log in all_logs if "API-TOKEN" in log] + self.assertEqual(1, len(logs_with_redacted_ttl)) +- self.assertEqual(81, len(logs_with_redacted)) ++ self.assertEqual(83, len(logs_with_redacted)) + self.assertEqual(0, len(logs_with_token)) + + @mock.patch("cloudinit.net.dhcp.maybe_perform_dhcp_discovery") +@@ -811,6 +822,19 @@ class TestEc2(test_helpers.HttprettyTestCase): + ) + self.assertIn("Crawl of metadata service took", self.logs.getvalue()) + ++ def test_get_instance_tags(self): ++ ds = self._setup_ds( ++ platform_data=self.valid_platform_data, ++ sys_cfg={"datasource": {"Ec2": {"strict_id": False}}}, ++ md={"md": TAGS_METADATA_2021_03_23}, ++ ) ++ self.assertTrue(ds.get_data()) ++ self.assertIn("tags", ds.metadata) ++ self.assertIn("instance", ds.metadata["tags"]) ++ instance_tags = ds.metadata["tags"]["instance"] ++ self.assertEqual(instance_tags["Application"], "test") ++ self.assertEqual(instance_tags["Environment"], "production") ++ + + class TestGetSecondaryAddresses(test_helpers.CiTestCase): + +diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers +index ac157a2f..9f71ea0c 100644 +--- a/tools/.github-cla-signers ++++ b/tools/.github-cla-signers +@@ -26,6 +26,7 @@ dermotbradley + dhensby + eandersson + eb3095 ++edudobay + emmanuelthome + eslerm + esposem +-- +2.31.1 + diff --git a/SOURCES/ci-Use-Network-Manager-and-Netplan-as-default-renderers.patch b/SOURCES/ci-Use-Network-Manager-and-Netplan-as-default-renderers.patch new file mode 100644 index 0000000..553d8fc --- /dev/null +++ b/SOURCES/ci-Use-Network-Manager-and-Netplan-as-default-renderers.patch @@ -0,0 +1,109 @@ +From 840d6f7ca86fe9822c613f0f2c21f136271ce3b6 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Thu, 19 May 2022 15:44:03 +0200 +Subject: [PATCH 2/5] Use Network-Manager and Netplan as default renderers for + RHEL and Fedora (#1465) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 24: Add native NetworkManager support (#1224) +RH-Commit: [2/3] e33081b15a8558967bb480ed659116e7e0872840 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2056964 +RH-Acked-by: Miroslav Rezanina +RH-Acked-by: Vitaly Kuznetsov + +commit 7703aa98b89c8daba207c28a0422268ead10019a +Author: Emanuele Giuseppe Esposito +Date: Thu May 19 15:05:01 2022 +0200 + + Use Network-Manager and Netplan as default renderers for RHEL and Fedora (#1465) + + This is adapted from Neal Gompa's PR: + https://github.com/canonical/cloud-init/pull/1435 + + The only difference is that we are not modifying renderers.py (thus + modifying the priority of all distros), but just tweaking cloud.cfg to + apply this change to Fedora and RHEL. Other distros can optionally + add themselves afterwards. + + net: Prefer Netplan and NetworkManager renderers by default + + NetworkManager is used by default on a variety of Linux distributions, + and exists as a cross-distribution network management service. + + Additionally, add information about the NetworkManager renderer to + the cloud-init documentation. + + Because Netplan can be explicitly used to manage NetworkManager, + it needs to be preferred before NetworkManager. + + This change is a follow-up to #1224, which added the native + NetworkManager renderer. + This patch has been deployed on Fedora's cloud-init package throughout + the development of Fedora Linux 36 to verify that it works. + + This should also make it tremendously easier for Linux distributions + to use cloud-init because now a standard configuration is supported + by default. + + Signed-off-by: Neal Gompa + + Signed-off-by: Emanuele Giuseppe Esposito + +Signed-off-by: Emanuele Giuseppe Esposito +--- + config/cloud.cfg.tmpl | 3 +++ + doc/rtd/topics/network-config.rst | 12 +++++++++++- + 2 files changed, 14 insertions(+), 1 deletion(-) + +diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl +index fb4b456c..86beee3c 100644 +--- a/config/cloud.cfg.tmpl ++++ b/config/cloud.cfg.tmpl +@@ -330,4 +330,7 @@ system_info: + {% elif variant in ["dragonfly"] %} + network: + renderers: ['freebsd'] ++{% elif variant in ["rhel", "fedora"] %} ++ network: ++ renderers: ['netplan', 'network-manager', 'networkd', 'sysconfig', 'eni'] + {% endif %} +diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst +index c461a3fe..f503caab 100644 +--- a/doc/rtd/topics/network-config.rst ++++ b/doc/rtd/topics/network-config.rst +@@ -188,6 +188,15 @@ generated configuration into an internal network configuration state. From + this state `Cloud-init`_ delegates rendering of the configuration to Distro + supported formats. The following ``renderers`` are supported in cloud-init: + ++- **NetworkManager** ++ ++`NetworkManager `_ is the standard Linux network ++configuration tool suite. It supports a wide range of networking setups. ++Configuration is typically stored in ``/etc/NetworkManager``. ++ ++It is the default for a number of Linux distributions, notably Fedora; ++CentOS/RHEL; and derivatives. ++ + - **ENI** + + /etc/network/interfaces or ``ENI`` is supported by the ``ifupdown`` package +@@ -215,6 +224,7 @@ is as follows: + - ENI + - Sysconfig + - Netplan ++- NetworkManager + + When applying the policy, `Cloud-init`_ checks if the current instance has the + correct binaries and paths to support the renderer. The first renderer that +@@ -223,7 +233,7 @@ supplying an updated configuration in cloud-config. :: + + system_info: + network: +- renderers: ['netplan', 'eni', 'sysconfig', 'freebsd', 'netbsd', 'openbsd'] ++ renderers: ['netplan', 'network-manager', 'eni', 'sysconfig', 'freebsd', 'netbsd', 'openbsd'] + + + Network Configuration Tools +-- +2.31.1 + diff --git a/SOURCES/ci-cc_set_hostname-do-not-write-localhost-when-no-hostn.patch b/SOURCES/ci-cc_set_hostname-do-not-write-localhost-when-no-hostn.patch new file mode 100644 index 0000000..a307e68 --- /dev/null +++ b/SOURCES/ci-cc_set_hostname-do-not-write-localhost-when-no-hostn.patch @@ -0,0 +1,801 @@ +From d1790e6462e509e3cd87fc449df84fbd02ca1d89 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Thu, 2 Jun 2022 16:03:43 +0200 +Subject: [PATCH 2/2] cc_set_hostname: do not write "localhost" when no + hostname is given (#1453) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 28: cc_set_hostname: do not write "localhost" when no hostname is given (#1453) +RH-Commit: [1/1] 4370e9149371dc89be82cb05d30d33e4d2638cec (eesposit/cloud-init-centos-) +RH-Bugzilla: 1980403 +RH-Acked-by: Miroslav Rezanina +RH-Acked-by: Mohamed Gamal Morsy + +commit 74e43496f353db52e15d96abeb54ad63baac5be9 +Author: Emanuele Giuseppe Esposito +Date: Tue May 31 16:03:44 2022 +0200 + + cc_set_hostname: do not write "localhost" when no hostname is given (#1453) + + Systemd used to sometimes ignore localhost in /etc/hostnames, and many programs + like cloud-init used this as a workaround to set a default hostname. + + From https://github.com/systemd/systemd/commit/d39079fcaa05e23540d2b1f0270fa31c22a7e9f1: + + We would sometimes ignore localhost-style names in /etc/hostname. That is + brittle. If the user configured some hostname, it's most likely because they + want to use that as the hostname. If they don't want to use such a hostname, + they should just not create the config. Everything becomes simples if we just + use the configured hostname as-is. + + This behaviour seems to have been a workaround for Anaconda installer and other + tools writing out /etc/hostname with the default of "localhost.localdomain". + Anaconda PR to stop doing that: rhinstaller/anaconda#3040. + That might have been useful as a work-around for other programs misbehaving if + /etc/hostname was not present, but nowadays it's not useful because systemd + mostly controls the hostname and it is perfectly happy without that file. + + Apart from making things simpler, this allows users to set a hostname like + "localhost" and have it honoured, if such a whim strikes them. + + As also suggested by the Anaconda PR, we need to stop writing default "localhost" + in /etc/hostnames, and let the right service (networking, user) do that if they + need to. Otherwise, "localhost" will permanently stay as hostname and will + prevent other tools like NetworkManager from setting the right one. + + Signed-off-by: Emanuele Giuseppe Esposito + + RHBZ: 1980403 + +Conflicts: + cloudinit/config/cc_update_etc_hosts.py + cloudinit/sources/DataSourceCloudSigma.py + cloudinit/util.py + tests/unittests/test_util.py + Additional imports and/or conditionals that are not present in this version + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/cmd/main.py | 2 +- + cloudinit/config/cc_apt_configure.py | 2 +- + cloudinit/config/cc_debug.py | 2 +- + cloudinit/config/cc_phone_home.py | 4 +- + cloudinit/config/cc_set_hostname.py | 6 ++- + cloudinit/config/cc_spacewalk.py | 2 +- + cloudinit/config/cc_update_etc_hosts.py | 4 +- + cloudinit/config/cc_update_hostname.py | 7 +++- + cloudinit/sources/DataSourceAliYun.py | 8 +++- + cloudinit/sources/DataSourceCloudSigma.py | 6 ++- + cloudinit/sources/DataSourceGCE.py | 5 ++- + cloudinit/sources/DataSourceScaleway.py | 3 +- + cloudinit/sources/__init__.py | 28 ++++++++++--- + cloudinit/util.py | 29 +++++++++++--- + .../unittests/config/test_cc_set_hostname.py | 40 ++++++++++++++++++- + tests/unittests/sources/test_aliyun.py | 2 +- + tests/unittests/sources/test_cloudsigma.py | 8 ++-- + tests/unittests/sources/test_digitalocean.py | 2 +- + tests/unittests/sources/test_gce.py | 4 +- + tests/unittests/sources/test_hetzner.py | 2 +- + tests/unittests/sources/test_init.py | 29 +++++++++----- + tests/unittests/sources/test_scaleway.py | 2 +- + tests/unittests/sources/test_vmware.py | 4 +- + tests/unittests/test_util.py | 17 ++++---- + tests/unittests/util.py | 3 +- + 25 files changed, 166 insertions(+), 55 deletions(-) + +diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py +index c9be41b3..816d31aa 100644 +--- a/cloudinit/cmd/main.py ++++ b/cloudinit/cmd/main.py +@@ -813,7 +813,7 @@ def _maybe_set_hostname(init, stage, retry_stage): + @param retry_stage: String represented logs upon error setting hostname. + """ + cloud = init.cloudify() +- (hostname, _fqdn) = util.get_hostname_fqdn( ++ (hostname, _fqdn, _) = util.get_hostname_fqdn( + init.cfg, cloud, metadata_only=True + ) + if hostname: # meta-data or user-data hostname content +diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py +index c558311a..0e6466ec 100644 +--- a/cloudinit/config/cc_apt_configure.py ++++ b/cloudinit/config/cc_apt_configure.py +@@ -753,7 +753,7 @@ def search_for_mirror_dns(configured, mirrortype, cfg, cloud): + raise ValueError("unknown mirror type") + + # if we have a fqdn, then search its domain portion first +- (_, fqdn) = util.get_hostname_fqdn(cfg, cloud) ++ fqdn = util.get_hostname_fqdn(cfg, cloud).fqdn + mydom = ".".join(fqdn.split(".")[1:]) + if mydom: + doms.append(".%s" % mydom) +diff --git a/cloudinit/config/cc_debug.py b/cloudinit/config/cc_debug.py +index c51818c3..a00f2823 100644 +--- a/cloudinit/config/cc_debug.py ++++ b/cloudinit/config/cc_debug.py +@@ -95,7 +95,7 @@ def handle(name, cfg, cloud, log, args): + "Datasource: %s\n" % (type_utils.obj_name(cloud.datasource)) + ) + to_print.write("Distro: %s\n" % (type_utils.obj_name(cloud.distro))) +- to_print.write("Hostname: %s\n" % (cloud.get_hostname(True))) ++ to_print.write("Hostname: %s\n" % (cloud.get_hostname(True).hostname)) + to_print.write("Instance ID: %s\n" % (cloud.get_instance_id())) + to_print.write("Locale: %s\n" % (cloud.get_locale())) + to_print.write("Launch IDX: %s\n" % (cloud.launch_index)) +diff --git a/cloudinit/config/cc_phone_home.py b/cloudinit/config/cc_phone_home.py +index a0e1da78..1cf270aa 100644 +--- a/cloudinit/config/cc_phone_home.py ++++ b/cloudinit/config/cc_phone_home.py +@@ -119,8 +119,8 @@ def handle(name, cfg, cloud, log, args): + + all_keys = {} + all_keys["instance_id"] = cloud.get_instance_id() +- all_keys["hostname"] = cloud.get_hostname() +- all_keys["fqdn"] = cloud.get_hostname(fqdn=True) ++ all_keys["hostname"] = cloud.get_hostname().hostname ++ all_keys["fqdn"] = cloud.get_hostname(fqdn=True).hostname + + pubkeys = { + "pub_key_dsa": "/etc/ssh/ssh_host_dsa_key.pub", +diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py +index eb0ca328..2674fa20 100644 +--- a/cloudinit/config/cc_set_hostname.py ++++ b/cloudinit/config/cc_set_hostname.py +@@ -76,7 +76,7 @@ def handle(name, cfg, cloud, log, _args): + if hostname_fqdn is not None: + cloud.distro.set_option("prefer_fqdn_over_hostname", hostname_fqdn) + +- (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) ++ (hostname, fqdn, is_default) = util.get_hostname_fqdn(cfg, cloud) + # Check for previous successful invocation of set-hostname + + # set-hostname artifact file accounts for both hostname and fqdn +@@ -94,6 +94,10 @@ def handle(name, cfg, cloud, log, _args): + if not hostname_changed: + log.debug("No hostname changes. Skipping set-hostname") + return ++ if is_default and hostname == "localhost": ++ # https://github.com/systemd/systemd/commit/d39079fcaa05e23540d2b1f0270fa31c22a7e9f1 ++ log.debug("Hostname is localhost. Let other services handle this.") ++ return + log.debug("Setting the hostname to %s (%s)", fqdn, hostname) + try: + cloud.distro.set_hostname(hostname, fqdn) +diff --git a/cloudinit/config/cc_spacewalk.py b/cloudinit/config/cc_spacewalk.py +index 3fa6c388..419c8b32 100644 +--- a/cloudinit/config/cc_spacewalk.py ++++ b/cloudinit/config/cc_spacewalk.py +@@ -89,7 +89,7 @@ def handle(name, cfg, cloud, log, _args): + if not is_registered(): + do_register( + spacewalk_server, +- cloud.datasource.get_hostname(fqdn=True), ++ cloud.datasource.get_hostname(fqdn=True).hostname, + proxy=cfg.get("proxy"), + log=log, + activation_key=cfg.get("activation_key"), +diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py +index f0aa9b0f..d2ee6f45 100644 +--- a/cloudinit/config/cc_update_etc_hosts.py ++++ b/cloudinit/config/cc_update_etc_hosts.py +@@ -62,7 +62,7 @@ def handle(name, cfg, cloud, log, _args): + hosts_fn = cloud.distro.hosts_fn + + if util.translate_bool(manage_hosts, addons=["template"]): +- (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) ++ (hostname, fqdn, _) = util.get_hostname_fqdn(cfg, cloud) + if not hostname: + log.warning( + "Option 'manage_etc_hosts' was set, but no hostname was found" +@@ -84,7 +84,7 @@ def handle(name, cfg, cloud, log, _args): + ) + + elif manage_hosts == "localhost": +- (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) ++ (hostname, fqdn, _) = util.get_hostname_fqdn(cfg, cloud) + if not hostname: + log.warning( + "Option 'manage_etc_hosts' was set, but no hostname was found" +diff --git a/cloudinit/config/cc_update_hostname.py b/cloudinit/config/cc_update_hostname.py +index 09f6f6da..e2046020 100644 +--- a/cloudinit/config/cc_update_hostname.py ++++ b/cloudinit/config/cc_update_hostname.py +@@ -56,7 +56,12 @@ def handle(name, cfg, cloud, log, _args): + if hostname_fqdn is not None: + cloud.distro.set_option("prefer_fqdn_over_hostname", hostname_fqdn) + +- (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) ++ (hostname, fqdn, is_default) = util.get_hostname_fqdn(cfg, cloud) ++ if is_default and hostname == "localhost": ++ # https://github.com/systemd/systemd/commit/d39079fcaa05e23540d2b1f0270fa31c22a7e9f1 ++ log.debug("Hostname is localhost. Let other services handle this.") ++ return ++ + try: + prev_fn = os.path.join(cloud.get_cpath("data"), "previous-hostname") + log.debug("Updating hostname to %s (%s)", fqdn, hostname) +diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py +index 37f512e3..b9390aca 100644 +--- a/cloudinit/sources/DataSourceAliYun.py ++++ b/cloudinit/sources/DataSourceAliYun.py +@@ -2,6 +2,7 @@ + + from cloudinit import dmi, sources + from cloudinit.sources import DataSourceEc2 as EC2 ++from cloudinit.sources import DataSourceHostname + + ALIYUN_PRODUCT = "Alibaba Cloud ECS" + +@@ -16,7 +17,12 @@ class DataSourceAliYun(EC2.DataSourceEc2): + extended_metadata_versions = [] + + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): +- return self.metadata.get("hostname", "localhost.localdomain") ++ hostname = self.metadata.get("hostname") ++ is_default = False ++ if hostname is None: ++ hostname = "localhost.localdomain" ++ is_default = True ++ return DataSourceHostname(hostname, is_default) + + def get_public_ssh_keys(self): + return parse_public_keys(self.metadata.get("public-keys", {})) +diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py +index de71c3e9..91ebb084 100644 +--- a/cloudinit/sources/DataSourceCloudSigma.py ++++ b/cloudinit/sources/DataSourceCloudSigma.py +@@ -11,6 +11,7 @@ from cloudinit import dmi + from cloudinit import log as logging + from cloudinit import sources + from cloudinit.cs_utils import SERIAL_PORT, Cepko ++from cloudinit.sources import DataSourceHostname + + LOG = logging.getLogger(__name__) + +@@ -90,9 +91,10 @@ class DataSourceCloudSigma(sources.DataSource): + the first part from uuid is being used. + """ + if re.match(r"^[A-Za-z0-9 -_\.]+$", self.metadata["name"]): +- return self.metadata["name"][:61] ++ ret = self.metadata["name"][:61] + else: +- return self.metadata["uuid"].split("-")[0] ++ ret = self.metadata["uuid"].split("-")[0] ++ return DataSourceHostname(ret, False) + + def get_public_ssh_keys(self): + return [self.ssh_public_key] +diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py +index c470bea8..f7ec6b52 100644 +--- a/cloudinit/sources/DataSourceGCE.py ++++ b/cloudinit/sources/DataSourceGCE.py +@@ -12,6 +12,7 @@ from cloudinit import log as logging + from cloudinit import sources, url_helper, util + from cloudinit.distros import ug_util + from cloudinit.net.dhcp import EphemeralDHCPv4 ++from cloudinit.sources import DataSourceHostname + + LOG = logging.getLogger(__name__) + +@@ -122,7 +123,9 @@ class DataSourceGCE(sources.DataSource): + + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): + # GCE has long FDQN's and has asked for short hostnames. +- return self.metadata["local-hostname"].split(".")[0] ++ return DataSourceHostname( ++ self.metadata["local-hostname"].split(".")[0], False ++ ) + + @property + def availability_zone(self): +diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py +index 8e5dd82c..8f08dc6d 100644 +--- a/cloudinit/sources/DataSourceScaleway.py ++++ b/cloudinit/sources/DataSourceScaleway.py +@@ -30,6 +30,7 @@ from cloudinit import log as logging + from cloudinit import net, sources, url_helper, util + from cloudinit.event import EventScope, EventType + from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError ++from cloudinit.sources import DataSourceHostname + + LOG = logging.getLogger(__name__) + +@@ -282,7 +283,7 @@ class DataSourceScaleway(sources.DataSource): + return ssh_keys + + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): +- return self.metadata["hostname"] ++ return DataSourceHostname(self.metadata["hostname"], False) + + @property + def availability_zone(self): +diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py +index 88028cfa..77b24fd7 100644 +--- a/cloudinit/sources/__init__.py ++++ b/cloudinit/sources/__init__.py +@@ -148,6 +148,11 @@ URLParams = namedtuple( + ], + ) + ++DataSourceHostname = namedtuple( ++ "DataSourceHostname", ++ ["hostname", "is_default"], ++) ++ + + class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): + +@@ -291,7 +296,7 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): + + def _get_standardized_metadata(self, instance_data): + """Return a dictionary of standardized metadata keys.""" +- local_hostname = self.get_hostname() ++ local_hostname = self.get_hostname().hostname + instance_id = self.get_instance_id() + availability_zone = self.availability_zone + # In the event of upgrade from existing cloudinit, pickled datasource +@@ -697,22 +702,33 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): + @param metadata_only: Boolean, set True to avoid looking up hostname + if meta-data doesn't have local-hostname present. + +- @return: hostname or qualified hostname. Optionally return None when ++ @return: a DataSourceHostname namedtuple ++ , (str, bool). ++ is_default is a bool and ++ it's true only if hostname is localhost and was ++ returned by util.get_hostname() as a default. ++ This is used to differentiate with a user-defined ++ localhost hostname. ++ Optionally return (None, False) when + metadata_only is True and local-hostname data is not available. + """ + defdomain = "localdomain" + defhost = "localhost" + domain = defdomain ++ is_default = False + + if not self.metadata or not self.metadata.get("local-hostname"): + if metadata_only: +- return None ++ return DataSourceHostname(None, is_default) + # this is somewhat questionable really. + # the cloud datasource was asked for a hostname + # and didn't have one. raising error might be more appropriate + # but instead, basically look up the existing hostname + toks = [] + hostname = util.get_hostname() ++ if hostname == "localhost": ++ # default hostname provided by socket.gethostname() ++ is_default = True + hosts_fqdn = util.get_fqdn_from_hosts(hostname) + if hosts_fqdn and hosts_fqdn.find(".") > 0: + toks = str(hosts_fqdn).split(".") +@@ -745,9 +761,9 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): + hostname = toks[0] + + if fqdn and domain != defdomain: +- return "%s.%s" % (hostname, domain) +- else: +- return hostname ++ hostname = "%s.%s" % (hostname, domain) ++ ++ return DataSourceHostname(hostname, is_default) + + def get_package_mirror_info(self): + return self.distro.get_package_mirror_info(data_source=self) +diff --git a/cloudinit/util.py b/cloudinit/util.py +index 569fc215..4cb21551 100644 +--- a/cloudinit/util.py ++++ b/cloudinit/util.py +@@ -32,7 +32,8 @@ import subprocess + import sys + import time + from base64 import b64decode, b64encode +-from errno import ENOENT ++from collections import deque, namedtuple ++from errno import EACCES, ENOENT + from functools import lru_cache + from typing import List + from urllib import parse +@@ -1072,6 +1073,12 @@ def dos2unix(contents): + return contents.replace("\r\n", "\n") + + ++HostnameFqdnInfo = namedtuple( ++ "HostnameFqdnInfo", ++ ["hostname", "fqdn", "is_default"], ++) ++ ++ + def get_hostname_fqdn(cfg, cloud, metadata_only=False): + """Get hostname and fqdn from config if present and fallback to cloud. + +@@ -1079,9 +1086,17 @@ def get_hostname_fqdn(cfg, cloud, metadata_only=False): + @param cloud: Cloud instance from init.cloudify(). + @param metadata_only: Boolean, set True to only query cloud meta-data, + returning None if not present in meta-data. +- @return: a Tuple of strings , . Values can be none when ++ @return: a namedtuple of ++ , , (str, str, bool). ++ Values can be none when + metadata_only is True and no cfg or metadata provides hostname info. ++ is_default is a bool and ++ it's true only if hostname is localhost and was ++ returned by util.get_hostname() as a default. ++ This is used to differentiate with a user-defined ++ localhost hostname. + """ ++ is_default = False + if "fqdn" in cfg: + # user specified a fqdn. Default hostname then is based off that + fqdn = cfg["fqdn"] +@@ -1095,12 +1110,16 @@ def get_hostname_fqdn(cfg, cloud, metadata_only=False): + else: + # no fqdn set, get fqdn from cloud. + # get hostname from cfg if available otherwise cloud +- fqdn = cloud.get_hostname(fqdn=True, metadata_only=metadata_only) ++ fqdn = cloud.get_hostname( ++ fqdn=True, metadata_only=metadata_only ++ ).hostname + if "hostname" in cfg: + hostname = cfg["hostname"] + else: +- hostname = cloud.get_hostname(metadata_only=metadata_only) +- return (hostname, fqdn) ++ hostname, is_default = cloud.get_hostname( ++ metadata_only=metadata_only ++ ) ++ return HostnameFqdnInfo(hostname, fqdn, is_default) + + + def get_fqdn_from_hosts(hostname, filename="/etc/hosts"): +diff --git a/tests/unittests/config/test_cc_set_hostname.py b/tests/unittests/config/test_cc_set_hostname.py +index fd994c4e..3d1d86ee 100644 +--- a/tests/unittests/config/test_cc_set_hostname.py ++++ b/tests/unittests/config/test_cc_set_hostname.py +@@ -11,6 +11,7 @@ from configobj import ConfigObj + + from cloudinit import cloud, distros, helpers, util + from cloudinit.config import cc_set_hostname ++from cloudinit.sources import DataSourceNone + from tests.unittests import helpers as t_help + + LOG = logging.getLogger(__name__) +@@ -153,7 +154,8 @@ class TestHostname(t_help.FilesystemMockingTestCase): + ) + ] not in m_subp.call_args_list + +- def test_multiple_calls_skips_unchanged_hostname(self): ++ @mock.patch("cloudinit.util.get_hostname", return_value="localhost") ++ def test_multiple_calls_skips_unchanged_hostname(self, get_hostname): + """Only new hostname or fqdn values will generate a hostname call.""" + distro = self._fetch_distro("debian") + paths = helpers.Paths({"cloud_dir": self.tmp}) +@@ -182,6 +184,42 @@ class TestHostname(t_help.FilesystemMockingTestCase): + self.logs.getvalue(), + ) + ++ @mock.patch("cloudinit.util.get_hostname", return_value="localhost") ++ def test_localhost_default_hostname(self, get_hostname): ++ """ ++ No hostname set. Default value returned is localhost, ++ but we shouldn't write it in /etc/hostname ++ """ ++ distro = self._fetch_distro("debian") ++ paths = helpers.Paths({"cloud_dir": self.tmp}) ++ ds = DataSourceNone.DataSourceNone({}, None, paths) ++ cc = cloud.Cloud(ds, paths, {}, distro, None) ++ self.patchUtils(self.tmp) ++ ++ util.write_file("/etc/hostname", "") ++ cc_set_hostname.handle("cc_set_hostname", {}, cc, LOG, []) ++ contents = util.load_file("/etc/hostname") ++ self.assertEqual("", contents.strip()) ++ ++ @mock.patch("cloudinit.util.get_hostname", return_value="localhost") ++ def test_localhost_user_given_hostname(self, get_hostname): ++ """ ++ User set hostname is localhost. We should write it in /etc/hostname ++ """ ++ distro = self._fetch_distro("debian") ++ paths = helpers.Paths({"cloud_dir": self.tmp}) ++ ds = DataSourceNone.DataSourceNone({}, None, paths) ++ cc = cloud.Cloud(ds, paths, {}, distro, None) ++ self.patchUtils(self.tmp) ++ ++ # user-provided localhost should not be ignored ++ util.write_file("/etc/hostname", "") ++ cc_set_hostname.handle( ++ "cc_set_hostname", {"hostname": "localhost"}, cc, LOG, [] ++ ) ++ contents = util.load_file("/etc/hostname") ++ self.assertEqual("localhost", contents.strip()) ++ + def test_error_on_distro_set_hostname_errors(self): + """Raise SetHostnameError on exceptions from distro.set_hostname.""" + distro = self._fetch_distro("debian") +diff --git a/tests/unittests/sources/test_aliyun.py b/tests/unittests/sources/test_aliyun.py +index 8a61d5ee..e628dc02 100644 +--- a/tests/unittests/sources/test_aliyun.py ++++ b/tests/unittests/sources/test_aliyun.py +@@ -149,7 +149,7 @@ class TestAliYunDatasource(test_helpers.HttprettyTestCase): + + def _test_host_name(self): + self.assertEqual( +- self.default_metadata["hostname"], self.ds.get_hostname() ++ self.default_metadata["hostname"], self.ds.get_hostname().hostname + ) + + @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") +diff --git a/tests/unittests/sources/test_cloudsigma.py b/tests/unittests/sources/test_cloudsigma.py +index a2f26245..3dca7ea8 100644 +--- a/tests/unittests/sources/test_cloudsigma.py ++++ b/tests/unittests/sources/test_cloudsigma.py +@@ -58,12 +58,14 @@ class DataSourceCloudSigmaTest(test_helpers.CiTestCase): + + def test_get_hostname(self): + self.datasource.get_data() +- self.assertEqual("test_server", self.datasource.get_hostname()) ++ self.assertEqual( ++ "test_server", self.datasource.get_hostname().hostname ++ ) + self.datasource.metadata["name"] = "" +- self.assertEqual("65b2fb23", self.datasource.get_hostname()) ++ self.assertEqual("65b2fb23", self.datasource.get_hostname().hostname) + utf8_hostname = b"\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".decode("utf-8") + self.datasource.metadata["name"] = utf8_hostname +- self.assertEqual("65b2fb23", self.datasource.get_hostname()) ++ self.assertEqual("65b2fb23", self.datasource.get_hostname().hostname) + + def test_get_public_ssh_keys(self): + self.datasource.get_data() +diff --git a/tests/unittests/sources/test_digitalocean.py b/tests/unittests/sources/test_digitalocean.py +index f3e6224e..47e46c66 100644 +--- a/tests/unittests/sources/test_digitalocean.py ++++ b/tests/unittests/sources/test_digitalocean.py +@@ -178,7 +178,7 @@ class TestDataSourceDigitalOcean(CiTestCase): + self.assertEqual(DO_META.get("vendor_data"), ds.get_vendordata_raw()) + self.assertEqual(DO_META.get("region"), ds.availability_zone) + self.assertEqual(DO_META.get("droplet_id"), ds.get_instance_id()) +- self.assertEqual(DO_META.get("hostname"), ds.get_hostname()) ++ self.assertEqual(DO_META.get("hostname"), ds.get_hostname().hostname) + + # Single key + self.assertEqual( +diff --git a/tests/unittests/sources/test_gce.py b/tests/unittests/sources/test_gce.py +index e030931b..1ce0c6ec 100644 +--- a/tests/unittests/sources/test_gce.py ++++ b/tests/unittests/sources/test_gce.py +@@ -126,7 +126,7 @@ class TestDataSourceGCE(test_helpers.HttprettyTestCase): + self.ds.get_data() + + shostname = GCE_META.get("instance/hostname").split(".")[0] +- self.assertEqual(shostname, self.ds.get_hostname()) ++ self.assertEqual(shostname, self.ds.get_hostname().hostname) + + self.assertEqual( + GCE_META.get("instance/id"), self.ds.get_instance_id() +@@ -147,7 +147,7 @@ class TestDataSourceGCE(test_helpers.HttprettyTestCase): + ) + + shostname = GCE_META_PARTIAL.get("instance/hostname").split(".")[0] +- self.assertEqual(shostname, self.ds.get_hostname()) ++ self.assertEqual(shostname, self.ds.get_hostname().hostname) + + def test_userdata_no_encoding(self): + """check that user-data is read.""" +diff --git a/tests/unittests/sources/test_hetzner.py b/tests/unittests/sources/test_hetzner.py +index f80ed45f..193b7e42 100644 +--- a/tests/unittests/sources/test_hetzner.py ++++ b/tests/unittests/sources/test_hetzner.py +@@ -116,7 +116,7 @@ class TestDataSourceHetzner(CiTestCase): + + self.assertTrue(m_readmd.called) + +- self.assertEqual(METADATA.get("hostname"), ds.get_hostname()) ++ self.assertEqual(METADATA.get("hostname"), ds.get_hostname().hostname) + + self.assertEqual(METADATA.get("public-keys"), ds.get_public_ssh_keys()) + +diff --git a/tests/unittests/sources/test_init.py b/tests/unittests/sources/test_init.py +index ce8fc970..79fc9c5b 100644 +--- a/tests/unittests/sources/test_init.py ++++ b/tests/unittests/sources/test_init.py +@@ -272,9 +272,11 @@ class TestDataSource(CiTestCase): + self.assertEqual( + "test-subclass-hostname", datasource.metadata["local-hostname"] + ) +- self.assertEqual("test-subclass-hostname", datasource.get_hostname()) ++ self.assertEqual( ++ "test-subclass-hostname", datasource.get_hostname().hostname ++ ) + datasource.metadata["local-hostname"] = "hostname.my.domain.com" +- self.assertEqual("hostname", datasource.get_hostname()) ++ self.assertEqual("hostname", datasource.get_hostname().hostname) + + def test_get_hostname_with_fqdn_returns_local_hostname_with_domain(self): + """Datasource.get_hostname with fqdn set gets qualified hostname.""" +@@ -285,7 +287,8 @@ class TestDataSource(CiTestCase): + self.assertTrue(datasource.get_data()) + datasource.metadata["local-hostname"] = "hostname.my.domain.com" + self.assertEqual( +- "hostname.my.domain.com", datasource.get_hostname(fqdn=True) ++ "hostname.my.domain.com", ++ datasource.get_hostname(fqdn=True).hostname, + ) + + def test_get_hostname_without_metadata_uses_system_hostname(self): +@@ -300,10 +303,12 @@ class TestDataSource(CiTestCase): + with mock.patch(mock_fqdn) as m_fqdn: + m_gethost.return_value = "systemhostname.domain.com" + m_fqdn.return_value = None # No maching fqdn in /etc/hosts +- self.assertEqual("systemhostname", datasource.get_hostname()) ++ self.assertEqual( ++ "systemhostname", datasource.get_hostname().hostname ++ ) + self.assertEqual( + "systemhostname.domain.com", +- datasource.get_hostname(fqdn=True), ++ datasource.get_hostname(fqdn=True).hostname, + ) + + def test_get_hostname_without_metadata_returns_none(self): +@@ -316,9 +321,13 @@ class TestDataSource(CiTestCase): + mock_fqdn = "cloudinit.sources.util.get_fqdn_from_hosts" + with mock.patch("cloudinit.sources.util.get_hostname") as m_gethost: + with mock.patch(mock_fqdn) as m_fqdn: +- self.assertIsNone(datasource.get_hostname(metadata_only=True)) + self.assertIsNone( +- datasource.get_hostname(fqdn=True, metadata_only=True) ++ datasource.get_hostname(metadata_only=True).hostname ++ ) ++ self.assertIsNone( ++ datasource.get_hostname( ++ fqdn=True, metadata_only=True ++ ).hostname + ) + self.assertEqual([], m_gethost.call_args_list) + self.assertEqual([], m_fqdn.call_args_list) +@@ -335,10 +344,12 @@ class TestDataSource(CiTestCase): + with mock.patch(mock_fqdn) as m_fqdn: + m_gethost.return_value = "systemhostname.domain.com" + m_fqdn.return_value = "fqdnhostname.domain.com" +- self.assertEqual("fqdnhostname", datasource.get_hostname()) ++ self.assertEqual( ++ "fqdnhostname", datasource.get_hostname().hostname ++ ) + self.assertEqual( + "fqdnhostname.domain.com", +- datasource.get_hostname(fqdn=True), ++ datasource.get_hostname(fqdn=True).hostname, + ) + + def test_get_data_does_not_write_instance_data_on_failure(self): +diff --git a/tests/unittests/sources/test_scaleway.py b/tests/unittests/sources/test_scaleway.py +index d7e8b969..56735dd0 100644 +--- a/tests/unittests/sources/test_scaleway.py ++++ b/tests/unittests/sources/test_scaleway.py +@@ -236,7 +236,7 @@ class TestDataSourceScaleway(HttprettyTestCase): + ].sort(), + ) + self.assertEqual( +- self.datasource.get_hostname(), ++ self.datasource.get_hostname().hostname, + MetadataResponses.FAKE_METADATA["hostname"], + ) + self.assertEqual( +diff --git a/tests/unittests/sources/test_vmware.py b/tests/unittests/sources/test_vmware.py +index dd331349..753bb774 100644 +--- a/tests/unittests/sources/test_vmware.py ++++ b/tests/unittests/sources/test_vmware.py +@@ -368,7 +368,9 @@ class TestDataSourceVMwareGuestInfo_InvalidPlatform(FilesystemMockingTestCase): + + def assert_metadata(test_obj, ds, metadata): + test_obj.assertEqual(metadata.get("instance-id"), ds.get_instance_id()) +- test_obj.assertEqual(metadata.get("local-hostname"), ds.get_hostname()) ++ test_obj.assertEqual( ++ metadata.get("local-hostname"), ds.get_hostname().hostname ++ ) + + expected_public_keys = metadata.get("public_keys") + if not isinstance(expected_public_keys, list): +diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py +index 3765511b..528b7f36 100644 +--- a/tests/unittests/test_util.py ++++ b/tests/unittests/test_util.py +@@ -19,6 +19,7 @@ import pytest + import yaml + + from cloudinit import importer, subp, util ++from cloudinit.sources import DataSourceHostname + from tests.unittests import helpers + from tests.unittests.helpers import CiTestCase + +@@ -331,8 +332,8 @@ class FakeCloud(object): + myargs["metadata_only"] = metadata_only + self.calls.append(myargs) + if fqdn: +- return self.fqdn +- return self.hostname ++ return DataSourceHostname(self.fqdn, False) ++ return DataSourceHostname(self.hostname, False) + + + class TestUtil(CiTestCase): +@@ -420,7 +421,7 @@ class TestShellify(CiTestCase): + class TestGetHostnameFqdn(CiTestCase): + def test_get_hostname_fqdn_from_only_cfg_fqdn(self): + """When cfg only has the fqdn key, derive hostname and fqdn from it.""" +- hostname, fqdn = util.get_hostname_fqdn( ++ hostname, fqdn, _ = util.get_hostname_fqdn( + cfg={"fqdn": "myhost.domain.com"}, cloud=None + ) + self.assertEqual("myhost", hostname) +@@ -428,7 +429,7 @@ class TestGetHostnameFqdn(CiTestCase): + + def test_get_hostname_fqdn_from_cfg_fqdn_and_hostname(self): + """When cfg has both fqdn and hostname keys, return them.""" +- hostname, fqdn = util.get_hostname_fqdn( ++ hostname, fqdn, _ = util.get_hostname_fqdn( + cfg={"fqdn": "myhost.domain.com", "hostname": "other"}, cloud=None + ) + self.assertEqual("other", hostname) +@@ -436,7 +437,7 @@ class TestGetHostnameFqdn(CiTestCase): + + def test_get_hostname_fqdn_from_cfg_hostname_with_domain(self): + """When cfg has only hostname key which represents a fqdn, use that.""" +- hostname, fqdn = util.get_hostname_fqdn( ++ hostname, fqdn, _ = util.get_hostname_fqdn( + cfg={"hostname": "myhost.domain.com"}, cloud=None + ) + self.assertEqual("myhost", hostname) +@@ -445,7 +446,7 @@ class TestGetHostnameFqdn(CiTestCase): + def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self): + """When cfg has a hostname without a '.' query cloud.get_hostname.""" + mycloud = FakeCloud("cloudhost", "cloudhost.mycloud.com") +- hostname, fqdn = util.get_hostname_fqdn( ++ hostname, fqdn, _ = util.get_hostname_fqdn( + cfg={"hostname": "myhost"}, cloud=mycloud + ) + self.assertEqual("myhost", hostname) +@@ -457,7 +458,7 @@ class TestGetHostnameFqdn(CiTestCase): + def test_get_hostname_fqdn_from_without_fqdn_or_hostname(self): + """When cfg has neither hostname nor fqdn cloud.get_hostname.""" + mycloud = FakeCloud("cloudhost", "cloudhost.mycloud.com") +- hostname, fqdn = util.get_hostname_fqdn(cfg={}, cloud=mycloud) ++ hostname, fqdn, _ = util.get_hostname_fqdn(cfg={}, cloud=mycloud) + self.assertEqual("cloudhost", hostname) + self.assertEqual("cloudhost.mycloud.com", fqdn) + self.assertEqual( +@@ -468,7 +469,7 @@ class TestGetHostnameFqdn(CiTestCase): + def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self): + """Calls to cloud.get_hostname pass the metadata_only parameter.""" + mycloud = FakeCloud("cloudhost", "cloudhost.mycloud.com") +- _hn, _fqdn = util.get_hostname_fqdn( ++ _hn, _fqdn, _def_hostname = util.get_hostname_fqdn( + cfg={}, cloud=mycloud, metadata_only=True + ) + self.assertEqual( +diff --git a/tests/unittests/util.py b/tests/unittests/util.py +index 79a6e1d0..6fb39506 100644 +--- a/tests/unittests/util.py ++++ b/tests/unittests/util.py +@@ -1,5 +1,6 @@ + # This file is part of cloud-init. See LICENSE file for license information. + from cloudinit import cloud, distros, helpers ++from cloudinit.sources import DataSourceHostname + from cloudinit.sources.DataSourceNone import DataSourceNone + + +@@ -37,7 +38,7 @@ def abstract_to_concrete(abclass): + + class DataSourceTesting(DataSourceNone): + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): +- return "hostname" ++ return DataSourceHostname("hostname", False) + + def persist_instance_data(self): + return True +-- +2.31.1 + diff --git a/SOURCES/ci-setup.py-adjust-udev-rules-default-path-1513.patch b/SOURCES/ci-setup.py-adjust-udev-rules-default-path-1513.patch new file mode 100644 index 0000000..c46af8d --- /dev/null +++ b/SOURCES/ci-setup.py-adjust-udev-rules-default-path-1513.patch @@ -0,0 +1,57 @@ +From f771d841dbdef8fbb1c1a3d1b8d51ff101354502 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Fri, 17 Jun 2022 09:41:23 +0200 +Subject: [PATCH 3/3] setup.py: adjust udev/rules default path (#1513) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 30: setup.py: adjust udev/rules default path (#1513) +RH-Commit: [2/2] b71362acefa15587b2c72e8981708065d2fcfa07 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2096270 +RH-Acked-by: Mohamed Gamal Morsy +RH-Acked-by: Vitaly Kuznetsov + +commit 70715125f3af118ae242770e61064c24f41e9a02 +Author: Emanuele Giuseppe Esposito +Date: Thu Jun 16 20:39:42 2022 +0200 + + setup.py: adjust udev/rules default path (#1513) + + RHEL must put cloudinit .rules files in /usr/lib/udev/rules.d + This place is a rhel standard and since it is used by all packages + cannot be modified. + + Signed-off-by: Emanuele Giuseppe Esposito + +Signed-off-by: Emanuele Giuseppe Esposito +--- + setup.py | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/setup.py b/setup.py +index a9132d2c..fdf27cd7 100755 +--- a/setup.py ++++ b/setup.py +@@ -302,6 +302,11 @@ data_files = [ + ), + ] + if not platform.system().endswith("BSD"): ++ ++ RULES_PATH = LIB ++ if os.path.isfile("/etc/redhat-release"): ++ RULES_PATH = "/usr/lib" ++ + data_files.extend( + [ + ( +@@ -309,7 +314,7 @@ if not platform.system().endswith("BSD"): + ["tools/hook-network-manager"], + ), + (ETC + "/dhcp/dhclient-exit-hooks.d/", ["tools/hook-dhclient"]), +- (LIB + "/udev/rules.d", [f for f in glob("udev/*.rules")]), ++ (RULES_PATH + "/udev/rules.d", [f for f in glob("udev/*.rules")]), + ( + ETC + "/systemd/system/sshd-keygen@.service.d/", + ["systemd/disable-sshd-keygen-if-cloud-init-active.conf"], +-- +2.35.1 + diff --git a/SOURCES/cloud-init-tmpfiles.conf b/SOURCES/cloud-init-tmpfiles.conf new file mode 100644 index 0000000..0c6d2a3 --- /dev/null +++ b/SOURCES/cloud-init-tmpfiles.conf @@ -0,0 +1 @@ +d /run/cloud-init 0700 root root - - diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec new file mode 100644 index 0000000..f0f9c43 --- /dev/null +++ b/SPECS/cloud-init.spec @@ -0,0 +1,580 @@ +Name: cloud-init +Version: 22.1 +Release: 5%{?dist} +Summary: Cloud instance init scripts +License: ASL 2.0 or GPLv3 +URL: http://launchpad.net/cloud-init +Source0: https://launchpad.net/cloud-init/trunk/%{version}/+download/%{name}-%{version}.tar.gz +Source1: cloud-init-tmpfiles.conf + +Patch0001: 0001-Add-initial-redhat-changes.patch +Patch0002: 0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch +Patch0003: 0003-Adding-_netdev-to-the-default-mount-configuration.patch +Patch0004: 0004-Setting-highest-autoconnect-priority-for-network-scr.patch +Patch0005: 0005-limit-permissions-on-def_log_file.patch +Patch0006: 0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch +# For bz#2056964 - [RHEL-9]Rebase cloud-init from Fedora so it can configure networking using NM keyfiles +Patch7: ci-Add-native-NetworkManager-support-1224.patch +# For bz#2056964 - [RHEL-9]Rebase cloud-init from Fedora so it can configure networking using NM keyfiles +Patch8: ci-Use-Network-Manager-and-Netplan-as-default-renderers.patch +# For bz#2056964 - [RHEL-9]Rebase cloud-init from Fedora so it can configure networking using NM keyfiles +Patch9: ci-Revert-Setting-highest-autoconnect-priority-for-netw.patch +# For bz#2088448 - Align cloud.cfg file and systemd with cloud-init upstream .tmpl files +Patch10: ci-Align-rhel-custom-files-with-upstream-1431.patch +# For bz#2088448 - Align cloud.cfg file and systemd with cloud-init upstream .tmpl files +Patch11: ci-Remove-rhel-specific-files.patch +# For bz#2091640 - [cloud][init] Add support for reading tags from instance metadata +Patch12: ci-Support-EC2-tags-in-instance-metadata-1309.patch +# For bz#1980403 - [RHV] RHEL 9 VM with cloud-init without hostname set doesn't result in the FQDN as hostname +Patch13: ci-cc_set_hostname-do-not-write-localhost-when-no-hostn.patch +# For bz#2061604 - cloud-config will change /etc/locale.conf back to en_US.UTF-8 on rhel-guest-image-9.0 +Patch14: ci-Honor-system-locale-for-RHEL-1355.patch +# For bz#2096270 - Adjust udev/rules default path[rhel-9] +Patch15: ci-setup.py-adjust-udev-rules-default-path-1513.patch +# For bz#2107463 - [RHEL-9.1] Cannot run sysconfig when changing the priority of network renderers +# For bz#2104389 - [RHEL-9.1]Failed to config static IP and IPv6 according to VMware Customization Config File +# For bz#2117532 - [RHEL9.1] Revert patch of configuring networking by NM keyfiles +# For bz#2098501 - [RHEL-9.1] IPv6 not workable when cloud-init configure network using NM keyfiles +Patch16: ci-Revert-Add-native-NetworkManager-support-1224.patch +# For bz#2107463 - [RHEL-9.1] Cannot run sysconfig when changing the priority of network renderers +# For bz#2104389 - [RHEL-9.1]Failed to config static IP and IPv6 according to VMware Customization Config File +# For bz#2117532 - [RHEL9.1] Revert patch of configuring networking by NM keyfiles +# For bz#2098501 - [RHEL-9.1] IPv6 not workable when cloud-init configure network using NM keyfiles +Patch17: ci-Revert-Use-Network-Manager-and-Netplan-as-default-re.patch +# For bz#2107463 - [RHEL-9.1] Cannot run sysconfig when changing the priority of network renderers +# For bz#2104389 - [RHEL-9.1]Failed to config static IP and IPv6 according to VMware Customization Config File +# For bz#2117532 - [RHEL9.1] Revert patch of configuring networking by NM keyfiles +# For bz#2098501 - [RHEL-9.1] IPv6 not workable when cloud-init configure network using NM keyfiles +Patch18: ci-Revert-Revert-Setting-highest-autoconnect-priority-f.patch + +# Source-git patches + +BuildArch: noarch + +BuildRequires: pkgconfig(systemd) +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: systemd + +# For tests +BuildRequires: iproute +BuildRequires: python3-configobj +# https://bugzilla.redhat.com/show_bug.cgi?id=1695953 +BuildRequires: python3-distro +# https://bugzilla.redhat.com/show_bug.cgi?id=1417029 +BuildRequires: python3-httpretty >= 0.8.14-2 +BuildRequires: python3-jinja2 +BuildRequires: python3-jsonpatch +BuildRequires: python3-oauthlib +BuildRequires: python3-prettytable +BuildRequires: python3-pyserial +BuildRequires: python3-PyYAML +BuildRequires: python3-requests +BuildRequires: python3-six +# dnf is needed to make cc_ntp unit tests work +# https://bugs.launchpad.net/cloud-init/+bug/1721573 +BuildRequires: /usr/bin/dnf + +Requires: e2fsprogs +Requires: iproute +Requires: libselinux-python3 +Requires: policycoreutils-python3 +Requires: procps +Requires: python3-configobj +# https://bugzilla.redhat.com/show_bug.cgi?id=1695953 +Requires: python3-distro +Requires: python3-jinja2 +Requires: python3-jsonpatch +Requires: python3-oauthlib +Requires: python3-prettytable +Requires: python3-pyserial +Requires: python3-PyYAML +Requires: python3-requests +Requires: python3-six +Requires: shadow-utils +Requires: util-linux +Requires: xfsprogs +Requires: dhcp-client +# https://bugzilla.redhat.com/show_bug.cgi?id=2032524 +Requires: gdisk +Requires: openssl +Requires: python3-netifaces + +%{?systemd_requires} + +%description +Cloud-init is a set of init scripts for cloud instances. Cloud instances +need special scripts to run during initialization to retrieve and install +ssh keys and to let the user run various scripts. + + +%prep +%autosetup -p1 + +# Change shebangs +sed -i -e 's|#!/usr/bin/env python|#!/usr/bin/env python3|' \ + -e 's|#!/usr/bin/python|#!/usr/bin/python3|' tools/* cloudinit/ssh_util.py + +%build +%py3_build + + +%install +%py3_install -- + +%if 0%{?fedora} +python3 tools/render-cloudcfg --variant fedora > $RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg +%endif + +sed -i "s,@@PACKAGED_VERSION@@,%{version}-%{release}," $RPM_BUILD_ROOT/%{python3_sitelib}/cloudinit/version.py + +mkdir -p $RPM_BUILD_ROOT/var/lib/cloud + +# /run/cloud-init needs a tmpfiles.d entry +mkdir -p $RPM_BUILD_ROOT/run/cloud-init +mkdir -p $RPM_BUILD_ROOT/%{_tmpfilesdir} +cp -p %{SOURCE1} $RPM_BUILD_ROOT/%{_tmpfilesdir}/%{name}.conf + +mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/rsyslog.d +cp -p tools/21-cloudinit.conf $RPM_BUILD_ROOT/%{_sysconfdir}/rsyslog.d/21-cloudinit.conf + +# Make installed NetworkManager hook name less generic +mv $RPM_BUILD_ROOT/etc/NetworkManager/dispatcher.d/hook-network-manager \ + $RPM_BUILD_ROOT/etc/NetworkManager/dispatcher.d/cloud-init-azure-hook + +[ ! -d $RPM_BUILD_ROOT%{_systemdgeneratordir} ] && mkdir -p $RPM_BUILD_ROOT%{_systemdgeneratordir} +python3 tools/render-cloudcfg --variant rhel systemd/cloud-init-generator.tmpl > $RPM_BUILD_ROOT%{_systemdgeneratordir}/cloud-init-generator +chmod 755 $RPM_BUILD_ROOT%{_systemdgeneratordir}/cloud-init-generator + +# installing man pages +mkdir -p ${RPM_BUILD_ROOT}%{_mandir}/man1/ +for man in cloud-id.1 cloud-init.1 cloud-init-per.1; do + install -c -m 0644 doc/man/${man} ${RPM_BUILD_ROOT}%{_mandir}/man1/${man} + chmod -x ${RPM_BUILD_ROOT}%{_mandir}/man1/* +done + +%clean +rm -rf $RPM_BUILD_ROOT + + +%post +if [ $1 -eq 1 ] ; then + # Initial installation + # Enabled by default per "runs once then goes away" exception + /bin/systemctl enable cloud-config.service >/dev/null 2>&1 || : + /bin/systemctl enable cloud-final.service >/dev/null 2>&1 || : + /bin/systemctl enable cloud-init.service >/dev/null 2>&1 || : + /bin/systemctl enable cloud-init-local.service >/dev/null 2>&1 || : + /bin/systemctl enable cloud-init.target >/dev/null 2>&1 || : +elif [ $1 -eq 2 ]; then + # Upgrade. If the upgrade is from a version older than 0.7.9-8, + # there will be stale systemd config + /bin/systemctl is-enabled cloud-config.service >/dev/null 2>&1 && + /bin/systemctl reenable cloud-config.service >/dev/null 2>&1 || : + + /bin/systemctl is-enabled cloud-final.service >/dev/null 2>&1 && + /bin/systemctl reenable cloud-final.service >/dev/null 2>&1 || : + + /bin/systemctl is-enabled cloud-init.service >/dev/null 2>&1 && + /bin/systemctl reenable cloud-init.service >/dev/null 2>&1 || : + + /bin/systemctl is-enabled cloud-init-local.service >/dev/null 2>&1 && + /bin/systemctl reenable cloud-init-local.service >/dev/null 2>&1 || : + + /bin/systemctl is-enabled cloud-init.target >/dev/null 2>&1 && + /bin/systemctl reenable cloud-init.target >/dev/null 2>&1 || : +fi + +%preun +if [ $1 -eq 0 ] ; then + # Package removal, not upgrade + /bin/systemctl --no-reload disable cloud-config.service >/dev/null 2>&1 || : + /bin/systemctl --no-reload disable cloud-final.service >/dev/null 2>&1 || : + /bin/systemctl --no-reload disable cloud-init.service >/dev/null 2>&1 || : + /bin/systemctl --no-reload disable cloud-init-local.service >/dev/null 2>&1 || : + /bin/systemctl --no-reload disable cloud-init.target >/dev/null 2>&1 || : + # One-shot services -> no need to stop +fi + +%postun +%systemd_postun cloud-config.service cloud-config.target cloud-final.service cloud-init.service cloud-init.target cloud-init-local.service + + +%files +%license LICENSE +%config(noreplace) %{_sysconfdir}/cloud/cloud.cfg +%dir %{_sysconfdir}/cloud/cloud.cfg.d +%config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/*.cfg +%doc %{_sysconfdir}/cloud/cloud.cfg.d/README +%dir %{_sysconfdir}/cloud/templates +%config(noreplace) %{_sysconfdir}/cloud/templates/* +%{_unitdir}/cloud-config.service +%{_unitdir}/cloud-config.target +%{_unitdir}/cloud-final.service +%{_unitdir}/cloud-init-hotplugd.service +%{_unitdir}/cloud-init-hotplugd.socket +%{_unitdir}/cloud-init-local.service +%{_unitdir}/cloud-init.service +%{_unitdir}/cloud-init.target +%{_tmpfilesdir}/%{name}.conf +%{python3_sitelib}/* +%{_libexecdir}/%{name} +%{_bindir}/cloud-init* +%doc %{_datadir}/doc/%{name} +%{_mandir}/man1/* +%dir %verify(not mode) /run/cloud-init +%dir /var/lib/cloud +/etc/NetworkManager/dispatcher.d/cloud-init-azure-hook +/etc/dhcp/dhclient-exit-hooks.d/hook-dhclient +%{_udevrulesdir}/66-azure-ephemeral.rules +%{_datadir}/bash-completion/completions/cloud-init +%{_bindir}/cloud-id +%{_systemdgeneratordir}/cloud-init-generator +%{_sysconfdir}/systemd/system/sshd-keygen@.service.d/disable-sshd-keygen-if-cloud-init-active.conf + +%dir %{_sysconfdir}/rsyslog.d +%config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf + +%changelog +* Wed Aug 17 2022 Miroslav Rezanina - 22.1-5 +- ci-Revert-Add-native-NetworkManager-support-1224.patch [bz#2107463 bz#2104389 bz#2117532 bz#2098501] +- ci-Revert-Use-Network-Manager-and-Netplan-as-default-re.patch [bz#2107463 bz#2104389 bz#2117532 bz#2098501] +- ci-Revert-Revert-Setting-highest-autoconnect-priority-f.patch [bz#2107463 bz#2104389 bz#2117532 bz#2098501] +- Resolves: bz#2107463 + ([RHEL-9.1] Cannot run sysconfig when changing the priority of network renderers) +- Resolves: bz#2104389 + ([RHEL-9.1]Failed to config static IP and IPv6 according to VMware Customization Config File) +- Resolves: bz#2117532 + ([RHEL9.1] Revert patch of configuring networking by NM keyfiles) +- Resolves: bz#2098501 + ([RHEL-9.1] IPv6 not workable when cloud-init configure network using NM keyfiles) + +* Thu Jun 23 2022 Jon Maloy - 22.1-4 +- ci-Honor-system-locale-for-RHEL-1355.patch [bz#2061604] +- ci-cloud-init.spec-adjust-path-for-66-azure-ephemeral.r.patch [bz#2096270] +- ci-setup.py-adjust-udev-rules-default-path-1513.patch [bz#2096270] +- Resolves: bz#2061604 + (cloud-config will change /etc/locale.conf back to en_US.UTF-8 on rhel-guest-image-9.0) +- Resolves: bz#2096270 + (Adjust udev/rules default path[rhel-9]) + +* Wed Jun 08 2022 Miroslav Rezanina - 22.1-3 +- ci-Support-EC2-tags-in-instance-metadata-1309.patch [bz#2091640] +- ci-cc_set_hostname-do-not-write-localhost-when-no-hostn.patch [bz#1980403] +- Resolves: bz#2091640 + ([cloud][init] Add support for reading tags from instance metadata) +- Resolves: bz#1980403 + ([RHV] RHEL 9 VM with cloud-init without hostname set doesn't result in the FQDN as hostname) + +* Tue May 31 2022 Miroslav Rezanina - 22.1-2 +- ci-Add-native-NetworkManager-support-1224.patch [bz#2056964] +- ci-Use-Network-Manager-and-Netplan-as-default-renderers.patch [bz#2056964] +- ci-Revert-Setting-highest-autoconnect-priority-for-netw.patch [bz#2056964] +- ci-Align-rhel-custom-files-with-upstream-1431.patch [bz#2088448] +- ci-Remove-rhel-specific-files.patch [bz#2088448] +- Resolves: bz#2056964 + ([RHEL-9]Rebase cloud-init from Fedora so it can configure networking using NM keyfiles) +- Resolves: bz#2088448 + (Align cloud.cfg file and systemd with cloud-init upstream .tmpl files) + +* Tue Apr 19 2022 Emanuele Giuseppe Esposito - 22.1-1 +- Rebase to 22.1 [bz#2065548] +- Resolves: bz#2065548 + ([RHEL-9.1] cloud-init rebase to 22.1) + +* Fri Feb 25 2022 Miroslav Rezanina - 21.1-19 +- ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch [bz#2053546] +- ci-Adding-_netdev-to-the-default-mount-configuration.patch [bz#1998445] +- ci-Setting-highest-autoconnect-priority-for-network-scr.patch [bz#2036060] +- Resolves: bz#2053546 + (cloud-init writes route6-$DEVICE config with a HEX netmask. ip route does not like : Error: inet6 prefix is expected rather than "fd00:fd00:fd00::/ffff:ffff:ffff:ffff::".) +- Resolves: bz#1998445 + ([Azure][RHEL-9] ordering cycle exists after reboot) +- Resolves: bz#2036060 + ([cloud-init][ESXi][RHEL-9] Failed to config static IP according to VMware Customization Config File) + +* Fri Feb 11 2022 Miroslav Rezanina - 21.1-18 +- ci-Add-_netdev-option-to-mount-Azure-ephemeral-disk-121.patch [bz#1998445] +- Resolves: bz#1998445 + ([Azure][RHEL-9] ordering cycle exists after reboot) + +* Mon Feb 07 2022 Miroslav Rezanina - 21.1-17 +- ci-Add-flexibility-to-IMDS-api-version-793.patch [bz#2042351] +- ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch [bz#2042351] +- ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch [bz#2042351] +- ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch [bz#2042351] +- ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch [bz#2042351] +- ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch [bz#2042351] +- ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch [bz#2042351] +- Resolves: bz#2042351 + ([RHEL-9] Support for provisioning Azure VM with userdata) + +* Fri Jan 21 2022 Miroslav Rezanina - 21.1-16 +- ci-Datasource-for-VMware-953.patch [bz#2040090] +- ci-Change-netifaces-dependency-to-0.10.4-965.patch [bz#2040090] +- ci-Update-dscheck_VMware-s-rpctool-check-970.patch [bz#2040090] +- ci-Revert-unnecesary-lcase-in-ds-identify-978.patch [bz#2040090] +- ci-Add-netifaces-package-as-a-Requires-in-cloud-init.sp.patch [bz#2040090] +- Resolves: bz#2040090 + ([cloud-init][RHEL9] Support for cloud-init datasource 'cloud-init-vmware-guestinfo') + +* Thu Jan 13 2022 Miroslav Rezanina - 21.1-15 +- ci-Add-gdisk-and-openssl-as-deps-to-fix-UEFI-Azure-init.patch [bz#2032524] +- Resolves: bz#2032524 + ([RHEL9] [Azure] cloud-init fails to configure the system) + +* Tue Dec 14 2021 Miroslav Rezanina - 21.1-14 +- ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch [bz#2028031] +- Resolves: bz#2028031 + ([RHEL-9] Above 19.2 of cloud-init fails to configure routes when configuring static and default routes to the same destination IP) + +* Mon Dec 06 2021 Miroslav Rezanina - 21.1-13 +- ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch [bz#2028381] +- Resolves: bz#2028381 + (cloud-init.service fails to start after package update) + +* Mon Nov 01 2021 Miroslav Rezanina - 21.1-12 +- ci-remove-unnecessary-EOF-string-in-disable-sshd-keygen.patch [bz#2016305] +- Resolves: bz#2016305 + (disable-sshd-keygen-if-cloud-init-active.conf:8: Missing '=', ignoring line) + +* Tue Oct 26 2021 Miroslav Rezanina - 21.1-11 +- ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch [bz#2015974] +- Resolves: bz#2015974 + (cloud-init fails to set host key permissions correctly) + +* Mon Oct 18 2021 Miroslav Rezanina - 21.1-10 +- ci-Inhibit-sshd-keygen-.service-if-cloud-init-is-active.patch [bz#2002492] +- ci-add-the-drop-in-also-in-the-files-section-of-cloud-i.patch [bz#2002492] +- Resolves: bz#2002492 + (util.py[WARNING]: Failed generating key type rsa to file /etc/ssh/ssh_host_rsa_key) + +* Fri Sep 10 2021 Miroslav Rezanina mrezanin@redhat.com - 21.1-9 +- ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch [bz#2002302] +- Resolves: bz#2002302 + (cloud-init fails with ValueError: need more than 1 value to unpack[rhel-9]) + +* Fri Sep 03 2021 Miroslav Rezanina - 21.1-8 +- ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch [bz#1995843] +- Resolves: bz#1995843 + ([cloudinit] Fix home permissions modified by ssh module) + +* Mon Aug 16 2021 Miroslav Rezanina - 21.1-7 +- ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch [bz#1979099] +- ci-Report-full-specific-version-with-cloud-init-version.patch [bz#1971002] +- Resolves: bz#1979099 + ([cloud-init]Customize ssh AuthorizedKeysFile causes login failure[RHEL-9.0]) +- Resolves: bz#1971002 + (cloud-init should report full specific full version with "cloud-init --version" [rhel-9]) + +* Mon Aug 09 2021 Mohan Boddu - 21.1-6 +- Rebuilt for IMA sigs, glibc 2.34, aarch64 flags + Related: rhbz#1991688 + +* Fri Aug 06 2021 Miroslav Rezanina - 21.1-5 +- ci-Add-dhcp-client-as-a-dependency.patch [bz#1964900] +- Resolves: bz#1964900 + ([Azure][RHEL-9] cloud-init must require dhcp-client on Azure) + +* Thu Jul 15 2021 Miroslav Rezanina - 21.1-4 +- ci-write-passwords-only-to-serial-console-lock-down-clo.patch [bz#1945892] +- ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch [bz#1979099] +- Resolves: bz#1945892 + (CVE-2021-3429 cloud-init: randomly generated passwords logged in clear-text to world-readable file [rhel-9.0]) +- Resolves: bz#1979099 + ([cloud-init]Customize ssh AuthorizedKeysFile causes login failure[RHEL-9.0]) + +* Fri Jul 02 2021 Miroslav Rezanina - 21.1-3 +- ci-Fix-requiring-device-number-on-EC2-derivatives-836.patch [bz#1943511] +- Resolves: bz#1943511 + ([Aliyun][RHEL9.0][cloud-init] cloud-init service failed to start with Alibaba instance) + +* Mon Jun 21 2021 Miroslav Rezanina - 21.1-2 +- ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch [bz#1970909] +- ci-Use-_systemdgeneratordir-macro-for-cloud-init-genera.patch [bz#1971480] +- Resolves: bz#1970909 + ([cloud-init] From RHEL 82+ cloud-init no longer displays sshd keys fingerprints from instance launched from a backup image[rhel-9]) +- Resolves: bz#1971480 + (Use systemdgenerators macro in spec file) + +* Thu Jun 10 2021 Miroslav Rezanina - 21.1-1 +- Rebase to 21.1 [bz#1958209] +- Resolves: bz#1958209 + ([RHEL-9.0] Rebase cloud-init to 21.1) + +* Wed Apr 21 2021 Miroslav Rezanina - 20.4-5 +- Removing python-mock dependency +- Resolves: bz#1922323 + +* Thu Apr 15 2021 Mohan Boddu - 20.4-4 +- Rebuilt for RHEL 9 BETA on Apr 15th 2021. Related: rhbz#1947937 + +* Wed Apr 07 2021 Miroslav Rezanina - 20.4-3.el9 +- ci-Removing-python-nose-and-python-tox-as-dependency.patch [bz#1916777 bz#1918892] +- Resolves: bz#1916777 + (cloud-init requires python-nose) +- Resolves: bz#1918892 + (cloud-init requires tox) + +* Tue Jan 26 2021 Fedora Release Engineering - 20.4-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild + +* Thu Dec 03 2020 Eduardo Otubo - 20.4-2 +- Updated to 20.4 [bz#1902250] + +* Mon Sep 07 2020 Eduardo Otubo - 19.4-7 +- Fix execution fail with backtrace + +* Mon Sep 07 2020 Eduardo Otubo - 19.4-6 +- Adding missing patches to spec file + +* Mon Jul 27 2020 Fedora Release Engineering - 19.4-5 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild + +* Mon May 25 2020 Miro Hrončok - 19.4-4 +- Rebuilt for Python 3.9 + +* Tue Apr 14 2020 Eduardo Otubo - 19.4-3 +- Fix BZ#1798729 - CVE-2020-8632 cloud-init: Too short random password length + in cc_set_password in config/cc_set_passwords.py +- Fix BZ#1798732 - CVE-2020-8631 cloud-init: Use of random.choice when + generating random password + +* Sun Feb 23 2020 Dusty Mabe - 19.4-2 +- Fix sed substitutions for unittest2 and assertItemsEqual +- Fix failing unittests by including `BuildRequires: passwd` + - The unittests started failing because of upstream commit + 7c07af2 where cloud-init can now support using `usermod` to + lock an account if `passwd` isn't installed. Since `passwd` + wasn't installed in our mock buildroot it was choosing to + use `usermod` and the unittests were failing. See: + https://github.com/canonical/cloud-init/commit/7c07af2 +- Add missing files to package + - /usr/bin/cloud-id + - /usr/share/bash-completion/completions/cloud-init + +* Fri Feb 14 2020 Eduardo Otubo - 19.4-1 +- Updated to 19.4 +- Rebasing the Fedora specific patches but removing patches that don't apply anymore + +* Tue Jan 28 2020 Fedora Release Engineering - 17.1-15 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild + +* Fri Nov 08 2019 Miro Hrončok - 17.1-14 +- Drop unneeded build dependency on python3-unittest2 + +* Thu Oct 03 2019 Miro Hrončok - 17.1-13 +- Rebuilt for Python 3.8.0rc1 (#1748018) + +* Sun Aug 18 2019 Miro Hrončok - 17.1-12 +- Rebuilt for Python 3.8 + +* Wed Jul 24 2019 Fedora Release Engineering - 17.1-11 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild + +* Tue Apr 23 2019 Björn Esser - 17.1-10 +- Add patch to replace platform.dist() [RH:1695953] +- Add (Build)Requires: python3-distro + +* Tue Apr 23 2019 Björn Esser - 17.1-9 +- Fix %%systemd_postun macro [RH:1695953] +- Add patch to fix failing test for EPOCHREALTIME bash env [RH:1695953] + +* Thu Jan 31 2019 Fedora Release Engineering - 17.1-8 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild + +* Thu Jul 12 2018 Fedora Release Engineering - 17.1-7 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild + +* Mon Jun 18 2018 Miro Hrončok - 17.1-6 +- Rebuilt for Python 3.7 + +* Sat Apr 21 2018 Lars Kellogg-Stedman - 17.1-5 +- Enable dhcp on EC2 interfaces with only local ipv4 addresses [RH:1569321] + (cherry pick upstream commit eb292c1) + +* Mon Mar 26 2018 Patrick Uiterwijk - 17.1-4 +- Make sure the patch does not add infinitely many entries + +* Mon Mar 26 2018 Patrick Uiterwijk - 17.1-3 +- Add patch to retain old values of /etc/sysconfig/network + +* Wed Feb 07 2018 Fedora Release Engineering - 17.1-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild + +* Wed Oct 4 2017 Garrett Holmstrom - 17.1-1 +- Updated to 17.1 + +* Tue Sep 26 2017 Ryan McCabe 0.7.9-10 +- AliCloud: Add support for the Alibaba Cloud datasource (rhbz#1482547) + +* Thu Jun 22 2017 Lars Kellogg-Stedman 0.7.9-9 +- RHEL/CentOS: Fix default routes for IPv4/IPv6 configuration. (rhbz#1438082) +- azure: ensure that networkmanager hook script runs (rhbz#1440831 rhbz#1460206) +- Fix ipv6 subnet detection (rhbz#1438082) + +* Tue May 23 2017 Lars Kellogg-Stedman 0.7.9-8 +- Update patches + +* Mon May 22 2017 Lars Kellogg-Stedman 0.7.9-7 +- Add missing sysconfig unit test data (rhbz#1438082) +- Fix dual stack IPv4/IPv6 configuration for RHEL (rhbz#1438082) +- sysconfig: Raise ValueError when multiple default gateways are present. (rhbz#1438082) +- Bounce network interface for Azure when using the built-in path. (rhbz#1434109) +- Do not write NM_CONTROLLED=no in generated interface config files (rhbz#1385172) + +* Wed May 10 2017 Lars Kellogg-Stedman 0.7.9-6 +- add power-state-change module to cloud_final_modules (rhbz#1252477) +- remove 'tee' command from logging configuration (rhbz#1424612) +- limit permissions on def_log_file (rhbz#1424612) +- Bounce network interface for Azure when using the built-in path. (rhbz#1434109) +- OpenStack: add 'dvs' to the list of physical link types. (rhbz#1442783) + +* Wed May 10 2017 Lars Kellogg-Stedman 0.7.9-5 +- systemd: replace generator with unit conditionals (rhbz#1440831) + +* Thu Apr 13 2017 Charalampos Stratakis 0.7.9-4 +- Import to RHEL 7 +Resolves: rhbz#1427280 + +* Tue Mar 07 2017 Lars Kellogg-Stedman 0.7.9-3 +- fixes for network config generation +- avoid dependency cycle at boot (rhbz#1420946) + +* Tue Jan 17 2017 Lars Kellogg-Stedman 0.7.9-2 +- use timeout from datasource config in openstack get_data (rhbz#1408589) + +* Thu Dec 01 2016 Lars Kellogg-Stedman - 0.7.9-1 +- Rebased on upstream 0.7.9. +- Remove dependency on run-parts + +* Wed Jan 06 2016 Lars Kellogg-Stedman - 0.7.6-8 +- make rh_subscription plugin do nothing in the absence of a valid + configuration [RH:1295953] +- move rh_subscription module to cloud_config stage + +* Wed Jan 06 2016 Lars Kellogg-Stedman - 0.7.6-7 +- correct permissions on /etc/ssh/sshd_config [RH:1296191] + +* Thu Sep 03 2015 Lars Kellogg-Stedman - 0.7.6-6 +- rebuild for ppc64le + +* Tue Jul 07 2015 Lars Kellogg-Stedman - 0.7.6-5 +- bump revision for new build + +* Tue Jul 07 2015 Lars Kellogg-Stedman - 0.7.6-4 +- ensure rh_subscription plugin is enabled by default + +* Wed Apr 29 2015 Lars Kellogg-Stedman - 0.7.6-3 +- added dependency on python-jinja2 [RH:1215913] +- added rhn_subscription plugin [RH:1227393] +- require pyserial to support smartos data source [RH:1226187] + +* Fri Jan 16 2015 Lars Kellogg-Stedman - 0.7.6-2 +- Rebased RHEL version to Fedora rawhide +- Backported fix for https://bugs.launchpad.net/cloud-init/+bug/1246485 +- Backported fix for https://bugs.launchpad.net/cloud-init/+bug/1411829 + +* Fri Nov 14 2014 Colin Walters - 0.7.6-1 +- New upstream version [RH:974327] +- Drop python-cheetah dependency (same as above bug)